From 52ff694756ca830383485783aa6fb9f1ce47b665 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Mon, 18 Mar 2024 19:59:14 -0700 Subject: [PATCH] Make FailureMessage sendable. (#1131) --- Nimble.xcodeproj/project.pbxproj | 4 + Sources/Nimble/ExpectationMessage.swift | 27 --- Sources/Nimble/FailureMessage.swift | 186 ++++++++++++++++---- Sources/Nimble/Utils/NSLocking+Nimble.swift | 11 ++ 4 files changed, 162 insertions(+), 66 deletions(-) create mode 100644 Sources/Nimble/Utils/NSLocking+Nimble.swift diff --git a/Nimble.xcodeproj/project.pbxproj b/Nimble.xcodeproj/project.pbxproj index 4d2f55b88..50a39b53f 100644 --- a/Nimble.xcodeproj/project.pbxproj +++ b/Nimble.xcodeproj/project.pbxproj @@ -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 */; }; @@ -322,6 +323,7 @@ 8952ADDC2B4F159400D9305F /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 896962402A5FABD000A7929D /* AsyncAllPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPass.swift; sourceTree = ""; }; 896962452A5FAD4500A7929D /* AsyncAllPassTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPassTest.swift; sourceTree = ""; }; + 897F84F32BA922B500BF354B /* NSLocking+Nimble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLocking+Nimble.swift"; sourceTree = ""; }; 898F28AF25D9F4C30052B8D0 /* AlwaysFailMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlwaysFailMatcher.swift; sourceTree = ""; }; 899441EE2902EE4B00C1FAF9 /* AsyncAwaitTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAwaitTest.swift; sourceTree = ""; }; 899441F32902EF0900C1FAF9 /* DSL+AsyncAwait.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DSL+AsyncAwait.swift"; sourceTree = ""; }; @@ -609,6 +611,7 @@ isa = PBXGroup; children = ( 1FD8CD261968AB07008ED995 /* PollAwait.swift */, + 897F84F32BA922B500BF354B /* NSLocking+Nimble.swift */, 89F5E08B290B8D22001F9377 /* AsyncAwait.swift */, 891A04702AB0164500B46613 /* AsyncTimerSequence.swift */, 1FD8CD271968AB07008ED995 /* SourceLocation.swift */, @@ -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 */, diff --git a/Sources/Nimble/ExpectationMessage.swift b/Sources/Nimble/ExpectationMessage.swift index 2bff900e4..c3072eef0 100644 --- a/Sources/Nimble/ExpectationMessage.swift +++ b/Sources/Nimble/ExpectationMessage.swift @@ -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 diff --git a/Sources/Nimble/FailureMessage.swift b/Sources/Nimble/FailureMessage.swift index 8b60b9c2e..12561b1e4 100644 --- a/Sources/Nimble/FailureMessage.swift +++ b/Sources/Nimble/FailureMessage.swift @@ -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 @@ -27,20 +89,33 @@ 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") @@ -48,45 +123,78 @@ public class FailureMessage: NSObject { .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 } } } diff --git a/Sources/Nimble/Utils/NSLocking+Nimble.swift b/Sources/Nimble/Utils/NSLocking+Nimble.swift new file mode 100644 index 000000000..67bd89e97 --- /dev/null +++ b/Sources/Nimble/Utils/NSLocking+Nimble.swift @@ -0,0 +1,11 @@ +import Foundation + +extension NSLocking { + internal func sync(_ closure: () throws -> T) rethrows -> T { + lock() + defer { + unlock() + } + return try closure() + } +}