diff --git a/Package.swift b/Package.swift index 4d9112c72..6c0b29812 100644 --- a/Package.swift +++ b/Package.swift @@ -56,6 +56,13 @@ let package = Package( ], swiftSettings: .packageSettings ), + .testTarget( + name: "SubexpressionShowcase", + dependencies: [ + "Testing", + ], + swiftSettings: .packageSettings + ), .macro( name: "TestingMacros", diff --git a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedMessage.swift b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedMessage.swift index 5cfbf647c..828c1d7a4 100644 --- a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedMessage.swift +++ b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedMessage.swift @@ -61,11 +61,19 @@ extension ABIv0 { /// The symbol associated with this message. var symbol: Symbol + /// How much to indent this message when presenting it. + /// + /// - Warning: This property is not yet part of the JSON schema. + var _indentation: Int? + /// The human-readable, unformatted text associated with this message. var text: String init(encoding message: borrowing Event.HumanReadableOutputRecorder.Message) { symbol = Symbol(encoding: message.symbol ?? .default) + if message.indentation > 0 { + _indentation = message.indentation + } text = message.conciseStringValue ?? message.stringValue } } diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index f7728ac49..40c633a86 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -39,6 +39,7 @@ add_library(Testing Expectations/Expectation.swift Expectations/Expectation+Macro.swift Expectations/ExpectationChecking+Macro.swift + Expectations/ExpectationContext.swift Issues/Confirmation.swift Issues/ErrorSnapshot.swift Issues/Issue.swift @@ -61,7 +62,7 @@ add_library(Testing SourceAttribution/Backtrace+Symbolication.swift SourceAttribution/CustomTestStringConvertible.swift SourceAttribution/Expression.swift - SourceAttribution/Expression+Macro.swift + SourceAttribution/ExpressionID.swift SourceAttribution/SourceContext.swift SourceAttribution/SourceLocation.swift SourceAttribution/SourceLocation+Macro.swift diff --git a/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift index b1e90c535..a2332402d 100644 --- a/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift @@ -129,6 +129,22 @@ private let _ansiEscapeCodePrefix = "\u{001B}[" private let _resetANSIEscapeCode = "\(_ansiEscapeCodePrefix)0m" extension Event.Symbol { + /// Get the string value to use for a message with no associated symbol. + /// + /// - Parameters: + /// - options: Options to use when writing the symbol. + /// + /// - Returns: A string representation of "no symbol" appropriate for writing + /// to a stream. + fileprivate static func placeholderStringValue(options: Event.ConsoleOutputRecorder.Options) -> String { +#if os(macOS) || (os(iOS) && targetEnvironment(macCatalyst)) + if options.useSFSymbols { + return " " + } +#endif + return " " + } + /// Get the string value for this symbol with the given write options. /// /// - Parameters: @@ -169,7 +185,7 @@ extension Event.Symbol { case .attachment: return "\(_ansiEscapeCodePrefix)94m\(symbolCharacter)\(_resetANSIEscapeCode)" case .details: - return symbolCharacter + return "\(symbolCharacter)" } } return "\(symbolCharacter)" @@ -303,18 +319,12 @@ extension Event.ConsoleOutputRecorder { /// - Returns: Whether any output was produced and written to this instance's /// destination. @discardableResult public func record(_ event: borrowing Event, in context: borrowing Event.Context) -> Bool { - let messages = _humanReadableOutputRecorder.record(event, in: context) - - // Padding to use in place of a symbol for messages that don't have one. - var padding = " " -#if os(macOS) || (os(iOS) && targetEnvironment(macCatalyst)) - if options.useSFSymbols { - padding = " " - } -#endif + let symbolPlaceholder = Event.Symbol.placeholderStringValue(options: options) + let messages = _humanReadableOutputRecorder.record(event, in: context) let lines = messages.lazy.map { [test = context.test] message in - let symbol = message.symbol?.stringValue(options: options) ?? padding + let symbol = message.symbol?.stringValue(options: options) ?? symbolPlaceholder + let indentation = String(repeating: " ", count: message.indentation) if case .details = message.symbol { // Special-case the detail symbol to apply grey to the entire line of @@ -323,17 +333,17 @@ extension Event.ConsoleOutputRecorder { // to the indentation provided by the symbol. var lines = message.stringValue.split(whereSeparator: \.isNewline) lines = CollectionOfOne(lines[0]) + lines.dropFirst().map { line in - "\(padding) \(line)" + "\(indentation)\(symbolPlaceholder) \(line)" } let stringValue = lines.joined(separator: "\n") if options.useANSIEscapeCodes, options.ansiColorBitDepth > 1 { - return "\(_ansiEscapeCodePrefix)90m\(symbol) \(stringValue)\(_resetANSIEscapeCode)\n" + return "\(_ansiEscapeCodePrefix)90m\(symbol) \(indentation)\(stringValue)\(_resetANSIEscapeCode)\n" } else { - return "\(symbol) \(stringValue)\n" + return "\(symbol) \(indentation)\(stringValue)\n" } } else { let colorDots = test.map { self.colorDots(for: $0.tags) } ?? "" - return "\(symbol) \(colorDots)\(message.stringValue)\n" + return "\(symbol) \(indentation)\(colorDots)\(message.stringValue)\n" } } diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index 98303f11c..df861c254 100644 --- a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift @@ -25,6 +25,15 @@ extension Event { /// The symbol associated with this message, if any. var symbol: Symbol? + /// How much to indent this message when presenting it. + /// + /// The way in which this additional indentation is rendered is + /// implementation-defined. Typically, the greater the value of this + /// property, the more whitespace characters are inserted. + /// + /// Rendering of indentation is optional. + var indentation = 0 + /// The human-readable message. var stringValue: String @@ -415,20 +424,22 @@ extension Event.HumanReadableOutputRecorder { } additionalMessages += _formattedComments(issue.comments) - if verbosity > 0, case let .expectationFailed(expectation) = issue.kind { + if verbosity >= 0, case let .expectationFailed(expectation) = issue.kind { let expression = expectation.evaluatedExpression - func addMessage(about expression: __Expression) { - let description = expression.expandedDebugDescription() - additionalMessages.append(Message(symbol: .details, stringValue: description)) - } - let subexpressions = expression.subexpressions - if subexpressions.isEmpty { - addMessage(about: expression) - } else { - for subexpression in subexpressions { - addMessage(about: subexpression) + func addMessage(about expression: __Expression, depth: Int) { + let description = if verbosity <= 0 { + expression.expandedDescription() + } else { + expression.expandedDebugDescription() + } + if description != expression.sourceCode { + additionalMessages.append(Message(symbol: .details, indentation: depth, stringValue: description)) + } + for subexpression in expression.subexpressions { + addMessage(about: subexpression, depth: depth + 1) } } + addMessage(about: expression, depth: 0) } let atSourceLocation = issue.sourceLocation.map { " at \($0)" } ?? "" diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 39a0ea550..7481cb390 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -241,7 +241,7 @@ extension ExitTest { func callExitTest( exitsWith expectedExitCondition: ExitCondition, observing observedValues: [any PartialKeyPath & Sendable], - expression: __Expression, + sourceCode: String, comments: @autoclosure () -> [Comment], isRequired: Bool, isolation: isolated (any Actor)? = #isolation, @@ -293,10 +293,13 @@ func callExitTest( let actualExitCondition = result.exitCondition // Plumb the exit test's result through the general expectation machinery. - return __checkValue( + var expectationContext = __ExpectationContext() + expectationContext.sourceCode[.root] = sourceCode + expectationContext.runtimeValues[.root] = { Expression.Value(reflecting: actualExitCondition) } + return check( expectedExitCondition == actualExitCondition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(actualExitCondition), + expectationContext: expectationContext, + mismatchedErrorDescription: nil, mismatchedExitConditionDescription: String(describingForTest: expectedExitCondition), comments: comments(), isRequired: isRequired, diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index 5012c93ca..9b7a92006 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -66,10 +66,10 @@ /// running in the current task and an instance of ``ExpectationFailedError`` is /// thrown. @freestanding(expression) public macro require( - _ optionalValue: T?, + _ optionalValue: consuming T?, _ comment: @autoclosure () -> Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation -) -> T = #externalMacro(module: "TestingMacros", type: "RequireMacro") +) -> T = #externalMacro(module: "TestingMacros", type: "RequireMacro") where T: ~Copyable /// Unwrap an optional boolean value or, if it is `nil`, fail and throw an /// error. @@ -124,10 +124,10 @@ public macro require( @freestanding(expression) @_documentation(visibility: private) public macro require( - _ optionalValue: T, + _ optionalValue: consuming T, _ comment: @autoclosure () -> Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation -) -> T = #externalMacro(module: "TestingMacros", type: "NonOptionalRequireMacro") +) -> T = #externalMacro(module: "TestingMacros", type: "NonOptionalRequireMacro") where T: ~Copyable // MARK: - Matching errors by type diff --git a/Sources/Testing/Expectations/Expectation.swift b/Sources/Testing/Expectations/Expectation.swift index 3254544b6..8888bf367 100644 --- a/Sources/Testing/Expectations/Expectation.swift +++ b/Sources/Testing/Expectations/Expectation.swift @@ -12,14 +12,14 @@ public struct Expectation: Sendable { /// The expression evaluated by this expectation. @_spi(ForToolsIntegrationOnly) - public var evaluatedExpression: Expression + public internal(set) var evaluatedExpression: Expression /// A description of the error mismatch that occurred, if any. /// /// If this expectation passed, the value of this property is `nil` because no /// error mismatch occurred. @_spi(Experimental) @_spi(ForToolsIntegrationOnly) - public var mismatchedErrorDescription: String? + public internal(set) var mismatchedErrorDescription: String? /// A description of the difference between the operands in the expression /// evaluated by this expectation, if the difference could be determined. @@ -28,7 +28,9 @@ public struct Expectation: Sendable { /// the difference is only computed when necessary to assist with diagnosing /// test failures. @_spi(Experimental) @_spi(ForToolsIntegrationOnly) - public var differenceDescription: String? + public var differenceDescription: String? { + evaluatedExpression.differenceDescription + } /// A description of the exit condition that was expected to be matched. /// diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index eff01e5bf..2590ab295 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -13,13 +13,12 @@ /// /// - Parameters: /// - condition: The condition to be evaluated. -/// - expression: The expression, corresponding to `condition`, that is being -/// evaluated (if available at compile time.) -/// - expressionWithCapturedRuntimeValues: The expression, corresponding to -/// `condition` and with runtime values captured, that is being evaluated -/// (if available at compile time.) -/// - difference: The difference between the operands in `condition`, if -/// available. Most callers should pass `nil`. +/// - expectationContext: The expectation context, created by the caller, that +/// contains information about `condition` and its subexpressions (if any.) +/// - mismatchedErrorDescription: A description of the thrown error that did +/// not match the expectation, if applicable. +/// - mismatchedExitConditionDescription: A description of the exit condition +/// of the child process that did not match the expectation, if applicable. /// - comments: An array of comments describing the expectation. This array /// may be empty. /// - isRequired: Whether or not the expectation is required. The value of @@ -55,47 +54,26 @@ /// By _returning_ the error this function "throws", we can customize whether or /// not we throw that error during macro resolution without affecting any errors /// thrown by the condition expression passed to it. -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkValue( +func check( _ condition: Bool, - expression: __Expression, - expressionWithCapturedRuntimeValues: @autoclosure () -> __Expression? = nil, - mismatchedErrorDescription: @autoclosure () -> String? = nil, - difference: @autoclosure () -> String? = nil, - mismatchedExitConditionDescription: @autoclosure () -> String? = nil, + expectationContext: consuming __ExpectationContext, + mismatchedErrorDescription: @autoclosure () -> String?, + mismatchedExitConditionDescription: @autoclosure () -> String?, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation ) -> Result { - // If the expression being evaluated is a negation (!x instead of x), flip - // the condition here so that we evaluate it in the correct sense. We loop - // in case of multiple prefix operators (!!(a == b), for example.) - var condition = condition - do { - var expression: __Expression? = expression - while case let .negation(subexpression, _) = expression?.kind { - defer { - expression = subexpression - } - condition = !condition - } - } - - // Capture the correct expression in the expectation. - var expression = expression - if !condition, let expressionWithCapturedRuntimeValues = expressionWithCapturedRuntimeValues() { - expression = expressionWithCapturedRuntimeValues - if expression.runtimeValue == nil, case .negation = expression.kind { - expression = expression.capturingRuntimeValue(condition) - } - } + let expectationContext = consume expectationContext // Post an event for the expectation regardless of whether or not it passed. // If the current event handler is not configured to handle events of this // kind, this event is discarded. - lazy var expectation = Expectation(evaluatedExpression: expression, isPassing: condition, isRequired: isRequired, sourceLocation: sourceLocation) + lazy var expectation = Expectation( + evaluatedExpression: expectationContext.finalize(successfully: condition), + isPassing: condition, + isRequired: isRequired, + sourceLocation: sourceLocation + ) if Configuration.deliverExpectationCheckedEvents { Event.post(.expectationChecked(expectation)) } @@ -108,7 +86,6 @@ public func __checkValue( // Since this expectation failed, populate its optional fields which are // only evaluated and included lazily upon failure. expectation.mismatchedErrorDescription = mismatchedErrorDescription() - expectation.differenceDescription = difference() expectation.mismatchedExitConditionDescription = mismatchedExitConditionDescription() // Ensure the backtrace is captured here so it has fewer extraneous frames @@ -120,702 +97,192 @@ public func __checkValue( return .failure(ExpectationFailedError(expectation: expectation)) } -// MARK: - Binary operators - -/// Call a binary operator, passing the left-hand and right-hand arguments. -/// -/// - Parameters: -/// - lhs: The left-hand argument to `op`. -/// - op: The binary operator to call. -/// - rhs: The right-hand argument to `op`. This closure may be invoked zero -/// or one time, but not twice or more. -/// -/// - Returns: A tuple containing the result of calling `op` and the value of -/// `rhs` (or `nil` if it was not evaluated.) -/// -/// - Throws: Whatever is thrown by `op`. -private func _callBinaryOperator( - _ lhs: T, - _ op: (T, () -> U) -> R, - _ rhs: () -> U -) -> (result: R, rhs: U?) { - // The compiler normally doesn't allow a nonescaping closure to call another - // nonescaping closure, but our use cases are safe (e.g. `true && false`) and - // we cannot force one function or the other to be escaping. Use - // withoutActuallyEscaping() to tell the compiler that what we're doing is - // okay. SEE: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0176-enforce-exclusive-access-to-memory.md#restrictions-on-recursive-uses-of-non-escaping-closures - var rhsValue: U? - let result: R = withoutActuallyEscaping(rhs) { rhs in - op(lhs, { - if rhsValue == nil { - rhsValue = rhs() - } - return rhsValue! - }) - } - return (result, rhsValue) -} +// MARK: - Expectation checks -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used by binary operators such as `>`: -/// -/// ```swift -/// #expect(2 > 1) -/// ``` +/// A function that evaluates some boolean condition value on behalf of +/// `#expect()` or `#require()`. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -@_disfavoredOverload public func __checkBinaryOperation( - _ lhs: T, _ op: (T, () -> U) -> Bool, _ rhs: @autoclosure () -> U, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result { - let (condition, rhs) = _callBinaryOperator(lhs, op, rhs) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, rhs), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} - -// MARK: - Function calls - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used by function calls: -/// -/// ```swift -/// #expect(x.update(i)) -/// ``` -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, repeat each U) throws -> Bool, _ arguments: repeat each U, - expression: __Expression, +public func __checkCondition( + _ condition: (inout __ExpectationContext) throws -> Bool, + sourceCode: [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation ) rethrows -> Result { - let condition = try functionCall(lhs, repeat each arguments) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, repeat each arguments), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} + var expectationContext = __ExpectationContext.init(sourceCode: sourceCode) + let condition = try condition(&expectationContext) -#if !SWT_FIXED_122011759 -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0) throws -> Bool, _ argument0: Arg0, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let condition = try functionCall(lhs, argument0) - return __checkValue( + return check( condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0), + expectationContext: expectationContext, + mismatchedErrorDescription: nil, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation ) } -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) +/// A function that evaluates some optional condition value on behalf of +/// `#expect()` or `#require()`. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0, Arg1) throws -> Bool, _ argument0: Arg0, _ argument1: Arg1, - expression: __Expression, +public func __checkCondition( + _ optionalValue: (inout __ExpectationContext) throws -> T?, + sourceCode: [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation -) rethrows -> Result { - let condition = try functionCall(lhs, argument0, argument1) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0, argument1), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} +) rethrows -> Result where T: ~Copyable { + var expectationContext = __ExpectationContext(sourceCode: sourceCode) + let optionalValue = try optionalValue(&expectationContext) -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0, Arg1, Arg2) throws -> Bool, _ argument0: Arg0, _ argument1: Arg1, _ argument2: Arg2, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let condition = try functionCall(lhs, argument0, argument1, argument2) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0, argument1, argument2), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0, Arg1, Arg2, Arg3) throws -> Bool, _ argument0: Arg0, _ argument1: Arg1, _ argument2: Arg2, _ argument3: Arg3, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let condition = try functionCall(lhs, argument0, argument1, argument2, argument3) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0, argument1, argument2, argument3), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} -#endif - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used by function calls where the arguments are all `inout`: -/// -/// ```swift -/// #expect(x.update(&i)) -/// ``` -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkInoutFunctionCall( - _ lhs: T, calling functionCall: (T, inout /*repeat each*/ U) throws -> Bool, _ arguments: inout /*repeat each*/ U, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let condition = try functionCall(lhs, /*repeat each*/ &arguments) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, /*repeat each*/ arguments), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used to conditionally unwrap optional values produced from -/// expanded function calls: -/// -/// ```swift -/// let z = try #require(x.update(i)) -/// ``` -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, repeat each U) throws -> R?, _ arguments: repeat each U, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let optionalValue = try functionCall(lhs, repeat each arguments) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, repeat each arguments), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} - -#if !SWT_FIXED_122011759 -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0) throws -> R?, _ argument0: Arg0, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let optionalValue = try functionCall(lhs, argument0) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0, Arg1) throws -> R?, _ argument0: Arg0, _ argument1: Arg1, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let optionalValue = try functionCall(lhs, argument0, argument1) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0, argument1), + let result = check( + optionalValue != nil, + expectationContext: expectationContext, + mismatchedErrorDescription: nil, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation ) -} -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0, Arg1, Arg2) throws -> R?, _ argument0: Arg0, _ argument1: Arg1, _ argument2: Arg2, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let optionalValue = try functionCall(lhs, argument0, argument1, argument2) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0, argument1, argument2), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) + switch result { + case .success: + return .success(optionalValue!) + case let .failure(error): + return .failure(error) + } } -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0, Arg1, Arg2, Arg3) throws -> R?, _ argument0: Arg0, _ argument1: Arg1, _ argument2: Arg2, _ argument3: Arg3, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let optionalValue = try functionCall(lhs, argument0, argument1, argument2, argument3) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0, argument1, argument2, argument3), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} -#endif +// MARK: - Asynchronous expectation checks -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used to conditionally unwrap optional values produced from -/// expanded function calls where the arguments are all `inout`: -/// -/// ```swift -/// let z = try #require(x.update(&i)) -/// ``` +/// A function that evaluates some asynchronous boolean condition value on +/// behalf of `#expect()` or `#require()`. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkInoutFunctionCall( - _ lhs: T, calling functionCall: (T, inout /*repeat each*/ U) throws -> R?, _ arguments: inout /*repeat each*/ U, - expression: __Expression, +public func __checkConditionAsync( + _ condition: (inout __ExpectationContext) async throws -> Bool, + sourceCode: [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, + isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation -) rethrows -> Result { - let optionalValue = try functionCall(lhs, /*repeat each*/ &arguments) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, /*repeat each*/ arguments), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} +) async rethrows -> Result { + var expectationContext = __ExpectationContext(sourceCode: sourceCode) + let condition = try await condition(&expectationContext) -// MARK: - Property access - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used by property accesses: -/// -/// ```swift -/// #expect(x.isFoodTruck) -/// ``` -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkPropertyAccess( - _ lhs: T, getting memberAccess: (T) -> Bool, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result { - let condition = memberAccess(lhs) - return __checkValue( + return check( condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, condition), + expectationContext: expectationContext, + mismatchedErrorDescription: nil, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation ) } -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used to conditionally unwrap optional values produced from -/// expanded property accesses: -/// -/// ```swift -/// let z = try #require(x.nearestFoodTruck) -/// ``` +/// A function that evaluates some asynchronous optional condition value on +/// behalf of `#expect()` or `#require()`. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkPropertyAccess( - _ lhs: T, getting memberAccess: (T) -> U?, - expression: __Expression, +public func __checkConditionAsync( + _ optionalValue: (inout __ExpectationContext) async throws -> sending T?, + sourceCode: [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, + isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation -) -> Result { - let optionalValue = memberAccess(lhs) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, optionalValue as U??), +) async rethrows -> Result where T: ~Copyable { + var expectationContext = __ExpectationContext(sourceCode: sourceCode) + let optionalValue = try await optionalValue(&expectationContext) + + let result = check( + optionalValue != nil, + expectationContext: expectationContext, + mismatchedErrorDescription: nil, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation ) -} - -// MARK: - Collection diffing -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used to implement difference-reporting support when -/// comparing collections. -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -@_disfavoredOverload public func __checkBinaryOperation( - _ lhs: T, _ op: (T, () -> T) -> Bool, _ rhs: @autoclosure () -> T, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result where T: BidirectionalCollection, T.Element: Equatable { - let (condition, rhs) = _callBinaryOperator(lhs, op, rhs) - func difference() -> String? { - guard let rhs else { - return nil - } - let difference = lhs.difference(from: rhs) - let insertions = difference.insertions.map(\.element) - let removals = difference.removals.map(\.element) - switch (!insertions.isEmpty, !removals.isEmpty) { - case (true, true): - return "inserted \(insertions), removed \(removals)" - case (true, false): - return "inserted \(insertions)" - case (false, true): - return "removed \(removals)" - case (false, false): - return "" - } + switch result { + case .success: + return .success(optionalValue!) + case let .failure(error): + return .failure(error) } - - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, rhs), - difference: difference(), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) } -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is necessary because `String` satisfies the requirements for -/// the difference-calculating overload above, but the output from that overload -/// may be unexpectedly complex. +// MARK: - "Escape hatch" expectation checks + +/// The "escape hatch" overload of `__check()` that is used when a developer has +/// opted out of most of the magic of macro expansion (presumably due to a +/// miscompile.) /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkBinaryOperation( - _ lhs: String, _ op: (String, () -> String) -> Bool, _ rhs: @autoclosure () -> String, - expression: __Expression, +public func __checkEscapedCondition( + _ condition: Bool, + sourceCode: String, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation ) -> Result { - let (condition, rhs) = _callBinaryOperator(lhs, op, rhs) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, rhs), - difference: nil, - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} + var expectationContext = __ExpectationContext() + expectationContext.sourceCode[.root] = sourceCode -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is necessary because ranges are collections and satisfy the -/// requirements for the difference-calculating overload above, but it doesn't -/// make sense to diff them and very large ranges can cause overflows or hangs. -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkBinaryOperation( - _ lhs: T, _ op: (T, () -> U) -> Bool, _ rhs: @autoclosure () -> U, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result where T: RangeExpression, U: RangeExpression { - let (condition, rhs) = _callBinaryOperator(lhs, op, rhs) - return __checkValue( + return check( condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, rhs), - difference: nil, + expectationContext: expectationContext, + mismatchedErrorDescription: nil, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation ) } -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. +/// The "escape hatch" overload of `__check()` that is used when a developer has +/// opted out of most of the magic of macro expansion (presumably due to a +/// miscompile.) /// -/// This overload is used for `v is T` expressions. +/// This overload is used when the expectation is unwrapping an optional value. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkCast( - _ value: V, - is _: T.Type, - expression: __Expression, +public func __checkEscapedCondition( + _ optionalValue: consuming T?, + sourceCode: String, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation -) -> Result { - return __checkValue( - value is T, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(value, type(of: value as Any)), - difference: nil, - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} - -// MARK: - Optional unwrapping +) -> Result where T: ~Copyable { + var expectationContext = __ExpectationContext() + expectationContext.sourceCode[.root] = sourceCode -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used to conditionally unwrap optional values: -/// -/// ```swift -/// let x: Int? = ... -/// let y = try #require(x) -/// ``` -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkValue( - _ optionalValue: T?, - expression: __Expression, - expressionWithCapturedRuntimeValues: @autoclosure () -> __Expression? = nil, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result { - // The double-optional below is because capturingRuntimeValue() takes optional - // values and interprets nil as "no value available". Rather, if optionalValue - // is `nil`, we want to actually store `nil` as the expression's evaluated - // value. The outer optional satisfies the generic constraint of - // capturingRuntimeValue(), and the inner optional represents the actual value - // (`nil`) that will be captured. - __checkValue( + let result = check( optionalValue != nil, - expression: expression, - expressionWithCapturedRuntimeValues: (expressionWithCapturedRuntimeValues() ?? expression).capturingRuntimeValue(optionalValue as T??), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ).map { - optionalValue.unsafelyUnwrapped - } -} - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used to conditionally unwrap optional values using the `??` -/// operator: -/// -/// ```swift -/// let x: Int? = ... -/// let y: Int? = ... -/// let z = try #require(x ?? y) -/// ``` -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -@_disfavoredOverload public func __checkBinaryOperation( - _ lhs: T?, _ op: (T?, () -> T?) -> T?, _ rhs: @autoclosure () -> T?, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result { - let (optionalValue, rhs) = _callBinaryOperator(lhs, op, rhs) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs as T??, rhs as T??), + expectationContext: expectationContext, + mismatchedErrorDescription: nil, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation ) -} -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used for `v as? T` expressions. -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkCast( - _ value: V, - as _: T.Type, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result { - // NOTE: this call to __checkValue() does not go through the optional - // bottleneck because we do not want to capture the nil value on failure (it - // looks odd in test output.) - let optionalValue = value as? T - return __checkValue( - optionalValue != nil, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(value, type(of: value as Any)), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ).map { - optionalValue.unsafelyUnwrapped + switch result { + case .success: + return .success(optionalValue!) + case let .failure(error): + return .failure(error) } } @@ -831,7 +298,7 @@ public func __checkCast( public func __checkClosureCall( throws errorType: E.Type, performing body: () throws -> some Any, - expression: __Expression, + sourceCode: String, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation @@ -840,7 +307,7 @@ public func __checkClosureCall( __checkClosureCall( throws: Never.self, performing: body, - expression: expression, + sourceCode: sourceCode, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -850,7 +317,7 @@ public func __checkClosureCall( performing: body, throws: { $0 is E }, mismatchExplanation: { "expected error of type \(errorType), but \(_description(of: $0)) was thrown instead" }, - expression: expression, + sourceCode: sourceCode, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -868,7 +335,7 @@ public func __checkClosureCall( public func __checkClosureCall( throws errorType: E.Type, performing body: () async throws -> sending some Any, - expression: __Expression, + sourceCode: String, comments: @autoclosure () -> [Comment], isRequired: Bool, isolation: isolated (any Actor)? = #isolation, @@ -878,7 +345,7 @@ public func __checkClosureCall( await __checkClosureCall( throws: Never.self, performing: body, - expression: expression, + sourceCode: sourceCode, comments: comments(), isRequired: isRequired, isolation: isolation, @@ -889,7 +356,7 @@ public func __checkClosureCall( performing: body, throws: { $0 is E }, mismatchExplanation: { "expected error of type \(errorType), but \(_description(of: $0)) was thrown instead" }, - expression: expression, + sourceCode: sourceCode, comments: comments(), isRequired: isRequired, isolation: isolation, @@ -911,7 +378,7 @@ public func __checkClosureCall( public func __checkClosureCall( throws _: Never.Type, performing body: () throws -> some Any, - expression: __Expression, + sourceCode: String, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation @@ -925,10 +392,13 @@ public func __checkClosureCall( mismatchExplanationValue = "an error was thrown when none was expected: \(_description(of: error))" } - return __checkValue( + var expectationContext = __ExpectationContext() + expectationContext.sourceCode[.root] = sourceCode + return check( success, - expression: expression, + expectationContext: expectationContext, mismatchedErrorDescription: mismatchExplanationValue, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -947,7 +417,7 @@ public func __checkClosureCall( public func __checkClosureCall( throws _: Never.Type, performing body: () async throws -> sending some Any, - expression: __Expression, + sourceCode: String, comments: @autoclosure () -> [Comment], isRequired: Bool, isolation: isolated (any Actor)? = #isolation, @@ -962,10 +432,13 @@ public func __checkClosureCall( mismatchExplanationValue = "an error was thrown when none was expected: \(_description(of: error))" } - return __checkValue( + var expectationContext = __ExpectationContext() + expectationContext.sourceCode[.root] = sourceCode + return check( success, - expression: expression, + expectationContext: expectationContext, mismatchedErrorDescription: mismatchExplanationValue, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -984,7 +457,7 @@ public func __checkClosureCall( public func __checkClosureCall( throws error: E, performing body: () throws -> some Any, - expression: __Expression, + sourceCode: String, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation @@ -993,7 +466,7 @@ public func __checkClosureCall( performing: body, throws: { true == (($0 as? E) == error) }, mismatchExplanation: { "expected error \(_description(of: error)), but \(_description(of: $0)) was thrown instead" }, - expression: expression, + sourceCode: sourceCode, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -1010,7 +483,7 @@ public func __checkClosureCall( public func __checkClosureCall( throws error: E, performing body: () async throws -> sending some Any, - expression: __Expression, + sourceCode: String, comments: @autoclosure () -> [Comment], isRequired: Bool, isolation: isolated (any Actor)? = #isolation, @@ -1020,7 +493,7 @@ public func __checkClosureCall( performing: body, throws: { true == (($0 as? E) == error) }, mismatchExplanation: { "expected error \(_description(of: error)), but \(_description(of: $0)) was thrown instead" }, - expression: expression, + sourceCode: sourceCode, comments: comments(), isRequired: isRequired, isolation: isolation, @@ -1040,14 +513,16 @@ public func __checkClosureCall( performing body: () throws -> R, throws errorMatcher: (any Error) throws -> Bool, mismatchExplanation: ((any Error) -> String)? = nil, - expression: __Expression, + sourceCode: String, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation ) -> Result { + var expectationContext = __ExpectationContext() + expectationContext.sourceCode[.root] = sourceCode + var errorMatches = false var mismatchExplanationValue: String? = nil - var expression = expression do { let result = try body() @@ -1057,7 +532,7 @@ public func __checkClosureCall( } mismatchExplanationValue = explanation } catch { - expression = expression.capturingRuntimeValues(error) + expectationContext.runtimeValues[.root] = { Expression.Value(reflecting: error) } let secondError = Issue.withErrorRecording(at: sourceLocation) { errorMatches = try errorMatcher(error) } @@ -1068,10 +543,11 @@ public func __checkClosureCall( } } - return __checkValue( + return check( errorMatches, - expression: expression, + expectationContext: expectationContext, mismatchedErrorDescription: mismatchExplanationValue, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -1088,15 +564,17 @@ public func __checkClosureCall( performing body: () async throws -> sending R, throws errorMatcher: (any Error) async throws -> Bool, mismatchExplanation: ((any Error) -> String)? = nil, - expression: __Expression, + sourceCode: String, comments: @autoclosure () -> [Comment], isRequired: Bool, isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation ) async -> Result { + var expectationContext = __ExpectationContext() + expectationContext.sourceCode[.root] = sourceCode + var errorMatches = false var mismatchExplanationValue: String? = nil - var expression = expression do { let result = try await body() @@ -1106,7 +584,7 @@ public func __checkClosureCall( } mismatchExplanationValue = explanation } catch { - expression = expression.capturingRuntimeValues(error) + expectationContext.runtimeValues[.root] = { Expression.Value(reflecting: error) } let secondError = await Issue.withErrorRecording(at: sourceLocation) { errorMatches = try await errorMatcher(error) } @@ -1117,10 +595,11 @@ public func __checkClosureCall( } } - return __checkValue( + return check( errorMatches, - expression: expression, + expectationContext: expectationContext, mismatchedErrorDescription: mismatchExplanationValue, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -1144,7 +623,7 @@ public func __checkClosureCall( exitsWith expectedExitCondition: ExitCondition, observing observedValues: [any PartialKeyPath & Sendable], performing body: @convention(thin) () -> Void, - expression: __Expression, + sourceCode: String, comments: @autoclosure () -> [Comment], isRequired: Bool, isolation: isolated (any Actor)? = #isolation, @@ -1153,7 +632,7 @@ public func __checkClosureCall( await callExitTest( exitsWith: expectedExitCondition, observing: observedValues, - expression: expression, + sourceCode: sourceCode, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation diff --git a/Sources/Testing/Expectations/ExpectationContext.swift b/Sources/Testing/Expectations/ExpectationContext.swift new file mode 100644 index 000000000..ac136cb6d --- /dev/null +++ b/Sources/Testing/Expectations/ExpectationContext.swift @@ -0,0 +1,537 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +private import _TestingInternals + +/// A type representing the context within a call to the `#expect()` and +/// `#require()` macros. +/// +/// When the compiler expands a call to either of these macros, it creates a +/// local instance of this type that is used to collect information about the +/// various subexpressions of the macro's condition argument. The nature of the +/// collected information is subject to change over time. +/// +/// - Warning: This type is used to implement the `#expect()` and `#require()` +/// macros. Do not use it directly. +public struct __ExpectationContext: ~Copyable { + /// The source code of any captured expressions. + var sourceCode: [__ExpressionID: String] + + /// The runtime values of any captured expressions. + /// + /// The values in this dictionary are generally gathered at runtime as + /// subexpressions are evaluated. Not all expressions captured at compile time + /// will have runtime values: notably, if an operand to a short-circuiting + /// binary operator like `&&` is not evaluated, the corresponding expression + /// will not be assigned a runtime value. + var runtimeValues: [__ExpressionID: () -> Expression.Value?] + + /// Computed differences between the operands or arguments of expressions. + /// + /// The values in this dictionary are gathered at runtime as subexpressions + /// are evaluated, much like ``runtimeValues``. + var differences: [__ExpressionID: () -> CollectionDifference?] + + init( + sourceCode: [__ExpressionID: String] = [:], + runtimeValues: [__ExpressionID: () -> Expression.Value?] = [:], + differences: [__ExpressionID: () -> CollectionDifference?] = [:] + ) { + self.sourceCode = sourceCode + self.runtimeValues = runtimeValues + self.differences = differences + } + + /// Collapse the given expression graph into one or more expressions with + /// nested subexpressions. + /// + /// - Parameters: + /// - expressionGraph: The expression graph to collapse. + /// - depth: How deep into the expression graph this call is. The first call + /// has a depth of `0`. + /// + /// - Returns: An array of expressions under the root node of + /// `expressionGraph`. The expression at the root of the graph is not + /// included in the result. + private borrowing func _squashExpressionGraph(_ expressionGraph: Graph, depth: Int) -> [__Expression] { + var result = [__Expression]() + + let childGraphs = expressionGraph.children.sorted { $0.key < $1.key } + for (_, childGraph) in childGraphs { + let subexpressions = _squashExpressionGraph(childGraph, depth: depth + 1) + if var subexpression = childGraph.value { + subexpression.subexpressions += subexpressions + result.append(subexpression) + } else { + // Hoist subexpressions of the child graph as there was no expression + // recorded for it. + result += subexpressions + } + } + + return result + } + + /// Perform whatever final work is needed on this instance in order to produce + /// an instance of `__Expression` corresponding to the condition expression + /// being evaluated. + /// + /// - Parameters: + /// - successfully: Whether or not the expectation is "successful" (i.e. its + /// condition expression evaluates to `true`). If the expectation failed, + /// more diagnostic information is gathered including the runtime values + /// of any subexpressions of the condition expression. + /// + /// - Returns: An expression value representing the condition expression that + /// was evaluated. + /// + /// - Bug: This function should ideally be `consuming`, but because it is used + /// in a `lazy var` declaration, the compiler currently disallows it. + borrowing func finalize(successfully: Bool) -> __Expression { + // Construct a graph containing the source code for all the subexpressions + // we've captured during evaluation. + var expressionGraph = Graph() + for (id, sourceCode) in sourceCode { + let keyPath = id.keyPath + expressionGraph.insertValue(__Expression(sourceCode), at: keyPath) + } + + // If the expectation failed, insert any captured runtime values into the + // graph alongside the source code. + if !successfully { + for (id, runtimeValue) in runtimeValues { + let keyPath = id.keyPath + if var expression = expressionGraph[keyPath], let runtimeValue = runtimeValue() { + expression.runtimeValue = runtimeValue + expressionGraph[keyPath] = expression + } + } + + for (id, difference) in differences { + let keyPath = id.keyPath + if var expression = expressionGraph[keyPath], let difference = difference() { + let differenceDescription = Self._description(of: difference) + expression.differenceDescription = differenceDescription + expressionGraph[keyPath] = expression + } + } + } + + // Flatten the expression graph. + var subexpressions = _squashExpressionGraph(expressionGraph, depth: 0) + var expression = if let rootExpression = expressionGraph.value { + // We had a root expression and can add all reported subexpressions to it. + // This should be the common case. + rootExpression + } else if subexpressions.count == 1 { + // We had no root expression, but we did have a single reported + // subexpression that can serve as our root. + subexpressions.removeFirst() + } else { + // We could not distinguish which subexpression should serve as the root + // expression. In practice this case should be treated as a bug. + __Expression(kind: .generic("")) + } + expression.subexpressions += subexpressions + + return expression + } +} + +@available(*, unavailable) +extension __ExpectationContext: Sendable {} + +// MARK: - Expression capturing + +extension __ExpectationContext { + /// Capture information about a value for use if the expectation currently + /// being evaluated fails. + /// + /// - Parameters: + /// - value: The value to pass through. + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Returns: `value`, verbatim. + /// + /// This function helps overloads of `callAsFunction(_:_:)` disambiguate + /// themselves and avoid accidental recursion. + @usableFromInline mutating func captureValue(_ value: T, _ id: __ExpressionID) -> T { + runtimeValues[id] = { Expression.Value(reflecting: value) } + return value + } + + /// Capture information about a value for use if the expectation currently + /// being evaluated fails. + /// + /// - Parameters: + /// - value: The value to pass through. + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Returns: `value`, verbatim. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func callAsFunction(_ value: T, _ id: __ExpressionID) -> T { + captureValue(value, id) + } + +#if SWT_SUPPORTS_MOVE_ONLY_EXPRESSION_EXPANSION + /// Capture information about a value for use if the expectation currently + /// being evaluated fails. + /// + /// - Parameters: + /// - value: The value to pass through. + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Returns: `value`, verbatim. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @_disfavoredOverload + public mutating func callAsFunction(_ value: consuming T, _ id: __ExpressionID) -> T where T: ~Copyable { + // TODO: add support for borrowing non-copyable expressions (need @lifetime) + return value + } +#endif + + /// Capture information about a value passed `inout` to a function call after + /// the function has returned. + /// + /// - Parameters: + /// - value: The value that was passed `inout` (i.e. with the `&` operator.) + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + public mutating func __inoutAfter(_ value: T, _ id: __ExpressionID) { + runtimeValues[id] = { Expression.Value(reflecting: value, timing: .after) } + } +} + +// MARK: - Collection comparison and diffing + +extension __ExpectationContext { + /// Convert an instance of `CollectionDifference` to one that is type-erased + /// over elements of type `Any`. + /// + /// - Parameters: + /// - difference: The difference to convert. + /// + /// - Returns: A type-erased copy of `difference`. + private static func _typeEraseCollectionDifference(_ difference: CollectionDifference) -> CollectionDifference { + CollectionDifference( + difference.lazy.map { change in + switch change { + case let .insert(offset, element, associatedWith): + return .insert(offset: offset, element: element as Any, associatedWith: associatedWith) + case let .remove(offset, element, associatedWith): + return .remove(offset: offset, element: element as Any, associatedWith: associatedWith) + } + } + )! + } + + /// Generate a description of a previously-computed collection difference. + /// + /// - Parameters: + /// - difference: The difference to describe. + /// + /// - Returns: A human-readable string describing `difference`. + private static func _description(of difference: CollectionDifference) -> String { + let insertions: [String] = difference.insertions.lazy + .map(\.element) + .map(String.init(describingForTest:)) + let removals: [String] = difference.removals.lazy + .map(\.element) + .map(String.init(describingForTest:)) + + var resultComponents = [String]() + if !insertions.isEmpty { + resultComponents.append("inserted [\(insertions.joined(separator: ", "))]") + } + if !removals.isEmpty { + resultComponents.append("removed [\(removals.joined(separator: ", "))]") + } + + return resultComponents.joined(separator: ", ") + } + + /// Compare two values using `==` or `!=`. + /// + /// - Parameters: + /// - lhs: The left-hand operand. + /// - lhsID: A value that uniquely identifies the expression represented by + /// `lhs` in the context of the expectation currently being evaluated. + /// - rhs: The left-hand operand. + /// - rhsID: A value that uniquely identifies the expression represented by + /// `rhs` in the context of the expectation currently being evaluated. + /// - op: A function that performs an operation on `lhs` and `rhs`. + /// - opID: A value that uniquely identifies the expression represented by + /// `op` in the context of the expectation currently being evaluated. + /// + /// - Returns: The result of calling `op(lhs, rhs)`. + /// + /// This overload of `__cmp()` serves as a catch-all for operands that are not + /// collections or otherwise are not interesting to the testing library. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func __cmp( + _ op: (T, U) throws -> R, + _ opID: __ExpressionID, + _ lhs: T, + _ lhsID: __ExpressionID, + _ rhs: U, + _ rhsID: __ExpressionID + ) rethrows -> R { + try captureValue(op(captureValue(lhs, lhsID), captureValue(rhs, rhsID)), opID) + } + + /// Compare two bidirectional collections using `==` or `!=`. + /// + /// This overload of `__cmp()` performs a diffing operation on `lhs` and `rhs` + /// if the result of `op(lhs, rhs)` is `false`. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + public mutating func __cmp( + _ op: (C, C) -> Bool, + _ opID: __ExpressionID, + _ lhs: C, + _ lhsID: __ExpressionID, + _ rhs: C, + _ rhsID: __ExpressionID + ) -> Bool where C: BidirectionalCollection, C.Element: Equatable { + let result = captureValue(op(captureValue(lhs, lhsID), captureValue(rhs, rhsID)), opID) + + if !result { + differences[opID] = { [lhs, rhs] in + Self._typeEraseCollectionDifference(lhs.difference(from: rhs)) + } + } + + return result + } + + /// Compare two range expressions using `==` or `!=`. + /// + /// This overload of `__cmp()` does _not_ perform a diffing operation on `lhs` + /// and `rhs`. Range expressions are not usefully diffable the way other kinds + /// of collections are. ([#639](https://github.com/swiftlang/swift-testing/issues/639)) + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func __cmp( + _ op: (R, R) -> Bool, + _ opID: __ExpressionID, + _ lhs: R, + _ lhsID: __ExpressionID, + _ rhs: R, + _ rhsID: __ExpressionID + ) -> Bool where R: RangeExpression & BidirectionalCollection, R.Element: Equatable { + captureValue(op(captureValue(lhs, lhsID), captureValue(rhs, rhsID)), opID) + } + + /// Compare two strings using `==` or `!=`. + /// + /// This overload of `__cmp()` performs a diffing operation on `lhs` and `rhs` + /// if the result of `op(lhs, rhs)` is `false`, but does so by _line_, not by + /// _character_. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + public mutating func __cmp( + _ op: (S, S) -> Bool, + _ opID: __ExpressionID, + _ lhs: S, + _ lhsID: __ExpressionID, + _ rhs: S, + _ rhsID: __ExpressionID + ) -> Bool where S: StringProtocol { + let result = captureValue(op(captureValue(lhs, lhsID), captureValue(rhs, rhsID)), opID) + + if !result { + differences[opID] = { [lhs, rhs] in + // Compare strings by line, not by character. + let lhsLines = String(lhs).split(whereSeparator: \.isNewline) + let rhsLines = String(rhs).split(whereSeparator: \.isNewline) + + if lhsLines.count == 1 && rhsLines.count == 1 { + // There are no newlines in either string, so there's no meaningful + // per-line difference. Bail. + return nil + } + + let diff = lhsLines.difference(from: rhsLines) + if diff.isEmpty { + // The strings must have compared on a per-character basis, or this + // operator doesn't behave the way we expected. Bail. + return nil + } + + return Self._typeEraseCollectionDifference(diff) + } + } + + return result + } +} + +// MARK: - Casting + +extension __ExpectationContext { + /// Perform a conditional cast (`as?`) on a value. + /// + /// - Parameters: + /// - value: The value to cast. + /// - valueID: A value that uniquely identifies the expression represented + /// by `value` in the context of the expectation being evaluated. + /// - type: The type to cast `value` to. + /// - valueID: A value that uniquely identifies the expression represented + /// by `type` in the context of the expectation being evaluated. + /// + /// - Returns: The result of the expression `value as? type`. + /// + /// If `value` cannot be cast to `type`, the previously-recorded context for + /// the expression `type` is assigned the runtime value `type(of: value)` so + /// that the _actual_ type of `value` is recorded in any resulting issue. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func __as(_ value: T, _ valueID: __ExpressionID, _ type: U.Type, _ typeID: __ExpressionID) -> U? { + let result = captureValue(value, valueID) as? U + + if result == nil { + let correctType = Swift.type(of: value as Any) + _ = captureValue(correctType, typeID) + } + + return result + } + + /// Check the type of a value using the `is` operator. + /// + /// - Parameters: + /// - value: The value to cast. + /// - valueID: A value that uniquely identifies the expression represented + /// by `value` in the context of the expectation being evaluated. + /// - type: The type `value` is expected to be. + /// - valueID: A value that uniquely identifies the expression represented + /// by `type` in the context of the expectation being evaluated. + /// + /// - Returns: The result of the expression `value as? type`. + /// + /// If `value` is not an instance of `type`, the previously-recorded context + /// for the expression `type` is assigned the runtime value `type(of: value)` + /// so that the _actual_ type of `value` is recorded in any resulting issue. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func __is(_ value: T, _ valueID: __ExpressionID, _ type: U.Type, _ typeID: __ExpressionID) -> Bool { + let result = captureValue(value, valueID) is U + + if !result { + let correctType = Swift.type(of: value as Any) + _ = captureValue(correctType, typeID) + } + + return result + } +} + +// MARK: - Implicit pointer conversion + +extension __ExpectationContext { + /// Convert some pointer to another pointer type and capture information about + /// it for use if the expectation currently being evaluated fails. + /// + /// - Parameters: + /// - value: The pointer to cast. + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Returns: `value`, cast to another type of pointer. + /// + /// This overload of `callAsFunction(_:_:)` handles the implicit conversions + /// between various pointer types that are normally provided by the compiler. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func callAsFunction(_ value: P1?, _ id: __ExpressionID) -> P2! where P1: _Pointer, P2: _Pointer { + captureValue(value, id).flatMap { value in + P2(bitPattern: Int(bitPattern: value)) + } + } +} + +// MARK: - String-to-C-string handling + +extension __ExpectationContext { + /// Capture information about a value for use if the expectation currently + /// being evaluated fails. + /// + /// - Parameters: + /// - value: The value to pass through. + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Returns: `value`, verbatim. + /// + /// This overload of `callAsFunction(_:_:)` helps the compiler disambiguate + /// string values when they need to be implicitly cast to C strings. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func callAsFunction(_ value: String, _ id: __ExpressionID) -> String { + captureValue(value, id) + } + + /// Capture information about a value for use if the expectation currently + /// being evaluated fails. + /// + /// - Parameters: + /// - value: The value to pass through. + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Returns: An optional value containing a copy of `value`. + /// + /// This overload of `callAsFunction(_:_:)` helps the compiler disambiguate + /// string values when they need to be implicitly cast to C strings. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func callAsFunction(_ value: String, _ id: __ExpressionID) -> String? { + captureValue(value, id) + } + + /// Capture information about a value for use if the expectation currently + /// being evaluated fails. + /// + /// - Parameters: + /// - value: The value to pass through. + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Returns: An optional value containing a copy of `value`. + /// + /// This overload of `callAsFunction(_:_:)` helps the compiler disambiguate + /// string values when they need to be implicitly cast to C strings. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func callAsFunction(_ value: String?, _ id: __ExpressionID) -> String? { + captureValue(value, id) + } +} diff --git a/Sources/Testing/SourceAttribution/Expression+Macro.swift b/Sources/Testing/SourceAttribution/Expression+Macro.swift deleted file mode 100644 index 8bbd46b13..000000000 --- a/Sources/Testing/SourceAttribution/Expression+Macro.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2023 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for Swift project authors -// - -extension __Expression { - /// Create an instance of this type representing a complete syntax node. - /// - /// - Parameters: - /// - syntaxNode: The complete syntax node (expression, declaration, - /// statement, etc.) - /// - /// - Returns: A new instance of this type. - /// - /// - Warning: This function is used to implement the `@Test`, `@Suite`, - /// `#expect()` and `#require()` macros. Do not call it directly. - public static func __fromSyntaxNode(_ syntaxNode: String) -> Self { - Self(kind: .generic(syntaxNode)) - } - - /// Create an instance of this type representing a string literal. - /// - /// - Parameters: - /// - sourceCode: The source code representation of the string literal, - /// including leading and trailing punctuation. - /// - stringValue: The actual string value of the string literal - /// - /// - Returns: A new instance of this type. - /// - /// - Warning: This function is used to implement the `@Test`, `@Suite`, - /// `#expect()` and `#require()` macros. Do not call it directly. - public static func __fromStringLiteral(_ sourceCode: String, _ stringValue: String) -> Self { - Self(kind: .stringLiteral(sourceCode: sourceCode, stringValue: stringValue)) - } - - /// Create an instance of this type representing a binary operation. - /// - /// - Parameters: - /// - lhs: The left-hand operand. - /// - op: The operator. - /// - rhs: The right-hand operand. - /// - /// - Returns: A new instance of this type. - /// - /// - Warning: This function is used to implement the `@Test`, `@Suite`, - /// `#expect()` and `#require()` macros. Do not call it directly. - public static func __fromBinaryOperation(_ lhs: Self, _ op: String, _ rhs: Self) -> Self { - return Self(kind: .binaryOperation(lhs: lhs, operator: op, rhs: rhs)) - } - - /// Create an instance of this type representing a function call. - /// - /// - Parameters: - /// - value: The value on which the member function is being invoked, if - /// any. - /// - functionName: The name of the member function. - /// - argumentLabel: Optionally, the argument label. - /// - argumentValue: The value of the argument to the function. - /// - /// - Returns: A new instance of this type. - /// - /// - Warning: This function is used to implement the `@Test`, `@Suite`, - /// `#expect()` and `#require()` macros. Do not call it directly. - public static func __fromFunctionCall(_ value: Self?, _ functionName: String, _ arguments: (label: String?, value: Self)...) -> Self { - let arguments = arguments.map(Kind.FunctionCallArgument.init) - return Self(kind: .functionCall(value: value, functionName: functionName, arguments: arguments)) - } - - /// Create an instance of this type representing a property access. - /// - /// - Parameters: - /// - value: The value whose property was accessed. - /// - keyPath: The key path, relative to `value`, that was accessed, not - /// including a leading backslash or period. - /// - /// - Returns: A new instance of this type. - /// - /// - Warning: This function is used to implement the `@Test`, `@Suite`, - /// `#expect()` and `#require()` macros. Do not call it directly. - public static func __fromPropertyAccess(_ value: Self, _ keyPath: Self) -> Self { - return Self(kind: .propertyAccess(value: value, keyPath: keyPath)) - } - - /// Create an instance of this type representing a negated expression - /// using the `!` operator.. - /// - /// - Parameters: - /// - expression: The expression that was negated. - /// - isParenthetical: Whether or not `expression` was enclosed in - /// parentheses (and the `!` operator was outside it.) This argument - /// affects how this expression is represented as a string. - /// - /// - Returns: A new instance of this type. - /// - /// - Warning: This function is used to implement the `@Test`, `@Suite`, - /// `#expect()` and `#require()` macros. Do not call it directly. - public static func __fromNegation(_ expression: Self, _ isParenthetical: Bool) -> Self { - return Self(kind: .negation(expression, isParenthetical: isParenthetical)) - } -} diff --git a/Sources/Testing/SourceAttribution/Expression.swift b/Sources/Testing/SourceAttribution/Expression.swift index 3f6a7e716..5a4553b25 100644 --- a/Sources/Testing/SourceAttribution/Expression.swift +++ b/Sources/Testing/SourceAttribution/Expression.swift @@ -36,64 +36,6 @@ public struct __Expression: Sendable { /// - Parameters: /// - sourceCode: The source code of the represented expression. case generic(_ sourceCode: String) - - /// The expression represents a string literal expression. - /// - /// - Parameters: - /// - sourceCode: The source code of the represented expression. Note that - /// this string is not the _value_ of the string literal, but the string - /// literal itself (including leading and trailing quote marks and - /// extended punctuation.) - /// - stringValue: The value of the string literal. - case stringLiteral(sourceCode: String, stringValue: String) - - /// The expression represents a binary operation. - /// - /// - Parameters: - /// - lhs: The left-hand operand. - /// - operator: The operator. - /// - rhs: The right-hand operand. - indirect case binaryOperation(lhs: __Expression, `operator`: String, rhs: __Expression) - - /// A type representing an argument to a function call, used by the - /// `__Expression.Kind.functionCall` case. - /// - /// This type is not part of the public interface of the testing library. - struct FunctionCallArgument: Sendable { - /// The label, if present, of the argument. - var label: String? - - /// The value, as an expression, of the argument. - var value: __Expression - } - - /// The expression represents a function call. - /// - /// - Parameters: - /// - value: The value on which the function was called, if any. - /// - functionName: The name of the function that was called. - /// - arguments: The arguments passed to the function. - indirect case functionCall(value: __Expression?, functionName: String, arguments: [FunctionCallArgument]) - - /// The expression represents a property access. - /// - /// - Parameters: - /// - value: The value whose property was accessed. - /// - keyPath: The key path, relative to `value`, that was accessed, not - /// including a leading backslash or period. - indirect case propertyAccess(value: __Expression, keyPath: __Expression) - - /// The expression negates another expression. - /// - /// - Parameters: - /// - expression: The expression that was negated. - /// - isParenthetical: Whether or not `expression` was enclosed in - /// parentheses (and the `!` operator was outside it.) This argument - /// affects how this expression is represented as a string. - /// - /// Unlike other cases in this enumeration, this case affects the runtime - /// behavior of the `__check()` family of functions. - indirect case negation(_ expression: __Expression, isParenthetical: Bool) } /// The kind of syntax node represented by this instance. @@ -107,30 +49,8 @@ public struct __Expression: Sendable { @_spi(ForToolsIntegrationOnly) public var sourceCode: String { switch kind { - case let .generic(sourceCode), let .stringLiteral(sourceCode, _): + case let .generic(sourceCode): return sourceCode - case let .binaryOperation(lhs, op, rhs): - return "\(lhs) \(op) \(rhs)" - case let .functionCall(value, functionName, arguments): - let argumentList = arguments.lazy - .map { argument in - if let argumentLabel = argument.label { - return "\(argumentLabel): \(argument.value.sourceCode)" - } - return argument.value.sourceCode - }.joined(separator: ", ") - if let value { - return "\(value.sourceCode).\(functionName)(\(argumentList))" - } - return "\(functionName)(\(argumentList))" - case let .propertyAccess(value, keyPath): - return "\(value.sourceCode).\(keyPath.sourceCode)" - case let .negation(expression, isParenthetical): - var sourceCode = expression.sourceCode - if isParenthetical { - sourceCode = "(\(sourceCode))" - } - return "!\(sourceCode)" } } @@ -149,6 +69,21 @@ public struct __Expression: Sendable { /// Information about the type of this value. public var typeInfo: TypeInfo + /// The timing when a runtime value was captured. + @_spi(Experimental) + public enum Timing: String, Sendable { + /// The value was captured after the containing expression was evaluated. + case after + } + + /// When the value represented by this instance was captured. + /// + /// The value of this property is typically `nil`. It may be set to a + /// non-`nil` value if this instance represents some `inout` argument passed + /// to a function with the `&` operator. + @_spi(Experimental) + public var timing: Timing? + /// The label associated with this value, if any. /// /// For non-child instances, or for child instances of members who do not @@ -172,9 +107,10 @@ public struct __Expression: Sendable { /// /// - Parameters: /// - subject: The subject this instance should describe. - init(reflecting subject: Any) { + /// - timing: When the value represented by this instance was captured. + init(reflecting subject: Any, timing: Timing? = nil) { var seenObjects: [ObjectIdentifier: AnyObject] = [:] - self.init(_reflecting: subject, label: nil, seenObjects: &seenObjects) + self.init(_reflecting: subject, label: nil, timing: timing, seenObjects: &seenObjects) } /// Initialize an instance of this type describing the specified subject and @@ -185,6 +121,7 @@ public struct __Expression: Sendable { /// - label: An optional label for this value. This should be a non-`nil` /// value when creating instances of this type which describe /// substructural values. + /// - timing: When the value represented by this instance was captured. /// - seenObjects: The objects which have been seen so far while calling /// this initializer recursively, keyed by their object identifiers. /// This is used to halt further recursion if a previously-seen object @@ -192,6 +129,7 @@ public struct __Expression: Sendable { private init( _reflecting subject: Any, label: String?, + timing: Timing?, seenObjects: inout [ObjectIdentifier: AnyObject] ) { let mirror = Mirror(reflecting: subject) @@ -240,6 +178,7 @@ public struct __Expression: Sendable { debugDescription = String(reflecting: subject) typeInfo = TypeInfo(describingTypeOf: subject) self.label = label + self.timing = timing isCollection = switch mirror.displayStyle { case .some(.collection), @@ -252,7 +191,7 @@ public struct __Expression: Sendable { if shouldIncludeChildren && (!mirror.children.isEmpty || isCollection) { self.children = mirror.children.map { child in - Self(_reflecting: child.value, label: child.label, seenObjects: &seenObjects) + Self(_reflecting: child.value, label: child.label, timing: timing, seenObjects: &seenObjects) } } } @@ -265,81 +204,13 @@ public struct __Expression: Sendable { @_spi(ForToolsIntegrationOnly) public var runtimeValue: Value? - /// Copy this instance and capture the runtime value corresponding to it. - /// - /// - Parameters: - /// - value: The captured runtime value. - /// - /// - Returns: A copy of `self` with information about the specified runtime - /// value captured for future use. - func capturingRuntimeValue(_ value: (some Any)?) -> Self { - var result = self - result.runtimeValue = value.map { Value(reflecting: $0) } - if case let .negation(subexpression, isParenthetical) = kind, let value = value as? Bool { - result.kind = .negation(subexpression.capturingRuntimeValue(!value), isParenthetical: isParenthetical) - } - return result - } - - /// Copy this instance and capture the runtime values corresponding to its - /// subexpressions. - /// - /// - Parameters: - /// - firstValue: The first captured runtime value. - /// - additionalValues: Any additional captured runtime values after the - /// first. - /// - /// - Returns: A copy of `self` with information about the specified runtime - /// values captured for future use. - /// - /// If the ``kind`` of `self` is ``Kind/generic`` or ``Kind/stringLiteral``, - /// this function is equivalent to ``capturingRuntimeValue(_:)``. - func capturingRuntimeValues(_ firstValue: (some Any)?, _ additionalValues: repeat (each T)?) -> Self { - var result = self - - // Convert the variadic generic argument list to an array. - var additionalValuesArray = [Any?]() - repeat additionalValuesArray.append(each additionalValues) - - switch kind { - case .generic, .stringLiteral: - result = capturingRuntimeValue(firstValue) - case let .binaryOperation(lhsExpr, op, rhsExpr): - result.kind = .binaryOperation( - lhs: lhsExpr.capturingRuntimeValues(firstValue), - operator: op, - rhs: rhsExpr.capturingRuntimeValues(additionalValuesArray.first ?? nil) - ) - case let .functionCall(value, functionName, arguments): - result.kind = .functionCall( - value: value?.capturingRuntimeValues(firstValue), - functionName: functionName, - arguments: zip(arguments, additionalValuesArray).map { argument, value in - .init(label: argument.label, value: argument.value.capturingRuntimeValues(value)) - } - ) - case let .propertyAccess(value, keyPath): - result.kind = .propertyAccess( - value: value.capturingRuntimeValues(firstValue), - keyPath: keyPath.capturingRuntimeValues(additionalValuesArray.first ?? nil) - ) - case let .negation(expression, isParenthetical): - result.kind = .negation( - expression.capturingRuntimeValues(firstValue, repeat each additionalValues), - isParenthetical: isParenthetical - ) - } - - return result - } - /// Get an expanded description of this instance that contains the source /// code and runtime value (or values) it represents. /// /// - Returns: A string describing this instance. @_spi(ForToolsIntegrationOnly) public func expandedDescription() -> String { - _expandedDescription(in: _ExpandedDescriptionContext()) + _expandedDescription(verbose: false) } /// Get an expanded description of this instance that contains the source @@ -351,149 +222,66 @@ public struct __Expression: Sendable { /// ``expandedDescription()``, similar to how `String(reflecting:)` produces /// a more detailed description than `String(describing:)`. func expandedDebugDescription() -> String { - var context = _ExpandedDescriptionContext() - context.includeTypeNames = true - context.includeParenthesesIfNeeded = false - return _expandedDescription(in: context) - } - - /// A structure describing the state tracked while calling - /// `_expandedDescription(in:)`. - private struct _ExpandedDescriptionContext { - /// The depth of recursion at which the function is being called. - var depth = 0 - - /// Whether or not to include type names in output. - var includeTypeNames = false - - /// Whether or not to enclose the resulting string in parentheses (as needed - /// depending on what information the resulting string contains.) - var includeParenthesesIfNeeded = true + _expandedDescription(verbose: true) } /// Get an expanded description of this instance that contains the source /// code and runtime value (or values) it represents. /// /// - Parameters: - /// - context: The context for this call. + /// - verbose: Whether or not to include more verbose output. /// /// - Returns: A string describing this instance. /// /// This function provides the implementation of ``expandedDescription()`` and /// ``expandedDebugDescription()``. - private func _expandedDescription(in context: _ExpandedDescriptionContext) -> String { - // Create a (default) context value to pass to recursive calls for - // subexpressions. - var childContext = context - do { - // Bump the depth so that recursive calls track the next depth level. - childContext.depth += 1 - - // Subexpressions do not automatically disable parentheses if the parent - // does; they must opt in. - childContext.includeParenthesesIfNeeded = true - } + private func _expandedDescription(verbose: Bool) -> String { + var result = sourceCode - var result = "" - switch kind { - case let .generic(sourceCode), let .stringLiteral(sourceCode, _): - result = if context.includeTypeNames, let qualifiedName = runtimeValue?.typeInfo.fullyQualifiedName { - "\(sourceCode): \(qualifiedName)" - } else { - sourceCode - } - case let .binaryOperation(lhsExpr, op, rhsExpr): - result = "\(lhsExpr._expandedDescription(in: childContext)) \(op) \(rhsExpr._expandedDescription(in: childContext))" - case let .functionCall(value, functionName, arguments): - var argumentContext = childContext - argumentContext.includeParenthesesIfNeeded = (arguments.count > 1) - let argumentList = arguments.lazy - .map { argument in - (argument.label, argument.value._expandedDescription(in: argumentContext)) - }.map { label, value in - if let label { - return "\(label): \(value)" - } - return value - }.joined(separator: ", ") - result = if let value { - "\(value._expandedDescription(in: childContext)).\(functionName)(\(argumentList))" - } else { - "\(functionName)(\(argumentList))" - } - case let .propertyAccess(value, keyPath): - var keyPathContext = childContext - keyPathContext.includeParenthesesIfNeeded = false - result = "\(value._expandedDescription(in: childContext)).\(keyPath._expandedDescription(in: keyPathContext))" - case let .negation(expression, isParenthetical): - childContext.includeParenthesesIfNeeded = !isParenthetical - var expandedDescription = expression._expandedDescription(in: childContext) - if isParenthetical { - expandedDescription = "(\(expandedDescription))" - } - result = "!\(expandedDescription)" - } - - // If this expression is at the root of the expression graph... - if context.depth == 0 { - if runtimeValue == nil { - // ... and has no value, don't bother reporting the placeholder string - // for it... - return result - } else if let runtimeValue, runtimeValue.typeInfo.describes(Bool.self) { - // ... or if it is a boolean value, also don't bother (because it can be - // inferred from context.) - return result - } + if verbose, let qualifiedName = runtimeValue?.typeInfo.fullyQualifiedName { + result = "\(result): \(qualifiedName)" } - let runtimeValueDescription = runtimeValue.map(String.init(describing:)) ?? "" - result = if runtimeValueDescription == "(Function)" { + if let runtimeValue { + let runtimeValueDescription = String(describingForTest: runtimeValue) // Hack: don't print string representations of function calls. - result - } else if runtimeValueDescription == result { - result - } else if context.includeParenthesesIfNeeded && context.depth > 0 { - "(\(result) → \(runtimeValueDescription))" + if runtimeValueDescription != "(Function)" && runtimeValueDescription != result { + switch runtimeValue.timing { + case .after: + result = "\(result) (after)" + default: + break + } + result = "\(result) → \(runtimeValueDescription)" + } } else { - "\(result) → \(runtimeValueDescription)" + result = "\(result) → " } + return result } /// The set of parsed and captured subexpressions contained in this instance. @_spi(ForToolsIntegrationOnly) - public var subexpressions: [Self] { - switch kind { - case .generic, .stringLiteral: - [] - case let .binaryOperation(lhs, _, rhs): - [lhs, rhs] - case let .functionCall(value, _, arguments): - if let value { - CollectionOfOne(value) + arguments.lazy.map(\.value) - } else { - arguments.lazy.map(\.value) - } - case let .propertyAccess(value: value, keyPath: keyPath): - [value, keyPath] - case let .negation(expression, _): - [expression] - } - } + public internal(set) var subexpressions = [Self]() - /// The string value associated with this instance if it represents a string - /// literal. + /// A description of the difference between the operands in this expression, + /// if that difference could be determined. + /// + /// The value of this property is set for the binary operators `==` and `!=` + /// when used to compare collections. /// - /// If this instance represents an expression other than a string literal, the - /// value of this property is `nil`. + /// If the containing expectation passed, the value of this property is `nil` + /// because the difference is only computed when necessary to assist with + /// diagnosing test failures. + @_spi(Experimental) @_spi(ForToolsIntegrationOnly) + public internal(set) var differenceDescription: String? + @_spi(ForToolsIntegrationOnly) + @available(*, deprecated, message: "The value of this property is always nil.") public var stringLiteralValue: String? { - if case let .stringLiteral(_, stringValue) = kind { - return stringValue - } - return nil + nil } } @@ -501,8 +289,8 @@ public struct __Expression: Sendable { extension __Expression: Codable {} extension __Expression.Kind: Codable {} -extension __Expression.Kind.FunctionCallArgument: Codable {} extension __Expression.Value: Codable {} +extension __Expression.Value.Timing: Codable {} // MARK: - CustomStringConvertible, CustomDebugStringConvertible diff --git a/Sources/Testing/SourceAttribution/ExpressionID.swift b/Sources/Testing/SourceAttribution/ExpressionID.swift new file mode 100644 index 000000000..934972c0f --- /dev/null +++ b/Sources/Testing/SourceAttribution/ExpressionID.swift @@ -0,0 +1,78 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +/// A type providing unique identifiers for expressions captured during +/// expansion of the `#expect()` and `#require()` macros. +/// +/// In the future, this type may use [`StaticBigInt`](https://developer.apple.com/documentation/swift/staticbigint) +/// as its source representation rather than a string literal. +/// +/// - Warning: This type is used to implement the `#expect()` and `#require()` +/// macros. Do not use it directly. +public struct __ExpressionID: Sendable { + /// The ID of the root node in an expression graph. + static var root: Self { + "" + } + + /// The string produced at compile time that encodes the unique identifier of + /// the represented expression. + var stringValue: String + + /// The number of bits in a nybble. + private static var _bitsPerNybble: Int { 4 } + + /// A representation of this instance suitable for use as a key path in an + /// instance of `Graph` where the key type is `UInt32`. + /// + /// The values in this collection, being swift-syntax node IDs, are never more + /// than 32 bits wide. + var keyPath: some RandomAccessCollection { + let nybbles = stringValue + .reversed().lazy + .compactMap { UInt8(String($0), radix: 16) } + + return nybbles + .enumerated() + .flatMap { i, nybble in + let nybbleOffset = i * Self._bitsPerNybble + return (0 ..< Self._bitsPerNybble).lazy + .filter { (nybble & (1 << $0)) != 0 } + .map { UInt32(nybbleOffset + $0) } + } + } +} + +// MARK: - Equatable, Hashable + +extension __ExpressionID: Equatable, Hashable {} + +#if DEBUG +// MARK: - CustomStringConvertible, CustomDebugStringConvertible + +extension __ExpressionID: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + stringValue + } + + public var debugDescription: String { + #""\#(stringValue)" → \#(Array(keyPath))"# + } +} +#endif + +// MARK: - ExpressibleByStringLiteral + +extension __ExpressionID: ExpressibleByStringLiteral { + public init(stringLiteral: String) { + stringValue = stringLiteral + } +} + diff --git a/Sources/Testing/Support/Additions/ResultAdditions.swift b/Sources/Testing/Support/Additions/ResultAdditions.swift index f14f68c85..e404af0f6 100644 --- a/Sources/Testing/Support/Additions/ResultAdditions.swift +++ b/Sources/Testing/Support/Additions/ResultAdditions.swift @@ -8,18 +8,18 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -extension Result { +extension Result where Success: ~Copyable { /// Handle this instance as if it were returned from a call to `#expect()`. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. - @inlinable public func __expected() where Success == Void {} + @inlinable public borrowing func __expected() where Success == Void {} /// Handle this instance as if it were returned from a call to `#require()`. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. - @inlinable public func __required() throws -> Success { + @inlinable public consuming func __required() throws -> Success { try get() } } diff --git a/Sources/TestingMacros/CMakeLists.txt b/Sources/TestingMacros/CMakeLists.txt index ad58fc35b..811061dc5 100644 --- a/Sources/TestingMacros/CMakeLists.txt +++ b/Sources/TestingMacros/CMakeLists.txt @@ -87,6 +87,7 @@ target_sources(TestingMacros PRIVATE Support/Additions/EditorPlaceholderExprSyntaxAdditions.swift Support/Additions/FunctionDeclSyntaxAdditions.swift Support/Additions/MacroExpansionContextAdditions.swift + Support/Additions/SyntaxProtocolAdditions.swift Support/Additions/TokenSyntaxAdditions.swift Support/Additions/TriviaPieceAdditions.swift Support/Additions/TypeSyntaxProtocolAdditions.swift @@ -100,7 +101,7 @@ target_sources(TestingMacros PRIVATE Support/CRC32.swift Support/DiagnosticMessage.swift Support/DiagnosticMessage+Diagnosing.swift - Support/SourceCodeCapturing.swift + Support/EffectfulExpressionHandling.swift Support/SourceLocationGeneration.swift TagMacro.swift TestDeclarationMacro.swift diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 341b27d7d..19c07e8aa 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -107,8 +107,9 @@ extension ConditionMacro { .firstIndex { $0.tokenKind == _sourceLocationLabel.tokenKind } // Construct the argument list to __check(). - let expandedFunctionName: TokenSyntax + var expandedFunctionName = TokenSyntax.identifier("__checkCondition") var checkArguments = [Argument]() + var effectKeywordsToApply: Set = [] do { if let trailingClosureIndex { @@ -122,17 +123,78 @@ extension ConditionMacro { // The trailing closure should be the focus of the source code capture. let primaryExpression = primaryExpression ?? macroArguments[trailingClosureIndex].expression - let sourceCode = parseCondition(from: primaryExpression, for: macro, in: context).expression - checkArguments.append(Argument(label: "expression", expression: sourceCode)) + let sourceCode: String = if let closureExpr = primaryExpression.as(ClosureExprSyntax.self), + closureExpr.signature == nil && closureExpr.statements.count == 1, + let item = closureExpr.statements.first?.item { + // TODO: capture closures as a different kind of Testing.Expression + // with a separate subexpression per code item. + + // If a closure contains a single statement or declaration, we can't + // meaningfully break it down as an expression, but we can still + // capture its source representation. + item.trimmedDescription + } else { + primaryExpression.trimmedDescription + } + checkArguments.append(Argument(label: "sourceCode", expression: StringLiteralExprSyntax(content: sourceCode))) expandedFunctionName = .identifier("__checkClosureCall") - } else { - // Get the condition expression and extract its parsed form and source - // code. The first argument is always the condition argument if there is - // no trailing closure argument. - let conditionArgument = parseCondition(from: macroArguments.first!.expression, for: macro, in: context) - checkArguments += conditionArgument.arguments + } else if let firstArgument = macroArguments.first { + let originalArgumentExpr = firstArgument.expression + effectKeywordsToApply = findEffectKeywords(in: originalArgumentExpr) + + var useEscapeHatch = false + if let asExpr = originalArgumentExpr.as(AsExprSyntax.self), asExpr.questionOrExclamationMark == nil { + // "Escape hatch" for x as Bool to avoid the full recursive expansion. + useEscapeHatch = true + } else if effectKeywordsToApply.contains(.consume) { + // `consume` expressions imply non-copyable values which cannot yet be + // safely used with the closure we generate below. + useEscapeHatch = true + } + + if useEscapeHatch { + checkArguments.append(firstArgument) + checkArguments.append(Argument(label: "sourceCode", expression: StringLiteralExprSyntax(content: originalArgumentExpr.trimmedDescription))) + expandedFunctionName = .identifier("__checkEscapedCondition") + + } else { + if effectKeywordsToApply.contains(.await) { + expandedFunctionName = .identifier("__checkConditionAsync") + } + + var expressionContextName = TokenSyntax.identifier("__ec") + let isNameUsed = originalArgumentExpr.tokens(viewMode: .sourceAccurate).lazy + .map(\.tokenKind) + .contains(expressionContextName.tokenKind) + if isNameUsed { + // BUG: We should use the unique name directly. SEE: swift-syntax-#2256 + let uniqueName = context.makeUniqueName("") + expressionContextName = .identifier("\(expressionContextName)\(uniqueName)") + } + let (closureExpr, rewrittenNodes) = rewrite( + originalArgumentExpr, + usingExpressionContextNamed: expressionContextName, + for: macro, + rootedAt: originalArgumentExpr, + effectKeywordsToApply: effectKeywordsToApply, + in: context + ) + checkArguments.append(Argument(expression: closureExpr)) + + // Sort the rewritten nodes. This isn't strictly necessary for + // correctness but it does make the produced code more consistent. + let sortedRewrittenNodes = rewrittenNodes.sorted { $0.id < $1.id } + let sourceCodeNodeIDs = sortedRewrittenNodes.compactMap { $0.expressionID(rootedAt: originalArgumentExpr) } + let sourceCodeExprs = sortedRewrittenNodes.map { StringLiteralExprSyntax(content: $0.trimmedDescription) } + let sourceCodeExpr = DictionaryExprSyntax { + for (nodeID, sourceCodeExpr) in zip(sourceCodeNodeIDs, sourceCodeExprs) { + DictionaryElementSyntax(key: nodeID, value: sourceCodeExpr) + } + } + checkArguments.append(Argument(label: "sourceCode", expression: sourceCodeExpr)) + } // Include all arguments other than the "condition", "comment", and // "sourceLocation" arguments here. @@ -141,15 +203,6 @@ extension ConditionMacro { .filter { $0 != isolationArgumentIndex } .filter { $0 != sourceLocationArgumentIndex } .map { macroArguments[$0] } - - if let primaryExpression { - let sourceCode = parseCondition(from: primaryExpression, for: macro, in: context).expression - checkArguments.append(Argument(label: "expression", expression: sourceCode)) - } else { - checkArguments.append(Argument(label: "expression", expression: conditionArgument.expression)) - } - - expandedFunctionName = conditionArgument.expandedFunctionName } // Capture any comments as well (either in source or as a macro argument.) @@ -187,11 +240,19 @@ extension ConditionMacro { } // Construct and return the call to __check(). - let call: ExprSyntax = "Testing.\(expandedFunctionName)(\(LabeledExprListSyntax(checkArguments)))" - if isThrowing { - return "\(call).__required()" + var call: ExprSyntax = "Testing.\(expandedFunctionName)(\(LabeledExprListSyntax(checkArguments)))" + call = if isThrowing { + "\(call).__required()" + } else { + "\(call).__expected()" + } + if effectKeywordsToApply.contains(.await) { + call = "await \(call)" + } + if !isThrowing && effectKeywordsToApply.contains(.try) { + call = "try \(call)" } - return "\(call).__expected()" + return call } /// Get the complete argument list for a given macro, including any trailing @@ -395,7 +456,7 @@ extension ExitTestConditionMacro { decls.append( """ @Sendable func \(bodyThunkName)() async throws -> Void { - return try await Testing.__requiringTry(Testing.__requiringAwait(\(bodyArgumentExpr.trimmed)))() + return \(applyEffectfulKeywords([.try, .await], to: bodyArgumentExpr))() } """ ) @@ -423,8 +484,7 @@ extension ExitTestConditionMacro { arguments[trailingClosureIndex].expression = ExprSyntax( ClosureExprSyntax { for decl in decls { - CodeBlockItemSyntax(item: .decl(decl)) - .with(\.trailingTrivia, .newline) + decl.with(\.trailingTrivia, .newline) } } ) diff --git a/Sources/TestingMacros/Support/Additions/SyntaxProtocolAdditions.swift b/Sources/TestingMacros/Support/Additions/SyntaxProtocolAdditions.swift new file mode 100644 index 000000000..0a56ac4ff --- /dev/null +++ b/Sources/TestingMacros/Support/Additions/SyntaxProtocolAdditions.swift @@ -0,0 +1,78 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +import SwiftSyntax + +extension SyntaxProtocol { + /// Get an expression representing the unique ID of this syntax node as well + /// as those of its parent nodes. + /// + /// - Parameters: + /// - effectiveRootNode: The node to treat as the root of the syntax tree + /// for the purposes of generating a value. + /// + /// - Returns: An expression representing a bitmask of node IDs including this + /// node's and all ancestors (up to but excluding `effectiveRootNode`) + /// encoded as an instance of `String`. + func expressionID(rootedAt effectiveRootNode: some SyntaxProtocol) -> ExprSyntax { + // Construct the unique chain of node IDs that leads to the node being + // rewritten. + var nodeIDChain = sequence(first: Syntax(self), next: \.parent) + .map { $0.id.indexInTree.toOpaque() } + +#if DEBUG + assert(nodeIDChain.sorted() == nodeIDChain.reversed(), "Child node had lower ID than parent node in sequence \(nodeIDChain). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + for id in nodeIDChain { + assert(id <= UInt32.max, "Node ID \(id) was not a 32-bit integer. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + } +#endif + + // The highest ID in the chain determines the number of bits needed, and the + // ID of this node will always be the highest (per the assertion above.) + let maxID = id.indexInTree.toOpaque() +#if DEBUG + assert(nodeIDChain.contains(maxID), "ID \(maxID) of syntax node '\(self.trimmed)' was not found in its node ID chain \(nodeIDChain). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") +#endif + + // Adjust all node IDs downards by the effective root node's ID, then remove + // the effective root node and its ancestors. This allows us to use lower + // bit ranges than we would if we always included those nodes. + do { + let effRootNodeID = effectiveRootNode.id.indexInTree.toOpaque() + if let effRootNodeIndex = nodeIDChain.lastIndex(of: effRootNodeID) { + nodeIDChain = nodeIDChain[.. (ExprSyntax, isParenthetical: Bool)? { +private func _negatedExpression(_ expr: ExprSyntax) -> ExprSyntax? { let expr = removeParentheses(from: expr) ?? expr if let op = expr.as(PrefixOperatorExprSyntax.self), op.operator.tokenKind == .prefixOperator("!") { if let negatedExpr = removeParentheses(from: op.expression) { - return (negatedExpr, true) + return negatedExpr } else { - return (op.expression, false) + return op.expression } } @@ -118,7 +80,7 @@ private func _negatedExpression(_ expr: ExprSyntax) -> (ExprSyntax, isParentheti /// not remove interior parentheses (e.g. `(foo, (bar))`.) func removeParentheses(from expr: ExprSyntax) -> ExprSyntax? { if let tuple = expr.as(TupleExprSyntax.self), - tuple.elements.count == 1, + tuple.elements.count == 1, let elementExpr = tuple.elements.first, elementExpr.label == nil { return removeParentheses(from: elementExpr.expression) ?? elementExpr.expression @@ -127,406 +89,626 @@ func removeParentheses(from expr: ExprSyntax) -> ExprSyntax? { return nil } -// MARK: - +// MARK: - Inserting expression context callouts -/// Parse a condition argument from a binary operation expression. -/// -/// - Parameters: -/// - expr: The expression to which `lhs` _et al._ belong. -/// - lhs: The left-hand operand expression. -/// - op: The operator expression. -/// - rhs: The right-hand operand expression. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -/// -/// This function currently only recognizes and converts simple binary operator -/// expressions. More complex expressions are treated as monolithic. -private func _parseCondition(from expr: ExprSyntax, leftOperand lhs: ExprSyntax, operator op: BinaryOperatorExprSyntax, rightOperand rhs: ExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - return Condition( - "__checkBinaryOperation", - arguments: [ - Argument(expression: lhs), - Argument(expression: "{ $0 \(op.trimmed) $1() }"), - Argument(expression: rhs) - ], - expression: createExpressionExprForBinaryOperation(lhs, op, rhs) - ) -} +/// A type that inserts calls to an `__ExpectationContext` instance into an +/// expression's syntax tree. +private final class _ContextInserter: SyntaxRewriter where C: MacroExpansionContext, M: FreestandingMacroExpansionSyntax { + /// The macro context in which the expression is being parsed. + var context: C -/// Parse a condition argument from an `is` expression. -/// -/// - Parameters: -/// - expr: The `is` expression. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(from expr: IsExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - let expression = expr.expression - let type = expr.type - - return Condition( - "__checkCast", - arguments: [ - Argument(expression: expression), - Argument(label: .identifier("is"), expression: "(\(type.trimmed)).self") - ], - expression: createExpressionExprForBinaryOperation(expression, expr.isKeyword, type) - ) -} + /// The macro expression. + var macro: M -/// Parse a condition argument from an `as?` expression. -/// -/// - Parameters: -/// - expr: The `as?` expression. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(from expr: AsExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - let expression = expr.expression - let type = expr.type - - switch expr.questionOrExclamationMark?.tokenKind { - case .postfixQuestionMark: - return Condition( - "__checkCast", - arguments: [ - Argument(expression: expression), - Argument(label: .identifier("as"), expression: "(\(type.trimmed)).self") - ], - expression: createExpressionExprForBinaryOperation(expression, TokenSyntax.unknown("as?"), type) - ) + /// The node to treat as the root node when expanding expressions. + var effectiveRootNode: Syntax - case .exclamationMark where !type.isNamed("Bool", inModuleNamed: "Swift") && !type.isOptional: - // Warn that as! will be evaluated before #expect() or #require(), which is - // probably not what the developer intended. We suppress the warning for - // casts to Bool and casts to optional types. Presumably such casts are not - // being performed for their optional-unwrapping behavior, but because the - // developer knows the type of the expression better than we do. - context.diagnose(.asExclamationMarkIsEvaluatedEarly(expr, in: macro)) - - default: - // Only diagnose for `x as! T`. `x as T` is perfectly fine if it otherwise - // compiles. For example, `#require(x as Int?)` should compile. - // - // If the token after "as" is something else entirely and got through the - // type checker, just leave it alone as we don't recognize it. - break + /// The name of the instance of `__ExpectationContext` to call. + var expressionContextNameExpr: DeclReferenceExprSyntax + + /// A list of any syntax nodes that have been rewritten. + /// + /// The nodes in this array are the _original_ nodes, not the rewritten nodes. + var rewrittenNodes = Set() + + /// Any postflight code the caller should insert into the closure containing + /// the rewritten syntax tree. + var teardownItems = [CodeBlockItemSyntax]() + + init(in context: C, for macro: M, rootedAt effectiveRootNode: Syntax, expressionContextName: TokenSyntax) { + self.context = context + self.macro = macro + self.effectiveRootNode = effectiveRootNode + self.expressionContextNameExpr = DeclReferenceExprSyntax(baseName: expressionContextName.trimmed) + super.init() } - return Condition(expression: expr) -} + /// Rewrite a given syntax node by inserting a call to the expression context + /// (or rather, its `callAsFunction(_:_:)` member). + /// + /// - Parameters: + /// - node: The node to rewrite. + /// - originalNode: The original node in the original syntax tree, if `node` + /// has already been partially rewritten or substituted. If `node` has not + /// been rewritten, this argument should equal it. + /// - functionName: If not `nil`, the name of the function to call (as a + /// member function of the expression context.) + /// - additionalArguments: Any additional arguments to pass to the function. + /// + /// - Returns: A rewritten copy of `node` that calls into the expression + /// context when it is evaluated at runtime. + private func _rewrite(_ node: E, originalWas originalNode: some SyntaxProtocol, calling functionName: TokenSyntax? = nil, passing additionalArguments: [Argument] = []) -> ExprSyntax where E: ExprSyntaxProtocol { + guard rewrittenNodes.insert(Syntax(originalNode)).inserted else { + // If this node has already been rewritten, we don't need to rewrite it + // again. (Currently, this can only happen when expanding binary operators + // which need a bit of extra help.) + return ExprSyntax(node) + } -/// Parse a condition argument from a closure expression. -/// -/// - Parameters: -/// - expr: The closure expression. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(from expr: ClosureExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - if expr.signature == nil && expr.statements.count == 1, let item = expr.statements.first?.item { - // TODO: capture closures as a different kind of Testing.Expression with a - // separate subexpression per code item. + let calledExpr: ExprSyntax = if let functionName { + ExprSyntax(MemberAccessExprSyntax(base: expressionContextNameExpr, name: functionName)) + } else { + ExprSyntax(expressionContextNameExpr) + } - // If a closure contains a single statement or declaration, we can't - // meaningfully break it down as an expression, but we can still capture its - // source representation. - return Condition(expression: expr, expressionNode: Syntax(item)) + var result = FunctionCallExprSyntax(calledExpression: calledExpr) { + LabeledExprSyntax(expression: node.trimmed) + LabeledExprSyntax(expression: originalNode.expressionID(rootedAt: effectiveRootNode)) + for argument in additionalArguments { + LabeledExprSyntax(argument) + } + } + + result.leftParen = .leftParenToken() + result.rightParen = .rightParenToken() + result.leadingTrivia = originalNode.leadingTrivia + result.trailingTrivia = originalNode.trailingTrivia + + // If the resulting expression has an optional type due to containing an + // optional chaining expression (e.g. `foo?`) *and* its immediate parent + // node passes through the syntactical effects of optional chaining, return + // it as optional-chained so that it parses correctly post-expansion. + switch node.parent?.kind { + case .memberAccessExpr, .subscriptCallExpr: + let optionalChainFinder = _OptionalChainFinder(viewMode: .sourceAccurate) + optionalChainFinder.walk(node) + if optionalChainFinder.optionalChainFound { + return ExprSyntax(OptionalChainingExprSyntax(expression: result)) + } + + default: + break + } + + return ExprSyntax(result) } - return Condition(expression: expr) -} + /// Rewrite a given syntax node by inserting a call to the expression context + /// (or rather, its `callAsFunction(_:_:)` member). + /// + /// - Parameters: + /// - node: The node to rewrite. + /// - functionName: If not `nil`, the name of the function to call (as a + /// member function of the expression context.) + /// - additionalArguments: Any additional arguments to pass to the function. + /// + /// - Returns: A rewritten copy of `node` that calls into the expression + /// context when it is evaluated at runtime. + /// + /// This function is equivalent to `_rewrite(node, originalWas: node)`. + private func _rewrite(_ node: E, calling functionName: TokenSyntax? = nil, passing additionalArguments: [Argument] = []) -> ExprSyntax where E: ExprSyntaxProtocol { + _rewrite(node, originalWas: node, calling: functionName, passing: additionalArguments) + } -/// A class that walks a syntax tree looking for optional chaining expressions -/// such as `a?.b.c`. -private final class _OptionalChainFinder: SyntaxVisitor { - /// Whether or not any optional chaining was found. - var optionalChainFound = false + /// Whether or not the parent node of the given node is capable of containing + /// a rewritten `DeclReferenceExprSyntax` instance. + /// + /// - Parameters: + /// - node: The node that might be rewritten. It does not need to be an + /// instance of `DeclReferenceExprSyntax`. + /// + /// - Returns: Whether or not the _parent_ of `node` will still be + /// syntactically valid if `node` is rewritten with `_rewrite(_:)`. + /// + /// Instances of `DeclReferenceExprSyntax` are often present in positions + /// where it would be syntactically invalid to extract them out as function + /// arguments. This function is used to constrain the cases where we do so to + /// those we know (or generally know) are "safe". + private func _isParentOfDeclReferenceExprValidForRewriting(_ node: some SyntaxProtocol) -> Bool { + guard let parentNode = node.parent else { + return false + } - override func visit(_ node: OptionalChainingExprSyntax) -> SyntaxVisitorContinueKind { - optionalChainFound = true - return .skipChildren + switch parentNode.kind { + case .labeledExpr, .functionParameter, + .prefixOperatorExpr, .postfixOperatorExpr, .infixOperatorExpr, + .asExpr, .isExpr, .optionalChainingExpr, .forceUnwrapExpr, + .arrayElement, .dictionaryElement: + return true + default: + return false + } } -} -/// Extract the underlying expression from an optional-chained expression as -/// well as the number of question marks required to reach it. -/// -/// - Parameters: -/// - expr: The expression to examine, typically the `base` expression of a -/// `MemberAccessExprSyntax` instance. -/// -/// - Returns: A copy of `expr` with trailing question marks from optional -/// chaining removed, as well as a string containing the number of question -/// marks needed to access a member of `expr` after it has been assigned to -/// another variable. If `expr` does not contain any optional chaining, it is -/// returned verbatim along with the empty string. -/// -/// This function is used when expanding member accesses (either functions or -/// properties) that could contain optional chaining expressions such as -/// `foo?.bar()`. Since, in this case, `bar()` is ultimately going to be called -/// on a closure argument (i.e. `$0`), it is necessary to determine the number -/// of question mark characters needed to correctly construct that expression -/// and to capture the underlying expression of `foo?` without question marks so -/// that it remains syntactically correct when used without `bar()`. -private func _exprFromOptionalChainedExpr(_ expr: some ExprSyntaxProtocol) -> (ExprSyntax, questionMarks: String) { - let originalExpr = expr - var expr = ExprSyntax(expr) - var questionMarkCount = 0 - - while let optionalExpr = expr.as(OptionalChainingExprSyntax.self) { - // If the rightmost base expression is an optional-chained member access - // expression (e.g. "bar?" in the member access expression - // "foo.bar?.isQuux"), drop the question mark. - expr = optionalExpr.expression - questionMarkCount += 1 - } - - // If the rightmost expression is not itself optional-chained, check if any of - // the member accesses in the expression use optional chaining and, if one - // does, ensure we preserve optional chaining in the macro expansion. - if questionMarkCount == 0 { - let optionalChainFinder = _OptionalChainFinder(viewMode: .sourceAccurate) - optionalChainFinder.walk(originalExpr) - if optionalChainFinder.optionalChainFound { - questionMarkCount = 1 + override func visit(_ node: DeclReferenceExprSyntax) -> ExprSyntax { + // DeclReferenceExprSyntax is used for operator tokens in identifier + // position. These generally appear when an operator function is passed to + // a higher-order function (e.g. `sort(by: <)`) and also for the unbounded + // range expression (`...`). Both are uninteresting to the testing library + // and can be dropped. + if node.baseName.isOperator { + return ExprSyntax(node) + } + + if _isParentOfDeclReferenceExprValidForRewriting(node) { + return _rewrite(node) } + + // SPECIAL CASE: If this node is the base expression of a member access + // expression, and that member access expression is the called expression of + // a function, it is generally safe to extract out (but may need `.self` + // added to the end.) + // + // Module names are an exception to this rule as they cannot be referred to + // directly in source. So for instance, the following expression will be + // expanded incorrectly: + // + // #expect(Testing.foo(bar)) + // + // These sorts of expressions are relatively rare, so we'll allow the bug + // for the sake of better diagnostics in the common case. + if let memberAccessExpr = node.parent?.as(MemberAccessExprSyntax.self), + ExprSyntax(node) == memberAccessExpr.base, + let functionCallExpr = memberAccessExpr.parent?.as(FunctionCallExprSyntax.self), + ExprSyntax(memberAccessExpr) == functionCallExpr.calledExpression { + return _rewrite( + MemberAccessExprSyntax( + base: node.trimmed, + declName: DeclReferenceExprSyntax(baseName: .keyword(.self)) + ), + originalWas: node + ) + } + + return ExprSyntax(node) } - let questionMarks = String(repeating: "?", count: questionMarkCount) + override func visit(_ node: TupleExprSyntax) -> ExprSyntax { + // We are conservative when descending into tuples because they could be + // tuple _types_ rather than _values_ (e.g. `(Int, Double)`) but those + // cannot be distinguished with syntax alone. + if _isParentOfDeclReferenceExprValidForRewriting(node) { + return _rewrite( + TupleExprSyntax { + for element in node.elements { + visit(element).trimmed + } + }, + originalWas: node + ) + } - return (expr, questionMarks) -} + return ExprSyntax(node) + } -/// Parse a condition argument from a member function call. -/// -/// - Parameters: -/// - expr: The function call expression. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(from expr: FunctionCallExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - // We do not support function calls with trailing closures because the - // transform required to forward them requires more information than is - // available solely from the syntax tree. - if expr.trailingClosure != nil { - return Condition(expression: expr) - } - - // We also do not support expansion of closure invocations as they are - // diagnostically uninteresting. - if expr.calledExpression.is(ClosureExprSyntax.self) { - return Condition(expression: expr) - } - - let memberAccessExpr = expr.calledExpression.as(MemberAccessExprSyntax.self) - let functionName = memberAccessExpr.map(\.declName.baseName).map(Syntax.init) ?? Syntax(expr.calledExpression) - let argumentList = expr.arguments.map(Argument.init) - - let inOutArguments: [InOutExprSyntax] = argumentList.lazy - .map(\.expression) - .compactMap({ $0.as(InOutExprSyntax.self) }) - if inOutArguments.count > 1 { - // There is more than one inout argument present. This requires that the - // corresponding __check() function support variadic generics, but there is - // a compiler bug preventing us from implementing variadic inout support. - return Condition(expression: expr) - } else if inOutArguments.count != 0 && inOutArguments.count != argumentList.count { - // There is a mix of inout and normal arguments. That's not feasible for - // us to support here, so back out. - return Condition(expression: expr) - } - - // Which __check() function are we calling? - let expandedFunctionName = inOutArguments.isEmpty ? "__checkFunctionCall" : "__checkInoutFunctionCall" - - let indexedArguments = argumentList.lazy - .enumerated() - .map { index, argument in - if argument.expression.is(InOutExprSyntax.self) { - return Argument(label: argument.label, expression: "&$\(raw: index + 1)") - } - return Argument(label: argument.label, expression: "$\(raw: index + 1)") + override func visit(_ node: MemberAccessExprSyntax) -> ExprSyntax { + if case .keyword = node.declName.baseName.tokenKind { + // Likely something like Foo.self or X.Type, which we can't reasonably + // break down further. + return ExprSyntax(node) } - let forwardedArguments = argumentList.lazy - .map(\.expression) - .map { Argument(expression: $0) } - - var baseExprForExpression: ExprSyntax? - var conditionArguments = [Argument]() - if let memberAccessExpr, var baseExpr = memberAccessExpr.base { - let questionMarks: String - (baseExpr, questionMarks) = _exprFromOptionalChainedExpr(baseExpr) - baseExprForExpression = baseExpr - - conditionArguments.append(Argument(expression: "\(baseExpr.trimmed).self")) // BUG: rdar://113152370 - conditionArguments.append( - Argument( - label: "calling", - expression: """ - { - $0\(raw: questionMarks).\(functionName.trimmed)(\(LabeledExprListSyntax(indexedArguments))) - } - """ + + // As with decl reference expressions, only certain kinds of member access + // expressions can be directly extracted out. + if _isParentOfDeclReferenceExprValidForRewriting(node) { + return _rewrite( + node.with(\.base, node.base.map(visit)), + originalWas: node ) + } + + return ExprSyntax(node.with(\.base, node.base.map(visit))) + } + + override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax { + _rewrite( + node + .with(\.calledExpression, visit(node.calledExpression)) + .with(\.arguments, visit(node.arguments)), + originalWas: node ) - } else { - // Substitute an empty tuple for the self argument, and call the function - // directly (without having to reorder the numbered closure arguments.) If - // the function takes zero arguments, we'll also need to suppress $0 in the - // closure body since it is unused. - let parameterList = forwardedArguments.isEmpty ? "_ in" : "" - conditionArguments.append(Argument(expression: "()")) - - // If memberAccessExpr is not nil here, that means it had a nil base - // expression (i.e. the base is inferred.) - var dot: TokenSyntax? - if memberAccessExpr != nil { - dot = .periodToken() + } + + override func visit(_ node: SubscriptCallExprSyntax) -> ExprSyntax { + _rewrite( + node + .with(\.calledExpression, visit(node.calledExpression)) + .with(\.arguments, visit(node.arguments)), + originalWas: node + ) + } + + override func visit(_ node: ClosureExprSyntax) -> ExprSyntax { + // We do not (currently) attempt to descent into closures. + ExprSyntax(node) + } + + override func visit(_ node: MacroExpansionExprSyntax) -> ExprSyntax { + // We do not attempt to descent into freestanding macros. + ExprSyntax(node) + } + + override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax { + // We do not (currently) attempt to descent into functions. + DeclSyntax(node) + } + + // MARK: - Operators + + override func visit(_ node: PrefixOperatorExprSyntax) -> ExprSyntax { + // Special-case negative number literals as a single expression. + if node.expression.is(IntegerLiteralExprSyntax.self) || node.expression.is(FloatLiteralExprSyntax.self) { + if node.operator.tokenKind == .prefixOperator("-") { + return ExprSyntax(node) + } } - conditionArguments.append( - Argument( - label: "calling", - expression: """ - { \(raw: parameterList) - \(dot)\(functionName.trimmed)(\(LabeledExprListSyntax(indexedArguments))) - } - """ + return _rewrite( + node + .with(\.expression, visit(node.expression)), + originalWas: node + ) + } + + override func visit(_ node: InfixOperatorExprSyntax) -> ExprSyntax { + if let op = node.operator.as(BinaryOperatorExprSyntax.self)?.operator.textWithoutBackticks, + op == "==" || op == "!=" || op == "===" || op == "!==" { + + return _rewrite( + ClosureExprSyntax { + InfixOperatorExprSyntax( + leftOperand: DeclReferenceExprSyntax(baseName: .dollarIdentifier("$0")) + .with(\.trailingTrivia, .space), + operator: BinaryOperatorExprSyntax(text: op), + rightOperand: DeclReferenceExprSyntax(baseName: .dollarIdentifier("$1")) + .with(\.leadingTrivia, .space) + ) + }, + originalWas: node, + calling: .identifier("__cmp"), + passing: [ + Argument(expression: visit(node.leftOperand)), + Argument(expression: node.leftOperand.expressionID(rootedAt: effectiveRootNode)), + Argument(expression: visit(node.rightOperand)), + Argument(expression: node.rightOperand.expressionID(rootedAt: effectiveRootNode)) + ] ) + } + + return _rewrite( + node + .with(\.leftOperand, visit(node.leftOperand)) + .with(\.rightOperand, visit(node.rightOperand)), + originalWas: node ) } - conditionArguments += forwardedArguments - return Condition( - expandedFunctionName, - arguments: conditionArguments, - expression: createExpressionExprForFunctionCall(baseExprForExpression, functionName, argumentList) - ) -} -/// Parse a condition argument from a property access. -/// -/// - Parameters: -/// - expr: The member access expression. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(from expr: MemberAccessExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - // Only handle member access expressions where the base expression is known - // and where there are no argument names (which would otherwise indicate a - // reference to a member function which wouldn't resolve to anything useful at - // runtime.) - guard var baseExpr = expr.base, expr.declName.argumentNames == nil else { - return Condition(expression: expr) - } - - let questionMarks: String - (baseExpr, questionMarks) = _exprFromOptionalChainedExpr(baseExpr) - - return Condition( - "__checkPropertyAccess", - arguments: [ - Argument(expression: "\(baseExpr.trimmed).self"), - Argument(label: "getting", expression: "{ $0\(raw: questionMarks).\(expr.declName.baseName) }") - ], - expression: createExpressionExprForPropertyAccess(baseExpr, expr.declName) - ) -} + override func visit(_ node: InOutExprSyntax) -> ExprSyntax { + // Swift's Law of Exclusivity means that only one subexpression in the + // expectation ought to be interacting with `value` when it is passed + // `inout`, so it should be sufficient to capture it in a `defer` statement + // that runs after the expression is evaluated. -/// Parse a condition argument from a property access. -/// -/// - Parameters: -/// - expr: The expression that was negated. -/// - isParenthetical: Whether or not `expression` was enclosed in -/// parentheses (and the `!` operator was outside it.) This argument -/// affects how this expression is represented as a string. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(negating expr: ExprSyntax, isParenthetical: Bool, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - var result = _parseCondition(from: expr, for: macro, in: context) - result.expression = createExpressionExprForNegation(of: result.expression, isParenthetical: isParenthetical) - return result -} + let teardownItem = CodeBlockItemSyntax( + item: .expr( + _rewrite(node.expression, calling: .identifier("__inoutAfter")) + ) + ) + teardownItems.append(teardownItem) -/// Parse a condition argument from an arbitrary expression. -/// -/// - Parameters: -/// - expr: The condition expression to parse. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(from expr: ExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - // Handle closures with a single expression in them (e.g. { $0.foo() }) - if let closureExpr = expr.as(ClosureExprSyntax.self) { - return _parseCondition(from: closureExpr, for: macro, in: context) + // The argument should not be expanded in-place as we can't return an + // argument passed `inout` and expect it to remain semantically correct. + return ExprSyntax(node) } - // If the condition involves the `try` or `await` keywords, assume we cannot - // expand it. This check cannot handle expressions like - // `try #expect(a.b(c))` where `b()` is throwing because the `try` keyword is - // outside the macro expansion. SEE: rdar://109470248 - let containsTryOrAwait = expr.tokens(viewMode: .sourceAccurate).lazy - .map(\.tokenKind) - .contains { $0 == .keyword(.try) || $0 == .keyword(.await) } - if containsTryOrAwait { - return Condition(expression: expr) + // MARK: - Casts + + /// Rewrite an `is` or `as?` cast. + /// + /// - Parameters: + /// - valueExpr: The expression to cast. + /// - isAsKeyword: The casting keyword (either `.is` or `.as`). + /// - type: The type to cast `valueExpr` to. + /// - originalNode: The original `IsExprSyntax` or `AsExprSyntax` node in + /// the original syntax tree. + /// + /// - Returns: A function call expression equivalent to the described cast. + private func _rewriteAsCast(_ valueExpr: ExprSyntax, _ isAsKeyword: Keyword, _ type: TypeSyntax, originalWas originalNode: some SyntaxProtocol) -> ExprSyntax { + rewrittenNodes.insert(Syntax(type)) + + return _rewrite( + visit(valueExpr).trimmed, + originalWas: originalNode, + calling: .identifier("__\(isAsKeyword)"), + passing: [ + Argument( + expression: MemberAccessExprSyntax( + base: TupleExprSyntax { + LabeledExprSyntax(expression: TypeExprSyntax(type: type.trimmed)) + }, + declName: DeclReferenceExprSyntax(baseName: .keyword(.self)) + ) + ), + Argument(expression: type.expressionID(rootedAt: effectiveRootNode)) + ] + ) } - if let infixOperator = expr.as(InfixOperatorExprSyntax.self), - let op = infixOperator.operator.as(BinaryOperatorExprSyntax.self) { - return _parseCondition(from: expr, leftOperand: infixOperator.leftOperand, operator: op, rightOperand: infixOperator.rightOperand, for: macro, in: context) + override func visit(_ node: AsExprSyntax) -> ExprSyntax { + switch node.questionOrExclamationMark?.tokenKind { + case .postfixQuestionMark: + return _rewriteAsCast(node.expression, .as, node.type, originalWas: node) + + case .exclamationMark where !node.type.isNamed("Bool", inModuleNamed: "Swift") && !node.type.isOptional: + // Warn that as! will be evaluated before #expect() or #require(), which is + // probably not what the developer intended. We suppress the warning for + // casts to Bool and casts to optional types. Presumably such casts are not + // being performed for their optional-unwrapping behavior, but because the + // developer knows the type of the expression better than we do. + context.diagnose(.asExclamationMarkIsEvaluatedEarly(node, in: macro)) + return _rewrite(node) + + case .exclamationMark: + // Only diagnose for `x as! T`. `x as T` is perfectly fine if it otherwise + // compiles. For example, `#require(x as Int?)` should compile. + return _rewrite(node) + + default: + // This is an "escape hatch" cast. Do not attempt to process the cast. + return ExprSyntax(node) + } + } + + override func visit(_ node: IsExprSyntax) -> ExprSyntax { + _rewriteAsCast(node.expression, .is, node.type, originalWas: node) } - // Handle `is` and `as?` expressions. - if let isExpr = expr.as(IsExprSyntax.self) { - return _parseCondition(from: isExpr, for: macro, in: context) - } else if let asExpr = expr.as(AsExprSyntax.self) { - return _parseCondition(from: asExpr, for: macro, in: context) + // MARK: - Literals + + override func visit(_ node: BooleanLiteralExprSyntax) -> ExprSyntax { + // Contrary to the comment immediately below this function, we *do* rewrite + // boolean literals so that expressions like `#expect(true)` are expanded. + _rewrite(node) } - // Handle function calls and member accesses. - if let functionCallExpr = expr.as(FunctionCallExprSyntax.self) { - return _parseCondition(from: functionCallExpr, for: macro, in: context) - } else if let memberAccessExpr = expr.as(MemberAccessExprSyntax.self) { - return _parseCondition(from: memberAccessExpr, for: macro, in: context) + // We don't currently rewrite numeric/string/array/dictionary literals. We + // could, but it's unclear what the benefit would be and it could seriously + // impact type checker time. + +#if SWT_DELVE_INTO_LITERALS + override func visit(_ node: IntegerLiteralExprSyntax) -> ExprSyntax { + _rewrite(node) } - // Handle negation. - if let negatedExpr = _negatedExpression(expr) { - return _parseCondition(negating: negatedExpr.0, isParenthetical: negatedExpr.isParenthetical, for: macro, in: context) + override func visit(_ node: FloatLiteralExprSyntax) -> ExprSyntax { + _rewrite(node) } - // Parentheses are parsed as if they were tuples, so (true && false) appears - // to the parser as a tuple containing one expression, `true && false`. - if let expr = removeParentheses(from: expr) { - return _parseCondition(from: expr, for: macro, in: context) + override func visit(_ node: StringLiteralExprSyntax) -> ExprSyntax { + _rewrite(node) } - return Condition(expression: expr) + override func visit(_ node: ArrayExprSyntax) -> ExprSyntax { + _rewrite( + node.with( + \.elements, ArrayElementListSyntax { + for element in node.elements { + ArrayElementSyntax(expression: visit(element.expression).trimmed) + } + } + ), + originalWas: node + ) + } + + override func visit(_ node: DictionaryExprSyntax) -> ExprSyntax { + guard case let .elements(elements) = node.content else { + return ExprSyntax(node) + } + return _rewrite( + node.with( + \.content, .elements( + DictionaryElementListSyntax { + for element in elements { + DictionaryElementSyntax(key: visit(element.key).trimmed, value: visit(element.value).trimmed) + } + } + ) + ), + originalWas: node + ) + } +#else + override func visit(_ node: ArrayExprSyntax) -> ExprSyntax { + return ExprSyntax(node) + } + + override func visit(_ node: DictionaryExprSyntax) -> ExprSyntax { + return ExprSyntax(node) + } +#endif +} + +extension ConditionMacro { + /// Rewrite and expand upon an expression node. + /// + /// - Parameters: + /// - node: The root of a syntax tree to rewrite. This node may not itself + /// be the root of the overall syntax tree—it's just the root of the + /// subtree that we're rewriting. + /// - expressionContextName: The name of the instance of + /// `__ExpectationContext` to call at runtime. + /// - macro: The macro expression. + /// - effectiveRootNode: The node to treat as the root of the syntax tree + /// for the purposes of generating expression ID values. + /// - effectKeywordsToApply: The set of effect keywords in the expanded + /// expression or its lexical context that may apply to `node`. + /// - context: The macro context in which the expression is being parsed. + /// + /// - Returns: A tuple containing the rewritten copy of `node`, a list of all + /// the nodes within `node` (possibly including `node` itself) that were + /// rewritten, and a code block containing code that should be inserted into + /// the lexical scope of `node` _before_ its rewritten equivalent. + static func rewrite( + _ node: some ExprSyntaxProtocol, + usingExpressionContextNamed expressionContextName: TokenSyntax, + for macro: some FreestandingMacroExpansionSyntax, + rootedAt effectiveRootNode: some SyntaxProtocol, + effectKeywordsToApply: Set, + in context: some MacroExpansionContext + ) -> (ClosureExprSyntax, rewrittenNodes: Set) { + _diagnoseTrivialBooleanValue(from: ExprSyntax(node), for: macro, in: context) + + let contextInserter = _ContextInserter(in: context, for: macro, rootedAt: Syntax(effectiveRootNode), expressionContextName: expressionContextName) + var expandedExpr = contextInserter.rewrite(node).cast(ExprSyntax.self) + let rewrittenNodes = contextInserter.rewrittenNodes + + // Insert additional effect keywords/thunks as needed. + var effectKeywordsToApply = effectKeywordsToApply + if isThrowing { + effectKeywordsToApply.insert(.try) + } + expandedExpr = applyEffectfulKeywords(effectKeywordsToApply, to: expandedExpr) + + // Construct the body of the closure that we'll pass to the expanded + // function. + var codeBlockItems = CodeBlockItemListSyntax { + if contextInserter.teardownItems.isEmpty { + expandedExpr.with(\.trailingTrivia, .newline) + } else { + // Insert a defer statement that runs any teardown items. + DeferStmtSyntax { + for teardownItem in contextInserter.teardownItems { + teardownItem.with(\.trailingTrivia, .newline) + } + }.with(\.trailingTrivia, .newline) + + // If we're inserting any additional code into the closure before + // the rewritten argument, we can't elide the return keyword. + ReturnStmtSyntax( + expression: expandedExpr.with(\.leadingTrivia, .space) + ).with(\.trailingTrivia, .newline) + } + } + + // Replace any dollar identifiers in the code block, then construct a + // capture list for the closure (if needed.) + var captureList: ClosureCaptureClauseSyntax? + do { + let dollarIDReplacer = _DollarIdentifierReplacer() + codeBlockItems = dollarIDReplacer.rewrite(codeBlockItems).cast(CodeBlockItemListSyntax.self) + if !dollarIDReplacer.dollarIdentifierTokenKinds.isEmpty { + let dollarIdentifierTokens = dollarIDReplacer.dollarIdentifierTokenKinds.map { tokenKind in + TokenSyntax(tokenKind, presence: .present) + } + captureList = ClosureCaptureClauseSyntax { + for token in dollarIdentifierTokens { + ClosureCaptureSyntax(name: _rewriteDollarIdentifier(token), expression: DeclReferenceExprSyntax(baseName: token)) + } + } + } + } + + // Enclose the code block in the final closure. + let closureExpr = ClosureExprSyntax( + signature: ClosureSignatureSyntax( + capture: captureList, + parameterClause: .parameterClause( + ClosureParameterClauseSyntax( + parameters: ClosureParameterListSyntax { + ClosureParameterSyntax( + firstName: expressionContextName, + colon: .colonToken().with(\.trailingTrivia, .space), + type: TypeSyntax( + AttributedTypeSyntax( + specifiers: [ + TypeSpecifierListSyntax.Element( + SimpleTypeSpecifierSyntax(specifier: .keyword(.inout)) + .with(\.trailingTrivia, .space) + ) + ], + baseType: MemberTypeSyntax( + baseType: IdentifierTypeSyntax(name: .identifier("Testing")), + name: .identifier("__ExpectationContext") + ) + ) + ) + ) + } + ) + ), + inKeyword: .keyword(.in) + .with(\.leadingTrivia, .space) + .with(\.trailingTrivia, .newline) + ), + statements: codeBlockItems + ) + + return (closureExpr, rewrittenNodes) + } +} + +// MARK: - Finding optional chains + +/// A class that walks a syntax tree looking for optional chaining expressions +/// such as `a?.b.c`. +private final class _OptionalChainFinder: SyntaxVisitor { + /// Whether or not any optional chaining was found. + var optionalChainFound = false + + override func visit(_ node: OptionalChainingExprSyntax) -> SyntaxVisitorContinueKind { + optionalChainFound = true + return .skipChildren + } } -// MARK: - +// MARK: - Replacing dollar identifiers -/// Parse a condition argument from an arbitrary expression. +/// Rewrite a dollar identifier as a normal (non-dollar) identifier. /// /// - Parameters: -/// - expr: The condition expression to parse. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. +/// - token: The dollar identifier token to rewrite. /// -/// - Returns: An instance of ``Condition`` describing `expr`. -func parseCondition(from expr: ExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - _diagnoseTrivialBooleanValue(from: expr, for: macro, in: context) - let result = _parseCondition(from: expr, for: macro, in: context) +/// - Returns: A copy of `token` as an identifier token. +private func _rewriteDollarIdentifier(_ token: TokenSyntax) -> TokenSyntax { + var result = TokenSyntax.identifier("__renamedCapture__\(token.trimmedDescription)") + + result.leadingTrivia = token.leadingTrivia + result.trailingTrivia = token.trailingTrivia + return result } + +/// A syntax rewriter that replaces _numeric_ dollar identifiers (e.g. `$0`) +/// with normal (non-dollar) identifiers. +private final class _DollarIdentifierReplacer: SyntaxRewriter { + /// The `tokenKind` properties of any dollar identifier tokens that have been + /// rewritten. + var dollarIdentifierTokenKinds = Set() + + override func visit(_ node: TokenSyntax) -> TokenSyntax { + if case let .dollarIdentifier(id) = node.tokenKind, id.dropFirst().allSatisfy(\.isWholeNumber) { + // This dollar identifier is numeric, so it's a closure argument. + dollarIdentifierTokenKinds.insert(node.tokenKind) + return _rewriteDollarIdentifier(node) + } + + return node + } + + override func visit(_ node: ClosureExprSyntax) -> ExprSyntax { + // Do not recurse into closure expressions because they will have their own + // argument lists that won't conflict with the enclosing scope's. + return ExprSyntax(node) + } +} diff --git a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift new file mode 100644 index 000000000..1fa8678d8 --- /dev/null +++ b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift @@ -0,0 +1,140 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +import SwiftSyntax + +// MARK: - Finding effect keywords + +/// A syntax visitor class that looks for effectful keywords in a given +/// expression. +private final class _EffectFinder: SyntaxAnyVisitor { + /// The effect keywords discovered so far. + var effectKeywords: Set = [] + + override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { + switch node.kind { + case .tryExpr: + effectKeywords.insert(.try) + case .awaitExpr: + effectKeywords.insert(.await) + case .consumeExpr: + effectKeywords.insert(.consume) + case .closureExpr, .functionDecl: + // Do not delve into closures or function declarations. + return .skipChildren + case .variableDecl: + // Delve into variable declarations. + return .visitChildren + default: + // Do not delve into declarations other than variables. + if node.isProtocol((any DeclSyntaxProtocol).self) { + return .skipChildren + } + } + + // Recurse into everything else. + return .visitChildren + } +} + +/// Find effectful keywords in a syntax node. +/// +/// - Parameters: +/// - node: The node to inspect. +/// +/// - Returns: A set of effectful keywords such as `await` that are present in +/// `node`. +/// +/// This function does not descend into function declarations or closure +/// expressions because they represent distinct lexical contexts and their +/// effects are uninteresting in the context of `node` unless they are called. +func findEffectKeywords(in node: some SyntaxProtocol) -> Set { + let effectFinder = _EffectFinder(viewMode: .sourceAccurate) + effectFinder.walk(node) + return effectFinder.effectKeywords +} + +// MARK: - Inserting effect keywords/thunks + +/// Make a function call expression to an effectful thunk function provided by +/// the testing library. +/// +/// - Parameters: +/// - thunkName: The unqualified name of the thunk function to call. This +/// token must be the name of a function in the `Testing` module. +/// - expr: The expression to thunk. +/// +/// - Returns: An expression representing a call to the function named +/// `thunkName`, passing `expr`. +private func _makeCallToEffectfulThunk(_ thunkName: TokenSyntax, passing expr: some ExprSyntaxProtocol) -> ExprSyntax { + var result = FunctionCallExprSyntax( + calledExpression: MemberAccessExprSyntax( + base: DeclReferenceExprSyntax(baseName: .identifier("Testing")), + declName: DeclReferenceExprSyntax(baseName: thunkName) + ) + ) { + LabeledExprSyntax(expression: expr.trimmed) + } + + result.leftParen = .leftParenToken() + result.rightParen = .rightParenToken() + + return ExprSyntax(result) +} + +/// Apply the given effectful keywords (i.e. `try` and `await`) to an expression +/// using thunk functions provided by the testing library. +/// +/// - Parameters: +/// - effectfulKeywords: The effectful keywords to apply. +/// - expr: The expression to apply the keywords and thunk functions to. +/// +/// - Returns: A copy of `expr` if no changes are needed, or an expression that +/// adds the keywords in `effectfulKeywords` to `expr`. +func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some ExprSyntaxProtocol) -> ExprSyntax { + let originalExpr = expr + var expr = ExprSyntax(expr) + + let needAwait = effectfulKeywords.contains(.await) && !expr.is(AwaitExprSyntax.self) + let needTry = effectfulKeywords.contains(.try) && !expr.is(TryExprSyntax.self) + + // First, add thunk function calls. + if needAwait { + expr = _makeCallToEffectfulThunk(.identifier("__requiringAwait"), passing: expr) + } + if needTry { + expr = _makeCallToEffectfulThunk(.identifier("__requiringTry"), passing: expr) + } + + // Then add keyword expressions. (We do this separately so we end up writing + // `try await __r(__r(self))` instead of `try __r(await __r(self))` which + // is less accepted by the compiler.) + if needAwait { + expr = ExprSyntax( + AwaitExprSyntax( + awaitKeyword: .keyword(.await).with(\.trailingTrivia, .space), + expression: expr + ) + ) + } + if needTry { + expr = ExprSyntax( + TryExprSyntax( + tryKeyword: .keyword(.try).with(\.trailingTrivia, .space), + expression: expr + ) + ) + } + + expr.leadingTrivia = originalExpr.leadingTrivia + expr.trailingTrivia = originalExpr.trailingTrivia + + return expr +} diff --git a/Sources/TestingMacros/Support/SourceCodeCapturing.swift b/Sources/TestingMacros/Support/SourceCodeCapturing.swift deleted file mode 100644 index 9fd687e8f..000000000 --- a/Sources/TestingMacros/Support/SourceCodeCapturing.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2023 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for Swift project authors -// - -import SwiftSyntax - -/// Get a swift-syntax expression initializing an instance of `__Expression` -/// from an arbitrary syntax node. -/// -/// - Parameters: -/// - node: A syntax node from which to construct an instance of -/// `__Expression`. -/// -/// - Returns: An expression value that initializes an instance of -/// `__Expression` for the specified syntax node. -func createExpressionExpr(from node: any SyntaxProtocol) -> ExprSyntax { - if let stringLiteralExpr = node.as(StringLiteralExprSyntax.self), - let stringValue = stringLiteralExpr.representedLiteralValue { - return ".__fromStringLiteral(\(literal: node.trimmedDescription), \(literal: stringValue))" - } - return ".__fromSyntaxNode(\(literal: node.trimmedDescription))" -} - -/// Get a swift-syntax expression initializing an instance of `__Expression` -/// from an arbitrary sequence of syntax nodes representing a binary operation. -/// -/// - Parameters: -/// - lhs: The left-hand operand. -/// - operator: The operator. -/// - rhs: The right-hand operand. -/// -/// - Returns: An expression value that initializes an instance of -/// `__Expression` for the specified syntax nodes. -func createExpressionExprForBinaryOperation(_ lhs: some SyntaxProtocol, _ `operator`: some SyntaxProtocol, _ rhs: some SyntaxProtocol) -> ExprSyntax { - let arguments = LabeledExprListSyntax { - LabeledExprSyntax(expression: createExpressionExpr(from: lhs)) - LabeledExprSyntax(expression: StringLiteralExprSyntax(content: `operator`.trimmedDescription)) - LabeledExprSyntax(expression: createExpressionExpr(from: rhs)) - } - - return ".__fromBinaryOperation(\(arguments))" -} - -/// Get a swift-syntax expression initializing an instance of `__Expression` -/// from an arbitrary sequence of syntax nodes representing a function call. -/// -/// - Parameters: -/// - value: The value on which the member function is being called, if any. -/// - functionName: The name of the member function being called. -/// - arguments: The arguments to the member function. -/// -/// - Returns: An expression value that initializes an instance of -/// `__Expression` for the specified syntax nodes. -func createExpressionExprForFunctionCall(_ value: (any SyntaxProtocol)?, _ functionName: some SyntaxProtocol, _ arguments: some Sequence) -> ExprSyntax { - let arguments = LabeledExprListSyntax { - if let value { - LabeledExprSyntax(expression: createExpressionExpr(from: value)) - } else { - LabeledExprSyntax(expression: NilLiteralExprSyntax()) - } - LabeledExprSyntax(expression: StringLiteralExprSyntax(content: functionName.trimmedDescription)) - for argument in arguments { - LabeledExprSyntax(expression: TupleExprSyntax { - if let argumentLabel = argument.label { - LabeledExprSyntax(expression: StringLiteralExprSyntax(content: argumentLabel.trimmedDescription)) - } else { - LabeledExprSyntax(expression: NilLiteralExprSyntax()) - } - LabeledExprSyntax(expression: createExpressionExpr(from: argument.expression)) - }) - } - } - - return ".__fromFunctionCall(\(arguments))" -} - -/// Get a swift-syntax expression initializing an instance of `__Expression` -/// from an arbitrary sequence of syntax nodes representing a property access. -/// -/// - Parameters: -/// - value: The value on which the property is being accessed, if any. -/// - keyPath: The name of the property being accessed. -/// -/// - Returns: An expression value that initializes an instance of -/// `__Expression` for the specified syntax nodes. -func createExpressionExprForPropertyAccess(_ value: ExprSyntax, _ keyPath: DeclReferenceExprSyntax) -> ExprSyntax { - let arguments = LabeledExprListSyntax { - LabeledExprSyntax(expression: createExpressionExpr(from: value)) - LabeledExprSyntax(expression: createExpressionExpr(from: keyPath.baseName)) - } - - return ".__fromPropertyAccess(\(arguments))" -} - -/// Get a swift-syntax expression initializing an instance of `__Expression` -/// from an arbitrary sequence of syntax nodes representing the negation of -/// another expression. -/// -/// - Parameters: -/// - expression: An expression representing a previously-initialized instance -/// of `__Expression` (that is, not the expression in source, but the result -/// of a call to ``createExpressionExpr(from:)`` etc.) -/// - isParenthetical: Whether or not `expression` was enclosed in -/// parentheses (and the `!` operator was outside it.) This argument -/// affects how this expression is represented as a string. -/// -/// - Returns: An expression value that initializes an instance of -/// `__Expression` for the specified syntax nodes. -func createExpressionExprForNegation(of expression: ExprSyntax, isParenthetical: Bool) -> ExprSyntax { - ".__fromNegation(\(expression.trimmed), \(literal: isParenthetical))" -} diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 463412d2a..3f1b206bf 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -235,17 +235,17 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { // detecting isolation to other global actors. lazy var isMainActorIsolated = !functionDecl.attributes(named: "MainActor", inModuleNamed: "_Concurrency").isEmpty var forwardCall: (ExprSyntax) -> ExprSyntax = { - "try await Testing.__requiringTry(Testing.__requiringAwait(\($0)))" + applyEffectfulKeywords([.try, .await], to: $0) } let forwardInit = forwardCall if functionDecl.noasyncAttribute != nil { if isMainActorIsolated { forwardCall = { - "try await MainActor.run { try Testing.__requiringTry(\($0)) }" + "try await MainActor.run { \(applyEffectfulKeywords([.try], to: $0)) }" } } else { forwardCall = { - "try { try Testing.__requiringTry(\($0)) }()" + "try { \(applyEffectfulKeywords([.try], to: $0)) }()" } } } diff --git a/Sources/_TestingInternals/include/TestSupport.h b/Sources/_TestingInternals/include/TestSupport.h index 37d42692e..c0f913ba8 100644 --- a/Sources/_TestingInternals/include/TestSupport.h +++ b/Sources/_TestingInternals/include/TestSupport.h @@ -37,6 +37,10 @@ static inline bool swt_pointersNotEqual4(const char *a, const char *b, const cha return a != b && b != c && c != d; } +static inline bool swt_nullableCString(const char *_Nullable string) { + return string != 0; +} + SWT_ASSUME_NONNULL_END #endif diff --git a/Tests/SubexpressionShowcase/SubexpressionShowcase.swift b/Tests/SubexpressionShowcase/SubexpressionShowcase.swift new file mode 100644 index 000000000..16ca169e2 --- /dev/null +++ b/Tests/SubexpressionShowcase/SubexpressionShowcase.swift @@ -0,0 +1,117 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +import Testing + +#warning("TODO: convert this scratchpad into actual unit tests") + +func f(_ x: Int, _ y: Int) -> Int { + x + y +} + +func g() throws -> Int { + 22 +} + +func io(_ x: inout Int) -> Int { + x += 1 + return x + 1 +} + +struct T { + func h(_ i: Int) -> Bool { false } + static func j(_ d: Double) -> Bool { false } +} + +@Test func runSubexpressionShowcase() async { + await withKnownIssue { + try await subexpressionShowcase() + } +} + +func subexpressionShowcase() async throws { + #expect(false || true) + #expect((Int)(123) == 124) + #expect((Int, Double)(123, 456.0) == (124, 457.0)) + #expect((123, 456) == (789, 0x12)) + #expect((try g() > 500) && true) + + do { + let n = Int.random(in: 0 ..< 100) + var m = n + #expect(io(&m) == n) + } + + let closure: (Int) -> Void = { + #expect((($0 + $0 + $0) as Int) == 0x10) + } + closure(11) + + struct S: ~Copyable { + borrowing func h() -> Bool { false } + consuming func j() -> Bool { false } + } +#if false + // Unsupported: move-only types have too many constraints that cannot be + // resolved by inspecting syntax. Borrowing calls cannot be boxed (at least + // until we get @lifetime) and the compiler forbids making consuming calls in + // a closure in case the closure gets called multiple times. + // + // A workaround is to explicitly write `consume` on an expression (or the + // unsupported `_borrow`) which will tell our macros code not to try expanding + // the expression. + let s = S() + #expect(s.h()) + #expect(s.j()) // consuming -- this DOES still fail, no syntax-level way to tell +#endif + + let s2 = S() + _ = try #require(.some(consume s2)) + + let t = T() + #expect(t.h(.random(in: 0 ..< 100))) + #expect(T.j(.greatestFiniteMagnitude)) + #expect(SubexpressionShowcase.T.j(.greatestFiniteMagnitude)) + + + let x = 9 + let y = 11 + #expect(x == y) + #expect(try f(x, y) == g()) + + let z: Int? = nil + let zDelta = 1 + #expect(z?.advanced(by: zDelta) != nil) + + let v: String? = nil + #expect(v?[...] != nil) + #expect(v?[...].first != nil) + + func k(_ x: @autoclosure () -> Bool) async -> Bool { + x() + } + #expect(await k(true)) + +#if false + // Unsupported: __ec is necessarily inout and captures non-sendable state, so + // this will fail to compile. Making __ec a class instead is possible, but + // adds a very large amount of code and locking overhead for what we can + // assume is an edge case. + func m(_ x: @autoclosure @Sendable () -> Bool) -> Bool { + x() + } + #expect(m(123 == 456)) +#endif + + _ = try #require(.some(Int32.Magnitude(1))) + + let n = 1 as Any + _ = try #require(n as? String) +} diff --git a/Tests/TestingMacrosTests/ConditionMacroTests.swift b/Tests/TestingMacrosTests/ConditionMacroTests.swift index 7ede6233c..31e43e0d3 100644 --- a/Tests/TestingMacrosTests/ConditionMacroTests.swift +++ b/Tests/TestingMacrosTests/ConditionMacroTests.swift @@ -23,183 +23,186 @@ struct ConditionMacroTests { @Test("#expect() macro", arguments: [ ##"#expect(true)"##: - ##"Testing.__checkValue(true, expression: .__fromSyntaxNode("true"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(true, "") }, sourceCode: ["": "true"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(false)"##: - ##"Testing.__checkValue(false, expression: .__fromSyntaxNode("false"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(false, "") }, sourceCode: ["": "false"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(false, "Custom message")"##: - ##"Testing.__checkValue(false, expression: .__fromSyntaxNode("false"), comments: ["Custom message"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(false, "") }, sourceCode: ["": "false"], comments: ["Custom message"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(2 > 1)"##: - ##"Testing.__checkBinaryOperation(2, { $0 > $1() }, 1, expression: .__fromBinaryOperation(.__fromSyntaxNode("2"), ">", .__fromSyntaxNode("1")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(2 > 1, "") }, sourceCode: ["": "2 > 1"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(((true || false) && true) || Bool.random())"##: - ##"Testing.__checkBinaryOperation(((true || false) && true), { $0 || $1() }, Bool.random(), expression: .__fromBinaryOperation(.__fromSyntaxNode("((true || false) && true)"), "||", .__fromSyntaxNode("Bool.random()")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(__ec((__ec(__ec((__ec(__ec(true, "f7a") || __ec(false, "877a"), "77a")), "7a") && __ec(true, "10003a"), "3a")), "2") || __ec(__ec(Bool.self, "e000000").random(), "2000000"), "") }, sourceCode: ["": "((true || false) && true) || Bool.random()", "2": "((true || false) && true)", "3a": "(true || false) && true", "7a": "(true || false)", "77a": "true || false", "f7a": "true", "877a": "false", "10003a": "true", "2000000": "Bool.random()", "e000000": "Bool"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(9 > 8 && 7 > 6, "Some comment")"##: - ##"Testing.__checkBinaryOperation(9 > 8, { $0 && $1() }, 7 > 6, expression: .__fromBinaryOperation(.__fromSyntaxNode("9 > 8"), "&&", .__fromSyntaxNode("7 > 6")), comments: ["Some comment"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(__ec(9 > 8, "2") && __ec(7 > 6, "400"), "") }, sourceCode: ["": "9 > 8 && 7 > 6", "2": "9 > 8", "400": "7 > 6"], comments: ["Some comment"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect("a" == "b")"##: - ##"Testing.__checkBinaryOperation("a", { $0 == $1() }, "b", expression: .__fromBinaryOperation(.__fromStringLiteral(#""a""#, "a"), "==", .__fromStringLiteral(#""b""#, "b")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec.__cmp({ $0 == $1 }, "", "a", "2", "b", "200") }, sourceCode: ["": #""a" == "b""#], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(!Bool.random())"##: - ##"Testing.__checkFunctionCall(Bool.self, calling: { $0.random() }, expression: .__fromNegation(.__fromFunctionCall(.__fromSyntaxNode("Bool"), "random"), false), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(!__ec(__ec(Bool.self, "1c").random(), "4"), "") }, sourceCode: ["": "!Bool.random()", "4": "Bool.random()", "1c": "Bool"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect((true && false))"##: - ##"Testing.__checkBinaryOperation(true, { $0 && $1() }, false, expression: .__fromBinaryOperation(.__fromSyntaxNode("true"), "&&", .__fromSyntaxNode("false")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec((__ec(__ec(true, "3c") && __ec(false, "21c"), "1c")), "") }, sourceCode: ["": "(true && false)", "1c": "true && false", "3c": "true", "21c": "false"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(try x())"##: - ##"Testing.__checkValue(try x(), expression: .__fromSyntaxNode("try x()"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"try Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try __ec(x(), "4") }, sourceCode: ["4": "x()"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(1 is Int)"##: - ##"Testing.__checkCast(1, is: (Int).self, expression: .__fromBinaryOperation(.__fromSyntaxNode("1"), "is", .__fromSyntaxNode("Int")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec.__is(1, "", (Int).self, "10") }, sourceCode: ["": "1 is Int", "10": "Int"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect("123") { 1 == 2 } then: { foo() }"##: - ##"Testing.__checkClosureCall(performing: { 1 == 2 }, then: { foo() }, expression: .__fromSyntaxNode("1 == 2"), comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkClosureCall(performing: { 1 == 2 }, then: { foo() }, sourceCode: "1 == 2", comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect("123") { let x = 0 }"##: - ##"Testing.__checkClosureCall(performing: { let x = 0 }, expression: .__fromSyntaxNode("let x = 0"), comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkClosureCall(performing: { let x = 0 }, sourceCode: "let x = 0", comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect("123") { let x = 0; return x == 0 }"##: - ##"Testing.__checkClosureCall(performing: { let x = 0; return x == 0 }, expression: .__fromSyntaxNode("{ let x = 0; return x == 0 }"), comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkClosureCall(performing: { let x = 0; return x == 0 }, sourceCode: "{ let x = 0; return x == 0 }", comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a, "b", c: c)"##: - ##"Testing.__checkValue(a, c: c, expression: .__fromSyntaxNode("a"), comments: ["b"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(a, "") }, sourceCode: ["": "a"], c: c, comments: ["b"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a())"##: - ##"Testing.__checkFunctionCall((), calling: { _ in a() }, expression: .__fromFunctionCall(nil, "a"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(a(), "") }, sourceCode: ["": "a()"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(b(c))"##: - ##"Testing.__checkFunctionCall((), calling: { b($1) }, c, expression: .__fromFunctionCall(nil, "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(b(__ec(c, "70")), "") }, sourceCode: ["": "b(c)", "70": "c"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b(c))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0.b($1) }, c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(__ec(a.self, "6").b(__ec(c, "700")), "") }, sourceCode: ["": "a.b(c)", "6": "a", "700": "c"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b(c, d: e))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0.b($1, d: $2) }, c, e, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c")), ("d", .__fromSyntaxNode("e"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(__ec(a.self, "6").b(__ec(c, "700"), d: __ec(e, "12100")), "") }, sourceCode: ["": "a.b(c, d: e)", "6": "a", "700": "c", "12100": "e"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b(&c))"##: - ##"Testing.__checkInoutFunctionCall(a.self, calling: { $0.b(&$1) }, &c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("&c"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, - ##"#expect(a.b(&c, &d))"##: - ##"Testing.__checkValue(a.b(&c, &d), expression: .__fromSyntaxNode("a.b(&c, &d)"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in defer { __ec.__inoutAfter(c, "1700") } return __ec(__ec(a.self, "6").b(&c), "") }, sourceCode: ["": "a.b(&c)", "6": "a", "1700": "c"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"#expect(a.b(&c, &d.e))"##: + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in defer { __ec.__inoutAfter(c, "1700") __ec.__inoutAfter(d.e, "58100") } return __ec(__ec(a.self, "6").b(&c, &d.e), "") }, sourceCode: ["": "a.b(&c, &d.e)", "6": "a", "1700": "c", "58100": "d.e"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b(&c, d))"##: - ##"Testing.__checkValue(a.b(&c, d), expression: .__fromSyntaxNode("a.b(&c, d)"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in defer { __ec.__inoutAfter(c, "1700") } return __ec(__ec(a.self, "6").b(&c, __ec(d, "18100")), "") }, sourceCode: ["": "a.b(&c, d)", "6": "a", "1700": "c", "18100": "d"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b(try c()))"##: - ##"Testing.__checkValue(a.b(try c()), expression: .__fromSyntaxNode("a.b(try c())"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"try Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec(a.self, "6").b(try __ec(c(), "1700")), "")) }, sourceCode: ["": "a.b(try c())", "6": "a", "1700": "c()"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a?.b(c))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0?.b($1) }, c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(__ec(a, "e")?.b(__ec(c, "1c00")), "") }, sourceCode: ["": "a?.b(c)", "e": "a", "1c00": "c"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a???.b(c))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0???.b($1) }, c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(__ec(a, "3e")???.b(__ec(c, "1c000")), "") }, sourceCode: ["": "a???.b(c)", "3e": "a", "1c000": "c"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a?.b.c(d))"##: - ##"Testing.__checkFunctionCall(a?.b.self, calling: { $0?.c($1) }, d, expression: .__fromFunctionCall(.__fromSyntaxNode("a?.b"), "c", (nil, .__fromSyntaxNode("d"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(__ec(a, "1e")?.b.c(__ec(d, "1c000")), "") }, sourceCode: ["": "a?.b.c(d)", "1e": "a", "1c000": "d"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect({}())"##: - ##"Testing.__checkValue({}(), expression: .__fromSyntaxNode("{}()"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec({}(), "") }, sourceCode: ["": "{}()"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b(c: d))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0.b(c: $1) }, d, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", ("c", .__fromSyntaxNode("d"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(__ec(a.self, "6").b(c: __ec(d, "1300")), "") }, sourceCode: ["": "a.b(c: d)", "6": "a", "1300": "d"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b { c })"##: - ##"Testing.__checkValue(a.b { c }, expression: .__fromSyntaxNode("a.b { c }"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(__ec(a.self, "6").b { c }, "") }, sourceCode: ["": "a.b { c }", "6": "a"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a, sourceLocation: someValue)"##: - ##"Testing.__checkValue(a, expression: .__fromSyntaxNode("a"), comments: [], isRequired: false, sourceLocation: someValue).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(a, "") }, sourceCode: ["": "a"], comments: [], isRequired: false, sourceLocation: someValue).__expected()"##, ##"#expect(a.isB)"##: - ##"Testing.__checkPropertyAccess(a.self, getting: { $0.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(a.isB, "") }, sourceCode: ["": "a.isB"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a???.isB)"##: - ##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(__ec(a, "1e")???.isB, "") }, sourceCode: ["": "a???.isB", "1e": "a"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a?.b.isB)"##: - ##"Testing.__checkPropertyAccess(a?.b.self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(__ec(a, "e")?.b.isB, "") }, sourceCode: ["": "a?.b.isB", "e": "a"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a?.b().isB)"##: - ##"Testing.__checkPropertyAccess(a?.b().self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b()"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in __ec(__ec(__ec(a, "1e")?.b(), "2")?.isB, "") }, sourceCode: ["": "a?.b().isB", "2": "a?.b()", "1e": "a"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(isolation: somewhere) {}"##: - ##"Testing.__checkClosureCall(performing: {}, expression: .__fromSyntaxNode("{}"), comments: [], isRequired: false, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkClosureCall(performing: {}, sourceCode: "{}", comments: [], isRequired: false, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ] ) func expectMacro(input: String, expectedOutput: String) throws { let (expectedOutput, _) = try parse(expectedOutput, removeWhitespace: true) let (actualOutput, _) = try parse(input, removeWhitespace: true) - #expect(expectedOutput == actualOutput) + let (actualActual, _) = try parse(input) + #expect(expectedOutput == actualOutput, "\(actualActual)") } @Test("#require() macro", arguments: [ ##"#require(true)"##: - ##"Testing.__checkValue(true, expression: .__fromSyntaxNode("true"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(true, "")) }, sourceCode: ["": "true"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(false)"##: - ##"Testing.__checkValue(false, expression: .__fromSyntaxNode("false"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(false, "")) }, sourceCode: ["": "false"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(false, "Custom message")"##: - ##"Testing.__checkValue(false, expression: .__fromSyntaxNode("false"), comments: ["Custom message"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(false, "")) }, sourceCode: ["": "false"], comments: ["Custom message"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(2 > 1)"##: - ##"Testing.__checkBinaryOperation(2, { $0 > $1() }, 1, expression: .__fromBinaryOperation(.__fromSyntaxNode("2"), ">", .__fromSyntaxNode("1")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(2 > 1, "")) }, sourceCode: ["": "2 > 1"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(((true || false) && true) || Bool.random())"##: - ##"Testing.__checkBinaryOperation(((true || false) && true), { $0 || $1() }, Bool.random(), expression: .__fromBinaryOperation(.__fromSyntaxNode("((true || false) && true)"), "||", .__fromSyntaxNode("Bool.random()")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec((__ec(__ec((__ec(__ec(true, "f7a") || __ec(false, "877a"), "77a")), "7a") && __ec(true, "10003a"), "3a")), "2") || __ec(__ec(Bool.self, "e000000").random(), "2000000"), "")) }, sourceCode: ["": "((true || false) && true) || Bool.random()", "2": "((true || false) && true)", "3a": "(true || false) && true", "7a": "(true || false)", "77a": "true || false", "f7a": "true", "877a": "false", "10003a": "true", "2000000": "Bool.random()", "e000000": "Bool"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(9 > 8 && 7 > 6, "Some comment")"##: - ##"Testing.__checkBinaryOperation(9 > 8, { $0 && $1() }, 7 > 6, expression: .__fromBinaryOperation(.__fromSyntaxNode("9 > 8"), "&&", .__fromSyntaxNode("7 > 6")), comments: ["Some comment"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec(9 > 8, "2") && __ec(7 > 6, "400"), "")) }, sourceCode: ["": "9 > 8 && 7 > 6", "2": "9 > 8", "400": "7 > 6"], comments: ["Some comment"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require("a" == "b")"##: - ##"Testing.__checkBinaryOperation("a", { $0 == $1() }, "b", expression: .__fromBinaryOperation(.__fromStringLiteral(#""a""#, "a"), "==", .__fromStringLiteral(#""b""#, "b")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec.__cmp({ $0 == $1 }, "", "a", "2", "b", "200")) }, sourceCode: ["": #""a" == "b""#], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(!Bool.random())"##: - ##"Testing.__checkFunctionCall(Bool.self, calling: { $0.random() }, expression: .__fromNegation(.__fromFunctionCall(.__fromSyntaxNode("Bool"), "random"), false), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(!__ec(__ec(Bool.self, "1c").random(), "4"), "")) }, sourceCode: ["": "!Bool.random()", "4": "Bool.random()", "1c": "Bool"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require((true && false))"##: - ##"Testing.__checkBinaryOperation(true, { $0 && $1() }, false, expression: .__fromBinaryOperation(.__fromSyntaxNode("true"), "&&", .__fromSyntaxNode("false")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec((__ec(__ec(true, "3c") && __ec(false, "21c"), "1c")), "")) }, sourceCode: ["": "(true && false)", "1c": "true && false", "3c": "true", "21c": "false"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(try x())"##: - ##"Testing.__checkValue(try x(), expression: .__fromSyntaxNode("try x()"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try __ec(x(), "4") }, sourceCode: ["4": "x()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(1 is Int)"##: - ##"Testing.__checkCast(1, is: (Int).self, expression: .__fromBinaryOperation(.__fromSyntaxNode("1"), "is", .__fromSyntaxNode("Int")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec.__is(1, "", (Int).self, "10")) }, sourceCode: ["": "1 is Int", "10": "Int"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require("123") { 1 == 2 } then: { foo() }"##: - ##"Testing.__checkClosureCall(performing: { 1 == 2 }, then: { foo() }, expression: .__fromSyntaxNode("1 == 2"), comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkClosureCall(performing: { 1 == 2 }, then: { foo() }, sourceCode: "1 == 2", comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require("123") { let x = 0 }"##: - ##"Testing.__checkClosureCall(performing: { let x = 0 }, expression: .__fromSyntaxNode("let x = 0"), comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkClosureCall(performing: { let x = 0 }, sourceCode: "let x = 0", comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require("123") { let x = 0; return x == 0 }"##: - ##"Testing.__checkClosureCall(performing: { let x = 0; return x == 0 }, expression: .__fromSyntaxNode("{ let x = 0; return x == 0 }"), comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkClosureCall(performing: { let x = 0; return x == 0 }, sourceCode: "{ let x = 0; return x == 0 }", comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a, "b", c: c)"##: - ##"Testing.__checkValue(a, c: c, expression: .__fromSyntaxNode("a"), comments: ["b"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(a, "")) }, sourceCode: ["": "a"], c: c, comments: ["b"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a())"##: - ##"Testing.__checkFunctionCall((), calling: { _ in a() }, expression: .__fromFunctionCall(nil, "a"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(a(), "")) }, sourceCode: ["": "a()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(b(c))"##: - ##"Testing.__checkFunctionCall((), calling: { b($1) }, c, expression: .__fromFunctionCall(nil, "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(b(__ec(c, "70")), "")) }, sourceCode: ["": "b(c)", "70": "c"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b(c))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0.b($1) }, c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec(a.self, "6").b(__ec(c, "700")), "")) }, sourceCode: ["": "a.b(c)", "6": "a", "700": "c"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b(c, d: e))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0.b($1, d: $2) }, c, e, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c")), ("d", .__fromSyntaxNode("e"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec(a.self, "6").b(__ec(c, "700"), d: __ec(e, "12100")), "")) }, sourceCode: ["": "a.b(c, d: e)", "6": "a", "700": "c", "12100": "e"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b(&c))"##: - ##"Testing.__checkInoutFunctionCall(a.self, calling: { $0.b(&$1) }, &c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("&c"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, - ##"#require(a.b(&c, &d))"##: - ##"Testing.__checkValue(a.b(&c, &d), expression: .__fromSyntaxNode("a.b(&c, &d)"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in defer { __ec.__inoutAfter(c, "1700") } return try Testing.__requiringTry(__ec(__ec(a.self, "6").b(&c), "")) }, sourceCode: ["": "a.b(&c)", "6": "a", "1700": "c"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"#require(a.b(&c, &d.e))"##: + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in defer { __ec.__inoutAfter(c, "1700") __ec.__inoutAfter(d.e, "58100") } return try Testing.__requiringTry(__ec(__ec(a.self, "6").b(&c, &d.e), "")) }, sourceCode: ["": "a.b(&c, &d.e)", "6": "a", "1700": "c", "58100": "d.e"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b(&c, d))"##: - ##"Testing.__checkValue(a.b(&c, d), expression: .__fromSyntaxNode("a.b(&c, d)"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in defer { __ec.__inoutAfter(c, "1700") } return try Testing.__requiringTry(__ec(__ec(a.self, "6").b(&c, __ec(d, "18100")), "")) }, sourceCode: ["": "a.b(&c, d)", "6": "a", "1700": "c", "18100": "d"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b(try c()))"##: - ##"Testing.__checkValue(a.b(try c()), expression: .__fromSyntaxNode("a.b(try c())"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec(a.self, "6").b(try __ec(c(), "1700")), "")) }, sourceCode: ["": "a.b(try c())", "6": "a", "1700": "c()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a?.b(c))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0?.b($1) }, c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec(a, "e")?.b(__ec(c, "1c00")), "")) }, sourceCode: ["": "a?.b(c)", "e": "a", "1c00": "c"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a???.b(c))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0???.b($1) }, c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec(a, "3e")???.b(__ec(c, "1c000")), "")) }, sourceCode: ["": "a???.b(c)", "3e": "a", "1c000": "c"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a?.b.c(d))"##: - ##"Testing.__checkFunctionCall(a?.b.self, calling: { $0?.c($1) }, d, expression: .__fromFunctionCall(.__fromSyntaxNode("a?.b"), "c", (nil, .__fromSyntaxNode("d"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec(a, "1e")?.b.c(__ec(d, "1c000")), "")) }, sourceCode: ["": "a?.b.c(d)", "1e": "a", "1c000": "d"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require({}())"##: - ##"Testing.__checkValue({}(), expression: .__fromSyntaxNode("{}()"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec({}(), "")) }, sourceCode: ["": "{}()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b(c: d))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0.b(c: $1) }, d, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", ("c", .__fromSyntaxNode("d"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec(a.self, "6").b(c: __ec(d, "1300")), "")) }, sourceCode: ["": "a.b(c: d)", "6": "a", "1300": "d"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b { c })"##: - ##"Testing.__checkValue(a.b { c }, expression: .__fromSyntaxNode("a.b { c }"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec(a.self, "6").b { c }, "")) }, sourceCode: ["": "a.b { c }", "6": "a"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a, sourceLocation: someValue)"##: - ##"Testing.__checkValue(a, expression: .__fromSyntaxNode("a"), comments: [], isRequired: true, sourceLocation: someValue).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(a, "")) }, sourceCode: ["": "a"], comments: [], isRequired: true, sourceLocation: someValue).__required()"##, ##"#require(a.isB)"##: - ##"Testing.__checkPropertyAccess(a.self, getting: { $0.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(a.isB, "")) }, sourceCode: ["": "a.isB"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a???.isB)"##: - ##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec(a, "1e")???.isB, "")) }, sourceCode: ["": "a???.isB", "1e": "a"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a?.b.isB)"##: - ##"Testing.__checkPropertyAccess(a?.b.self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec(a, "e")?.b.isB, "")) }, sourceCode: ["": "a?.b.isB", "e": "a"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a?.b().isB)"##: - ##"Testing.__checkPropertyAccess(a?.b().self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b()"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(__ec(__ec(a, "1e")?.b(), "2")?.isB, "")) }, sourceCode: ["": "a?.b().isB", "2": "a?.b()", "1e": "a"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(isolation: somewhere) {}"##: - ##"Testing.__checkClosureCall(performing: {}, expression: .__fromSyntaxNode("{}"), comments: [], isRequired: true, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkClosureCall(performing: {}, sourceCode: "{}", comments: [], isRequired: true, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ] ) func requireMacro(input: String, expectedOutput: String) throws { let (expectedOutput, _) = try parse(expectedOutput, removeWhitespace: true) let (actualOutput, _) = try parse(input, removeWhitespace: true) - #expect(expectedOutput == actualOutput) + let (actualActual, _) = try parse(input) + #expect(expectedOutput == actualOutput, "\(actualActual)") } @Test("Unwrapping #require() macro", arguments: [ ##"#require(Optional.none)"##: - ##"Testing.__checkPropertyAccess(Optional.self, getting: { $0.none }, expression: .__fromPropertyAccess(.__fromSyntaxNode("Optional"), .__fromSyntaxNode("none")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(Optional.none, "")) }, sourceCode: ["": "Optional.none"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(nil ?? 123)"##: - ##"Testing.__checkBinaryOperation(nil, { $0 ?? $1() }, 123, expression: .__fromBinaryOperation(.__fromSyntaxNode("nil"), "??", .__fromSyntaxNode("123")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(nil ?? 123, "")) }, sourceCode: ["": "nil ?? 123"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(123 ?? nil)"##: - ##"Testing.__checkBinaryOperation(123, { $0 ?? $1() }, nil, expression: .__fromBinaryOperation(.__fromSyntaxNode("123"), "??", .__fromSyntaxNode("nil")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(123 ?? nil, "")) }, sourceCode: ["": "123 ?? nil"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(123 as? Double)"##: - ##"Testing.__checkCast(123,as: (Double).self, expression: .__fromBinaryOperation(.__fromSyntaxNode("123"), "as?", .__fromSyntaxNode("Double")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec.__as(123, "", (Double).self, "20")) }, sourceCode: ["": "123 as? Double", "20": "Double"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(123 as Double)"##: - ##"Testing.__checkValue(123 as Double, expression: .__fromSyntaxNode("123 as Double"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkEscapedCondition(123 as Double, sourceCode: "123 as Double", comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(123 as! Double)"##: - ##"Testing.__checkValue(123 as! Double, expression: .__fromSyntaxNode("123 as! Double"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try Testing.__requiringTry(__ec(123 as! Double, "")) }, sourceCode: ["": "123 as! Double"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ] ) func unwrappingRequireMacro(input: String, expectedOutput: String) throws { - let (expectedOutput, _) = try parse(expectedOutput) - let (actualOutput, _) = try parse(input) - #expect(expectedOutput == actualOutput) + let (expectedOutput, _) = try parse(expectedOutput, removeWhitespace: true) + let (actualOutput, _) = try parse(input, removeWhitespace: true) + let (actualActual, _) = try parse(input) + #expect(expectedOutput == actualOutput, "\(actualActual)") } @Test("Capturing comments above #expect()/#require()", @@ -212,7 +215,7 @@ struct ConditionMacroTests { """ // Source comment /** Doc comment */ - Testing.__checkValue(try x(), expression: .__fromSyntaxNode("try x()"), comments: [.__line("// Source comment"),.__documentationBlock("/** Doc comment */"),"Argument comment"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + try Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try __ec(x(), "4") }, sourceCode: ["4": "x()"], comments: [.__line("// Source comment"),.__documentationBlock("/** Doc comment */"),"Argument comment"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() """, """ @@ -225,7 +228,7 @@ struct ConditionMacroTests { // Ignore me // Capture me - Testing.__checkValue(try x(), expression: .__fromSyntaxNode("try x()"), comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + try Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try __ec(x(), "4") }, sourceCode: ["4": "x()"], comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() """, """ @@ -238,14 +241,15 @@ struct ConditionMacroTests { // Ignore me \t // Capture me - Testing.__checkValue(try x(), expression: .__fromSyntaxNode("try x()"), comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + try Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) in try __ec(x(), "4") }, sourceCode: ["4": "x()"], comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() """, ] ) func commentCapture(input: String, expectedOutput: String) throws { - let (expectedOutput, _) = try parse(expectedOutput) - let (actualOutput, _) = try parse(input) - #expect(expectedOutput == actualOutput) + let (expectedOutput, _) = try parse(expectedOutput, removeWhitespace: true) + let (actualOutput, _) = try parse(input, removeWhitespace: true) + let (actualActual, _) = try parse(input) + #expect(expectedOutput == actualOutput, "\(actualActual)") } @Test("#expect(false) and #require(false) warn they always fail", @@ -364,25 +368,4 @@ struct ConditionMacroTests { #expect(diagnostic.diagMessage.severity == .warning) #expect(diagnostic.message.contains("is redundant")) } - - @Test("Macro expansion is performed within a test function") - func macroExpansionInTestFunction() throws { - let input = ##""" - @Test("Random number generation") func rng() { - let number = Int.random(in: 1 ..< .max) - #expect((number > 0 && foo() != bar(at: 9)) != !true, "\(number) must be greater than 0") - } - """## - - let rawExpectedOutput = ##""" - @Test("Random number generation") func rng() { - let number = Int.random(in: 1 ..< .max) - Testing.__checkBinaryOperation((number > 0 && foo() != bar(at: 9)), { $0 != $1() }, !true, expression: .__fromBinaryOperation(.__fromSyntaxNode("(number > 0 && foo() != bar(at: 9))"), "!=", .__fromSyntaxNode("!true")), comments: ["\(number) must be greater than 0"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() - } - """## - - let (expectedOutput, _) = try parse(rawExpectedOutput, activeMacros: ["expect"], removeWhitespace: true) - let (actualOutput, _) = try parse(input, activeMacros: ["expect"], removeWhitespace: true) - #expect(expectedOutput == actualOutput) - } } diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 4013882a1..5ad8e254e 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -22,6 +22,12 @@ import CoreGraphics import UniformTypeIdentifiers #endif +extension Character { + var isPathSeparator: Bool { + self == "/" || self == #"\"# + } +} + @Suite("Attachment Tests") struct AttachmentTests { @Test func saveValue() { @@ -91,7 +97,7 @@ struct AttachmentTests { // Write the attachment to disk, then read it back. let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectory(), appending: suffixes.next()!) createdFilePaths.append(filePath) - let fileName = try #require(filePath.split { $0 == "/" || $0 == #"\"# }.last) + let fileName = try #require(filePath.split(whereSeparator: \.isPathSeparator).last) if i == 0 { #expect(fileName == baseFileName) } else { @@ -118,7 +124,7 @@ struct AttachmentTests { defer { remove(filePath) } - let fileName = try #require(filePath.split { $0 == "/" || $0 == #"\"# }.last) + let fileName = try #require(filePath.split(whereSeparator: \.isPathSeparator).last) #expect(fileName == "loremipsum-\(suffix).tgz.gif.jpeg.html") try compare(attachableValue, toContentsOfFileAtPath: filePath) } @@ -579,7 +585,7 @@ struct MySendableAttachable: Attachable, Sendable { var string: String func withUnsafeBufferPointer(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - #expect(attachment.attachableValue.string == string) + #expect(attachment.attachableValue.string == self.string) var string = string return try string.withUTF8 { buffer in try body(.init(buffer)) diff --git a/Tests/TestingTests/EventRecorderTests.swift b/Tests/TestingTests/EventRecorderTests.swift index 1922a7841..ae156f7ab 100644 --- a/Tests/TestingTests/EventRecorderTests.swift +++ b/Tests/TestingTests/EventRecorderTests.swift @@ -108,8 +108,8 @@ struct EventRecorderTests { await runTest(for: WrittenTests.self, configuration: configuration) let buffer = stream.buffer.rawValue - #expect(buffer.contains(#"\#(Event.Symbol.details.unicodeCharacter) "abc": Swift.String"#)) - #expect(buffer.contains(#"\#(Event.Symbol.details.unicodeCharacter) lhs: Swift.String → "987""#)) + #expect(buffer.contains(#"\#(Event.Symbol.details.unicodeCharacter) "abc" == "xyz": Swift.Bool → false"#)) + #expect(buffer.contains(#"\#(Event.Symbol.details.unicodeCharacter) lhs: Swift.String → "987""#)) #expect(buffer.contains(#""Animal Crackers" (aka 'WrittenTests')"#)) #expect(buffer.contains(#""Not A Lobster" (aka 'actuallyCrab()')"#)) diff --git a/Tests/TestingTests/EventTests.swift b/Tests/TestingTests/EventTests.swift index 941dcadb9..14eca5b84 100644 --- a/Tests/TestingTests/EventTests.swift +++ b/Tests/TestingTests/EventTests.swift @@ -21,7 +21,6 @@ struct EventTests { Expectation( evaluatedExpression: __Expression("SyntaxNode"), mismatchedErrorDescription: "Mismatched Error Description", - differenceDescription: "Difference Description", isPassing: false, isRequired: true, sourceLocation: SourceLocation(fileID: "M/f.swift", filePath: "/f.swift", line: 1, column: 1) diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 73fcfbb30..1c9d77c22 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -242,8 +242,8 @@ private import _TestingInternals func exitConditionMatching() { #expect(Optional.none == Optional.none) #expect(Optional.none === Optional.none) - #expect(Optional.none !== .success) - #expect(Optional.none !== .failure) + #expect(Optional.none !== .some(.success)) + #expect(Optional.none !== .some(.failure)) #expect(ExitCondition.success == .success) #expect(ExitCondition.success === .success) diff --git a/Tests/TestingTests/IssueTests.swift b/Tests/TestingTests/IssueTests.swift index 53fe92b84..ef1face12 100644 --- a/Tests/TestingTests/IssueTests.swift +++ b/Tests/TestingTests/IssueTests.swift @@ -14,6 +14,22 @@ private import _TestingInternals #if canImport(XCTest) import XCTest +func expression(_ expression: __Expression, contains string: String) -> Bool { + if expression.expandedDescription().contains(string) { + return true + } + + return expression.subexpressions.contains { TestingTests.expression($0, contains: string) } +} + +func assert(_ expression: __Expression, contains string: String) { + XCTAssertTrue(TestingTests.expression(expression, contains: string), "\(expression) did not contain \(string)") +} + +func assert(_ expression: __Expression, doesNotContain string: String) { + XCTAssertFalse(TestingTests.expression(expression, contains: string), "\(expression) did not contain \(string)") +} + final class IssueTests: XCTestCase { func testExpect() async throws { var configuration = Configuration() @@ -55,7 +71,7 @@ final class IssueTests: XCTestCase { }.run(configuration: configuration) await Test { () throws in - try #expect({ throw MyError() }()) + #expect(try { throw MyError() }()) }.run(configuration: configuration) } @@ -160,9 +176,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains("rhs → 1")) - XCTAssertFalse(desc.contains("((")) + assert(expectation.evaluatedExpression, contains: "TypeWithMemberFunctions.f(rhs) → false") + assert(expectation.evaluatedExpression, contains: "rhs → 1") } } @@ -184,9 +199,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains("label: rhs → 1")) - XCTAssertFalse(desc.contains("((")) + assert(expectation.evaluatedExpression, contains: "TypeWithMemberFunctions.g(label: rhs) → false") + assert(expectation.evaluatedExpression, contains: "rhs → 1") } } @@ -208,9 +222,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertFalse(desc.contains("(Function)")) - XCTAssertFalse(desc.contains("((")) + assert(expectation.evaluatedExpression, contains: "TypeWithMemberFunctions.h({ }) → false") + assert(expectation.evaluatedExpression, doesNotContain: "(Function)") } } @@ -276,9 +289,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - // The presence of `try` means we don't do complex expansion (yet.) XCTAssertNotNil(expectation.evaluatedExpression) - XCTAssertNil(expectation.evaluatedExpression.runtimeValue) + XCTAssertNotNil(expectation.evaluatedExpression.runtimeValue) } } @@ -318,7 +330,7 @@ final class IssueTests: XCTestCase { } func testExpressionLiterals() async { - func expectIssue(containing content: String, in testFunction: @escaping @Sendable () async throws -> Void) async { + func expectIssue(containing content: String..., in testFunction: @escaping @Sendable () async throws -> Void) async { let issueRecorded = expectation(description: "Issue recorded") var configuration = Configuration() @@ -328,8 +340,9 @@ final class IssueTests: XCTestCase { return } XCTAssertTrue(issue.comments.isEmpty) - let expandedExpressionDescription = expectation.evaluatedExpression.expandedDescription() - XCTAssert(expandedExpressionDescription.contains(content)) + for content in content { + assert(expectation.evaluatedExpression, contains: content) + } issueRecorded.fulfill() } @@ -340,13 +353,13 @@ final class IssueTests: XCTestCase { @Sendable func someInt() -> Int { 0 } @Sendable func someString() -> String { "a" } - await expectIssue(containing: "(someInt() → 0) == 1") { + await expectIssue(containing: "someInt() == 1 → false", "someInt() → 0") { #expect(someInt() == 1) } - await expectIssue(containing: "1 == (someInt() → 0)") { + await expectIssue(containing: "1 == someInt() → false", "someInt() → 0") { #expect(1 == someInt()) } - await expectIssue(containing: "(someString() → \"a\") == \"b\"") { + await expectIssue(containing: #"someString() == "b" → false"#, #"someString() → "a""#) { #expect(someString() == "b") } } @@ -354,12 +367,12 @@ final class IssueTests: XCTestCase { struct ExpressionRuntimeValueCapture_Value {} func testExpressionRuntimeValueCapture() throws { - var expression = __Expression.__fromSyntaxNode("abc123") + var expression = __Expression("abc123") XCTAssertEqual(expression.sourceCode, "abc123") XCTAssertNil(expression.runtimeValue) do { - expression = expression.capturingRuntimeValues(987 as Int) + expression.runtimeValue = __Expression.Value(reflecting: 987 as Int) XCTAssertEqual(expression.sourceCode, "abc123") let runtimeValue = try XCTUnwrap(expression.runtimeValue) XCTAssertEqual(String(describing: runtimeValue), "987") @@ -368,7 +381,7 @@ final class IssueTests: XCTestCase { } do { - expression = expression.capturingRuntimeValues(ExpressionRuntimeValueCapture_Value()) + expression.runtimeValue = __Expression.Value(reflecting: ExpressionRuntimeValueCapture_Value()) XCTAssertEqual(expression.sourceCode, "abc123") let runtimeValue = try XCTUnwrap(expression.runtimeValue) XCTAssertEqual(String(describing: runtimeValue), "ExpressionRuntimeValueCapture_Value()") @@ -377,7 +390,7 @@ final class IssueTests: XCTestCase { } do { - expression = expression.capturingRuntimeValues((123, "abc") as (Int, String), ()) + expression.runtimeValue = __Expression.Value(reflecting: (123, "abc") as (Int, String)) XCTAssertEqual(expression.sourceCode, "abc123") let runtimeValue = try XCTUnwrap(expression.runtimeValue) XCTAssertEqual(String(describing: runtimeValue), #"(123, "abc")"#) @@ -391,12 +404,12 @@ final class IssueTests: XCTestCase { } func testExpressionRuntimeValueChildren() throws { - var expression = __Expression.__fromSyntaxNode("abc123") + var expression = __Expression("abc123") XCTAssertEqual(expression.sourceCode, "abc123") XCTAssertNil(expression.runtimeValue) do { - expression = expression.capturingRuntimeValues(ExpressionRuntimeValueCapture_Value()) + expression.runtimeValue = __Expression.Value(reflecting: ExpressionRuntimeValueCapture_Value()) let runtimeValue = try XCTUnwrap(expression.runtimeValue) XCTAssertEqual(String(describing: runtimeValue), "ExpressionRuntimeValueCapture_Value()") XCTAssertEqual(runtimeValue.typeInfo.fullyQualifiedName, "TestingTests.IssueTests.ExpressionRuntimeValueCapture_Value") @@ -406,7 +419,7 @@ final class IssueTests: XCTestCase { } do { - expression = expression.capturingRuntimeValues(ExpressionRuntimeValueCapture_ValueWithChildren(contents: [123, "abc"])) + expression.runtimeValue = __Expression.Value(reflecting: ExpressionRuntimeValueCapture_ValueWithChildren(contents: [123, "abc"])) let runtimeValue = try XCTUnwrap(expression.runtimeValue) XCTAssertEqual(String(describing: runtimeValue), #"ExpressionRuntimeValueCapture_ValueWithChildren(contents: [123, "abc"])"#) XCTAssertEqual(runtimeValue.typeInfo.fullyQualifiedName, "TestingTests.IssueTests.ExpressionRuntimeValueCapture_ValueWithChildren") @@ -429,7 +442,7 @@ final class IssueTests: XCTestCase { } do { - expression = expression.capturingRuntimeValues([]) + expression.runtimeValue = __Expression.Value(reflecting: []) let runtimeValue = try XCTUnwrap(expression.runtimeValue) XCTAssertEqual(String(describing: runtimeValue), "[]") XCTAssertEqual(runtimeValue.typeInfo.fullyQualifiedName, "Swift.Array") @@ -455,9 +468,8 @@ final class IssueTests: XCTestCase { XCTFail("Unexpected issue kind \(issue.kind)") return } - let expandedExpressionDescription = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(expandedExpressionDescription.contains("someString() → \"abc123\"")) - XCTAssertTrue(expandedExpressionDescription.contains("Int → String")) + assert(expectation.evaluatedExpression, contains: #"someString() → "abc123""#) + assert(expectation.evaluatedExpression, contains: "Int → String") if expectation.isRequired { requireRecorded.fulfill() @@ -1062,15 +1074,14 @@ final class IssueTests: XCTestCase { } } - func testCollectionDifference() async { + func testCollectionDifference() async throws { var configuration = Configuration() configuration.eventHandler = { event, _ in guard case let .issueRecorded(issue) = event.kind else { return } guard case let .expectationFailed(expectation) = issue.kind else { - XCTFail("Unexpected issue kind \(issue.kind)") - return + return XCTFail("Unexpected issue kind \(issue.kind)") } guard let differenceDescription = expectation.differenceDescription else { return XCTFail("Unexpected nil differenceDescription") @@ -1086,7 +1097,28 @@ final class IssueTests: XCTestCase { }.run(configuration: configuration) } - func testCollectionDifferenceSkippedForStrings() async { + func testCollectionDifferenceForStrings() async throws { + var configuration = Configuration() + configuration.eventHandler = { event, _ in + guard case let .issueRecorded(issue) = event.kind else { + return + } + guard case let .expectationFailed(expectation) = issue.kind else { + return XCTFail("Unexpected issue kind \(issue.kind)") + } + guard let differenceDescription = expectation.differenceDescription else { + return XCTFail("Unexpected nil differenceDescription") + } + XCTAssertTrue(differenceDescription.contains(#"inserted ["hello""#)) + XCTAssertTrue(differenceDescription.contains(#"removed ["helbo""#)) + } + + await Test { + #expect("hello\nworld" == "helbo\nworld") + }.run(configuration: configuration) + } + + func testCollectionDifferenceSkippedForStringsWithoutNewlines() async throws { var configuration = Configuration() configuration.eventHandler = { event, _ in guard case let .issueRecorded(issue) = event.kind else { @@ -1104,7 +1136,25 @@ final class IssueTests: XCTestCase { }.run(configuration: configuration) } - func testCollectionDifferenceSkippedForRanges() async { + func testCollectionDifferenceSkippedForStringsWithCharacterDifferencesOnly() async throws { + var configuration = Configuration() + configuration.eventHandler = { event, _ in + guard case let .issueRecorded(issue) = event.kind else { + return + } + guard case let .expectationFailed(expectation) = issue.kind else { + XCTFail("Unexpected issue kind \(issue.kind)") + return + } + XCTAssertNil(expectation.differenceDescription) + } + + await Test { + #expect("hello\n" == "hello\r") + }.run(configuration: configuration) + } + + func testCollectionDifferenceSkippedForRanges() async throws { var configuration = Configuration() configuration.eventHandler = { event, _ in guard case let .issueRecorded(issue) = event.kind else { @@ -1167,33 +1217,6 @@ final class IssueTests: XCTestCase { }.run(configuration: configuration) } - func testNegatedExpressionsExpandToCaptureNegatedExpression() async { - var configuration = Configuration() - configuration.eventHandler = { event, _ in - guard case let .issueRecorded(issue) = event.kind else { - return - } - guard case let .expectationFailed(expectation) = issue.kind else { - XCTFail("Unexpected issue \(issue)") - return - } - XCTAssertNotNil(expectation.evaluatedExpression.runtimeValue) - XCTAssertTrue(expectation.evaluatedExpression.runtimeValue!.typeInfo.describes(Bool.self)) - guard case let .negation(subexpression, isParenthetical) = expectation.evaluatedExpression.kind else { - XCTFail("Expected expression's kind was negation, but it was \(expectation.evaluatedExpression.kind)") - return - } - XCTAssertTrue(isParenthetical) - XCTAssertNotNil(subexpression.runtimeValue) - XCTAssertTrue(subexpression.runtimeValue!.typeInfo.describes(Bool.self)) - } - - @Sendable func g() -> Int { 1 } - await Test { - #expect(!(g() == 1)) - }.run(configuration: configuration) - } - func testLazyExpectDoesNotEvaluateRightHandValue() async { var configuration = Configuration() configuration.eventHandler = { event, _ in @@ -1206,7 +1229,7 @@ final class IssueTests: XCTestCase { return } let expression = expectation.evaluatedExpression - XCTAssertTrue(expression.expandedDescription().contains("")) + assert(expression, contains: "") } @Sendable func rhs() -> Bool { @@ -1265,9 +1288,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains("7")) - XCTAssertFalse(desc.contains("Optional(7)")) + assert(expectation.evaluatedExpression, contains: "7") + assert(expectation.evaluatedExpression, doesNotContain: "Optional(7)") } } @@ -1289,8 +1311,7 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains("nil")) + assert(expectation.evaluatedExpression, contains: "nil") } } @@ -1343,8 +1364,7 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains("Delicious Food, Yay!")) + assert(expectation.evaluatedExpression, contains: "Delicious Food, Yay!") } } @@ -1405,9 +1425,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains(".b → customDesc")) - XCTAssertFalse(desc.contains(".customDesc")) + assert(expectation.evaluatedExpression, contains: ".b → customDesc") + assert(expectation.evaluatedExpression, doesNotContain: ".customDesc") } } @@ -1432,9 +1451,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains(".A → SWTTestEnumeration(rawValue: \(SWTTestEnumeration.A.rawValue))")) - XCTAssertFalse(desc.contains(".SWTTestEnumeration")) + assert(expectation.evaluatedExpression, contains: ".A → SWTTestEnumeration(rawValue: \(SWTTestEnumeration.A.rawValue))") + assert(expectation.evaluatedExpression, doesNotContain: ".SWTTestEnumeration") } } @@ -1456,7 +1474,7 @@ struct IssueCodingTests { private static let issueKinds: [Issue.Kind] = [ Issue.Kind.apiMisused, Issue.Kind.errorCaught(NSError(domain: "Domain", code: 13, userInfo: ["UserInfoKey": "UserInfoValue"])), - Issue.Kind.expectationFailed(Expectation(evaluatedExpression: .__fromSyntaxNode("abc"), isPassing: true, isRequired: true, sourceLocation: #_sourceLocation)), + Issue.Kind.expectationFailed(Expectation(evaluatedExpression: .init("abc"), isPassing: true, isRequired: true, sourceLocation: #_sourceLocation)), Issue.Kind.knownIssueNotRecorded, Issue.Kind.system, Issue.Kind.timeLimitExceeded(timeLimitComponents: (13, 42)), diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index 02f2cc768..bf63af057 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -261,6 +261,11 @@ struct MultiLineSuite { _ = try #require(x?[...].last) } +@Test(.hidden) func canHaveVariableNamed__ec() throws { + let __ec = 1 + #expect(__ec == 1) +} + @Suite("Miscellaneous tests") struct MiscellaneousTests { @Test("Free function's name") diff --git a/Tests/TestingTests/Support/CartesianProductTests.swift b/Tests/TestingTests/Support/CartesianProductTests.swift index b817b37f6..699ab93e7 100644 --- a/Tests/TestingTests/Support/CartesianProductTests.swift +++ b/Tests/TestingTests/Support/CartesianProductTests.swift @@ -31,8 +31,9 @@ struct CartesianProductTests { // Test the size of the product is correct. let (c1, c2, product) = computeCartesianProduct() #expect(product.underestimatedCount == c1.underestimatedCount * c2.underestimatedCount) - #expect(Array(product).count == c1.count * c2.count) - #expect(Array(product).count == 26 * 100) + let productCount = Array(product).count + #expect(productCount == c1.count * c2.count) + #expect(productCount == (26 * 100) as Int) } @Test("First element is correct") @@ -58,8 +59,9 @@ struct CartesianProductTests { // NOTE: we need to break out the tuple elements because tuples aren't // directly equatable. - #expect(Array(product).map(\.0) == possibleValues.map(\.0)) - #expect(Array(product).map(\.1) == possibleValues.map(\.1)) + let productArray = Array(product) + #expect(productArray.map(\.0) == possibleValues.map(\.0)) + #expect(productArray.map(\.1) == possibleValues.map(\.1)) } @Test("Cartesian product with empty first input is empty") diff --git a/Tests/TestingTests/Traits/TimeLimitTraitTests.swift b/Tests/TestingTests/Traits/TimeLimitTraitTests.swift index 8978e86fd..210d55e58 100644 --- a/Tests/TestingTests/Traits/TimeLimitTraitTests.swift +++ b/Tests/TestingTests/Traits/TimeLimitTraitTests.swift @@ -202,14 +202,12 @@ struct TimeLimitTraitTests { configuration.eventHandler = { event, context in guard case let .issueRecorded(issue) = event.kind, case .timeLimitExceeded = issue.kind, - let test = context.test, - let testCase = context.testCase - else { + let test = context.test else { return } issueRecorded() #expect(test.timeLimit == .milliseconds(10)) - #expect(testCase != nil) + #expect(context.testCase != nil) } await Test(.timeLimit(.milliseconds(10))) { diff --git a/Tests/TestingTests/VariadicGenericTests.swift b/Tests/TestingTests/VariadicGenericTests.swift index 0f54f6528..d70c9a9c1 100644 --- a/Tests/TestingTests/VariadicGenericTests.swift +++ b/Tests/TestingTests/VariadicGenericTests.swift @@ -12,7 +12,43 @@ import Testing private import _TestingInternals @Test func variadicCStringArguments() async throws { - #expect(swt_pointersNotEqual2("abc", "123")) - #expect(swt_pointersNotEqual3("abc", "123", "def")) - #expect(swt_pointersNotEqual4("abc", "123", "def", "456")) + let abc = "abc" + let _123 = "123" + let def = "def" + let _456 = "456" + #expect(0 == strcmp(abc, abc)) + #expect(0 != strcmp(abc, _123)) + #expect(swt_pointersNotEqual2(abc, _123)) + #expect(swt_pointersNotEqual3(abc, _123, def)) + #expect(swt_pointersNotEqual4(abc, _123, def, _456)) + + let nilString: String? = nil + #expect(swt_nullableCString(nilString) == false) + + let lhs = "abc" + let rhs = "123" + #expect(0 != strcmp(lhs, rhs)) +} + +@Test func inoutAsPointerPassedToCFunction() { + let num = CLong.random(in: 0 ..< 100) + let str = String(describing: num) + str.withCString { str in + var endptr: UnsafeMutablePointer? + #expect(num == strtol(str, &endptr, 10)) + #expect(endptr != nil) + #expect(endptr?.pointee == 0) + } +} + +@Test func utf16PointerConversions() throws { + _ = try withUnsafeTemporaryAllocation(of: UTF16.CodeUnit.self, capacity: 1) { buffer in + func f(_ p: UnsafeRawPointer?) -> Bool { true } + func g(_ p: UnsafeMutableRawPointer?) -> Bool { true } + func h(_ p: UnsafeMutablePointer?) -> Bool { true } + #expect(f(buffer.baseAddress)) + #expect(g(buffer.baseAddress)) + #expect(h(buffer.baseAddress)) + return try #require(String.decodeCString(buffer.baseAddress, as: UTF16.self)?.result) + } }