diff --git a/Documentation/ABI/TestContent.md b/Documentation/ABI/TestContent.md index cb68a2d6e..be2493530 100644 --- a/Documentation/ABI/TestContent.md +++ b/Documentation/ABI/TestContent.md @@ -126,7 +126,10 @@ or a third-party library are inadvertently loaded into the same process. If the value at `type` does not match the test content record's expected type, the accessor function must return `false` and must not modify `outValue`. - +When building for **Embedded Swift**, the value passed as `type` by Swift +Testing is unspecified because type metadata pointers are not available in that +environment. + [^mightNotBeSwift]: Although this document primarily deals with Swift, the test content record section is generally language-agnostic. The use of languages diff --git a/Package.swift b/Package.swift index 8085d7bc8..4194416fb 100644 --- a/Package.swift +++ b/Package.swift @@ -20,17 +20,49 @@ let git = Context.gitInformation /// distribution as a package dependency. let buildingForDevelopment = (git?.currentTag == nil) +/// Whether or not this package is being built for Embedded Swift. +/// +/// This value is `true` if `SWT_EMBEDDED` is set in the environment to `true` +/// when `swift build` is invoked. This inference is experimental and is subject +/// to change in the future. +/// +/// - Bug: There is currently no way for us to tell if we are being asked to +/// build for an Embedded Swift target at the package manifest level. +/// ([swift-syntax-#8431](https://github.com/swiftlang/swift-package-manager/issues/8431)) +let buildingForEmbedded: Bool = { + guard let envvar = Context.environment["SWT_EMBEDDED"] else { + return false + } + return Bool(envvar) ?? ((Int(envvar) ?? 0) != 0) +}() + let package = Package( name: "swift-testing", - platforms: [ - .macOS(.v10_15), - .iOS(.v13), - .watchOS(.v6), - .tvOS(.v13), - .macCatalyst(.v13), - .visionOS(.v1), - ], + platforms: { + if !buildingForEmbedded { + [ + .macOS(.v10_15), + .iOS(.v13), + .watchOS(.v6), + .tvOS(.v13), + .macCatalyst(.v13), + .visionOS(.v1), + ] + } else { + // Open-source main-branch toolchains (currently required to build this + // package for Embedded Swift) have higher Apple platform deployment + // targets than we would otherwise require. + [ + .macOS(.v14), + .iOS(.v18), + .watchOS(.v10), + .tvOS(.v18), + .macCatalyst(.v18), + .visionOS(.v1), + ] + } + }(), products: { var result = [Product]() @@ -185,6 +217,31 @@ package.targets.append(contentsOf: [ ]) #endif +extension BuildSettingCondition { + /// Creates a build setting condition that evaluates to `true` for Embedded + /// Swift. + /// + /// - Parameters: + /// - nonEmbeddedCondition: The value to return if the target is not + /// Embedded Swift. If `nil`, the build condition evaluates to `false`. + /// + /// - Returns: A build setting condition that evaluates to `true` for Embedded + /// Swift or is equal to `nonEmbeddedCondition` for non-Embedded Swift. + static func whenEmbedded(or nonEmbeddedCondition: @autoclosure () -> Self? = nil) -> Self? { + if !buildingForEmbedded { + if let nonEmbeddedCondition = nonEmbeddedCondition() { + nonEmbeddedCondition + } else { + // The caller did not supply a fallback. + .when(platforms: []) + } + } else { + // Enable unconditionally because the target is Embedded Swift. + nil + } + } +} + extension Array where Element == PackageDescription.SwiftSetting { /// Settings intended to be applied to every Swift target in this package. /// Analogous to project-level build settings in an Xcode project. @@ -195,6 +252,10 @@ extension Array where Element == PackageDescription.SwiftSetting { result.append(.unsafeFlags(["-require-explicit-sendable"])) } + if buildingForEmbedded { + result.append(.enableExperimentalFeature("Embedded")) + } + result += [ .enableUpcomingFeature("ExistentialAny"), @@ -214,11 +275,14 @@ extension Array where Element == PackageDescription.SwiftSetting { .define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])), - .define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])), - .define("SWT_NO_PROCESS_SPAWNING", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])), - .define("SWT_NO_SNAPSHOT_TYPES", .when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android])), - .define("SWT_NO_DYNAMIC_LINKING", .when(platforms: [.wasi])), - .define("SWT_NO_PIPES", .when(platforms: [.wasi])), + .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), + .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), + .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android]))), + .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), + .define("SWT_NO_PIPES", .whenEmbedded(or: .when(platforms: [.wasi]))), + + .define("SWT_NO_LEGACY_TEST_DISCOVERY", .whenEmbedded()), + .define("SWT_NO_LIBDISPATCH", .whenEmbedded()), ] return result @@ -271,11 +335,14 @@ extension Array where Element == PackageDescription.CXXSetting { var result = Self() result += [ - .define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])), - .define("SWT_NO_PROCESS_SPAWNING", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])), - .define("SWT_NO_SNAPSHOT_TYPES", .when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android])), - .define("SWT_NO_DYNAMIC_LINKING", .when(platforms: [.wasi])), - .define("SWT_NO_PIPES", .when(platforms: [.wasi])), + .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), + .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), + .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android]))), + .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), + .define("SWT_NO_PIPES", .whenEmbedded(or: .when(platforms: [.wasi]))), + + .define("SWT_NO_LEGACY_TEST_DISCOVERY", .whenEmbedded()), + .define("SWT_NO_LIBDISPATCH", .whenEmbedded()), ] // Capture the testing library's version as a C++ string constant. diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 6341ce422..cff8eb5a1 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -280,11 +280,13 @@ extension ExitTest: DiscoverableAsTestContent { asTypeAt typeAddress: UnsafeRawPointer, withHintAt hintAddress: UnsafeRawPointer? = nil ) -> CBool { +#if !hasFeature(Embedded) let callerExpectedType = TypeInfo(describing: typeAddress.load(as: Any.Type.self)) let selfType = TypeInfo(describing: Self.self) guard callerExpectedType == selfType else { return false } +#endif let id = ID(id) if let hintedID = hintAddress?.load(as: ID.self), hintedID != id { return false diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 5d1b204ae..35f716525 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -44,9 +44,11 @@ extension Test { into outValue: UnsafeMutableRawPointer, asTypeAt typeAddress: UnsafeRawPointer ) -> CBool { +#if !hasFeature(Embedded) guard typeAddress.load(as: Any.Type.self) == Generator.self else { return false } +#endif outValue.initializeMemory(as: Generator.self, to: .init(rawValue: generator)) return true } diff --git a/Sources/_TestDiscovery/TestContentKind.swift b/Sources/_TestDiscovery/TestContentKind.swift index 4e6955acc..30f201a83 100644 --- a/Sources/_TestDiscovery/TestContentKind.swift +++ b/Sources/_TestDiscovery/TestContentKind.swift @@ -52,9 +52,11 @@ extension TestContentKind: Equatable, Hashable { } } +#if !hasFeature(Embedded) // MARK: - Codable extension TestContentKind: Codable {} +#endif // MARK: - ExpressibleByStringLiteral, ExpressibleByIntegerLiteral diff --git a/Sources/_TestDiscovery/TestContentRecord.swift b/Sources/_TestDiscovery/TestContentRecord.swift index 25f46fa44..d893664ee 100644 --- a/Sources/_TestDiscovery/TestContentRecord.swift +++ b/Sources/_TestDiscovery/TestContentRecord.swift @@ -139,6 +139,34 @@ public struct TestContentRecord where T: DiscoverableAsTestContent & ~Copyabl /// The type of the `hint` argument to ``load(withHint:)``. public typealias Hint = T.TestContentAccessorHint + /// Invoke an accessor function to load a test content record. + /// + /// - Parameters: + /// - accessor: The accessor function to call. + /// - typeAddress: A pointer to the type of test content record. + /// - hint: An optional hint value. + /// + /// - Returns: An instance of the test content type `T`, or `nil` if the + /// underlying test content record did not match `hint` or otherwise did not + /// produce a value. + /// + /// Do not call this function directly. Instead, call ``load(withHint:)``. + private static func _load(using accessor: _TestContentRecordAccessor, withTypeAt typeAddress: UnsafeRawPointer, withHint hint: Hint? = nil) -> T? { + withUnsafeTemporaryAllocation(of: T.self, capacity: 1) { buffer in + let initialized = if let hint { + withUnsafePointer(to: hint) { hint in + accessor(buffer.baseAddress!, typeAddress, hint, 0) + } + } else { + accessor(buffer.baseAddress!, typeAddress, nil, 0) + } + guard initialized else { + return nil + } + return buffer.baseAddress!.move() + } + } + /// Load the value represented by this record. /// /// - Parameters: @@ -157,21 +185,14 @@ public struct TestContentRecord where T: DiscoverableAsTestContent & ~Copyabl return nil } - return withUnsafePointer(to: T.self) { type in - withUnsafeTemporaryAllocation(of: T.self, capacity: 1) { buffer in - let initialized = if let hint { - withUnsafePointer(to: hint) { hint in - accessor(buffer.baseAddress!, type, hint, 0) - } - } else { - accessor(buffer.baseAddress!, type, nil, 0) - } - guard initialized else { - return nil - } - return buffer.baseAddress!.move() - } +#if !hasFeature(Embedded) + return withUnsafePointer(to: T.self) { typeAddress in + Self._load(using: accessor, withTypeAt: typeAddress, withHint: hint) } +#else + let typeAddress = UnsafeRawPointer(bitPattern: UInt(T.testContentKind.rawValue)).unsafelyUnwrapped + return Self._load(using: accessor, withTypeAt: typeAddress, withHint: hint) +#endif } } @@ -188,7 +209,11 @@ extension TestContentRecord: Sendable where Context: Sendable {} extension TestContentRecord: CustomStringConvertible { public var description: String { +#if !hasFeature(Embedded) let typeName = String(describing: Self.self) +#else + let typeName = "TestContentRecord" +#endif switch _recordStorage { case let .atAddress(recordAddress): let recordAddress = imageAddress.map { imageAddress in diff --git a/Tests/TestingTests/DiscoveryTests.swift b/Tests/TestingTests/DiscoveryTests.swift index a730f8b53..2b53cd467 100644 --- a/Tests/TestingTests/DiscoveryTests.swift +++ b/Tests/TestingTests/DiscoveryTests.swift @@ -94,9 +94,11 @@ struct DiscoveryTests { 0xABCD1234, 0, { outValue, type, hint, _ in +#if !hasFeature(Embedded) guard type.load(as: Any.Type.self) == MyTestContent.self else { return false } +#endif if let hint, hint.load(as: TestContentAccessorHint.self) != expectedHint { return false }