From a46df3fcc725f2c5aca1bc0e0d6a0a29c1ff8c68 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 20 Aug 2024 20:16:01 -0400 Subject: [PATCH 01/13] Fixes to exit tests. (#615) This PR supersedes #603, #613, and #614. Exit tests remain an experimental feature. ## Clarify that 8-bit exit codes aren't a problem on macOS and Windows. (#603) The documentation for the experimental exit tests feature currently says that on POSIX-like systems, only the low 8 bits of a process' exit code are preserved. This would be true if we used `wait()`, `wait4()`, etc. and `WEXITSTATUS()`, but we use `waitid()` instead which is [supposed to](https://pubs.opengroup.org/onlinepubs/9699919799/functions/exit.html) preserve the full exit code. It does so on Darwin, but not on Linux; Windows doesn't use `waitid()` but does report the full exit code. Now, we're not currently building for any other POSIX-like systems that support processes (WASI/Wasm doesn't count here), so I've left in some weasel words and added a canary unit test. It will let us know if/when we add a platform that where `waitid()` doesn't preserve all the bits of the exit code, and we can amend the documentation in that case. ## Implement an equality operator for ExitCondition. (#613) This PR implements `==` and `===` for `ExitCondition`, part of the experimental exit tests feature. These operators are necessary in order to allow for exit tests to support more complex matching by trailing closure (e.g. to support inspecting `stdout`.) Because `.failure` is a fuzzy case, `==` fuzzy-matches while `===` exactly matches. `Hashable` conformance is unavailable. Example usage: ```swift let lhs: ExitCondition = .failure let rhs: ExitCondition = .signal(SIGTERM) print(lhs == rhs) // prints "true" print(lhs === rhs) // prints "false" ``` ## Allow throwing an error from an exit test's body. (#614) This PR amends the signatures of the exit test macros (`#expect(exitsWith:) {}` and `try #require(exitsWith:) {}`) to allow bodies to throw errors. If they do, they are treated as uncaught errors and the child process terminates abnormally (in the same way it does if an error is thrown from the main function of a Swift program.) ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitCondition.swift | 152 +++++++++++++++--- Sources/Testing/ExitTests/ExitTest.swift | 16 +- .../Expectations/Expectation+Macro.swift | 14 +- .../ExpectationChecking+Macro.swift | 4 +- Sources/TestingMacros/ConditionMacro.swift | 12 +- Tests/TestingTests/ExitTestTests.swift | 48 ++++++ .../Support/FileHandleTests.swift | 12 +- 7 files changed, 224 insertions(+), 34 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitCondition.swift b/Sources/Testing/ExitTests/ExitCondition.swift index e3074740d..205f7d515 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,159 @@ 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 { + 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 + } + } + + /// 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 { + !(lhs == rhs) + } + + /// 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 { + !(lhs === rhs) + } } 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..c3cf787b0 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -440,7 +440,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 +490,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 +517,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 +554,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..edc6fac04 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -1103,7 +1103,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 +1111,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/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 690489702..2ee461017 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -362,7 +362,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 +370,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/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/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 From cd91e93e48ad85f1bc2c0eaa1d88fb11e489ab66 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 20 Aug 2024 20:16:55 -0400 Subject: [PATCH 02/13] Allow move-only types as suites. (#619) This PR enables using move-only types as suites. For example: ```swift @Suite struct NumberOfBeesTests: ~Copyable { @Test consuming func countBees() { var count = 0 for species in allSpecies { if species is Bee { count += species.populationCount } } #expect(count > 0) } } ``` Move-only types have a number of constraints in Swift, and those constraints aren't lifted in a test target, but generally speaking a move-only type should be able to do all the things any other type can do _as a test suite_. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/Parameterization/TypeInfo.swift | 14 ++-- Sources/Testing/Test+Macro.swift | 72 +++++++++++++------ .../TestingMacros/TestDeclarationMacro.swift | 6 +- .../TestingTests/NonCopyableSuiteTests.swift | 32 +++++++++ Tests/TestingTests/ObjCInteropTests.swift | 4 +- 5 files changed, 96 insertions(+), 32 deletions(-) create mode 100644 Tests/TestingTests/NonCopyableSuiteTests.swift diff --git a/Sources/Testing/Parameterization/TypeInfo.swift b/Sources/Testing/Parameterization/TypeInfo.swift index 671674531..5dde2700a 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) } @@ -172,7 +172,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 +301,9 @@ 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 + // == and ObjectIdentifier do not support move-only metatypes, so compare + // the bits of the types directly. SEE: rdar://134276458 + return unsafeBitCast(lhs, to: UnsafeRawPointer.self) == unsafeBitCast(rhs, to: UnsafeRawPointer.self) default: return lhs.fullyQualifiedNameComponents == rhs.fullyQualifiedNameComponents } 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/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 1f9025f08..28e416804 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -278,17 +278,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/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)) } } From 22b0ef1d822bfc7269c6f1c42059e9d8790b0119 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 20 Aug 2024 20:17:24 -0400 Subject: [PATCH 03/13] Remove stray use of old `SWT_BUILDING_WITH_CMAKE` condition. (#621) We no longer define `SWT_BUILDING_WITH_CMAKE`, but one file's still checking for it. Fix. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift | 4 ---- 1 file changed, 4 deletions(-) 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 From 097db6c526a0a4f1086fc8a8a53ae7ba24e4cbdb Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 20 Aug 2024 20:17:58 -0400 Subject: [PATCH 04/13] Warn when passing a non-optional value to `try #require(T?)`. (#620) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds an overload of `try #require()` that warns the developer if they pass a non-optional, non-`Bool` value For example, this code: ```swift let x = 0 let y = try #require(x) ``` Will produce the diagnostic: > ⚠️ '#require(\_:\_:)' is redundant because 'x' never equals 'nil' ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Expectations/Expectation+Macro.swift | 28 +++++++++++++++++++ Sources/TestingMacros/ConditionMacro.swift | 20 +++++++++++++ .../Support/DiagnosticMessage.swift | 20 +++++++++++++ Sources/TestingMacros/TestingMacrosMain.swift | 1 + .../ConditionMacroTests.swift | 13 +++++++++ .../TestSupport/Parse.swift | 1 + Tests/TestingTests/BacktraceTests.swift | 4 +-- Tests/TestingTests/IssueTests.swift | 2 +- Tests/TestingTests/MiscellaneousTests.swift | 10 +++---- 9 files changed, 90 insertions(+), 9 deletions(-) diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index c3cf787b0..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. diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 2ee461017..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 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/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/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/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..1a6130cd1 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 {} } From 8534698a52d0f057ea9491c8585533350265ab3d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 20 Aug 2024 20:18:26 -0400 Subject: [PATCH 05/13] Reduce overhead of `.expectationChecked` event handling in `#expect()`. (#610) This PR refactors the implementation of `#expect()` and `#require()` a bit such that they don't incur more than minimal overhead posting `.expectationChecked` events if nobody is listening for them (which, currently, nobody is.) We considered removing `.expectationChecked` outright, but XCTest has historically had a number of requests for a way to observe calls to `XCTAssert()` etc. even when they pass, so we opted not to remove the event kind at this time. This PR also introduces a cache for fully-qualified type names so that we don't need to call into the runtime to get them as often. Overall speedup is approximately **90% or 11x**. Test time for a tight loop of 1,000,000 `#expect()` calls goes from 5.898893 seconds down to 0.515558291 seconds (as measured on my work computer.) Resolves rdar://133517028. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 1 + .../ExpectationChecking+Macro.swift | 6 ++- .../Testing/Parameterization/TypeInfo.swift | 11 +++++ .../Testing/Running/Runner.RuntimeState.swift | 44 ++++++++++++++++--- Tests/TestingTests/MiscellaneousTests.swift | 11 +++++ 5 files changed, 64 insertions(+), 9 deletions(-) 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 edc6fac04..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 { diff --git a/Sources/Testing/Parameterization/TypeInfo.swift b/Sources/Testing/Parameterization/TypeInfo.swift index 5dde2700a..d02bfaa11 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/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index 1a6130cd1..5d412f996 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -530,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)) + } } From bba4bdf0033cf581fc7f0c64c934633923ec9464 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 20 Aug 2024 20:22:49 -0400 Subject: [PATCH 06/13] Implement an overload of `confirmation()` that takes an unbounded range. (#598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unbounded ranges are not meaningful when used with `confirmation()` because _any_ confirmation count matches. As well, the `...` operator produces an instance of `UnboundedRange` which is a non-nominal type and cannot conform to `RangeExpression`. It may be non-obvious to a developer why `...` doesn't work as the `expectedCount` argument to that function when other range operators work as expected. This PR implements a stub overload of `confirmation()` that takes an unbounded range. The stub overload is marked unavailable and cannot be called. Example usage: ```swift await confirmation("Stuff happens", expectedCount: ...) { stuff in // ... } ``` Generated diagnostic: > 🛑 'confirmation(\_:expectedCount:sourceLocation:\_:)' is unavailable: Unbounded range '...' has no effect when used with a confirmation. As a reminder, using a range expression with `confirmation()` is an experimental feature and has not been API-reviewed. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Issues/Confirmation.swift | 16 ++++++++++++++++ Sources/Testing/Support/Environment.swift | 2 +- .../Support/AvailabilityGuards.swift | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) 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/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/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") } } From 61d4df9d41b562abb87893a98b6e7d4e063968bf Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 21 Aug 2024 10:24:01 -0400 Subject: [PATCH 07/13] Fix a merge conflict on main-next. (#627) This PR fixes a conflict between #610 and #619. #610 added a string cache to `TypeInfo` using `ObjectIdentifier` instances as keys. #619 added support for move-only types to `TypeInfo`. Due to rdar://134276458, move-only types cannot be used with `ObjectIdentifier`. This PR uses `UInt` instead until that issue can be resolved in a future stdlib update. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/Parameterization/TypeInfo.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/Parameterization/TypeInfo.swift b/Sources/Testing/Parameterization/TypeInfo.swift index d02bfaa11..1ba3bea06 100644 --- a/Sources/Testing/Parameterization/TypeInfo.swift +++ b/Sources/Testing/Parameterization/TypeInfo.swift @@ -312,9 +312,7 @@ extension TypeInfo: Hashable { public static func ==(lhs: Self, rhs: Self) -> Bool { switch (lhs._kind, rhs._kind) { case let (.type(lhs), .type(rhs)): - // == and ObjectIdentifier do not support move-only metatypes, so compare - // the bits of the types directly. SEE: rdar://134276458 - return unsafeBitCast(lhs, to: UnsafeRawPointer.self) == unsafeBitCast(rhs, to: UnsafeRawPointer.self) + return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) default: return lhs.fullyQualifiedNameComponents == rhs.fullyQualifiedNameComponents } @@ -325,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 { From 80e1e94459673fdd081ab8325950033b143520ab Mon Sep 17 00:00:00 2001 From: Saleem Abdulrasool Date: Wed, 21 Aug 2024 07:37:53 -0700 Subject: [PATCH 08/13] Testing: use `clock_gettime` on Android Android recommends `clock_gettime` over `timespec_get` which is available at Level 29 and newer. This is needed to build swift-testing for Android. --- Sources/Testing/Events/Clock.swift | 6 ++++++ 1 file changed, 6 insertions(+) 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 From 6ba948ac9894a72576b03800998e7e47ebe48ae7 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 21 Aug 2024 12:14:51 -0400 Subject: [PATCH 09/13] Fix CMake script missing _synchronizationAPI define --- cmake/modules/shared/AvailabilityDefinitions.cmake | 1 + 1 file changed, 1 insertion(+) 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\">") From 92bd26c621f70d4571e077b4b6469d0a647cde77 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 21 Aug 2024 14:22:20 -0400 Subject: [PATCH 10/13] Disallow `@Test` on member functions of `XCTestCase` subclasses. (#626) This PR adds a diagnostic if we detect that `@Test` has been used on a member function of an `XCTestCase` subclass. This was meant to be disallowed, but we neglected to add a check after adopting `lexicalContext`. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/TestingMacros/SuiteDeclarationMacro.swift | 6 +++++- Sources/TestingMacros/TestDeclarationMacro.swift | 10 ++++++++++ .../TestingMacrosTests/TestDeclarationMacroTests.swift | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) 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/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 28e416804..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 { 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 {}": From 2e3ed1f437aff015d32e7ff68c8ff468ecd7360b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 21 Aug 2024 14:23:06 -0400 Subject: [PATCH 11/13] Improve test coverage of CustomTestStringConvertible.swift. (#628) This PR adds 100% test coverage to CustomTestStringConvertible.swift. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../CustomTestStringConvertibleTests.swift | 79 +++++++++++++++++++ Tests/TestingTests/TypeInfoTests.swift | 2 +- 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 Tests/TestingTests/CustomTestStringConvertibleTests.swift 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/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 {} From d080ee2ff59e4a66109e9823aa91ca67b8dca7e7 Mon Sep 17 00:00:00 2001 From: Saleem Abdulrasool Date: Wed, 21 Aug 2024 11:23:01 -0700 Subject: [PATCH 12/13] Testing: add some force unwraps for Android Add some force unwraps to address nullability differences on Android from other platforms. --- Sources/Testing/Support/FileHandle.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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()) } From 888caadaf01013775187b9d96168a23ad15e7efa Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 22 Aug 2024 09:27:05 -0400 Subject: [PATCH 13/13] Fix build errors in `ExitCondition` operators on platforms without exit tests. (#635) On platforms without exit tests, `ExitCondition` is marked unavailable. On those platforms, the new operators on `ExitCondition` call each other and the compiler complains because they're calling unavailable symbols. Silence the compiler. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitCondition.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/Testing/ExitTests/ExitCondition.swift b/Sources/Testing/ExitTests/ExitCondition.swift index 205f7d515..ed6552e09 100644 --- a/Sources/Testing/ExitTests/ExitCondition.swift +++ b/Sources/Testing/ExitTests/ExitCondition.swift @@ -105,6 +105,9 @@ extension ExitCondition { /// /// 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 @@ -116,6 +119,7 @@ extension ExitCondition { default: lhs === rhs } +#endif } /// Check whether or not two values of this type are _not_ equal. @@ -145,7 +149,11 @@ extension ExitCondition { /// /// 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. @@ -215,6 +223,10 @@ extension ExitCondition { /// /// 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 } }