Skip to content

Commit 7907a4a

Browse files
authored
[Experimental] Add Embedded Swift support to the _TestDiscovery target. (#1043)
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 ``` ### 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.
1 parent 27d09f0 commit 7907a4a

File tree

7 files changed

+136
-33
lines changed

7 files changed

+136
-33
lines changed

Documentation/ABI/TestContent.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ or a third-party library are inadvertently loaded into the same process. If the
126126
value at `type` does not match the test content record's expected type, the
127127
accessor function must return `false` and must not modify `outValue`.
128128

129-
<!-- TODO: discuss this argument's value in Embedded Swift (no metatypes) -->
129+
When building for **Embedded Swift**, the value passed as `type` by Swift
130+
Testing is unspecified because type metadata pointers are not available in that
131+
environment.
132+
<!-- TODO: specify what they are instead (FQN type name C strings maybe?) -->
130133

131134
[^mightNotBeSwift]: Although this document primarily deals with Swift, the test
132135
content record section is generally language-agnostic. The use of languages

Package.swift

+85-18
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,49 @@ let git = Context.gitInformation
2020
/// distribution as a package dependency.
2121
let buildingForDevelopment = (git?.currentTag == nil)
2222

23+
/// Whether or not this package is being built for Embedded Swift.
24+
///
25+
/// This value is `true` if `SWT_EMBEDDED` is set in the environment to `true`
26+
/// when `swift build` is invoked. This inference is experimental and is subject
27+
/// to change in the future.
28+
///
29+
/// - Bug: There is currently no way for us to tell if we are being asked to
30+
/// build for an Embedded Swift target at the package manifest level.
31+
/// ([swift-syntax-#8431](https://github.com/swiftlang/swift-package-manager/issues/8431))
32+
let buildingForEmbedded: Bool = {
33+
guard let envvar = Context.environment["SWT_EMBEDDED"] else {
34+
return false
35+
}
36+
return Bool(envvar) ?? ((Int(envvar) ?? 0) != 0)
37+
}()
38+
2339
let package = Package(
2440
name: "swift-testing",
2541

26-
platforms: [
27-
.macOS(.v10_15),
28-
.iOS(.v13),
29-
.watchOS(.v6),
30-
.tvOS(.v13),
31-
.macCatalyst(.v13),
32-
.visionOS(.v1),
33-
],
42+
platforms: {
43+
if !buildingForEmbedded {
44+
[
45+
.macOS(.v10_15),
46+
.iOS(.v13),
47+
.watchOS(.v6),
48+
.tvOS(.v13),
49+
.macCatalyst(.v13),
50+
.visionOS(.v1),
51+
]
52+
} else {
53+
// Open-source main-branch toolchains (currently required to build this
54+
// package for Embedded Swift) have higher Apple platform deployment
55+
// targets than we would otherwise require.
56+
[
57+
.macOS(.v14),
58+
.iOS(.v18),
59+
.watchOS(.v10),
60+
.tvOS(.v18),
61+
.macCatalyst(.v18),
62+
.visionOS(.v1),
63+
]
64+
}
65+
}(),
3466

3567
products: {
3668
var result = [Product]()
@@ -185,6 +217,31 @@ package.targets.append(contentsOf: [
185217
])
186218
#endif
187219

220+
extension BuildSettingCondition {
221+
/// Creates a build setting condition that evaluates to `true` for Embedded
222+
/// Swift.
223+
///
224+
/// - Parameters:
225+
/// - nonEmbeddedCondition: The value to return if the target is not
226+
/// Embedded Swift. If `nil`, the build condition evaluates to `false`.
227+
///
228+
/// - Returns: A build setting condition that evaluates to `true` for Embedded
229+
/// Swift or is equal to `nonEmbeddedCondition` for non-Embedded Swift.
230+
static func whenEmbedded(or nonEmbeddedCondition: @autoclosure () -> Self? = nil) -> Self? {
231+
if !buildingForEmbedded {
232+
if let nonEmbeddedCondition = nonEmbeddedCondition() {
233+
nonEmbeddedCondition
234+
} else {
235+
// The caller did not supply a fallback.
236+
.when(platforms: [])
237+
}
238+
} else {
239+
// Enable unconditionally because the target is Embedded Swift.
240+
nil
241+
}
242+
}
243+
}
244+
188245
extension Array where Element == PackageDescription.SwiftSetting {
189246
/// Settings intended to be applied to every Swift target in this package.
190247
/// Analogous to project-level build settings in an Xcode project.
@@ -195,6 +252,10 @@ extension Array where Element == PackageDescription.SwiftSetting {
195252
result.append(.unsafeFlags(["-require-explicit-sendable"]))
196253
}
197254

255+
if buildingForEmbedded {
256+
result.append(.enableExperimentalFeature("Embedded"))
257+
}
258+
198259
result += [
199260
.enableUpcomingFeature("ExistentialAny"),
200261

@@ -214,11 +275,14 @@ extension Array where Element == PackageDescription.SwiftSetting {
214275

215276
.define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])),
216277

217-
.define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
218-
.define("SWT_NO_PROCESS_SPAWNING", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
219-
.define("SWT_NO_SNAPSHOT_TYPES", .when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android])),
220-
.define("SWT_NO_DYNAMIC_LINKING", .when(platforms: [.wasi])),
221-
.define("SWT_NO_PIPES", .when(platforms: [.wasi])),
278+
.define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))),
279+
.define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))),
280+
.define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android]))),
281+
.define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))),
282+
.define("SWT_NO_PIPES", .whenEmbedded(or: .when(platforms: [.wasi]))),
283+
284+
.define("SWT_NO_LEGACY_TEST_DISCOVERY", .whenEmbedded()),
285+
.define("SWT_NO_LIBDISPATCH", .whenEmbedded()),
222286
]
223287

224288
return result
@@ -271,11 +335,14 @@ extension Array where Element == PackageDescription.CXXSetting {
271335
var result = Self()
272336

273337
result += [
274-
.define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
275-
.define("SWT_NO_PROCESS_SPAWNING", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
276-
.define("SWT_NO_SNAPSHOT_TYPES", .when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android])),
277-
.define("SWT_NO_DYNAMIC_LINKING", .when(platforms: [.wasi])),
278-
.define("SWT_NO_PIPES", .when(platforms: [.wasi])),
338+
.define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))),
339+
.define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))),
340+
.define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android]))),
341+
.define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))),
342+
.define("SWT_NO_PIPES", .whenEmbedded(or: .when(platforms: [.wasi]))),
343+
344+
.define("SWT_NO_LEGACY_TEST_DISCOVERY", .whenEmbedded()),
345+
.define("SWT_NO_LIBDISPATCH", .whenEmbedded()),
279346
]
280347

281348
// Capture the testing library's version as a C++ string constant.

Sources/Testing/ExitTests/ExitTest.swift

+2
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,13 @@ extension ExitTest: DiscoverableAsTestContent {
280280
asTypeAt typeAddress: UnsafeRawPointer,
281281
withHintAt hintAddress: UnsafeRawPointer? = nil
282282
) -> CBool {
283+
#if !hasFeature(Embedded)
283284
let callerExpectedType = TypeInfo(describing: typeAddress.load(as: Any.Type.self))
284285
let selfType = TypeInfo(describing: Self.self)
285286
guard callerExpectedType == selfType else {
286287
return false
287288
}
289+
#endif
288290
let id = ID(id)
289291
if let hintedID = hintAddress?.load(as: ID.self), hintedID != id {
290292
return false

Sources/Testing/Test+Discovery.swift

+2
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ extension Test {
4444
into outValue: UnsafeMutableRawPointer,
4545
asTypeAt typeAddress: UnsafeRawPointer
4646
) -> CBool {
47+
#if !hasFeature(Embedded)
4748
guard typeAddress.load(as: Any.Type.self) == Generator.self else {
4849
return false
4950
}
51+
#endif
5052
outValue.initializeMemory(as: Generator.self, to: .init(rawValue: generator))
5153
return true
5254
}

Sources/_TestDiscovery/TestContentKind.swift

+2
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,11 @@ extension TestContentKind: Equatable, Hashable {
5252
}
5353
}
5454

55+
#if !hasFeature(Embedded)
5556
// MARK: - Codable
5657

5758
extension TestContentKind: Codable {}
59+
#endif
5860

5961
// MARK: - ExpressibleByStringLiteral, ExpressibleByIntegerLiteral
6062

Sources/_TestDiscovery/TestContentRecord.swift

+39-14
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,34 @@ public struct TestContentRecord<T> where T: DiscoverableAsTestContent & ~Copyabl
139139
/// The type of the `hint` argument to ``load(withHint:)``.
140140
public typealias Hint = T.TestContentAccessorHint
141141

142+
/// Invoke an accessor function to load a test content record.
143+
///
144+
/// - Parameters:
145+
/// - accessor: The accessor function to call.
146+
/// - typeAddress: A pointer to the type of test content record.
147+
/// - hint: An optional hint value.
148+
///
149+
/// - Returns: An instance of the test content type `T`, or `nil` if the
150+
/// underlying test content record did not match `hint` or otherwise did not
151+
/// produce a value.
152+
///
153+
/// Do not call this function directly. Instead, call ``load(withHint:)``.
154+
private static func _load(using accessor: _TestContentRecordAccessor, withTypeAt typeAddress: UnsafeRawPointer, withHint hint: Hint? = nil) -> T? {
155+
withUnsafeTemporaryAllocation(of: T.self, capacity: 1) { buffer in
156+
let initialized = if let hint {
157+
withUnsafePointer(to: hint) { hint in
158+
accessor(buffer.baseAddress!, typeAddress, hint, 0)
159+
}
160+
} else {
161+
accessor(buffer.baseAddress!, typeAddress, nil, 0)
162+
}
163+
guard initialized else {
164+
return nil
165+
}
166+
return buffer.baseAddress!.move()
167+
}
168+
}
169+
142170
/// Load the value represented by this record.
143171
///
144172
/// - Parameters:
@@ -157,21 +185,14 @@ public struct TestContentRecord<T> where T: DiscoverableAsTestContent & ~Copyabl
157185
return nil
158186
}
159187

160-
return withUnsafePointer(to: T.self) { type in
161-
withUnsafeTemporaryAllocation(of: T.self, capacity: 1) { buffer in
162-
let initialized = if let hint {
163-
withUnsafePointer(to: hint) { hint in
164-
accessor(buffer.baseAddress!, type, hint, 0)
165-
}
166-
} else {
167-
accessor(buffer.baseAddress!, type, nil, 0)
168-
}
169-
guard initialized else {
170-
return nil
171-
}
172-
return buffer.baseAddress!.move()
173-
}
188+
#if !hasFeature(Embedded)
189+
return withUnsafePointer(to: T.self) { typeAddress in
190+
Self._load(using: accessor, withTypeAt: typeAddress, withHint: hint)
174191
}
192+
#else
193+
let typeAddress = UnsafeRawPointer(bitPattern: UInt(T.testContentKind.rawValue)).unsafelyUnwrapped
194+
return Self._load(using: accessor, withTypeAt: typeAddress, withHint: hint)
195+
#endif
175196
}
176197
}
177198

@@ -188,7 +209,11 @@ extension TestContentRecord: Sendable where Context: Sendable {}
188209

189210
extension TestContentRecord: CustomStringConvertible {
190211
public var description: String {
212+
#if !hasFeature(Embedded)
191213
let typeName = String(describing: Self.self)
214+
#else
215+
let typeName = "TestContentRecord"
216+
#endif
192217
switch _recordStorage {
193218
case let .atAddress(recordAddress):
194219
let recordAddress = imageAddress.map { imageAddress in

Tests/TestingTests/DiscoveryTests.swift

+2
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,11 @@ struct DiscoveryTests {
9494
0xABCD1234,
9595
0,
9696
{ outValue, type, hint, _ in
97+
#if !hasFeature(Embedded)
9798
guard type.load(as: Any.Type.self) == MyTestContent.self else {
9899
return false
99100
}
101+
#endif
100102
if let hint, hint.load(as: TestContentAccessorHint.self) != expectedHint {
101103
return false
102104
}

0 commit comments

Comments
 (0)