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/ABI/EntryPoints/ABIEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift index d8876ddf1..2e950bb74 100644 --- a/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift @@ -9,11 +9,7 @@ // #if canImport(Foundation) && !SWT_NO_ABI_ENTRY_POINT -#if SWT_BUILDING_WITH_CMAKE -@_implementationOnly import _TestingInternals -#else private import _TestingInternals -#endif extension ABIv0 { /// The type of the entry point to the testing library used by tools that want diff --git a/Sources/Testing/Events/Clock.swift b/Sources/Testing/Events/Clock.swift index 794f4df8b..43c4364fc 100644 --- a/Sources/Testing/Events/Clock.swift +++ b/Sources/Testing/Events/Clock.swift @@ -41,7 +41,13 @@ extension Test { /// The wall-clock time corresponding to this instant. fileprivate(set) var wall: TimeValue = { var wall = timespec() +#if os(Android) + // Android headers recommend `clock_gettime` over `timespec_get` which + // is available with API Level 29+ for `TIME_UTC`. + clock_gettime(CLOCK_REALTIME, &wall) +#else timespec_get(&wall, TIME_UTC) +#endif return TimeValue(wall) }() #endif diff --git a/Sources/Testing/ExitTests/ExitCondition.swift b/Sources/Testing/ExitTests/ExitCondition.swift index e3074740d..ed6552e09 100644 --- a/Sources/Testing/ExitTests/ExitCondition.swift +++ b/Sources/Testing/ExitTests/ExitCondition.swift @@ -44,9 +44,9 @@ public enum ExitCondition: Sendable { /// | Linux | [``](https://sourceware.org/glibc/manual/latest/html_node/Exit-Status.html), `` | /// | Windows | [``](https://learn.microsoft.com/en-us/cpp/c-runtime-library/exit-success-exit-failure) | /// - /// On POSIX-like systems including macOS and Linux, only the low unsigned 8 - /// bits (0–255) of the exit code are reliably preserved and reported to - /// a parent process. + /// On macOS and Windows, the full exit code reported by the process is + /// yielded to the parent process. Linux and other POSIX-like systems may only + /// reliably report the low unsigned 8 bits (0–255) of the exit code. case exitCode(_ exitCode: CInt) /// The process terminated with the given signal. @@ -62,43 +62,171 @@ public enum ExitCondition: Sendable { /// | macOS | [``](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/signal.3.html) | /// | Linux | [``](https://sourceware.org/glibc/manual/latest/html_node/Standard-Signals.html) | /// | Windows | [``](https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-constants) | + /// + /// On Windows, by default, the C runtime will terminate a process with exit + /// code `-3` if a raised signal is not handled, exactly as if `exit(-3)` were + /// called. As a result, this case is unavailable on that platform. Developers + /// should use ``failure`` instead when testing signal handling on Windows. #if os(Windows) @available(*, unavailable, message: "On Windows, use .failure instead.") #endif case signal(_ signal: CInt) } -// MARK: - +// MARK: - Equatable #if SWT_NO_EXIT_TESTS @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif extension ExitCondition { - /// Check whether this instance matches another. + /// Check whether or not two values of this type are equal. /// /// - Parameters: - /// - other: The other instance to compare against. + /// - lhs: One value to compare. + /// - rhs: Another value to compare. /// - /// - Returns: Whether or not this instance is equal to, or at least covers, - /// the other instance. - func matches(_ other: ExitCondition) -> Bool { - return switch (self, other) { - case (.failure, .failure): - true + /// - Returns: Whether or not `lhs` and `rhs` are equal. + /// + /// Two instances of this type can be compared; if either instance is equal to + /// ``failure``, it will compare equal to any instance except ``success``. To + /// check if two instances are exactly equal, use the ``===(_:_:)`` operator: + /// + /// ```swift + /// let lhs: ExitCondition = .failure + /// let rhs: ExitCondition = .signal(SIGINT) + /// print(lhs == rhs) // prints "true" + /// print(lhs === rhs) // prints "false" + /// ``` + /// + /// This special behavior means that the ``==(_:_:)`` operator is not + /// transitive, and does not satisfy the requirements of + /// [`Equatable`](https://developer.apple.com/documentation/swift/equatable) + /// or [`Hashable`](https://developer.apple.com/documentation/swift/hashable). + /// + /// For any values `a` and `b`, `a == b` implies that `a != b` is `false`. + public static func ==(lhs: Self, rhs: Self) -> Bool { +#if SWT_NO_EXIT_TESTS + fatalError("Unsupported") +#else + return switch (lhs, rhs) { case let (.failure, .exitCode(exitCode)), let (.exitCode(exitCode), .failure): exitCode != EXIT_SUCCESS +#if !os(Windows) + case (.failure, .signal), (.signal, .failure): + // All terminating signals are considered failures. + true +#endif + default: + lhs === rhs + } +#endif + } + + /// Check whether or not two values of this type are _not_ equal. + /// + /// - Parameters: + /// - lhs: One value to compare. + /// - rhs: Another value to compare. + /// + /// - Returns: Whether or not `lhs` and `rhs` are _not_ equal. + /// + /// Two instances of this type can be compared; if either instance is equal to + /// ``failure``, it will compare equal to any instance except ``success``. To + /// check if two instances are not exactly equal, use the ``!==(_:_:)`` + /// operator: + /// + /// ```swift + /// let lhs: ExitCondition = .failure + /// let rhs: ExitCondition = .signal(SIGINT) + /// print(lhs != rhs) // prints "false" + /// print(lhs !== rhs) // prints "true" + /// ``` + /// + /// This special behavior means that the ``!=(_:_:)`` operator is not + /// transitive, and does not satisfy the requirements of + /// [`Equatable`](https://developer.apple.com/documentation/swift/equatable) + /// or [`Hashable`](https://developer.apple.com/documentation/swift/hashable). + /// + /// For any values `a` and `b`, `a == b` implies that `a != b` is `false`. + public static func !=(lhs: Self, rhs: Self) -> Bool { +#if SWT_NO_EXIT_TESTS + fatalError("Unsupported") +#else + !(lhs == rhs) +#endif + } + + /// Check whether or not two values of this type are identical. + /// + /// - Parameters: + /// - lhs: One value to compare. + /// - rhs: Another value to compare. + /// + /// - Returns: Whether or not `lhs` and `rhs` are identical. + /// + /// Two instances of this type can be compared; if either instance is equal to + /// ``failure``, it will compare equal to any instance except ``success``. To + /// check if two instances are exactly equal, use the ``===(_:_:)`` operator: + /// + /// ```swift + /// let lhs: ExitCondition = .failure + /// let rhs: ExitCondition = .signal(SIGINT) + /// print(lhs == rhs) // prints "true" + /// print(lhs === rhs) // prints "false" + /// ``` + /// + /// This special behavior means that the ``==(_:_:)`` operator is not + /// transitive, and does not satisfy the requirements of + /// [`Equatable`](https://developer.apple.com/documentation/swift/equatable) + /// or [`Hashable`](https://developer.apple.com/documentation/swift/hashable). + /// + /// For any values `a` and `b`, `a === b` implies that `a !== b` is `false`. + public static func ===(lhs: Self, rhs: Self) -> Bool { + return switch (lhs, rhs) { + case (.failure, .failure): + true case let (.exitCode(lhs), .exitCode(rhs)): lhs == rhs #if !os(Windows) case let (.signal(lhs), .signal(rhs)): lhs == rhs - case (.signal, .failure), (.failure, .signal): - // All terminating signals are considered failures. - true - case (.signal, .exitCode), (.exitCode, .signal): - // Signals do not match exit codes. - false #endif + default: + false } } + + /// Check whether or not two values of this type are _not_ identical. + /// + /// - Parameters: + /// - lhs: One value to compare. + /// - rhs: Another value to compare. + /// + /// - Returns: Whether or not `lhs` and `rhs` are _not_ identical. + /// + /// Two instances of this type can be compared; if either instance is equal to + /// ``failure``, it will compare equal to any instance except ``success``. To + /// check if two instances are not exactly equal, use the ``!==(_:_:)`` + /// operator: + /// + /// ```swift + /// let lhs: ExitCondition = .failure + /// let rhs: ExitCondition = .signal(SIGINT) + /// print(lhs != rhs) // prints "false" + /// print(lhs !== rhs) // prints "true" + /// ``` + /// + /// This special behavior means that the ``!=(_:_:)`` operator is not + /// transitive, and does not satisfy the requirements of + /// [`Equatable`](https://developer.apple.com/documentation/swift/equatable) + /// or [`Hashable`](https://developer.apple.com/documentation/swift/hashable). + /// + /// For any values `a` and `b`, `a === b` implies that `a !== b` is `false`. + public static func !==(lhs: Self, rhs: Self) -> Bool { +#if SWT_NO_EXIT_TESTS + fatalError("Unsupported") +#else + !(lhs === rhs) +#endif + } } diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 798e95d0a..c99e0945c 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -21,7 +21,7 @@ public struct ExitTest: Sendable { public var expectedExitCondition: ExitCondition /// The body closure of the exit test. - fileprivate var body: @Sendable () async -> Void + fileprivate var body: @Sendable () async throws -> Void /// The source location of the exit test. /// @@ -37,12 +37,16 @@ public struct ExitTest: Sendable { /// terminate the process in a way that causes the corresponding expectation /// to fail. public func callAsFunction() async -> Never { - await body() + do { + try await body() + } catch { + _errorInMain(error) + } // Run some glue code that terminates the process with an exit condition // that does not match the expected one. If the exit test's body doesn't // terminate, we'll manually call exit() and cause the test to fail. - let expectingFailure = expectedExitCondition.matches(.failure) + let expectingFailure = expectedExitCondition == .failure exit(expectingFailure ? EXIT_SUCCESS : EXIT_FAILURE) } } @@ -63,7 +67,7 @@ public protocol __ExitTestContainer { static var __sourceLocation: SourceLocation { get } /// The body function of the exit test. - static var __body: @Sendable () async -> Void { get } + static var __body: @Sendable () async throws -> Void { get } } extension ExitTest { @@ -118,7 +122,7 @@ extension ExitTest { /// convention. func callExitTest( exitsWith expectedExitCondition: ExitCondition, - performing body: @escaping @Sendable () async -> Void, + performing body: @escaping @Sendable () async throws -> Void, expression: __Expression, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -150,7 +154,7 @@ func callExitTest( } return __checkValue( - expectedExitCondition.matches(actualExitCondition), + expectedExitCondition == actualExitCondition, expression: expression, expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(actualExitCondition), mismatchedExitConditionDescription: String(describingForTest: expectedExitCondition), diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index dd4b8875d..1df9acc3b 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -101,6 +101,34 @@ public macro require( sourceLocation: SourceLocation = #_sourceLocation ) -> Bool = #externalMacro(module: "TestingMacros", type: "AmbiguousRequireMacro") +/// Unwrap an optional value or, if it is `nil`, fail and throw an error. +/// +/// - Parameters: +/// - optionalValue: The optional value to be unwrapped. +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// +/// - Returns: The unwrapped value of `optionalValue`. +/// +/// - Throws: An instance of ``ExpectationFailedError`` if `optionalValue` is +/// `nil`. +/// +/// If `optionalValue` is `nil`, an ``Issue`` is recorded for the test that is +/// running in the current task and an instance of ``ExpectationFailedError`` is +/// thrown. +/// +/// This overload of ``require(_:_:sourceLocation:)-6w9oo`` is used when a +/// non-optional, non-`Bool` value is passed to `#require()`. It emits a warning +/// diagnostic indicating that the expectation is redundant. +@freestanding(expression) +@_documentation(visibility: private) +public macro require( + _ optionalValue: T, + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation +) -> T = #externalMacro(module: "TestingMacros", type: "NonOptionalRequireMacro") + // MARK: - Matching errors by type /// Check that an expression always throws an error of a given type. @@ -440,7 +468,9 @@ public macro require( /// a clean environment for execution, it is not called within the context of /// the original test. If `expression` does not terminate the child process, the /// process is terminated automatically as if the main function of the child -/// process were allowed to return naturally. +/// process were allowed to return naturally. If an error is thrown from +/// `expression`, it is handed as if the error were thrown from `main()` and the +/// process is terminated. /// /// Once the child process terminates, the parent process resumes and compares /// its exit status against `exitCondition`. If they match, the exit test has @@ -488,8 +518,8 @@ public macro require( /// issues should be attributed. /// - expression: The expression to be evaluated. /// -/// - Throws: An instance of ``ExpectationFailedError`` if `condition` evaluates -/// to `false`. +/// - Throws: An instance of ``ExpectationFailedError`` if the exit condition of +/// the child process does not equal `expectedExitCondition`. /// /// Use this overload of `#require()` when an expression will cause the current /// process to terminate and the nature of that termination will determine if @@ -515,7 +545,9 @@ public macro require( /// a clean environment for execution, it is not called within the context of /// the original test. If `expression` does not terminate the child process, the /// process is terminated automatically as if the main function of the child -/// process were allowed to return naturally. +/// process were allowed to return naturally. If an error is thrown from +/// `expression`, it is handed as if the error were thrown from `main()` and the +/// process is terminated. /// /// Once the child process terminates, the parent process resumes and compares /// its exit status against `exitCondition`. If they match, the exit test has @@ -550,5 +582,5 @@ public macro require( exitsWith expectedExitCondition: ExitCondition, _ comment: @autoclosure () -> Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation, - performing expression: @convention(thin) () async -> Void + performing expression: @convention(thin) () async throws -> Void ) = #externalMacro(module: "TestingMacros", type: "ExitTestRequireMacro") diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index 2a3e137a3..1a269a941 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 { @@ -1103,7 +1105,7 @@ public func __checkClosureCall( @_spi(Experimental) public func __checkClosureCall( exitsWith expectedExitCondition: ExitCondition, - performing body: @convention(thin) () async -> Void, + performing body: @convention(thin) () async throws -> Void, expression: __Expression, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -1111,7 +1113,7 @@ public func __checkClosureCall( ) async -> Result { await callExitTest( exitsWith: expectedExitCondition, - performing: { await body() }, + performing: { try await body() }, expression: expression, comments: comments(), isRequired: isRequired, diff --git a/Sources/Testing/Issues/Confirmation.swift b/Sources/Testing/Issues/Confirmation.swift index 2ce3f1910..b1a1d23e6 100644 --- a/Sources/Testing/Issues/Confirmation.swift +++ b/Sources/Testing/Issues/Confirmation.swift @@ -179,6 +179,22 @@ public func confirmation( return try await body(confirmation) } +/// An overload of ``confirmation(_:expectedCount:sourceLocation:_:)-9bfdc`` +/// that handles the unbounded range operator (`...`). +/// +/// This overload is necessary because `UnboundedRange` does not conform to +/// `RangeExpression`. It effectively always succeeds because any number of +/// confirmations matches, so it is marked unavailable and is not implemented. +@available(*, unavailable, message: "Unbounded range '...' has no effect when used with a confirmation.") +public func confirmation( + _ comment: Comment? = nil, + expectedCount: UnboundedRange, + sourceLocation: SourceLocation = #_sourceLocation, + _ body: (Confirmation) async throws -> R +) async rethrows -> R { + fatalError("Unsupported") +} + @_spi(Experimental) extension Confirmation { /// A protocol that describes a range expression that can be used with diff --git a/Sources/Testing/Parameterization/TypeInfo.swift b/Sources/Testing/Parameterization/TypeInfo.swift index 671674531..1ba3bea06 100644 --- a/Sources/Testing/Parameterization/TypeInfo.swift +++ b/Sources/Testing/Parameterization/TypeInfo.swift @@ -18,7 +18,7 @@ public struct TypeInfo: Sendable { /// /// - Parameters: /// - type: The concrete metatype. - case type(_ type: Any.Type) + case type(_ type: any ~Copyable.Type) /// The type info represents a metatype, but a reference to that metatype is /// not available at runtime. @@ -38,7 +38,7 @@ public struct TypeInfo: Sendable { /// /// If this instance was created from a type name, or if it was previously /// encoded and decoded, the value of this property is `nil`. - public var type: Any.Type? { + public var type: (any ~Copyable.Type)? { if case let .type(type) = _kind { return type } @@ -57,7 +57,7 @@ public struct TypeInfo: Sendable { /// /// - Parameters: /// - type: The type which this instance should describe. - init(describing type: Any.Type) { + init(describing type: any ~Copyable.Type) { _kind = .type(type) } @@ -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 @@ -172,7 +183,9 @@ extension TypeInfo { } switch _kind { case let .type(type): - return _mangledTypeName(type) + // _mangledTypeName() works with move-only types, but its signature has + // not been updated yet. SEE: rdar://134278607 + return _mangledTypeName(unsafeBitCast(type, to: Any.Type.self)) case let .nameOnly(_, _, mangledName): return mangledName } @@ -299,7 +312,7 @@ extension TypeInfo: Hashable { public static func ==(lhs: Self, rhs: Self) -> Bool { switch (lhs._kind, rhs._kind) { case let (.type(lhs), .type(rhs)): - return lhs == rhs + return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) default: return lhs.fullyQualifiedNameComponents == rhs.fullyQualifiedNameComponents } @@ -310,6 +323,21 @@ extension TypeInfo: Hashable { } } +// MARK: - ObjectIdentifier support + +extension ObjectIdentifier { + /// Initialize an instance of this type from a type reference. + /// + /// - Parameters: + /// - type: The type to initialize this instance from. + /// + /// - Bug: The standard library should support this conversion. + /// ([134276458](rdar://134276458), [134415960](rdar://134415960)) + fileprivate init(_ type: any ~Copyable.Type) { + self.init(unsafeBitCast(type, to: Any.Type.self)) + } +} + // MARK: - Codable extension TypeInfo: Codable { 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/Environment.swift b/Sources/Testing/Support/Environment.swift index e4441cfd9..f94d662fe 100644 --- a/Sources/Testing/Support/Environment.swift +++ b/Sources/Testing/Support/Environment.swift @@ -188,7 +188,7 @@ enum Environment { return nil case let errorCode: let error = Win32Error(rawValue: errorCode) - fatalError("unexpected error when getting environment variable '\(name)': \(error) (\(errorCode))") + fatalError("Unexpected error when getting environment variable '\(name)': \(error) (\(errorCode))") } } else if count > buffer.count { // Try again with the larger count. diff --git a/Sources/Testing/Support/FileHandle.swift b/Sources/Testing/Support/FileHandle.swift index d38f65f2a..453cedcd0 100644 --- a/Sources/Testing/Support/FileHandle.swift +++ b/Sources/Testing/Support/FileHandle.swift @@ -257,7 +257,7 @@ extension FileHandle { try withUnsafeCFILEHandle { file in try withUnsafeTemporaryAllocation(byteCount: 1024, alignment: 1) { buffer in repeat { - let countRead = fread(buffer.baseAddress, 1, buffer.count, file) + let countRead = fread(buffer.baseAddress!, 1, buffer.count, file) if 0 != ferror(file) { throw CError(rawValue: swt_errno()) } @@ -295,7 +295,7 @@ extension FileHandle { } } - let countWritten = fwrite(bytes.baseAddress, MemoryLayout.stride, bytes.count, file) + let countWritten = fwrite(bytes.baseAddress!, MemoryLayout.stride, bytes.count, file) if countWritten < bytes.count { throw CError(rawValue: swt_errno()) } diff --git a/Sources/Testing/Test+Macro.swift b/Sources/Testing/Test+Macro.swift index 3a9ea30d9..7fc2cf0eb 100644 --- a/Sources/Testing/Test+Macro.swift +++ b/Sources/Testing/Test+Macro.swift @@ -106,7 +106,7 @@ extension Test { /// - Warning: This function is used to implement the `@Suite` macro. Do not /// call it directly. public static func __type( - _ containingType: Any.Type, + _ containingType: any ~Copyable.Type, displayName: String? = nil, traits: [any SuiteTrait], sourceLocation: SourceLocation @@ -159,7 +159,7 @@ extension Test { /// call it directly. public static func __function( named testFunctionName: String, - in containingType: Any.Type?, + in containingType: (any ~Copyable.Type)?, xcTestCompatibleSelector: __XCTestCompatibleSelector?, displayName: String? = nil, traits: [any TestTrait], @@ -167,7 +167,13 @@ extension Test { parameters: [__Parameter] = [], testFunction: @escaping @Sendable () async throws -> Void ) -> Self { - let containingTypeInfo = containingType.map(TypeInfo.init(describing:)) + // Don't use Optional.map here due to a miscompile/crash. Expand out to an + // if expression instead. SEE: rdar://134280902 + let containingTypeInfo: TypeInfo? = if let containingType { + TypeInfo(describing: containingType) + } else { + nil + } let caseGenerator = { @Sendable in Case.Generator(testFunction: testFunction) } return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: containingTypeInfo, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: []) } @@ -235,7 +241,7 @@ extension Test { /// call it directly. public static func __function( named testFunctionName: String, - in containingType: Any.Type?, + in containingType: (any ~Copyable.Type)?, xcTestCompatibleSelector: __XCTestCompatibleSelector?, displayName: String? = nil, traits: [any TestTrait], @@ -244,7 +250,11 @@ extension Test { parameters paramTuples: [__Parameter], testFunction: @escaping @Sendable (C.Element) async throws -> Void ) -> Self where C: Collection & Sendable, C.Element: Sendable { - let containingTypeInfo = containingType.map(TypeInfo.init(describing:)) + let containingTypeInfo: TypeInfo? = if let containingType { + TypeInfo(describing: containingType) + } else { + nil + } let parameters = paramTuples.parameters let caseGenerator = { @Sendable in Case.Generator(arguments: try await collection(), parameters: parameters, testFunction: testFunction) } return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: containingTypeInfo, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters) @@ -366,7 +376,7 @@ extension Test { /// call it directly. public static func __function( named testFunctionName: String, - in containingType: Any.Type?, + in containingType: (any ~Copyable.Type)?, xcTestCompatibleSelector: __XCTestCompatibleSelector?, displayName: String? = nil, traits: [any TestTrait], @@ -375,7 +385,11 @@ extension Test { parameters paramTuples: [__Parameter], testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void ) -> Self where C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable { - let containingTypeInfo = containingType.map(TypeInfo.init(describing:)) + let containingTypeInfo: TypeInfo? = if let containingType { + TypeInfo(describing: containingType) + } else { + nil + } let parameters = paramTuples.parameters let caseGenerator = { @Sendable in try await Case.Generator(arguments: collection1(), collection2(), parameters: parameters, testFunction: testFunction) } return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: containingTypeInfo, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters) @@ -390,7 +404,7 @@ extension Test { /// call it directly. public static func __function( named testFunctionName: String, - in containingType: Any.Type?, + in containingType: (any ~Copyable.Type)?, xcTestCompatibleSelector: __XCTestCompatibleSelector?, displayName: String? = nil, traits: [any TestTrait], @@ -399,7 +413,11 @@ extension Test { parameters paramTuples: [__Parameter], testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void ) -> Self where C: Collection & Sendable, C.Element == (E1, E2), E1: Sendable, E2: Sendable { - let containingTypeInfo = containingType.map(TypeInfo.init(describing:)) + let containingTypeInfo: TypeInfo? = if let containingType { + TypeInfo(describing: containingType) + } else { + nil + } let parameters = paramTuples.parameters let caseGenerator = { @Sendable in Case.Generator(arguments: try await collection(), parameters: parameters, testFunction: testFunction) } return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: containingTypeInfo, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters) @@ -417,7 +435,7 @@ extension Test { /// call it directly. public static func __function( named testFunctionName: String, - in containingType: Any.Type?, + in containingType: (any ~Copyable.Type)?, xcTestCompatibleSelector: __XCTestCompatibleSelector?, displayName: String? = nil, traits: [any TestTrait], @@ -426,7 +444,11 @@ extension Test { parameters paramTuples: [__Parameter], testFunction: @escaping @Sendable ((Key, Value)) async throws -> Void ) -> Self where Key: Sendable, Value: Sendable { - let containingTypeInfo = containingType.map(TypeInfo.init(describing:)) + let containingTypeInfo: TypeInfo? = if let containingType { + TypeInfo(describing: containingType) + } else { + nil + } let parameters = paramTuples.parameters let caseGenerator = { @Sendable in Case.Generator(arguments: try await dictionary(), parameters: parameters, testFunction: testFunction) } return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: containingTypeInfo, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters) @@ -438,7 +460,7 @@ extension Test { /// call it directly. public static func __function( named testFunctionName: String, - in containingType: Any.Type?, + in containingType: (any ~Copyable.Type)?, xcTestCompatibleSelector: __XCTestCompatibleSelector?, displayName: String? = nil, traits: [any TestTrait], @@ -447,7 +469,11 @@ extension Test { parameters paramTuples: [__Parameter], testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void ) -> Self where C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable { - let containingTypeInfo = containingType.map(TypeInfo.init(describing:)) + let containingTypeInfo: TypeInfo? = if let containingType { + TypeInfo(describing: containingType) + } else { + nil + } let parameters = paramTuples.parameters let caseGenerator = { @Sendable in Case.Generator(arguments: try await zippedCollections(), parameters: parameters) { @@ -460,22 +486,22 @@ extension Test { // MARK: - Helper functions -/// A value that abstracts away whether or not the `try` keyword is needed on an -/// expression. +/// A function that abstracts away whether or not the `try` keyword is needed on +/// an expression. /// -/// - Warning: This value is used to implement the `@Test` macro. Do not use +/// - Warning: This function is used to implement the `@Test` macro. Do not use /// it directly. -@inlinable public var __requiringTry: Void { - @inlinable get throws {} +@inlinable public func __requiringTry(_ value: consuming T) throws -> T where T: ~Copyable { + value } -/// A value that abstracts away whether or not the `await` keyword is needed on -/// an expression. +/// A function that abstracts away whether or not the `await` keyword is needed +/// on an expression. /// -/// - Warning: This value is used to implement the `@Test` macro. Do not use +/// - Warning: This function is used to implement the `@Test` macro. Do not use /// it directly. -@inlinable public var __requiringAwait: Void { - @inlinable get async {} +@inlinable public func __requiringAwait(_ value: consuming T, isolation: isolated (any Actor)? = #isolation) async -> T where T: ~Copyable { + value } #if !SWT_NO_GLOBAL_ACTORS diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 690489702..b82e23c3a 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -293,6 +293,26 @@ public struct AmbiguousRequireMacro: RefinedConditionMacro { } } +/// A type describing the expansion of the `#require()` macro when it is passed +/// a non-optional, non-`Bool` value. +/// +/// This type is otherwise exactly equivalent to ``RequireMacro``. +public struct NonOptionalRequireMacro: RefinedConditionMacro { + public typealias Base = RequireMacro + + public static func expansion( + of macro: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext + ) throws -> ExprSyntax { + if let argument = macro.arguments.first { + context.diagnose(.nonOptionalRequireIsRedundant(argument.expression, in: macro)) + } + + // Perform the normal macro expansion for #require(). + return try RequireMacro.expansion(of: macro, in: context) + } +} + // MARK: - /// A syntax visitor that looks for uses of `#expect()` and `#require()` nested @@ -362,7 +382,7 @@ extension ExitTestConditionMacro { static var __sourceLocation: Testing.SourceLocation { \(createSourceLocationExpr(of: macro, context: context)) } - static var __body: @Sendable () async -> Void { + static var __body: @Sendable () async throws -> Void { \(bodyArgumentExpr.trimmed) } static var __expectedExitCondition: Testing.ExitCondition { @@ -370,7 +390,15 @@ extension ExitTestConditionMacro { } } """ - arguments[trailingClosureIndex].expression = "{ \(enumDecl) }" + + // Explicitly include a closure signature to work around a compiler bug + // type-checking thin throwing functions after macro expansion. + // SEE: rdar://133979438 + arguments[trailingClosureIndex].expression = """ + { () async throws in + \(enumDecl) + } + """ // Replace the exit test body (as an argument to the macro) with a stub // closure that hosts the type we created above. diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index 5658123a0..a79255bc8 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -63,7 +63,11 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { diagnostics += diagnoseIssuesWithLexicalContext(context.lexicalContext, containing: declaration, attribute: suiteAttribute) diagnostics += diagnoseIssuesWithLexicalContext(declaration, containing: declaration, attribute: suiteAttribute) - // Suites inheriting from XCTestCase are not supported. + // Suites inheriting from XCTestCase are not supported. This check is + // duplicated in TestDeclarationMacro but is not part of + // diagnoseIssuesWithLexicalContext() because it doesn't need to recurse + // across the entire lexical context list, just the innermost type + // declaration. if let declaration = declaration.asProtocol((any DeclGroupSyntax).self), declaration.inherits(fromTypeNamed: "XCTestCase", inModuleNamed: "XCTest") { diagnostics.append(.xcTestCaseNotSupported(declaration, whenUsing: suiteAttribute)) diff --git a/Sources/TestingMacros/Support/AvailabilityGuards.swift b/Sources/TestingMacros/Support/AvailabilityGuards.swift index 1a6fa1f05..8a628094f 100644 --- a/Sources/TestingMacros/Support/AvailabilityGuards.swift +++ b/Sources/TestingMacros/Support/AvailabilityGuards.swift @@ -118,7 +118,7 @@ private func _createAvailabilityTraitExpr( return ".__unavailable(message: \(message), sourceLocation: \(sourceLocationExpr))" default: - fatalError("Unsupported keyword \(whenKeyword) passed to \(#function)") + fatalError("Unsupported keyword \(whenKeyword) passed to \(#function). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") } } diff --git a/Sources/TestingMacros/Support/DiagnosticMessage.swift b/Sources/TestingMacros/Support/DiagnosticMessage.swift index 2096068d5..8fa2b5519 100644 --- a/Sources/TestingMacros/Support/DiagnosticMessage.swift +++ b/Sources/TestingMacros/Support/DiagnosticMessage.swift @@ -670,6 +670,26 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage { ) } + /// Create a diagnostic messages stating that the expression passed to + /// `#require()` is not optional and the macro is redundant. + /// + /// - Parameters: + /// - expr: The non-optional expression. + /// + /// - Returns: A diagnostic message. + static func nonOptionalRequireIsRedundant(_ expr: ExprSyntax, in macro: some FreestandingMacroExpansionSyntax) -> Self { + // We do not provide fix-its because we cannot see the leading "try" keyword + // so we can't provide a valid fix-it to remove the macro either. We can + // provide a fix-it to add "as Optional", but only providing that fix-it may + // confuse or mislead developers (and that's presumably usually the *wrong* + // fix-it to select anyway.) + Self( + syntax: Syntax(expr), + message: "\(_macroName(macro)) is redundant because '\(expr.trimmed)' never equals 'nil'", + severity: .warning + ) + } + /// Create a diagnostic message stating that a condition macro nested inside /// an exit test will not record any diagnostics. /// diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 1f9025f08..dc573ac12 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -59,6 +59,16 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { // Check if the lexical context is appropriate for a suite or test. diagnostics += diagnoseIssuesWithLexicalContext(context.lexicalContext, containing: declaration, attribute: testAttribute) + // Suites inheriting from XCTestCase are not supported. We are a bit + // conservative here in this check and only check the immediate context. + // Presumably, if there's an intermediate lexical context that is *not* a + // type declaration, then it must be a function or closure (disallowed + // elsewhere) and thus the test function is not a member of any type. + if let containingTypeDecl = context.lexicalContext.first?.asProtocol((any DeclGroupSyntax).self), + containingTypeDecl.inherits(fromTypeNamed: "XCTestCase", inModuleNamed: "XCTest") { + diagnostics.append(.containingNodeUnsupported(containingTypeDecl, whenUsing: testAttribute, on: declaration)) + } + // Only one @Test attribute is supported. let suiteAttributes = function.attributes(named: "Test") if suiteAttributes.count > 1 { @@ -278,17 +288,17 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { // detecting isolation to other global actors. lazy var isMainActorIsolated = !functionDecl.attributes(named: "MainActor", inModuleNamed: "Swift").isEmpty var forwardCall: (ExprSyntax) -> ExprSyntax = { - "try await (\($0), Testing.__requiringTry, Testing.__requiringAwait).0" + "try await Testing.__requiringTry(Testing.__requiringAwait(\($0)))" } let forwardInit = forwardCall if functionDecl.noasyncAttribute != nil { if isMainActorIsolated { forwardCall = { - "try await MainActor.run { try (\($0), Testing.__requiringTry).0 }" + "try await MainActor.run { try Testing.__requiringTry(\($0)) }" } } else { forwardCall = { - "try { try (\($0), Testing.__requiringTry).0 }()" + "try { try Testing.__requiringTry(\($0)) }()" } } } diff --git a/Sources/TestingMacros/TestingMacrosMain.swift b/Sources/TestingMacros/TestingMacrosMain.swift index 1bad7bc8b..8603a2031 100644 --- a/Sources/TestingMacros/TestingMacrosMain.swift +++ b/Sources/TestingMacros/TestingMacrosMain.swift @@ -23,6 +23,7 @@ struct TestingMacrosMain: CompilerPlugin { ExpectMacro.self, RequireMacro.self, AmbiguousRequireMacro.self, + NonOptionalRequireMacro.self, ExitTestExpectMacro.self, ExitTestRequireMacro.self, TagMacro.self, diff --git a/Tests/TestingMacrosTests/ConditionMacroTests.swift b/Tests/TestingMacrosTests/ConditionMacroTests.swift index 4bdbd88bf..5f4869055 100644 --- a/Tests/TestingMacrosTests/ConditionMacroTests.swift +++ b/Tests/TestingMacrosTests/ConditionMacroTests.swift @@ -331,6 +331,19 @@ struct ConditionMacroTests { #expect(diagnostics.isEmpty) } + @Test("#require(non-optional value) produces a diagnostic", + arguments: [ + "#requireNonOptional(expression)", + ] + ) + func requireNonOptionalProducesDiagnostic(input: String) throws { + let (_, diagnostics) = try parse(input) + + let diagnostic = try #require(diagnostics.first) + #expect(diagnostic.diagMessage.severity == .warning) + #expect(diagnostic.message.contains("is redundant")) + } + #if !SWT_NO_EXIT_TESTS @Test("Expectation inside an exit test diagnoses", arguments: [ diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index 6330d3fcf..fffa06664 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -82,6 +82,10 @@ struct TestDeclarationMacroTests { "Attribute 'Suite' cannot be applied to a subclass of 'XCTestCase'", "@Suite final class C: XCTest.XCTestCase {}": "Attribute 'Suite' cannot be applied to a subclass of 'XCTestCase'", + "final class C: XCTestCase { @Test func f() {} }": + "Attribute 'Test' cannot be applied to a function within class 'C'", + "final class C: XCTest.XCTestCase { @Test func f() {} }": + "Attribute 'Test' cannot be applied to a function within class 'C'", // Unsupported inheritance "@Suite protocol P {}": diff --git a/Tests/TestingMacrosTests/TestSupport/Parse.swift b/Tests/TestingMacrosTests/TestSupport/Parse.swift index 734c39fc0..4fcfb22c3 100644 --- a/Tests/TestingMacrosTests/TestSupport/Parse.swift +++ b/Tests/TestingMacrosTests/TestSupport/Parse.swift @@ -22,6 +22,7 @@ fileprivate let allMacros: [String: any Macro.Type] = [ "expect": ExpectMacro.self, "require": RequireMacro.self, "requireAmbiguous": AmbiguousRequireMacro.self, // different name needed only for unit testing + "requireNonOptional": NonOptionalRequireMacro.self, // different name needed only for unit testing "expectExitTest": ExitTestRequireMacro.self, // different name needed only for unit testing "requireExitTest": ExitTestRequireMacro.self, // different name needed only for unit testing "Suite": SuiteDeclarationMacro.self, diff --git a/Tests/TestingTests/BacktraceTests.swift b/Tests/TestingTests/BacktraceTests.swift index 522865e30..f5a2c497c 100644 --- a/Tests/TestingTests/BacktraceTests.swift +++ b/Tests/TestingTests/BacktraceTests.swift @@ -34,8 +34,8 @@ struct BacktraceTests { } @Test("Backtrace.current() is populated") - func currentBacktrace() throws { - let backtrace = try #require(Backtrace.current()) + func currentBacktrace() { + let backtrace = Backtrace.current() #expect(!backtrace.addresses.isEmpty) } diff --git a/Tests/TestingTests/CustomTestStringConvertibleTests.swift b/Tests/TestingTests/CustomTestStringConvertibleTests.swift new file mode 100644 index 000000000..f327c88e1 --- /dev/null +++ b/Tests/TestingTests/CustomTestStringConvertibleTests.swift @@ -0,0 +1,79 @@ +// +// 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 +// + +@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing + +private import _TestingInternals + +@Suite("CustomTestStringConvertible Tests") +struct CustomTestStringConvertibleTests { + @Test func optionals() { + #expect(String(describingForTest: 0 as Int?) == "0") + #expect(String(describingForTest: "abc" as String?) == #""abc""#) + #expect(String(describingForTest: nil as Int?) == "nil") + #expect(String(describingForTest: nil as String?) == "nil") + #expect(String(describingForTest: nil as _OptionalNilComparisonType) == "nil") + } + + @Test func strings() { + #expect(String(describingForTest: "abc") == #""abc""#) + #expect(String(describingForTest: "abc"[...] as Substring) == #""abc""#) + } + + @Test func ranges() { + #expect(String(describingForTest: 0 ... 1) == "0 ... 1") + #expect(String(describingForTest: 0...) == "0...") + #expect(String(describingForTest: ...1) == "...1") + #expect(String(describingForTest: ..<1) == "..<1") + #expect(String(describingForTest: 0 ..< 1) == "0 ..< 1") + } + + @Test func types() { + #expect(String(describingForTest: Self.self) == "CustomTestStringConvertibleTests") + #expect(String(describingForTest: NonCopyableType.self) == "NonCopyableType") + } + + @Test func enumerations() { + #expect(String(describingForTest: SWTTestEnumeration.A) == "SWTTestEnumeration(rawValue: \(SWTTestEnumeration.A.rawValue))") + #expect(String(describingForTest: SomeEnum.elitSedDoEiusmod) == ".elitSedDoEiusmod") + } + + @Test func otherProtocols() { + #expect(String(describingForTest: CustomStringConvertibleType()) == "Lorem ipsum") + #expect(String(describingForTest: TextOutputStreamableType()) == "Dolor sit amet") + #expect(String(describingForTest: CustomDebugStringConvertibleType()) == "Consectetur adipiscing") + } +} + +// MARK: - Fixtures + +private struct NonCopyableType: ~Copyable {} + +private struct CustomStringConvertibleType: CustomStringConvertible { + var description: String { + "Lorem ipsum" + } +} + +private struct TextOutputStreamableType: TextOutputStreamable { + func write(to target: inout some TextOutputStream) { + target.write("Dolor sit amet") + } +} + +private struct CustomDebugStringConvertibleType: CustomDebugStringConvertible { + var debugDescription: String { + "Consectetur adipiscing" + } +} + +private enum SomeEnum { + case elitSedDoEiusmod +} diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 55c22a9bb..fad1da180 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -32,6 +32,9 @@ private import _TestingInternals await Task.yield() exit(123) } + await #expect(exitsWith: .failure) { + throw MyError() + } #if !os(Windows) await #expect(exitsWith: .signal(SIGKILL)) { _ = kill(getpid(), SIGKILL) @@ -197,6 +200,51 @@ private import _TestingInternals }.run(configuration: configuration) } } + +#if !os(Linux) + @Test("Exit test reports > 8 bits of the exit code") + func fullWidthExitCode() async { + // On macOS and Linux, we use waitid() which per POSIX should report the + // full exit code, not just the low 8 bits. This behaviour is not + // well-documented and while Darwin correctly reports the full value, Linux + // does not (at least as of this writing) and other POSIX-like systems may + // also have issues. This test serves as a canary when adding new platforms + // that we need to document the difference. + // + // Windows does not have the 8-bit exit code restriction and always reports + // the full CInt value back to the testing library. + await #expect(exitsWith: .exitCode(512)) { + exit(512) + } + } +#endif + + @Test("Exit condition matching operators (==, !=, ===, !==)") + func exitConditionMatching() { + #expect(ExitCondition.success == .success) + #expect(ExitCondition.success === .success) + #expect(ExitCondition.success == .exitCode(EXIT_SUCCESS)) + #expect(ExitCondition.success === .exitCode(EXIT_SUCCESS)) + #expect(ExitCondition.success != .exitCode(EXIT_FAILURE)) + #expect(ExitCondition.success !== .exitCode(EXIT_FAILURE)) + + #expect(ExitCondition.failure == .failure) + #expect(ExitCondition.failure === .failure) + + #expect(ExitCondition.exitCode(EXIT_FAILURE &+ 1) != .exitCode(EXIT_FAILURE)) + #expect(ExitCondition.exitCode(EXIT_FAILURE &+ 1) !== .exitCode(EXIT_FAILURE)) + +#if !os(Windows) + #expect(ExitCondition.success != .exitCode(EXIT_FAILURE)) + #expect(ExitCondition.success !== .exitCode(EXIT_FAILURE)) + #expect(ExitCondition.success != .signal(SIGINT)) + #expect(ExitCondition.success !== .signal(SIGINT)) + #expect(ExitCondition.signal(SIGINT) == .signal(SIGINT)) + #expect(ExitCondition.signal(SIGINT) === .signal(SIGINT)) + #expect(ExitCondition.signal(SIGTERM) != .signal(SIGINT)) + #expect(ExitCondition.signal(SIGTERM) !== .signal(SIGINT)) +#endif + } } // MARK: - Fixtures diff --git a/Tests/TestingTests/IssueTests.swift b/Tests/TestingTests/IssueTests.swift index fbb9ce28c..e0618e344 100644 --- a/Tests/TestingTests/IssueTests.swift +++ b/Tests/TestingTests/IssueTests.swift @@ -113,7 +113,7 @@ final class IssueTests: XCTestCase { await Test { let x: String? = nil - _ = try #require(x ?? "hello") + _ = try #require(x ?? ("hello" as String?)) }.run(configuration: configuration) } diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index f77b698d6..5d412f996 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -369,9 +369,8 @@ struct MiscellaneousTests { #expect(firstParameter.index == 0) #expect(firstParameter.firstName == "i") #expect(firstParameter.secondName == nil) - let firstParameterTypeInfo = try #require(firstParameter.typeInfo) - #expect(firstParameterTypeInfo.fullyQualifiedName == "Swift.Int") - #expect(firstParameterTypeInfo.unqualifiedName == "Int") + #expect(firstParameter.typeInfo.fullyQualifiedName == "Swift.Int") + #expect(firstParameter.typeInfo.unqualifiedName == "Int") } catch {} do { @@ -386,9 +385,8 @@ struct MiscellaneousTests { #expect(secondParameter.index == 1) #expect(secondParameter.firstName == "j") #expect(secondParameter.secondName == "k") - let secondParameterTypeInfo = try #require(secondParameter.typeInfo) - #expect(secondParameterTypeInfo.fullyQualifiedName == "Swift.String") - #expect(secondParameterTypeInfo.unqualifiedName == "String") + #expect(secondParameter.typeInfo.fullyQualifiedName == "Swift.String") + #expect(secondParameter.typeInfo.unqualifiedName == "String") } catch {} } @@ -532,4 +530,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/Tests/TestingTests/NonCopyableSuiteTests.swift b/Tests/TestingTests/NonCopyableSuiteTests.swift new file mode 100644 index 000000000..56a530199 --- /dev/null +++ b/Tests/TestingTests/NonCopyableSuiteTests.swift @@ -0,0 +1,32 @@ +// +// 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 +// + +@testable @_spi(ForToolsIntegrationOnly) import Testing + +@Suite("Non-Copyable Tests") +struct NonCopyableTests: ~Copyable { + @Test static func staticMe() {} + @Test borrowing func borrowMe() {} + @Test consuming func consumeMe() {} + @Test mutating func mutateMe() {} + + @Test borrowing func typeComparison() { + let lhs = TypeInfo(describing: Self.self) + let rhs = TypeInfo(describing: Self.self) + + #expect(lhs == rhs) + #expect(lhs.hashValue == rhs.hashValue) + } + + @available(_mangledTypeNameAPI, *) + @Test borrowing func mangledTypeName() { + #expect(TypeInfo(describing: Self.self).mangledName != nil) + } +} diff --git a/Tests/TestingTests/ObjCInteropTests.swift b/Tests/TestingTests/ObjCInteropTests.swift index 179460af4..be12e520d 100644 --- a/Tests/TestingTests/ObjCInteropTests.swift +++ b/Tests/TestingTests/ObjCInteropTests.swift @@ -78,7 +78,9 @@ struct ObjCAndXCTestInteropTests { #expect(steps.count > 0) for step in steps { let selector = try #require(step.test.xcTestCompatibleSelector) - let testCaseClass = try #require(step.test.containingTypeInfo?.type as? NSObject.Type) + // A compiler crash occurs here without the bitcast. SEE: rdar://134277439 + let type = unsafeBitCast(step.test.containingTypeInfo?.type, to: Any.Type?.self) + let testCaseClass = try #require(type as? NSObject.Type) #expect(testCaseClass.instancesRespond(to: selector)) } } diff --git a/Tests/TestingTests/Support/FileHandleTests.swift b/Tests/TestingTests/Support/FileHandleTests.swift index a8d6e8285..89a3d246f 100644 --- a/Tests/TestingTests/Support/FileHandleTests.swift +++ b/Tests/TestingTests/Support/FileHandleTests.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -@testable import Testing +@testable @_spi(Experimental) import Testing private import _TestingInternals #if !SWT_NO_FILE_IO @@ -63,6 +63,16 @@ struct FileHandleTests { } } +#if !SWT_NO_EXIT_TESTS + @Test("Writing requires contiguous storage") + func writeIsContiguous() async { + await #expect(exitsWith: .failure) { + let fileHandle = try FileHandle.null(mode: "wb") + try fileHandle.write([1, 2, 3, 4, 5].lazy.filter { $0 == 1 }) + } + } +#endif + @Test("Can read from a file") func canRead() throws { let bytes: [UInt8] = (0 ..< 8192).map { _ in diff --git a/Tests/TestingTests/TypeInfoTests.swift b/Tests/TestingTests/TypeInfoTests.swift index e333a2c41..a8d8327b2 100644 --- a/Tests/TestingTests/TypeInfoTests.swift +++ b/Tests/TestingTests/TypeInfoTests.swift @@ -76,4 +76,4 @@ extension String { enum NestedType {} } -enum SomeEnum {} +private enum SomeEnum {} 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\">")