Skip to content

[Experimental] Add Embedded Swift support to the _TestDiscovery target. #1043

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Documentation/ABI/TestContent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

<!-- TODO: discuss this argument's value in Embedded Swift (no metatypes) -->
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.
<!-- TODO: specify what they are instead (FQN type name C strings maybe?) -->

[^mightNotBeSwift]: Although this document primarily deals with Swift, the test
content record section is generally language-agnostic. The use of languages
Expand Down
103 changes: 85 additions & 18 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]()
Expand Down Expand Up @@ -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.
Expand All @@ -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"),

Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Sources/Testing/Test+Discovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/_TestDiscovery/TestContentKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@ extension TestContentKind: Equatable, Hashable {
}
}

#if !hasFeature(Embedded)
// MARK: - Codable

extension TestContentKind: Codable {}
#endif

// MARK: - ExpressibleByStringLiteral, ExpressibleByIntegerLiteral

Expand Down
53 changes: 39 additions & 14 deletions Sources/_TestDiscovery/TestContentRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,34 @@ public struct TestContentRecord<T> 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:
Expand All @@ -157,21 +185,14 @@ public struct TestContentRecord<T> 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
}
}

Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Tests/TestingTests/DiscoveryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down