Skip to content

Commit 09a88e5

Browse files
committed
[WIP] Third-party testing library discovery
1 parent 3670d67 commit 09a88e5

12 files changed

Lines changed: 776 additions & 23 deletions

File tree

Sources/Testing/ABI/ABI.Record+Streaming.swift

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@ extension ABI.Version {
2424

2525
let humanReadableOutputRecorder = Event.HumanReadableOutputRecorder()
2626
return { [eventHandler = eventHandlerCopy] event, context in
27-
if case .testDiscovered = event.kind, let test = context.test {
27+
if case let .libraryDiscovered(library) = event.kind {
28+
if let libraryRecord = ABI.Record<Self>(encoding: library) {
29+
try? JSON.withEncoding(of: libraryRecord) { libraryJSON in
30+
eventHandler(libraryJSON)
31+
}
32+
}
33+
} else if case .testDiscovered = event.kind, let test = context.test {
2834
try? JSON.withEncoding(of: ABI.Record<Self>(encoding: test)) { testJSON in
2935
eventHandler(testJSON)
3036
}
@@ -47,24 +53,25 @@ extension ABI.Xcode16 {
4753
forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void
4854
) -> Event.Handler {
4955
return { event, context in
50-
if case .testDiscovered = event.kind {
56+
switch event.kind {
57+
case .libraryDiscovered, .testDiscovered:
5158
// Discard events of this kind rather than forwarding them to avoid a
5259
// crash in Xcode 16 (which does not expect any events to occur before
5360
// .runStarted.)
5461
return
55-
}
56-
57-
struct EventAndContextSnapshot: Codable {
58-
var event: Event.Snapshot
59-
var eventContext: Event.Context.Snapshot
60-
}
61-
let snapshot = EventAndContextSnapshot(
62-
event: Event.Snapshot(snapshotting: event),
63-
eventContext: Event.Context.Snapshot(snapshotting: context)
64-
)
65-
try? JSON.withEncoding(of: snapshot) { eventAndContextJSON in
66-
eventAndContextJSON.withUnsafeBytes { eventAndContextJSON in
67-
eventHandler(eventAndContextJSON)
62+
default:
63+
struct EventAndContextSnapshot: Codable {
64+
var event: Event.Snapshot
65+
var eventContext: Event.Context.Snapshot
66+
}
67+
let snapshot = EventAndContextSnapshot(
68+
event: Event.Snapshot(snapshotting: event),
69+
eventContext: Event.Context.Snapshot(snapshotting: context)
70+
)
71+
try? JSON.withEncoding(of: snapshot) { eventAndContextJSON in
72+
eventAndContextJSON.withUnsafeBytes { eventAndContextJSON in
73+
eventHandler(eventAndContextJSON)
74+
}
6875
}
6976
}
7077
}

Sources/Testing/ABI/ABI.Record.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ extension ABI {
1818
struct Record<V>: Sendable where V: ABI.Version {
1919
/// An enumeration describing the various kinds of record.
2020
enum Kind: Sendable {
21+
/// A testing library.
22+
///
23+
/// - Warning: Testing libraries are not yet part of the JSON schema.
24+
case library(EncodedLibrary<V>)
25+
2126
/// A test record.
2227
case test(EncodedTest<V>)
2328

@@ -28,6 +33,13 @@ extension ABI {
2833
/// The kind of record.
2934
var kind: Kind
3035

36+
init?(encoding library: borrowing Library) {
37+
guard V.includesExperimentalFields else {
38+
return nil
39+
}
40+
kind = .library(EncodedLibrary(encoding: library))
41+
}
42+
3143
init(encoding test: borrowing Test) {
3244
kind = .test(EncodedTest(encoding: test))
3345
}
@@ -58,6 +70,9 @@ extension ABI.Record: Codable {
5870
var container = encoder.container(keyedBy: CodingKeys.self)
5971
try container.encode(V.versionNumber, forKey: .version)
6072
switch kind {
73+
case let .library(library):
74+
try container.encode("_library", forKey: .kind)
75+
try container.encode(library, forKey: .payload)
6176
case let .test(test):
6277
try container.encode("test", forKey: .kind)
6378
try container.encode(test, forKey: .payload)
@@ -92,6 +107,9 @@ extension ABI.Record: Codable {
92107
try validateVersionNumber(versionNumber)
93108

94109
switch try container.decode(String.self, forKey: .kind) {
110+
case "_library":
111+
let library = try container.decode(ABI.EncodedLibrary<V>.self, forKey: .payload)
112+
kind = .library(library)
95113
case "test":
96114
let test = try container.decode(ABI.EncodedTest<V>.self, forKey: .payload)
97115
kind = .test(test)

Sources/Testing/ABI/ABI.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ extension ABI {
6767
forVersionNumber versionNumber: VersionNumber,
6868
givenSwiftCompilerVersion swiftCompilerVersion: @autoclosure () -> VersionNumber = swiftCompilerVersion
6969
) -> (any Version.Type)? {
70+
// Special-case the experimental ABI version number (which is intentionally
71+
// higher than any Swift release's version number).
72+
if versionNumber == ExperimentalVersion.versionNumber {
73+
return ExperimentalVersion.self
74+
}
75+
7076
if versionNumber > ABI.HighestVersion.versionNumber {
7177
// If the caller requested an ABI version higher than the current Swift
7278
// compiler version and it's not an ABI version we've explicitly defined,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
extension ABI {
12+
/// A type implementing the JSON encoding of ``Library`` for the ABI entry
13+
/// point and event stream output.
14+
///
15+
/// The properties and members of this type are documented in ABI/JSON.md.
16+
///
17+
/// This type is not part of the public interface of the testing library. It
18+
/// assists in converting values to JSON; clients that consume this JSON are
19+
/// expected to write their own decoders.
20+
///
21+
/// - Warning: Testing libraries are not yet part of the JSON schema.
22+
struct EncodedLibrary<V>: Sendable where V: ABI.Version {
23+
/// The human-readable name of the library.
24+
var name: String
25+
26+
/// The canonical form of the "hint" to run the testing library's tests at
27+
/// runtime.
28+
var canonicalHint: String
29+
30+
init(encoding library: borrowing Library) {
31+
name = library.name
32+
canonicalHint = library.canonicalHint
33+
}
34+
}
35+
}
36+
37+
// MARK: - Codable
38+
39+
extension ABI.EncodedLibrary: Codable {}

Sources/Testing/ABI/EntryPoints/EntryPoint.swift

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,25 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
3030

3131
do {
3232
#if !SWT_NO_EXIT_TESTS
33-
// If an exit test was specified, run it. `exitTest` returns `Never`.
34-
if let exitTest = ExitTest.findInEnvironmentForEntryPoint() {
35-
await exitTest()
36-
}
33+
// If an exit test was specified, run it. `exitTest` returns `Never`.
34+
if let exitTest = ExitTest.findInEnvironmentForEntryPoint() {
35+
await exitTest()
36+
}
3737
#endif
3838

3939
let args = try args ?? parseCommandLineArguments(from: CommandLine.arguments)
40+
41+
#if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY
42+
// If the user requested a different testing library, run it instead of
43+
// Swift Testing. (If they requested Swift Testing, we're already here so
44+
// there's no real need to recurse).
45+
if args.experimentalListLibraries != true,
46+
let library = args.testingLibrary.flatMap(Library.init(withHint:)),
47+
library.canonicalHint != "swift-testing" {
48+
return await library.callEntryPoint(passing: args)
49+
}
50+
#endif
51+
4052
// Configure the test runner.
4153
var configuration = try configurationForEntryPoint(from: args)
4254

@@ -91,9 +103,10 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
91103

92104
// The set of matching tests (or, in the case of `swift test list`, the set
93105
// of all tests.)
94-
let tests: [Test]
106+
var tests = [Test]()
107+
var libraries = [Library]()
95108

96-
if args.listTests ?? false {
109+
if args.listTests == true {
97110
tests = await Array(Test.all)
98111

99112
if args.verbosity > .min {
@@ -112,6 +125,29 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
112125
for test in tests {
113126
Event.post(.testDiscovered, for: (test, nil), configuration: configuration)
114127
}
128+
} else if args.experimentalListLibraries == true {
129+
#if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY
130+
libraries = Array(Library.all)
131+
#else
132+
libraries = [Library.swiftTesting]
133+
#endif
134+
135+
if args.verbosity > .min {
136+
for library in libraries {
137+
// Print the test ID to stdout (classical CLI behavior.)
138+
let libraryDescription = "\(library.name) (swift test --experimental-testing-library \(library.canonicalHint))"
139+
#if SWT_TARGET_OS_APPLE && !SWT_NO_FILE_IO
140+
try? FileHandle.stdout.write("\(libraryDescription)\n")
141+
#else
142+
print(libraryDescription)
143+
#endif
144+
}
145+
}
146+
147+
// Post an event for every discovered library (as with tests above).
148+
for library in libraries {
149+
Event.post(.libraryDiscovered(library), for: (nil, nil), configuration: configuration)
150+
}
115151
} else {
116152
// Run the tests.
117153
let runner = await Runner(configuration: configuration)
@@ -122,7 +158,7 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
122158
// If there were no matching tests, exit with a dedicated exit code so that
123159
// the caller (assumed to be Swift Package Manager) can implement special
124160
// handling.
125-
if tests.isEmpty {
161+
if tests.isEmpty && libraries.isEmpty {
126162
exitCode.withLock { exitCode in
127163
if exitCode == EXIT_SUCCESS {
128164
exitCode = EXIT_NO_TESTS_FOUND
@@ -207,6 +243,9 @@ public struct __CommandLineArguments_v0: Sendable {
207243
/// The value of the `--list-tests` argument.
208244
public var listTests: Bool?
209245

246+
/// The value of the `--experimental-list-libraries` argument.
247+
public var experimentalListLibraries: Bool?
248+
210249
/// The value of the `--parallel` or `--no-parallel` argument.
211250
public var parallel: Bool?
212251

@@ -331,13 +370,17 @@ public struct __CommandLineArguments_v0: Sendable {
331370

332371
/// The value of the `--attachments-path` argument.
333372
public var attachmentsPath: String?
373+
374+
/// The value of the `--testing-library` argument.
375+
public var testingLibrary: String?
334376
}
335377

336378
extension __CommandLineArguments_v0: Codable {
337379
// Explicitly list the coding keys so that storage properties like _verbosity
338380
// do not end up with leading underscores when encoded.
339381
enum CodingKeys: String, CodingKey {
340382
case listTests
383+
case experimentalListLibraries
341384
case parallel
342385
case experimentalMaximumParallelizationWidth
343386
case symbolicateBacktraces
@@ -353,6 +396,7 @@ extension __CommandLineArguments_v0: Codable {
353396
case repetitions
354397
case repeatUntil
355398
case attachmentsPath
399+
case testingLibrary
356400
}
357401
}
358402

@@ -466,6 +510,11 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
466510
}
467511
#endif
468512

513+
// Testing library
514+
if let testingLibrary = Environment.variable(named: "SWT_EXPERIMENTAL_LIBRARY") ?? args.argumentValue(forLabel: "--testing-library") {
515+
result.testingLibrary = testingLibrary
516+
}
517+
469518
// XML output
470519
if let xunitOutputPath = args.argumentValue(forLabel: "--xunit-output") {
471520
result.xunitOutput = xunitOutputPath
@@ -484,6 +533,9 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
484533
// makes invocation from e.g. Wasmtime a bit more intuitive/idiomatic.
485534
result.listTests = true
486535
}
536+
if Environment.flag(named: "SWT_EXPERIMENTAL_LIST_LIBRARIES") == true || args.contains("--experimental-list-libraries") {
537+
result.experimentalListLibraries = true
538+
}
487539

488540
// Parallelization (on by default)
489541
if args.contains("--no-parallel") {

0 commit comments

Comments
 (0)