Skip to content

Commit

Permalink
Make FailureMessage sendable.
Browse files Browse the repository at this point in the history
  • Loading branch information
younata committed Mar 19, 2024
1 parent 6ea97e8 commit 1bb1b71
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 66 deletions.
4 changes: 4 additions & 0 deletions Nimble.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
892FDF1329D3EA7700523A80 /* AsyncExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892FDF1229D3EA7700523A80 /* AsyncExpression.swift */; };
896962412A5FABD000A7929D /* AsyncAllPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896962402A5FABD000A7929D /* AsyncAllPass.swift */; };
8969624A2A5FAD5F00A7929D /* AsyncAllPassTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896962452A5FAD4500A7929D /* AsyncAllPassTest.swift */; };
897F84F42BA922B500BF354B /* NSLocking+Nimble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897F84F32BA922B500BF354B /* NSLocking+Nimble.swift */; };
898F28B025D9F4C30052B8D0 /* AlwaysFailMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 898F28AF25D9F4C30052B8D0 /* AlwaysFailMatcher.swift */; };
899441EF2902EE4B00C1FAF9 /* AsyncAwaitTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 899441EE2902EE4B00C1FAF9 /* AsyncAwaitTest.swift */; };
899441F82902EF2500C1FAF9 /* DSL+AsyncAwait.swift in Sources */ = {isa = PBXBuildFile; fileRef = 899441F32902EF0900C1FAF9 /* DSL+AsyncAwait.swift */; };
Expand Down Expand Up @@ -322,6 +323,7 @@
8952ADDC2B4F159400D9305F /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
896962402A5FABD000A7929D /* AsyncAllPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPass.swift; sourceTree = "<group>"; };
896962452A5FAD4500A7929D /* AsyncAllPassTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPassTest.swift; sourceTree = "<group>"; };
897F84F32BA922B500BF354B /* NSLocking+Nimble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLocking+Nimble.swift"; sourceTree = "<group>"; };
898F28AF25D9F4C30052B8D0 /* AlwaysFailMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlwaysFailMatcher.swift; sourceTree = "<group>"; };
899441EE2902EE4B00C1FAF9 /* AsyncAwaitTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAwaitTest.swift; sourceTree = "<group>"; };
899441F32902EF0900C1FAF9 /* DSL+AsyncAwait.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DSL+AsyncAwait.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -609,6 +611,7 @@
isa = PBXGroup;
children = (
1FD8CD261968AB07008ED995 /* PollAwait.swift */,
897F84F32BA922B500BF354B /* NSLocking+Nimble.swift */,
89F5E08B290B8D22001F9377 /* AsyncAwait.swift */,
891A04702AB0164500B46613 /* AsyncTimerSequence.swift */,
1FD8CD271968AB07008ED995 /* SourceLocation.swift */,
Expand Down Expand Up @@ -875,6 +878,7 @@
1FD8CD571968AB07008ED995 /* Contain.swift in Sources */,
7A0A26231E7F52360092A34E /* ToSucceed.swift in Sources */,
89F5E0862908E655001F9377 /* Polling+AsyncAwait.swift in Sources */,
897F84F42BA922B500BF354B /* NSLocking+Nimble.swift in Sources */,
899441F82902EF2500C1FAF9 /* DSL+AsyncAwait.swift in Sources */,
1FD8CD491968AB07008ED995 /* BeGreaterThanOrEqualTo.swift in Sources */,
1FE661571E6574E30035F243 /* ExpectationMessage.swift in Sources */,
Expand Down
27 changes: 0 additions & 27 deletions Sources/Nimble/ExpectationMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,33 +174,6 @@ public indirect enum ExpectationMessage: Sendable {
}
}

extension FailureMessage {
internal func toExpectationMessage() -> ExpectationMessage {
let defaultMessage = FailureMessage()
if expected != defaultMessage.expected || _stringValueOverride != nil {
return .fail(stringValue)
}

var message: ExpectationMessage = .fail(userDescription ?? "")
if actualValue != "" && actualValue != nil {
message = .expectedCustomValueTo(postfixMessage, actual: actualValue ?? "")
} else if postfixMessage != defaultMessage.postfixMessage {
if actualValue == nil {
message = .expectedTo(postfixMessage)
} else {
message = .expectedActualValueTo(postfixMessage)
}
}
if postfixActual != defaultMessage.postfixActual {
message = .appends(message, postfixActual)
}
if let extended = extendedMessage {
message = .details(message, extended)
}
return message
}
}

#if canImport(Darwin)
import class Foundation.NSObject

Expand Down
186 changes: 147 additions & 39 deletions Sources/Nimble/FailureMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,81 @@ import Foundation
/// Encapsulates the failure message that matchers can report to the end user.
///
/// This is shared state between Nimble and matchers that mutate this value.
public class FailureMessage: NSObject {
public var expected: String = "expected"
public var actualValue: String? = "" // empty string -> use default; nil -> exclude
public var to: String = "to"
public var postfixMessage: String = "match"
public var postfixActual: String = ""
public final class FailureMessage: NSObject, @unchecked Sendable {
private let lock = NSRecursiveLock()

private var _expected: String = "expected"
private var _actualValue: String? = "" // empty string -> use default; nil -> exclude
private var _to: String = "to"
private var _postfixMessage: String = "match"
private var _postfixActual: String = ""
/// An optional message that will be appended as a new line and provides additional details
/// about the failure. This message will only be visible in the issue navigator / in logs but
/// not directly in the source editor since only a single line is presented there.
public var extendedMessage: String?
public var userDescription: String?
private var _extendedMessage: String?
private var _userDescription: String?

public var stringValue: String {
public var expected: String {
get {
return lock.sync { return _expected }
}
set {
lock.sync { _expected = newValue }
}
}
public var actualValue: String? {
get {
return lock.sync { return _actualValue }
}
set {
lock.sync { _actualValue = newValue }
}
} // empty string -> use default; nil -> exclude
public var to: String {
get {
return lock.sync { return _to }
}
set {
lock.sync { _to = newValue }
}
}
public var postfixMessage: String {
get {
return lock.sync { return _postfixMessage }
}
set {
lock.sync { _postfixMessage = newValue }
}
}
public var postfixActual: String {
get {
return lock.sync { return _postfixActual }
}
set {
lock.sync { _postfixActual = newValue }
}
}
/// An optional message that will be appended as a new line and provides additional details
/// about the failure. This message will only be visible in the issue navigator / in logs but
/// not directly in the source editor since only a single line is presented there.
public var extendedMessage: String? {
get {
return lock.sync { return _extendedMessage }
}
set {
lock.sync { _extendedMessage = newValue }
}
}
public var userDescription: String? {
get {
return lock.sync { return _userDescription }
}
set {
lock.sync { _userDescription = newValue }
}
}

private var _stringValue: String {
get {
if let value = _stringValueOverride {
return value
Expand All @@ -27,66 +89,112 @@ public class FailureMessage: NSObject {
_stringValueOverride = newValue
}
}
public var stringValue: String {
get {
return lock.sync { return _stringValue }
}
set {
lock.sync { _stringValue = newValue }
}
}

internal var _stringValueOverride: String?
internal var hasOverriddenStringValue: Bool {
private var _stringValueOverride: String?
private var _hasOverriddenStringValue: Bool {
return _stringValueOverride != nil
}

internal var hasOverriddenStringValue: Bool {
return lock.sync { return _hasOverriddenStringValue }
}

public override init() {
super.init()
}

public init(stringValue: String) {
_stringValueOverride = stringValue
}

internal func stripNewlines(_ str: String) -> String {
private func stripNewlines(_ str: String) -> String {
let whitespaces = CharacterSet.whitespacesAndNewlines
return str
.components(separatedBy: "\n")
.map { line in line.trimmingCharacters(in: whitespaces) }
.joined(separator: "")
}

internal func computeStringValue() -> String {
var value = "\(expected) \(to) \(postfixMessage)"
if let actualValue = actualValue {
value = "\(expected) \(to) \(postfixMessage), got \(actualValue)\(postfixActual)"
}
value = stripNewlines(value)
private func computeStringValue() -> String {
return lock.sync {
var value = "\(_expected) \(_to) \(_postfixMessage)"
if let actualValue = _actualValue {
value = "\(_expected) \(_to) \(_postfixMessage), got \(actualValue)\(_postfixActual)"
}
value = stripNewlines(value)

if let extendedMessage = extendedMessage {
value += "\n\(extendedMessage)"
}
if let extendedMessage = _extendedMessage {
value += "\n\(extendedMessage)"
}

if let userDescription = userDescription {
return "\(userDescription)\n\(value)"
}
if let userDescription = _userDescription {
return "\(userDescription)\n\(value)"
}

return value
return value
}
}

internal func appendMessage(_ msg: String) {
if hasOverriddenStringValue {
stringValue += "\(msg)"
} else if actualValue != nil {
postfixActual += msg
} else {
postfixMessage += msg
lock.sync {
if _hasOverriddenStringValue {
_stringValue += "\(msg)"
} else if _actualValue != nil {
_postfixActual += msg
} else {
_postfixMessage += msg
}
}
}

internal func appendDetails(_ msg: String) {
if hasOverriddenStringValue {
if let desc = userDescription {
stringValue = "\(desc)\n\(stringValue)"
lock.sync {
if _hasOverriddenStringValue {
if let desc = _userDescription {
_stringValue = "\(desc)\n\(_stringValue)"
}
_stringValue += "\n\(msg)"
} else {
if let desc = _userDescription {
_userDescription = desc
}
_extendedMessage = msg
}
}
}

internal func toExpectationMessage() -> ExpectationMessage {
lock.sync {
let defaultMessage = FailureMessage()
if _expected != defaultMessage._expected || _hasOverriddenStringValue {
return .fail(_stringValue)
}

var message: ExpectationMessage = .fail(_userDescription ?? "")
if _actualValue != "" && _actualValue != nil {
message = .expectedCustomValueTo(_postfixMessage, actual: _actualValue ?? "")
} else if _postfixMessage != defaultMessage._postfixMessage {
if _actualValue == nil {
message = .expectedTo(_postfixMessage)
} else {
message = .expectedActualValueTo(_postfixMessage)
}
}
if _postfixActual != defaultMessage._postfixActual {
message = .appends(message, _postfixActual)
}
stringValue += "\n\(msg)"
} else {
if let desc = userDescription {
userDescription = desc
if let extended = _extendedMessage {
message = .details(message, extended)
}
extendedMessage = msg
return message
}
}
}
11 changes: 11 additions & 0 deletions Sources/Nimble/Utils/NSLocking+Nimble.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

extension NSLocking {
internal func sync<T>(_ closure: () throws -> T) rethrows -> T {
lock()
defer {
unlock()
}
return try closure()
}
}

0 comments on commit 1bb1b71

Please sign in to comment.