Skip to content

Commit 1b7dba9

Browse files
authored
[SWT-0002] A stable JSON-based ABI for tools integration (#479)
One of the core components of Swift Testing is its ability to interoperate with Xcode 16, VS Code, and other tools. Swift Testing has been fully open-sourced across all platforms supported by Swift, and can be added as a package dependency (or—eventually—linked from the Swift toolchain.) Because Swift Testing may be used in various forms, and because integration with various tools is critical to its success, we need it to have a stable interface that can be used regardless of how it's been added to a package. Read the full proposal [here](https://github.com/apple/swift-testing/blob/main/Documentation/Proposals/0002-json-abi.md). ### 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 22d7705 commit 1b7dba9

17 files changed

+665
-126
lines changed

Diff for: Documentation/ABI/JSON.md

+5-9
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,9 @@ See https://swift.org/CONTRIBUTORS.txt for Swift project authors
1111
-->
1212

1313
This document outlines the JSON schemas used by the testing library for its ABI
14-
entry point and for the `--experimental-event-stream-output` command-line
15-
argument. For more information about the ABI entry point, see the documentation
16-
for [ABI.EntryPoint_v0](https://github.com/search?q=repo%3Aapple%2Fswift-testing%EntryPoint_v0&type=code).
17-
18-
> [!WARNING]
19-
> This JSON schema is still being developed and is subject to any and all
20-
> changes including removal from the package.
14+
entry point and for the `--event-stream-output-path` command-line argument. For
15+
more information about the ABI entry point, see the documentation for
16+
[ABIv0.EntryPoint](https://github.com/search?q=repo%3Aapple%2Fswift-testing%EntryPoint&type=code).
2117

2218
## Modified Backus-Naur form
2319

@@ -103,8 +99,8 @@ encoded as a single [JSON Lines](https://jsonlines.org) value.
10399

104100
A stream consists of a sequence of values encoded as [JSON Lines](https://jsonlines.org).
105101
A single instance of `<output-stream>` is defined per test process and can be
106-
accessed by passing `--experimental-event-stream-output` to the test executable
107-
created by `swift build --build-tests`.
102+
accessed by passing `--event-stream-output-path` to the test executable created
103+
by `swift build --build-tests`.
108104

109105
```
110106
<output-stream> ::= <output-record>\n | <output-record>\n <output-stream>

Diff for: Documentation/Proposals/0002-json-abi.md

+423
Large diffs are not rendered by default.

Diff for: Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2023 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+
#if canImport(Foundation) && !SWT_NO_ABI_ENTRY_POINT
12+
private import _TestingInternals
13+
14+
extension ABIv0 {
15+
/// The type of the entry point to the testing library used by tools that want
16+
/// to remain version-agnostic regarding the testing library.
17+
///
18+
/// - Parameters:
19+
/// - configurationJSON: A buffer to memory representing the test
20+
/// configuration and options. If `nil`, a new instance is synthesized
21+
/// from the command-line arguments to the current process.
22+
/// - recordHandler: A JSON record handler to which is passed a buffer to
23+
/// memory representing each record as described in `ABI/JSON.md`.
24+
///
25+
/// - Returns: Whether or not the test run finished successfully.
26+
///
27+
/// - Throws: Any error that occurred prior to running tests. Errors that are
28+
/// thrown while tests are running are handled by the testing library.
29+
public typealias EntryPoint = @convention(thin) @Sendable (
30+
_ configurationJSON: UnsafeRawBufferPointer?,
31+
_ recordHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void
32+
) async throws -> Bool
33+
34+
/// The entry point to the testing library used by tools that want to remain
35+
/// version-agnostic regarding the testing library.
36+
///
37+
/// The value of this property is a Swift function that can be used by tools
38+
/// that do not link directly to the testing library and wish to invoke tests
39+
/// in a binary that has been loaded into the current process. The value of
40+
/// this property is accessible from C and C++ as a function with name
41+
/// `"swt_abiv0_getEntryPoint"` and can be dynamically looked up at runtime
42+
/// using `dlsym()` or a platform equivalent.
43+
///
44+
/// The value of this property can be thought of as equivalent to a call to
45+
/// `swift test --event-stream-output-path` except that, instead of streaming
46+
/// JSON records to a named pipe or file, it streams them to an in-process
47+
/// callback.
48+
public static var entryPoint: EntryPoint {
49+
return { configurationJSON, recordHandler in
50+
try await Testing.entryPoint(
51+
configurationJSON: configurationJSON,
52+
recordHandler: recordHandler
53+
) == EXIT_SUCCESS
54+
}
55+
}
56+
}
57+
58+
/// An exported C function that is the equivalent of
59+
/// ``ABIv0/entryPoint-swift.type.property``.
60+
///
61+
/// - Returns: The value of ``ABIv0/entryPoint-swift.type.property`` cast to an
62+
/// untyped pointer.
63+
@_cdecl("swt_abiv0_getEntryPoint")
64+
@usableFromInline func abiv0_getEntryPoint() -> UnsafeRawPointer {
65+
unsafeBitCast(ABIv0.entryPoint, to: UnsafeRawPointer.self)
66+
}
67+
68+
// MARK: - Xcode 16 Beta 1 compatibility
69+
70+
/// An older signature for ``ABIv0/EntryPoint-swift.typealias`` used by Xcode 16
71+
/// Beta 1.
72+
///
73+
/// This type will be removed in a future update.
74+
@available(*, deprecated, message: "Use ABIv0.EntryPoint instead.")
75+
typealias ABIEntryPoint_v0 = @Sendable (
76+
_ argumentsJSON: UnsafeRawBufferPointer?,
77+
_ recordHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void
78+
) async throws -> CInt
79+
80+
/// An older signature for ``ABIv0/entryPoint-swift.type.property`` used by
81+
/// Xcode 16 Beta 1.
82+
///
83+
/// This function will be removed in a future update.
84+
@available(*, deprecated, message: "Use ABIv0.entryPoint (swt_abiv0_getEntryPoint()) instead.")
85+
@_cdecl("swt_copyABIEntryPoint_v0")
86+
@usableFromInline func copyABIEntryPoint_v0() -> UnsafeMutableRawPointer {
87+
let result = UnsafeMutablePointer<ABIEntryPoint_v0>.allocate(capacity: 1)
88+
result.initialize { configurationJSON, recordHandler in
89+
try await entryPoint(
90+
configurationJSON: configurationJSON,
91+
eventStreamVersionIfNil: -1,
92+
recordHandler: recordHandler
93+
)
94+
}
95+
return .init(result)
96+
}
97+
98+
/// A common implementation for ``ABIv0/entryPoint-swift.type.property`` and
99+
/// ``copyABIEntryPoint_v0()`` that provides Xcode 16 Beta 1 compatibility.
100+
///
101+
/// This function will be removed (with its logic incorporated into
102+
/// ``ABIv0/entryPoint-swift.type.property``) in a future update.
103+
private func entryPoint(
104+
configurationJSON: UnsafeRawBufferPointer?,
105+
eventStreamVersionIfNil: Int? = nil,
106+
recordHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void
107+
) async throws -> CInt {
108+
var args = try configurationJSON.map { configurationJSON in
109+
try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON)
110+
}
111+
112+
// If the caller needs a nil event stream version to default to a specific
113+
// JSON schema, apply it here as if they'd specified it in the configuration
114+
// JSON blob.
115+
if let eventStreamVersionIfNil, args?.eventStreamVersion == nil {
116+
args?.eventStreamVersion = eventStreamVersionIfNil
117+
}
118+
119+
let eventHandler = try eventHandlerForStreamingEvents(version: args?.eventStreamVersion, forwardingTo: recordHandler)
120+
let exitCode = await entryPoint(passing: args, eventHandler: eventHandler)
121+
return exitCode
122+
}
123+
#endif

Diff for: Sources/Testing/EntryPoints/EntryPoint.swift renamed to Sources/Testing/ABI/EntryPoints/EntryPoint.swift

+19-16
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ private import _TestingInternals
2525
/// to this function.
2626
///
2727
/// External callers cannot call this function directly. The can use
28-
/// ``copyABIEntryPoint_v0()`` to get a reference to an ABI-stable version of
29-
/// this function.
28+
/// ``ABIv0/entryPoint-swift.type.property`` to get a reference to an ABI-stable
29+
/// version of this function.
3030
func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Handler?) async -> CInt {
3131
let exitCode = Locked(rawValue: EXIT_SUCCESS)
3232

@@ -200,7 +200,7 @@ public struct __CommandLineArguments_v0: Sendable {
200200
/// The value of the `--xunit-output` argument.
201201
public var xunitOutput: String?
202202

203-
/// The value of the `--experimental-event-stream-output` argument.
203+
/// The value of the `--event-stream-output-path` argument.
204204
///
205205
/// Data is written to this file in the [JSON Lines](https://jsonlines.org)
206206
/// text format. For each event handled by the resulting event handler, a JSON
@@ -215,18 +215,18 @@ public struct __CommandLineArguments_v0: Sendable {
215215
///
216216
/// The file is closed when this process terminates or the test run completes,
217217
/// whichever occurs first.
218-
public var experimentalEventStreamOutput: String?
218+
public var eventStreamOutputPath: String?
219219

220220
/// The version of the event stream schema to use when writing events to
221-
/// ``experimentalEventStreamOutput``.
221+
/// ``eventStreamOutput``.
222222
///
223223
/// The corresponding stable schema is used to encode events to the event
224224
/// stream (for example, ``ABIv0/Record`` is used if the value of this
225225
/// property is `0`.)
226226
///
227227
/// If the value of this property is `nil`, the testing library assumes that
228228
/// the newest available schema should be used.
229-
public var experimentalEventStreamVersion: Int?
229+
public var eventStreamVersion: Int?
230230

231231
/// The value(s) of the `--filter` argument.
232232
public var filter: [String]?
@@ -252,8 +252,8 @@ extension __CommandLineArguments_v0: Codable {
252252
case quiet
253253
case _verbosity = "verbosity"
254254
case xunitOutput
255-
case experimentalEventStreamOutput
256-
case experimentalEventStreamVersion
255+
case eventStreamOutputPath
256+
case eventStreamVersion
257257
case filter
258258
case skip
259259
case repetitions
@@ -288,7 +288,8 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
288288
// NOTE: While the output event stream is opened later, it is necessary to
289289
// open the configuration file early (here) in order to correctly construct
290290
// the resulting __CommandLineArguments_v0 instance.
291-
if let configurationIndex = args.firstIndex(of: "--experimental-configuration-path"), !isLastArgument(at: configurationIndex) {
291+
if let configurationIndex = args.firstIndex(of: "--configuration-path") ?? args.firstIndex(of: "--experimental-configuration-path"),
292+
!isLastArgument(at: configurationIndex) {
292293
let path = args[args.index(after: configurationIndex)]
293294
let file = try FileHandle(forReadingAtPath: path)
294295
let configurationJSON = try file.readToEnd()
@@ -302,12 +303,14 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
302303
}
303304

304305
// Event stream output (experimental)
305-
if let eventOutputIndex = args.firstIndex(of: "--experimental-event-stream-output"), !isLastArgument(at: eventOutputIndex) {
306-
result.experimentalEventStreamOutput = args[args.index(after: eventOutputIndex)]
306+
if let eventOutputIndex = args.firstIndex(of: "--event-stream-output-path") ?? args.firstIndex(of: "--experimental-event-stream-output"),
307+
!isLastArgument(at: eventOutputIndex) {
308+
result.eventStreamOutputPath = args[args.index(after: eventOutputIndex)]
307309
}
308310
// Event stream output (experimental)
309-
if let eventOutputVersionIndex = args.firstIndex(of: "--experimental-event-stream-version"), !isLastArgument(at: eventOutputVersionIndex) {
310-
result.experimentalEventStreamVersion = Int(args[args.index(after: eventOutputVersionIndex)])
311+
if let eventOutputVersionIndex = args.firstIndex(of: "--event-stream-version") ?? args.firstIndex(of: "--experimental-event-stream-version"),
312+
!isLastArgument(at: eventOutputVersionIndex) {
313+
result.eventStreamVersion = Int(args[args.index(after: eventOutputVersionIndex)])
311314
}
312315
#endif
313316

@@ -404,9 +407,9 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr
404407

405408
#if canImport(Foundation)
406409
// Event stream output (experimental)
407-
if let eventStreamOutputPath = args.experimentalEventStreamOutput {
410+
if let eventStreamOutputPath = args.eventStreamOutputPath {
408411
let file = try FileHandle(forWritingAtPath: eventStreamOutputPath)
409-
let eventHandler = try eventHandlerForStreamingEvents(version: args.experimentalEventStreamVersion) { json in
412+
let eventHandler = try eventHandlerForStreamingEvents(version: args.eventStreamVersion) { json in
410413
try? _writeJSONLine(json, to: file)
411414
}
412415
configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in
@@ -489,7 +492,7 @@ func eventHandlerForStreamingEvents(version: Int?, forwardingTo eventHandler: @e
489492
case nil, 0:
490493
ABIv0.Record.eventHandler(forwardingTo: eventHandler)
491494
case let .some(unsupportedVersion):
492-
throw _EntryPointError.invalidArgument("--experimental-event-stream-version", value: "\(unsupportedVersion)")
495+
throw _EntryPointError.invalidArgument("--event-stream-version", value: "\(unsupportedVersion)")
493496
}
494497
}
495498

Diff for: Sources/Testing/EntryPoints/ABIv0/ABIv0.Record+Streaming.swift renamed to Sources/Testing/ABI/v0/ABIv0.Record+Streaming.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ extension ABIv0.Record {
1515
///
1616
/// - Parameters:
1717
/// - eventHandler: The event handler to forward events to. See
18-
/// ``ABIv0/EntryPoint`` for more information.
18+
/// ``ABIv0/EntryPoint-swift.typealias`` for more information.
1919
///
2020
/// - Returns: An event handler.
2121
///
@@ -74,7 +74,7 @@ extension EventAndContextSnapshot: Codable {}
7474
///
7575
/// - Parameters:
7676
/// - eventHandler: The event handler to forward events to. See
77-
/// ``ABIEntryPoint_v0`` for more information.
77+
/// ``ABIv0/EntryPoint-swift.typealias`` for more information.
7878
///
7979
/// - Returns: An event handler.
8080
///

Diff for: Sources/Testing/EntryPoints/ABIv0/ABIv0.swift renamed to Sources/Testing/ABI/v0/ABIv0.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
//
1010

1111
/// A namespace for ABI version 0 symbols.
12-
enum ABIv0: Sendable {}
12+
@_spi(ForToolsIntegrationOnly)
13+
public enum ABIv0: Sendable {}

Diff for: Sources/Testing/CMakeLists.txt

+11-11
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@
77
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors
88

99
add_library(Testing
10-
EntryPoints/ABIEntryPoint.swift
11-
EntryPoints/ABIv0/ABIv0.Record.swift
12-
EntryPoints/ABIv0/ABIv0.Record+Streaming.swift
13-
EntryPoints/ABIv0/ABIv0.swift
14-
EntryPoints/ABIv0/Encoded/ABIv0.EncodedEvent.swift
15-
EntryPoints/ABIv0/Encoded/ABIv0.EncodedInstant.swift
16-
EntryPoints/ABIv0/Encoded/ABIv0.EncodedIssue.swift
17-
EntryPoints/ABIv0/Encoded/ABIv0.EncodedMessage.swift
18-
EntryPoints/ABIv0/Encoded/ABIv0.EncodedTest.swift
19-
EntryPoints/EntryPoint.swift
20-
EntryPoints/SwiftPMEntryPoint.swift
10+
ABI/EntryPoints/ABIEntryPoint.swift
11+
ABI/EntryPoints/EntryPoint.swift
12+
ABI/EntryPoints/SwiftPMEntryPoint.swift
13+
ABI/v0/ABIv0.Record.swift
14+
ABI/v0/ABIv0.Record+Streaming.swift
15+
ABI/v0/ABIv0.swift
16+
ABI/v0/Encoded/ABIv0.EncodedEvent.swift
17+
ABI/v0/Encoded/ABIv0.EncodedInstant.swift
18+
ABI/v0/Encoded/ABIv0.EncodedIssue.swift
19+
ABI/v0/Encoded/ABIv0.EncodedMessage.swift
20+
ABI/v0/Encoded/ABIv0.EncodedTest.swift
2121
Events/Clock.swift
2222
Events/Event.swift
2323
Events/Recorder/Event.ConsoleOutputRecorder.swift

Diff for: Sources/Testing/EntryPoints/ABIEntryPoint.swift

-73
This file was deleted.

0 commit comments

Comments
 (0)