diff --git a/Package.swift b/Package.swift index 4d7891274..5bf6c100d 100644 --- a/Package.swift +++ b/Package.swift @@ -148,6 +148,7 @@ extension Array where Element == PackageDescription.SwiftSetting { .enableExperimentalFeature("AvailabilityMacro=_clockAPI:macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0"), .enableExperimentalFeature("AvailabilityMacro=_regexAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"), .enableExperimentalFeature("AvailabilityMacro=_swiftVersionAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"), + .enableExperimentalFeature("AvailabilityMacro=_synchronizationAPI:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"), .enableExperimentalFeature("AvailabilityMacro=_distantFuture:macOS 99.0, iOS 99.0, watchOS 99.0, tvOS 99.0, visionOS 99.0"), ] diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index 2a3e137a3..badf41599 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -95,8 +95,10 @@ public func __checkValue( // 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. - var expectation = Expectation(evaluatedExpression: expression, isPassing: condition, isRequired: isRequired, sourceLocation: sourceLocation) - Event.post(.expectationChecked(expectation)) + lazy var expectation = Expectation(evaluatedExpression: expression, isPassing: condition, isRequired: isRequired, sourceLocation: sourceLocation) + if Configuration.deliverExpectationCheckedEvents { + Event.post(.expectationChecked(expectation)) + } // Early exit if the expectation passed. if condition { @@ -835,10 +837,11 @@ public func __checkClosureCall( /// `#require()` macros. Do not call it directly. public func __checkClosureCall( throws errorType: E.Type, - performing body: () async throws -> some Any, + performing body: () async throws -> sending some Any, expression: __Expression, comments: @autoclosure () -> [Comment], isRequired: Bool, + isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation ) async -> Result where E: Error { if errorType == Never.self { @@ -848,6 +851,7 @@ public func __checkClosureCall( expression: expression, comments: comments(), isRequired: isRequired, + isolation: isolation, sourceLocation: sourceLocation ) } else { @@ -858,6 +862,7 @@ public func __checkClosureCall( expression: expression, comments: comments(), isRequired: isRequired, + isolation: isolation, sourceLocation: sourceLocation ) } @@ -911,10 +916,11 @@ public func __checkClosureCall( /// `#require()` macros. Do not call it directly. public func __checkClosureCall( throws _: Never.Type, - performing body: () async throws -> some Any, + performing body: () async throws -> sending some Any, expression: __Expression, comments: @autoclosure () -> [Comment], isRequired: Bool, + isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation ) async -> Result { var success = true @@ -973,10 +979,11 @@ public func __checkClosureCall( /// `#require()` macros. Do not call it directly. public func __checkClosureCall( throws error: E, - performing body: () async throws -> some Any, + performing body: () async throws -> sending some Any, expression: __Expression, comments: @autoclosure () -> [Comment], isRequired: Bool, + isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation ) async -> Result where E: Error & Equatable { await __checkClosureCall( @@ -986,6 +993,7 @@ public func __checkClosureCall( expression: expression, comments: comments(), isRequired: isRequired, + isolation: isolation, sourceLocation: sourceLocation ) } @@ -1047,12 +1055,13 @@ public func __checkClosureCall( /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. public func __checkClosureCall( - performing body: () async throws -> R, + performing body: () async throws -> sending R, throws errorMatcher: (any Error) async throws -> Bool, mismatchExplanation: ((any Error) -> String)? = nil, expression: __Expression, comments: @autoclosure () -> [Comment], isRequired: Bool, + isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation ) async -> Result { var errorMatches = false diff --git a/Sources/Testing/Issues/Confirmation.swift b/Sources/Testing/Issues/Confirmation.swift index 2ce3f1910..b842ce4f2 100644 --- a/Sources/Testing/Issues/Confirmation.swift +++ b/Sources/Testing/Issues/Confirmation.swift @@ -55,6 +55,7 @@ extension Confirmation { /// `body` is invoked. The default value of this argument is `1`, indicating /// that the event should occur exactly once. Pass `0` if the event should /// _never_ occur when `body` is invoked. +/// - isolation: The actor to which `body` is isolated, if any. /// - sourceLocation: The source location to which any recorded issues should /// be attributed. /// - body: The function to invoke. @@ -94,12 +95,14 @@ extension Confirmation { public func confirmation( _ comment: Comment? = nil, expectedCount: Int = 1, + isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation = #_sourceLocation, - _ body: (Confirmation) async throws -> R + _ body: (Confirmation) async throws -> sending R ) async rethrows -> R { try await confirmation( comment, expectedCount: expectedCount ... expectedCount, + isolation: isolation, sourceLocation: sourceLocation, body ) @@ -114,6 +117,7 @@ public func confirmation( /// function. /// - expectedCount: A range of integers indicating the number of times the /// expected event should occur when `body` is invoked. +/// - isolation: The actor to which `body` is isolated, if any. /// - sourceLocation: The source location to which any recorded issues should /// be attributed. /// - body: The function to invoke. @@ -156,13 +160,14 @@ public func confirmation( /// preconditions have been met, and records an issue if they have not. /// /// If an exact count is expected, use -/// ``confirmation(_:expectedCount:sourceLocation:_:)-7kfko`` instead. +/// ``confirmation(_:expectedCount:isolation:sourceLocation:_:)`` instead. @_spi(Experimental) public func confirmation( _ comment: Comment? = nil, expectedCount: some Confirmation.ExpectedCount, + isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation = #_sourceLocation, - _ body: (Confirmation) async throws -> R + _ body: (Confirmation) async throws -> sending R ) async rethrows -> R { let confirmation = Confirmation() defer { @@ -182,7 +187,7 @@ public func confirmation( @_spi(Experimental) extension Confirmation { /// A protocol that describes a range expression that can be used with - /// ``confirmation(_:expectedCount:sourceLocation:_:)-41gmd``. + /// ``confirmation(_:expectedCount:isolation:sourceLocation:_:)-9rt6m``. /// /// This protocol represents any expression that describes a range of /// confirmation counts. For example, the expression `1 ..< 10` automatically diff --git a/Sources/Testing/Issues/Issue+Recording.swift b/Sources/Testing/Issues/Issue+Recording.swift index 32cc9f511..a45bbb822 100644 --- a/Sources/Testing/Issues/Issue+Recording.swift +++ b/Sources/Testing/Issues/Issue+Recording.swift @@ -196,6 +196,7 @@ extension Issue { /// - sourceLocation: The source location to attribute any caught error to. /// - configuration: The test configuration to use when recording an issue. /// The default value is ``Configuration/current``. + /// - isolation: The actor to which `body` is isolated, if any. /// - body: An asynchronous closure that might throw an error. /// /// - Returns: The issue representing the caught error, if any error was @@ -204,6 +205,7 @@ extension Issue { static func withErrorRecording( at sourceLocation: SourceLocation, configuration: Configuration? = nil, + isolation: isolated (any Actor)? = #isolation, _ body: () async throws -> Void ) async -> (any Error)? { // Ensure that we are capturing backtraces for errors before we start diff --git a/Sources/Testing/Issues/Issue.swift b/Sources/Testing/Issues/Issue.swift index fe69c9b60..91602ef7c 100644 --- a/Sources/Testing/Issues/Issue.swift +++ b/Sources/Testing/Issues/Issue.swift @@ -33,7 +33,7 @@ public struct Issue: Sendable { /// ``Confirmation/confirm(count:)`` should have been called. /// /// This issue can occur when calling - /// ``confirmation(_:expectedCount:sourceLocation:_:)`` when the + /// ``confirmation(_:expectedCount:isolation:sourceLocation:_:)`` when the /// confirmation passed to these functions' `body` closures is confirmed too /// few or too many times. indirect case confirmationMiscounted(actual: Int, expected: Int) @@ -48,9 +48,9 @@ public struct Issue: Sendable { /// ``Confirmation/confirm(count:)`` should have been called. /// /// This issue can occur when calling - /// ``confirmation(_:expectedCount:sourceLocation:_:)-41gmd`` when the - /// confirmation passed to these functions' `body` closures is confirmed too - /// few or too many times. + /// ``confirmation(_:expectedCount:isolation:sourceLocation:_:)-9rt6m`` when + /// the confirmation passed to these functions' `body` closures is confirmed + /// too few or too many times. @_spi(Experimental) indirect case confirmationOutOfRange(actual: Int, expected: any Confirmation.ExpectedCount) diff --git a/Sources/Testing/Issues/KnownIssue.swift b/Sources/Testing/Issues/KnownIssue.swift index c9c03be56..70c9c3875 100644 --- a/Sources/Testing/Issues/KnownIssue.swift +++ b/Sources/Testing/Issues/KnownIssue.swift @@ -110,7 +110,7 @@ public typealias KnownIssueMatcher = @Sendable (_ issue: Issue) -> Bool /// Because all errors thrown by `body` are caught as known issues, this /// function is not throwing. If only some errors or issues are known to occur /// while others should continue to cause test failures, use -/// ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)-5vi5n`` +/// ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)`` /// instead. public func withKnownIssue( _ comment: Comment? = nil, @@ -161,7 +161,7 @@ public func withKnownIssue( /// /// It is not necessary to specify both `precondition` and `issueMatcher` if /// only one is relevant. If all errors and issues should be considered known -/// issues, use ``withKnownIssue(_:isIntermittent:sourceLocation:_:)-95r6o`` +/// issues, use ``withKnownIssue(_:isIntermittent:sourceLocation:_:)`` /// instead. /// /// - Note: `issueMatcher` may be invoked more than once for the same issue. @@ -200,6 +200,7 @@ public func withKnownIssue( /// - isIntermittent: Whether or not the known issue occurs intermittently. If /// this argument is `true` and the known issue does not occur, no secondary /// issue is recorded. +/// - isolation: The actor to which `body` is isolated, if any. /// - sourceLocation: The source location to which any recorded issues should /// be attributed. /// - body: The function to invoke. @@ -218,15 +219,16 @@ public func withKnownIssue( /// Because all errors thrown by `body` are caught as known issues, this /// function is not throwing. If only some errors or issues are known to occur /// while others should continue to cause test failures, use -/// ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)-47y3z`` +/// ``withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:when:matching:)`` /// instead. public func withKnownIssue( _ comment: Comment? = nil, isIntermittent: Bool = false, + isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation = #_sourceLocation, _ body: () async throws -> Void ) async { - try? await withKnownIssue(comment, isIntermittent: isIntermittent, sourceLocation: sourceLocation, body, matching: { _ in true }) + try? await withKnownIssue(comment, isIntermittent: isIntermittent, isolation: isolation, sourceLocation: sourceLocation, body, matching: { _ in true }) } /// Invoke a function that has a known issue that is expected to occur during @@ -237,6 +239,7 @@ public func withKnownIssue( /// - isIntermittent: Whether or not the known issue occurs intermittently. If /// this argument is `true` and the known issue does not occur, no secondary /// issue is recorded. +/// - isolation: The actor to which `body` is isolated, if any. /// - sourceLocation: The source location to which any recorded issues should /// be attributed. /// - body: The function to invoke. @@ -269,13 +272,14 @@ public func withKnownIssue( /// /// It is not necessary to specify both `precondition` and `issueMatcher` if /// only one is relevant. If all errors and issues should be considered known -/// issues, use ``withKnownIssue(_:isIntermittent:sourceLocation:_:)-3g6b7`` +/// issues, use ``withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:when:matching:)`` /// instead. /// /// - Note: `issueMatcher` may be invoked more than once for the same issue. public func withKnownIssue( _ comment: Comment? = nil, isIntermittent: Bool = false, + isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation = #_sourceLocation, _ body: () async throws -> Void, when precondition: () async -> Bool = { true }, diff --git a/Sources/Testing/Parameterization/TypeInfo.swift b/Sources/Testing/Parameterization/TypeInfo.swift index 671674531..ccf0cd824 100644 --- a/Sources/Testing/Parameterization/TypeInfo.swift +++ b/Sources/Testing/Parameterization/TypeInfo.swift @@ -74,6 +74,9 @@ public struct TypeInfo: Sendable { // MARK: - Name extension TypeInfo { + /// An in-memory cache of fully-qualified type name components. + private static let _fullyQualifiedNameComponentsCache = Locked<[ObjectIdentifier: [String]]>() + /// The complete name of this type, with the names of all referenced types /// fully-qualified by their module names when possible. /// @@ -92,6 +95,10 @@ extension TypeInfo { public var fullyQualifiedNameComponents: [String] { switch _kind { case let .type(type): + if let cachedResult = Self._fullyQualifiedNameComponentsCache.rawValue[ObjectIdentifier(type)] { + return cachedResult + } + var result = String(reflecting: type) .split(separator: ".") .map(String.init) @@ -109,6 +116,10 @@ extension TypeInfo { // those out as they're uninteresting to us. result = result.filter { !$0.starts(with: "(unknown context at") } + Self._fullyQualifiedNameComponentsCache.withLock { fullyQualifiedNameComponentsCache in + fullyQualifiedNameComponentsCache[ObjectIdentifier(type)] = result + } + return result case let .nameOnly(fullyQualifiedNameComponents, _, _): return fullyQualifiedNameComponents diff --git a/Sources/Testing/Running/Runner.RuntimeState.swift b/Sources/Testing/Running/Runner.RuntimeState.swift index 3928a5e6b..34ad152c5 100644 --- a/Sources/Testing/Running/Runner.RuntimeState.swift +++ b/Sources/Testing/Running/Runner.RuntimeState.swift @@ -8,6 +8,8 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +private import Synchronization + extension Runner { /// A type which collects the task-scoped runtime state for a running /// ``Runner`` instance, the tests it runs, and other objects it interacts @@ -111,7 +113,10 @@ extension Configuration { /// - Returns: A unique number identifying `self` that can be /// passed to `_removeFromAll(identifiedBy:)`` to unregister it. private func _addToAll() -> UInt64 { - Self._all.withLock { all in + if deliverExpectationCheckedEvents, #available(_synchronizationAPI, *) { + Self._deliverExpectationCheckedEventsCount.add(1, ordering: .sequentiallyConsistent) + } + return Self._all.withLock { all in let id = all.nextID all.nextID += 1 all.instances[id] = self @@ -123,12 +128,37 @@ extension Configuration { /// /// - Parameters: /// - id: The unique identifier of this instance, as previously returned by - /// `_addToAll()`. If `nil`, this function has no effect. - private func _removeFromAll(identifiedBy id: UInt64?) { - if let id { - Self._all.withLock { all in - _ = all.instances.removeValue(forKey: id) - } + /// `_addToAll()`. + private func _removeFromAll(identifiedBy id: UInt64) { + let configuration = Self._all.withLock { all in + all.instances.removeValue(forKey: id) + } + if let configuration, configuration.deliverExpectationCheckedEvents, #available(_synchronizationAPI, *) { + Self._deliverExpectationCheckedEventsCount.subtract(1, ordering: .sequentiallyConsistent) + } + } + + /// An atomic counter that tracks the number of "current" configurations that + /// have set ``deliverExpectationCheckedEvents`` to `true`. + /// + /// On older Apple platforms, this property is not available and ``all`` is + /// directly consulted instead (which is less efficient.) + @available(_synchronizationAPI, *) + private static let _deliverExpectationCheckedEventsCount = Atomic(0) + + /// Whether or not events of the kind + /// ``Event/Kind-swift.enum/expectationChecked(_:)`` should be delivered to + /// the event handler of _any_ configuration set as current for a task in the + /// current process. + /// + /// To determine if an individual instance of ``Configuration`` is listening + /// for these events, consult the per-instance + /// ``Configuration/deliverExpectationCheckedEvents`` property. + static var deliverExpectationCheckedEvents: Bool { + if #available(_synchronizationAPI, *) { + _deliverExpectationCheckedEventsCount.load(ordering: .sequentiallyConsistent) > 0 + } else { + all.contains(where: \.deliverExpectationCheckedEvents) } } } diff --git a/Sources/Testing/Support/Versions.swift b/Sources/Testing/Support/Versions.swift index 0218aa185..75bb4b88f 100644 --- a/Sources/Testing/Support/Versions.swift +++ b/Sources/Testing/Support/Versions.swift @@ -125,7 +125,7 @@ let simulatorVersion: String = { /// /// This value is not part of the public interface of the testing library. var testingLibraryVersion: String { - SWT_TESTING_LIBRARY_VERSION + swt_getTestingLibraryVersion().flatMap(String.init(validatingCString:)) ?? "unknown" } /// A human-readable string describing the Swift Standard Library's version. diff --git a/Sources/Testing/Testing.docc/Expectations.md b/Sources/Testing/Testing.docc/Expectations.md index de2f625e6..92185876a 100644 --- a/Sources/Testing/Testing.docc/Expectations.md +++ b/Sources/Testing/Testing.docc/Expectations.md @@ -77,7 +77,7 @@ the test when the code doesn't satisfy a requirement, use ### Confirming that asynchronous events occur - -- ``confirmation(_:expectedCount:sourceLocation:_:)`` +- ``confirmation(_:expectedCount:isolation:sourceLocation:_:)`` - ``Confirmation`` ### Retrieving information about checked expectations diff --git a/Sources/Testing/Testing.docc/MigratingFromXCTest.md b/Sources/Testing/Testing.docc/MigratingFromXCTest.md index 18cd202fc..b511e0c09 100644 --- a/Sources/Testing/Testing.docc/MigratingFromXCTest.md +++ b/Sources/Testing/Testing.docc/MigratingFromXCTest.md @@ -434,7 +434,7 @@ Some tests, especially those that test asynchronously-delivered events, cannot be readily converted to use Swift concurrency. The testing library offers functionality called _confirmations_ which can be used to implement these tests. Instances of ``Confirmation`` are created and used within the scope of the -function ``confirmation(_:expectedCount:sourceLocation:_:)``. +function ``confirmation(_:expectedCount:isolation:sourceLocation:_:)``. Confirmations function similarly to the expectations API of XCTest, however, they don't block or suspend the caller while waiting for a condition to be fulfilled. @@ -531,8 +531,8 @@ to tell XCTest and its infrastructure that the issue shouldn't cause the test to fail. The testing library has an equivalent function with synchronous and asynchronous variants: -- ``withKnownIssue(_:isIntermittent:sourceLocation:_:)-95r6o`` -- ``withKnownIssue(_:isIntermittent:sourceLocation:_:)-3g6b7`` +- ``withKnownIssue(_:isIntermittent:sourceLocation:_:)`` +- ``withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:)`` This function can be used to annotate a section of a test as having a known issue: @@ -627,8 +627,8 @@ Additional options can be specified when calling `XCTExpectFailure()`: The testing library includes overloads of `withKnownIssue()` that take additional arguments with similar behavior: -- ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)-5vi5n`` -- ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)-47y3z`` +- ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)`` +- ``withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:when:matching:)`` To conditionally enable known-issue matching or to match only certain kinds of issues: diff --git a/Sources/Testing/Testing.docc/known-issues.md b/Sources/Testing/Testing.docc/known-issues.md index 495e49d64..31906a5df 100644 --- a/Sources/Testing/Testing.docc/known-issues.md +++ b/Sources/Testing/Testing.docc/known-issues.md @@ -22,10 +22,10 @@ at runtime not to mark the test as failing when issues occur. ### Recording known issues in tests -- ``withKnownIssue(_:isIntermittent:sourceLocation:_:)-95r6o`` -- ``withKnownIssue(_:isIntermittent:sourceLocation:_:)-3g6b7`` -- ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)-5vi5n`` -- ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)-47y3z`` +- ``withKnownIssue(_:isIntermittent:sourceLocation:_:)`` +- ``withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:)`` +- ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)`` +- ``withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:when:matching:)`` - ``KnownIssueMatcher`` ### Describing a failure or warning diff --git a/Sources/Testing/Testing.docc/testing-asynchronous-code.md b/Sources/Testing/Testing.docc/testing-asynchronous-code.md index 2aa2f68af..548cf07b0 100644 --- a/Sources/Testing/Testing.docc/testing-asynchronous-code.md +++ b/Sources/Testing/Testing.docc/testing-asynchronous-code.md @@ -31,9 +31,9 @@ expected event happens. ### Confirm that an event happens -Call ``confirmation(_:expectedCount:sourceLocation:_:)`` in your asynchronous -test function to create a `Confirmation` for the expected event. In the trailing -closure parameter, call the code under test. Swift Testing passes a +Call ``confirmation(_:expectedCount:isolation:sourceLocation:_:)`` in your +asynchronous test function to create a `Confirmation` for the expected event. In +the trailing closure parameter, call the code under test. Swift Testing passes a `Confirmation` as the parameter to the closure, which you call as a function in the event handler for the code under test when the event you're testing for occurs: diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 690489702..d7a276bcb 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -33,7 +33,9 @@ public import SwiftSyntaxMacros /// /// The `__check()` function that implements expansions of these macros must /// take any developer-supplied arguments _before_ the ones inserted during -/// macro expansion (starting with the `"expression"` argument.) +/// macro expansion (starting with the `"expression"` argument.) The `isolation` +/// argument (if present) and `sourceLocation` argument are placed at the end of +/// the generated function call's argument list. public protocol ConditionMacro: ExpressionMacro, Sendable { /// Whether or not the macro's expansion may throw an error. static var isThrowing: Bool { get } @@ -41,6 +43,10 @@ public protocol ConditionMacro: ExpressionMacro, Sendable { // MARK: - +/// The token used as the label of the argument passed to `#expect()` and +/// `#require()` and used for actor isolation. +private var _isolationLabel: TokenSyntax { .identifier("isolation") } + /// The token used as the label of the source location argument passed to /// `#expect()` and `#require()`. private var _sourceLocationLabel: TokenSyntax { .identifier("sourceLocation") } @@ -89,6 +95,9 @@ extension ConditionMacro { // never the first argument.) commentIndex = macroArguments.dropFirst().lastIndex { $0.label == nil } } + let isolationArgumentIndex = macroArguments.lazy + .compactMap(\.label) + .firstIndex { $0.tokenKind == _isolationLabel.tokenKind } let sourceLocationArgumentIndex = macroArguments.lazy .compactMap(\.label) .firstIndex { $0.tokenKind == _sourceLocationLabel.tokenKind } @@ -103,6 +112,7 @@ extension ConditionMacro { // arguments here. checkArguments += macroArguments.indices.lazy .filter { $0 != commentIndex } + .filter { $0 != isolationArgumentIndex } .filter { $0 != sourceLocationArgumentIndex } .map { macroArguments[$0] } @@ -124,6 +134,7 @@ extension ConditionMacro { // "sourceLocation" arguments here. checkArguments += macroArguments.dropFirst().indices.lazy .filter { $0 != commentIndex } + .filter { $0 != isolationArgumentIndex } .filter { $0 != sourceLocationArgumentIndex } .map { macroArguments[$0] } @@ -160,6 +171,10 @@ extension ConditionMacro { checkArguments.append(Argument(label: "isRequired", expression: BooleanLiteralExprSyntax(isThrowing))) + if let isolationArgumentIndex { + checkArguments.append(macroArguments[isolationArgumentIndex]) + } + if let sourceLocationArgumentIndex { checkArguments.append(macroArguments[sourceLocationArgumentIndex]) } else { diff --git a/Sources/_TestingInternals/CMakeLists.txt b/Sources/_TestingInternals/CMakeLists.txt index 42c27932e..128b0aa48 100644 --- a/Sources/_TestingInternals/CMakeLists.txt +++ b/Sources/_TestingInternals/CMakeLists.txt @@ -11,6 +11,7 @@ set(CMAKE_CXX_SCAN_FOR_MODULES 0) include(LibraryVersion) add_library(_TestingInternals STATIC Discovery.cpp + Versions.cpp WillThrow.cpp) target_include_directories(_TestingInternals PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) diff --git a/Sources/_TestingInternals/Versions.cpp b/Sources/_TestingInternals/Versions.cpp new file mode 100644 index 000000000..e9f0db698 --- /dev/null +++ b/Sources/_TestingInternals/Versions.cpp @@ -0,0 +1,20 @@ +// +// 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 +// + +#include "Versions.h" + +const char *swt_getTestingLibraryVersion(void) { +#if defined(_SWT_TESTING_LIBRARY_VERSION) + return _SWT_TESTING_LIBRARY_VERSION; +#else +#warning _SWT_TESTING_LIBRARY_VERSION not defined: testing library version is unavailable + return nullptr; +#endif +} diff --git a/Sources/_TestingInternals/include/Defines.h b/Sources/_TestingInternals/include/Defines.h index 9af70b384..a93d3f8b2 100644 --- a/Sources/_TestingInternals/include/Defines.h +++ b/Sources/_TestingInternals/include/Defines.h @@ -32,15 +32,4 @@ /// An attribute that renames a C symbol in Swift. #define SWT_SWIFT_NAME(name) __attribute__((swift_name(#name))) -/// The testing library version from the package manifest. -/// -/// - Bug: The value provided to the compiler (`_SWT_TESTING_LIBRARY_VERSION`) -/// is not visible in Swift, so this second macro is needed. -/// ((#43521)[https://github.com/swiftlang/swift/issues/43521]) -#if defined(_SWT_TESTING_LIBRARY_VERSION) -#define SWT_TESTING_LIBRARY_VERSION _SWT_TESTING_LIBRARY_VERSION -#else -#define SWT_TESTING_LIBRARY_VERSION "unknown" -#endif - #endif // SWT_DEFINES_H diff --git a/Sources/_TestingInternals/include/Versions.h b/Sources/_TestingInternals/include/Versions.h new file mode 100644 index 000000000..e30aec043 --- /dev/null +++ b/Sources/_TestingInternals/include/Versions.h @@ -0,0 +1,28 @@ +// +// 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 +// + +#if !defined(SWT_VERSIONS_H) +#define SWT_VERSIONS_H + +#include "Defines.h" + +SWT_ASSUME_NONNULL_BEGIN + +/// Get the human-readable version of the testing library. +/// +/// - Returns: A human-readable string describing the version of the testing +/// library, or `nullptr` if no version information is available. This +/// string's value and format may vary between platforms, releases, or any +/// other conditions. Do not attempt to parse it. +SWT_EXTERN const char *_Nullable swt_getTestingLibraryVersion(void); + +SWT_ASSUME_NONNULL_END + +#endif diff --git a/Tests/TestingMacrosTests/ConditionMacroTests.swift b/Tests/TestingMacrosTests/ConditionMacroTests.swift index 4bdbd88bf..1da09a227 100644 --- a/Tests/TestingMacrosTests/ConditionMacroTests.swift +++ b/Tests/TestingMacrosTests/ConditionMacroTests.swift @@ -88,6 +88,8 @@ struct ConditionMacroTests { ##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), 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()"##, + ##"#expect(isolation: somewhere) {}"##: + ##"Testing.__checkClosureCall(performing: {}, expression: .__fromSyntaxNode("{}"), comments: [], isRequired: false, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ] ) func expectMacro(input: String, expectedOutput: String) throws { @@ -164,6 +166,8 @@ struct ConditionMacroTests { ##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), 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()"##, + ##"#require(isolation: somewhere) {}"##: + ##"Testing.__checkClosureCall(performing: {}, expression: .__fromSyntaxNode("{}"), comments: [], isRequired: true, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ] ) func requireMacro(input: String, expectedOutput: String) throws { diff --git a/Tests/TestingTests/ConfirmationTests.swift b/Tests/TestingTests/ConfirmationTests.swift index b545c0ab6..11d04b48c 100644 --- a/Tests/TestingTests/ConfirmationTests.swift +++ b/Tests/TestingTests/ConfirmationTests.swift @@ -62,6 +62,12 @@ struct ConfirmationTests { } } #endif + + @Test("Main actor isolation") + @MainActor + func mainActorIsolated() async { + await confirmation { $0() } + } } // MARK: - Fixtures diff --git a/Tests/TestingTests/KnownIssueTests.swift b/Tests/TestingTests/KnownIssueTests.swift index 95f6344a9..ac32f1a03 100644 --- a/Tests/TestingTests/KnownIssueTests.swift +++ b/Tests/TestingTests/KnownIssueTests.swift @@ -376,5 +376,12 @@ final class KnownIssueTests: XCTestCase { await fulfillment(of: [issueRecorded, knownIssueNotRecorded], timeout: 0.0) } + + @MainActor + func testMainActorIsolated() async { + await Test { + await withKnownIssue(isIntermittent: true) { () async in } + }.run(configuration: .init()) + } } #endif diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index f77b698d6..7a03a3292 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -166,6 +166,7 @@ struct TestsWithStaticMemberAccessBySelfKeyword { @Test(.hidden, arguments: Self.x) func f(i: Int) {} +#if FIXED_135346598 @Test(.hidden, arguments: Self.f(max: 100)) func g(i: Int) {} @@ -178,6 +179,7 @@ struct TestsWithStaticMemberAccessBySelfKeyword { @Test(.hidden, arguments: [Box(rawValue: Self.f(max:))]) func j(i: Box<@Sendable (Int) -> Range>) {} +#endif struct Nested { static let x = 0 ..< 100 @@ -532,4 +534,15 @@ struct MiscellaneousTests { failureBreakpoint() #expect(failureBreakpointValue == 1) } + + @available(_clockAPI, *) + @Test("Repeated calls to #expect() run in reasonable time", .disabled("time-sensitive")) + func repeatedlyExpect() { + let duration = Test.Clock().measure { + for _ in 0 ..< 1_000_000 { + #expect(true as Bool) + } + } + #expect(duration < .seconds(1)) + } } diff --git a/cmake/modules/LibraryVersion.cmake b/cmake/modules/LibraryVersion.cmake index 9713175c4..1057e0df2 100644 --- a/cmake/modules/LibraryVersion.cmake +++ b/cmake/modules/LibraryVersion.cmake @@ -6,34 +6,38 @@ # See http://swift.org/LICENSE.txt for license information # See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# The current version of the Swift Testing release. For release branches, +# remember to remove -dev. +set(SWT_TESTING_LIBRARY_VERSION "6.0") + find_package(Git QUIET) if(Git_FOUND) + # Get the commit hash corresponding to the current build. Limit length to 15 + # to match `swift --version` output format. execute_process( - COMMAND ${GIT_EXECUTABLE} describe --tags --exact-match + COMMAND ${GIT_EXECUTABLE} rev-parse --short=15 --verify HEAD WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - OUTPUT_VARIABLE GIT_TAG + OUTPUT_VARIABLE GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) - if(GIT_TAG) - add_compile_definitions( - "$<$:_SWT_TESTING_LIBRARY_VERSION=${GIT_TAG}>") - else() - execute_process( - COMMAND ${GIT_EXECUTABLE} rev-parse --verify HEAD - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - OUTPUT_VARIABLE GIT_REVISION - OUTPUT_STRIP_TRAILING_WHITESPACE) - execute_process( - COMMAND ${GIT_EXECUTABLE} status -s - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - OUTPUT_VARIABLE GIT_STATUS - OUTPUT_STRIP_TRAILING_WHITESPACE) - if(GIT_STATUS) - add_compile_definitions( - "$<$:_SWT_TESTING_LIBRARY_VERSION=${GIT_REVISION} (modified)>") - else() - add_compile_definitions( - "$<$:_SWT_TESTING_LIBRARY_VERSION=${GIT_REVISION}>") - endif() + + # Check if there are local changes. + execute_process( + COMMAND ${GIT_EXECUTABLE} status -s + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + OUTPUT_VARIABLE GIT_STATUS + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(GIT_STATUS) + set(GIT_VERSION "${GIT_VERSION} - modified") endif() endif() + +# Combine the hard-coded Swift version with available Git information. +if(GIT_VERSION) +set(SWT_TESTING_LIBRARY_VERSION "${SWT_TESTING_LIBRARY_VERSION} (${GIT_VERSION})") +endif() + +# All done! +message(STATUS "Swift Testing version: ${SWT_TESTING_LIBRARY_VERSION}") +add_compile_definitions( + "$<$:_SWT_TESTING_LIBRARY_VERSION=\"${SWT_TESTING_LIBRARY_VERSION}\">") diff --git a/cmake/modules/shared/AvailabilityDefinitions.cmake b/cmake/modules/shared/AvailabilityDefinitions.cmake index e7595f223..24e186aef 100644 --- a/cmake/modules/shared/AvailabilityDefinitions.cmake +++ b/cmake/modules/shared/AvailabilityDefinitions.cmake @@ -13,4 +13,5 @@ add_compile_options( "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_clockAPI:macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_regexAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_swiftVersionAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0\">" + "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_synchronizationAPI:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_distantFuture:macOS 99.0, iOS 99.0, watchOS 99.0, tvOS 99.0, visionOS 99.0\">") diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index 9e64a0a13..f37a58dd4 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -20,7 +20,8 @@ add_compile_options( if(APPLE) add_compile_definitions("SWT_TARGET_OS_APPLE") endif() -if(CMAKE_SYSTEM_NAME IN_LIST "iOS;watchOS;tvOS;visionOS;WASI") +set(SWT_NO_EXIT_TESTS_LIST "iOS" "watchOS" "tvOS" "visionOS" "WASI") +if(CMAKE_SYSTEM_NAME IN_LIST SWT_NO_EXIT_TESTS_LIST) add_compile_definitions("SWT_NO_EXIT_TESTS") endif() if(NOT APPLE)