Skip to content

Commit 7dbf939

Browse files
committed
[WIP] Third-party testing library discovery
1 parent 1f2893c commit 7dbf939

12 files changed

Lines changed: 775 additions & 22 deletions

File tree

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

Lines changed: 21 additions & 14 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,23 +53,24 @@ extension ABI.Xcode16 {
4753
forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: borrowing RawSpan) -> 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-
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+
eventHandler(eventAndContextJSON)
73+
}
6774
}
6875
}
6976
}

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
@@ -34,13 +34,25 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
3434

3535
do {
3636
#if !SWT_NO_EXIT_TESTS
37-
// If an exit test was specified, run it. `exitTest` returns `Never`.
38-
if let exitTest = ExitTest.findInEnvironmentForEntryPoint() {
39-
await exitTest()
40-
}
37+
// If an exit test was specified, run it. `exitTest` returns `Never`.
38+
if let exitTest = ExitTest.findInEnvironmentForEntryPoint() {
39+
await exitTest()
40+
}
4141
#endif
4242

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

@@ -95,9 +107,10 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
95107

96108
// The set of matching tests (or, in the case of `swift test list`, the set
97109
// of all tests.)
98-
let tests: [Test]
110+
var tests = [Test]()
111+
var libraries = [Library]()
99112

100-
if args.listTests ?? false {
113+
if args.listTests == true {
101114
tests = await Array(Test.all)
102115

103116
if args.verbosity > .min {
@@ -116,6 +129,29 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
116129
for test in tests {
117130
Event.post(.testDiscovered, for: (test, nil), configuration: configuration)
118131
}
132+
} else if args.experimentalListLibraries == true {
133+
#if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY
134+
libraries = Array(Library.all)
135+
#else
136+
libraries = [Library.swiftTesting]
137+
#endif
138+
139+
if args.verbosity > .min {
140+
for library in libraries {
141+
// Print the test ID to stdout (classical CLI behavior.)
142+
let libraryDescription = "\(library.name) (swift test --experimental-testing-library \(library.canonicalHint))"
143+
#if SWT_TARGET_OS_APPLE && !SWT_NO_FILE_IO
144+
try? FileHandle.stdout.write("\(libraryDescription)\n")
145+
#else
146+
print(libraryDescription)
147+
#endif
148+
}
149+
}
150+
151+
// Post an event for every discovered library (as with tests above).
152+
for library in libraries {
153+
Event.post(.libraryDiscovered(library), for: (nil, nil), configuration: configuration)
154+
}
119155
} else {
120156
// Run the tests.
121157
let runner = await Runner(configuration: configuration)
@@ -126,7 +162,7 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
126162
// If there were no matching tests, exit with a dedicated exit code so that
127163
// the caller (assumed to be Swift Package Manager) can implement special
128164
// handling.
129-
if tests.isEmpty {
165+
if tests.isEmpty && libraries.isEmpty {
130166
exitCode.withLock { exitCode in
131167
if exitCode == EXIT_SUCCESS {
132168
exitCode = EXIT_NO_TESTS_FOUND
@@ -211,6 +247,9 @@ public struct __CommandLineArguments_v0: Sendable {
211247
/// The value of the `--list-tests` argument.
212248
public var listTests: Bool?
213249

250+
/// The value of the `--experimental-list-libraries` argument.
251+
public var experimentalListLibraries: Bool?
252+
214253
/// The value of the `--parallel` or `--no-parallel` argument.
215254
public var parallel: Bool?
216255

@@ -335,13 +374,17 @@ public struct __CommandLineArguments_v0: Sendable {
335374

336375
/// The value of the `--attachments-path` argument.
337376
public var attachmentsPath: String?
377+
378+
/// The value of the `--testing-library` argument.
379+
public var testingLibrary: String?
338380
}
339381

340382
extension __CommandLineArguments_v0: Codable {
341383
// Explicitly list the coding keys so that storage properties like _verbosity
342384
// do not end up with leading underscores when encoded.
343385
enum CodingKeys: String, CodingKey {
344386
case listTests
387+
case experimentalListLibraries
345388
case parallel
346389
case experimentalMaximumParallelizationWidth
347390
case symbolicateBacktraces
@@ -357,6 +400,7 @@ extension __CommandLineArguments_v0: Codable {
357400
case repetitions
358401
case repeatUntil
359402
case attachmentsPath
403+
case testingLibrary
360404
}
361405
}
362406

@@ -468,6 +512,11 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
468512
}
469513
#endif
470514

515+
// Testing library
516+
if let testingLibrary = Environment.variable(named: "SWT_EXPERIMENTAL_LIBRARY") ?? args.argumentValue(forLabel: "--testing-library") {
517+
result.testingLibrary = testingLibrary
518+
}
519+
471520
// XML output
472521
if let xunitOutputPath = args.argumentValue(forLabel: "--xunit-output") {
473522
result.xunitOutput = xunitOutputPath
@@ -486,6 +535,9 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
486535
// makes invocation from e.g. Wasmtime a bit more intuitive/idiomatic.
487536
result.listTests = true
488537
}
538+
if Environment.flag(named: "SWT_EXPERIMENTAL_LIST_LIBRARIES") == true || args.contains("--experimental-list-libraries") {
539+
result.experimentalListLibraries = true
540+
}
489541

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

0 commit comments

Comments
 (0)