From 0f49e7324037554c450710062b27a12090fc416e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 30 Mar 2025 12:15:51 -0400 Subject: [PATCH 1/6] Add Embedded Swift support to the `_TestDiscovery` target. This PR adds preliminary/experimental support for Embedded Swift _to the `_TestDiscovery` target only_ when building Swift Testing as a package. To try it out, you must set the environment variable `SWT_EMBEDDED` to `true` before building. Tested with the following incantation using the 2025-03-28 main-branch toolchain: ```sh SWT_EMBEDDED=1 swift build --target _TestDiscovery --triple arm64-apple-macosx ``` --- Package.swift | 104 +++++++++++++++--- .../DiscoverableAsTestContent.swift | 2 + Sources/_TestDiscovery/SectionBounds.swift | 10 +- Sources/_TestDiscovery/TestContentKind.swift | 4 +- .../_TestDiscovery/TestContentRecord.swift | 53 ++++++--- 5 files changed, 135 insertions(+), 38 deletions(-) diff --git a/Package.swift b/Package.swift index 8085d7bc8..5387e7f1d 100644 --- a/Package.swift +++ b/Package.swift @@ -20,17 +20,53 @@ 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 + } + let result = Bool(envvar) ?? ((Int(envvar) ?? 0) != 0) + if result { + print("Building for Embedded Swift...") + } + return result +}() + 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 +221,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. @@ -197,6 +258,7 @@ extension Array where Element == PackageDescription.SwiftSetting { result += [ .enableUpcomingFeature("ExistentialAny"), + .enableExperimentalFeature("Embedded", .whenEmbedded()), .enableExperimentalFeature("AccessLevelOnImport"), .enableUpcomingFeature("InternalImportsByDefault"), @@ -214,11 +276,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 +336,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/_TestDiscovery/DiscoverableAsTestContent.swift b/Sources/_TestDiscovery/DiscoverableAsTestContent.swift index a4b400bad..d4b15f8db 100644 --- a/Sources/_TestDiscovery/DiscoverableAsTestContent.swift +++ b/Sources/_TestDiscovery/DiscoverableAsTestContent.swift @@ -48,8 +48,10 @@ public protocol DiscoverableAsTestContent: Sendable, ~Copyable { #endif } +#if !SWT_NO_LEGACY_TEST_DISCOVERY extension DiscoverableAsTestContent where Self: ~Copyable { public static var _testContentTypeNameHint: String { "__🟡$" } } +#endif diff --git a/Sources/_TestDiscovery/SectionBounds.swift b/Sources/_TestDiscovery/SectionBounds.swift index 1a3ae8e11..6b5f0292f 100644 --- a/Sources/_TestDiscovery/SectionBounds.swift +++ b/Sources/_TestDiscovery/SectionBounds.swift @@ -52,11 +52,11 @@ extension SectionBounds.Kind { /// The Mach-O segment and section name for this instance as a pair of /// null-terminated UTF-8 C strings and pass them to a function. /// - /// The values of this property within this function are instances of - /// `StaticString` rather than `String` because the latter's inner storage is - /// sometimes Objective-C-backed and touching it here can cause a recursive - /// access to an internal libobjc lock, whereas `StaticString`'s internal - /// storage is immediately available. + /// The values of this property are instances of `StaticString` rather than + /// `String` because the latter's inner storage is sometimes backed by + /// Objective-C and touching it here can cause a recursive access to an + /// internal libobjc lock, whereas `StaticString`'s internal storage is + /// immediately available. fileprivate var segmentAndSectionName: (segmentName: StaticString, sectionName: StaticString) { switch self { case .testContent: diff --git a/Sources/_TestDiscovery/TestContentKind.swift b/Sources/_TestDiscovery/TestContentKind.swift index 645b06424..b5bdd409d 100644 --- a/Sources/_TestDiscovery/TestContentKind.swift +++ b/Sources/_TestDiscovery/TestContentKind.swift @@ -52,16 +52,18 @@ extension TestContentKind: Equatable, Hashable { } } +#if !hasFeature(Embedded) // MARK: - Codable extension TestContentKind: Codable {} +#endif // MARK: - ExpressibleByStringLiteral, ExpressibleByIntegerLiteral extension TestContentKind: ExpressibleByStringLiteral, ExpressibleByIntegerLiteral { @inlinable public init(stringLiteral stringValue: StaticString) { - precondition(stringValue.utf8CodeUnitCount == MemoryLayout.stride, #""\#(stringValue)".utf8CodeUnitCount = \#(stringValue.utf8CodeUnitCount), expected \#(MemoryLayout.stride)"#) let rawValue = stringValue.withUTF8Buffer { stringValue in + precondition(stringValue.count == MemoryLayout.stride, #""\#(stringValue)".utf8CodeUnitCount = \#(stringValue.count), expected \#(MemoryLayout.stride)"#) let bigEndian = UnsafeRawBufferPointer(stringValue).loadUnaligned(as: UInt32.self) return UInt32(bigEndian: bigEndian) } 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 From a224964e3e699f332470301d656265079ec3a149 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 30 Mar 2025 12:33:32 -0400 Subject: [PATCH 2/6] Work around incorrect detection of Embedded feature flag in Xcode --- Package.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 5387e7f1d..3f16a3681 100644 --- a/Package.swift +++ b/Package.swift @@ -256,9 +256,12 @@ extension Array where Element == PackageDescription.SwiftSetting { result.append(.unsafeFlags(["-require-explicit-sendable"])) } + if buildingForEmbedded { + result.append(.enableExperimentalFeature("Embedded")) + } + result += [ .enableUpcomingFeature("ExistentialAny"), - .enableExperimentalFeature("Embedded", .whenEmbedded()), .enableExperimentalFeature("AccessLevelOnImport"), .enableUpcomingFeature("InternalImportsByDefault"), From b5aee8fae53c4ab80964bae07aa4cb6a38f49c49 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 30 Mar 2025 12:40:23 -0400 Subject: [PATCH 3/6] Disable typeAddress arg checking in Embedded --- Documentation/ABI/TestContent.md | 5 ++++- Sources/Testing/ExitTests/ExitTest.swift | 2 ++ Sources/Testing/Test+Discovery.swift | 2 ++ Tests/TestingTests/DiscoveryTests.swift | 2 ++ 4 files changed, 10 insertions(+), 1 deletion(-) 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/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 69346b74e..7e69357a8 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -266,11 +266,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/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 } From 18ec370bbfe3f46ab2f466ffdfedf89009a7cdeb Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 30 Mar 2025 13:03:08 -0400 Subject: [PATCH 4/6] Avoid using Unicode tables --- Package.swift | 6 +----- Sources/_TestDiscovery/TestContentKind.swift | 13 +++++++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index 3f16a3681..4194416fb 100644 --- a/Package.swift +++ b/Package.swift @@ -33,11 +33,7 @@ let buildingForEmbedded: Bool = { guard let envvar = Context.environment["SWT_EMBEDDED"] else { return false } - let result = Bool(envvar) ?? ((Int(envvar) ?? 0) != 0) - if result { - print("Building for Embedded Swift...") - } - return result + return Bool(envvar) ?? ((Int(envvar) ?? 0) != 0) }() let package = Package( diff --git a/Sources/_TestDiscovery/TestContentKind.swift b/Sources/_TestDiscovery/TestContentKind.swift index b5bdd409d..77b9c6c0b 100644 --- a/Sources/_TestDiscovery/TestContentKind.swift +++ b/Sources/_TestDiscovery/TestContentKind.swift @@ -85,7 +85,20 @@ extension TestContentKind: CustomStringConvertible { withUnsafeBytes(of: rawValue.bigEndian) { bytes in if bytes.allSatisfy(Unicode.ASCII.isASCII) { let characters = String(decoding: bytes, as: Unicode.ASCII.self) +#if !hasFeature(Embedded) let allAlphanumeric = characters.allSatisfy { $0.isLetter || $0.isWholeNumber } +#else + let allAlphanumeric = bytes.allSatisfy { b in + switch b { + case UInt8(ascii: "A") ... UInt8(ascii: "Z"), + UInt8(ascii: "a") ... UInt8(ascii: "z"), + UInt8(ascii: "0") ... UInt8(ascii: "9"): + true + default: + false + } + } +#endif if allAlphanumeric { return characters } From 7de2ff7e6a1d5afe499c7b3bfa09bb22c41b28fd Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 30 Mar 2025 16:28:39 -0400 Subject: [PATCH 5/6] Use C-standard isprint() --- Sources/_TestDiscovery/TestContentKind.swift | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/Sources/_TestDiscovery/TestContentKind.swift b/Sources/_TestDiscovery/TestContentKind.swift index 77b9c6c0b..eb15165b6 100644 --- a/Sources/_TestDiscovery/TestContentKind.swift +++ b/Sources/_TestDiscovery/TestContentKind.swift @@ -84,23 +84,9 @@ extension TestContentKind: CustomStringConvertible { private var _fourCCValue: String? { withUnsafeBytes(of: rawValue.bigEndian) { bytes in if bytes.allSatisfy(Unicode.ASCII.isASCII) { - let characters = String(decoding: bytes, as: Unicode.ASCII.self) -#if !hasFeature(Embedded) - let allAlphanumeric = characters.allSatisfy { $0.isLetter || $0.isWholeNumber } -#else - let allAlphanumeric = bytes.allSatisfy { b in - switch b { - case UInt8(ascii: "A") ... UInt8(ascii: "Z"), - UInt8(ascii: "a") ... UInt8(ascii: "z"), - UInt8(ascii: "0") ... UInt8(ascii: "9"): - true - default: - false - } - } -#endif + let allAlphanumeric = bytes.allSatisfy { 0 != isprint(CInt($0)) } if allAlphanumeric { - return characters + return String(decoding: bytes, as: Unicode.ASCII.self) } } return nil From 44f935df35095c9e0f12531a7a14d5395a7b79ed Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 30 Mar 2025 16:36:13 -0400 Subject: [PATCH 6/6] Explicitly include ctype.h --- Sources/_TestingInternals/include/Includes.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/_TestingInternals/include/Includes.h b/Sources/_TestingInternals/include/Includes.h index 5ba496ee9..bfc87b001 100644 --- a/Sources/_TestingInternals/include/Includes.h +++ b/Sources/_TestingInternals/include/Includes.h @@ -26,6 +26,7 @@ /// /// - Note: Avoid including headers that aren't actually used. +#include #include #include /// Guard against including `signal.h` on WASI. The `signal.h` header file