Skip to content

Commit 7efce39

Browse files
authored
Plumb the schema version through the JSON ABI layers. (#956)
This PR plumbs the schema version (0 or 1) through the Swift types and functions that produce our JSON output, allowing them to change behaviour based on which version is in use. As a proof-of-concept as much as anything else, this PR also adds an (unsupported) `_tags` field to the JSON output for a test record, but only with ABI version 1. I've split this PR into two commits: the first plumbs the version through as an integer argument, while the second uses the type system to represent different ABI versions. The latter uses a protocol that we can extend so that different ABI versions have differing behaviours (although that's too coarse-grained for things like `_tags`.) ### 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 85bfa51 commit 7efce39

16 files changed

+290
-175
lines changed

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

Lines changed: 28 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//
1010

1111
#if canImport(Foundation) && (!SWT_NO_FILE_IO || !SWT_NO_ABI_ENTRY_POINT)
12-
extension ABI.Record {
12+
extension ABI.Version {
1313
/// Post-process encoded JSON and write it to a file.
1414
///
1515
/// - Parameters:
@@ -43,25 +43,6 @@ extension ABI.Record {
4343
}
4444
}
4545

46-
/// Create an event handler that encodes events as JSON and forwards them to
47-
/// an ABI-friendly event handler.
48-
///
49-
/// - Parameters:
50-
/// - encodeAsJSONLines: Whether or not to ensure JSON passed to
51-
/// `eventHandler` is encoded as JSON Lines (i.e. that it does not contain
52-
/// extra newlines.)
53-
/// - eventHandler: The event handler to forward events to. See
54-
/// ``ABIv0/EntryPoint-swift.typealias`` for more information.
55-
///
56-
/// - Returns: An event handler.
57-
///
58-
/// The resulting event handler outputs data as JSON. For each event handled
59-
/// by the resulting event handler, a JSON object representing it and its
60-
/// associated context is created and is passed to `eventHandler`.
61-
///
62-
/// Note that ``configurationForEntryPoint(from:)`` calls this function and
63-
/// performs additional postprocessing before writing JSON data to ensure it
64-
/// does not contain any newline characters.
6546
static func eventHandler(
6647
encodeAsJSONLines: Bool,
6748
forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void
@@ -75,12 +56,12 @@ extension ABI.Record {
7556
let humanReadableOutputRecorder = Event.HumanReadableOutputRecorder()
7657
return { [eventHandler = eventHandlerCopy] event, context in
7758
if case .testDiscovered = event.kind, let test = context.test {
78-
try? JSON.withEncoding(of: Self(encoding: test)) { testJSON in
59+
try? JSON.withEncoding(of: ABI.Record<Self>(encoding: test)) { testJSON in
7960
eventHandler(testJSON)
8061
}
8162
} else {
8263
let messages = humanReadableOutputRecorder.record(event, in: context, verbosity: 0)
83-
if let eventRecord = Self(encoding: event, in: context, messages: messages) {
64+
if let eventRecord = ABI.Record<Self>(encoding: event, in: context, messages: messages) {
8465
try? JSON.withEncoding(of: eventRecord, eventHandler)
8566
}
8667
}
@@ -89,63 +70,33 @@ extension ABI.Record {
8970
}
9071

9172
#if !SWT_NO_SNAPSHOT_TYPES
92-
// MARK: - Experimental event streaming
73+
// MARK: - Xcode 16 Beta 1 compatibility
9374

94-
/// A type containing an event snapshot and snapshots of the contents of an
95-
/// event context suitable for streaming over JSON.
96-
///
97-
/// This type is not part of the public interface of the testing library.
98-
/// External adopters are not necessarily written in Swift and are expected to
99-
/// decode the JSON produced for this type in implementation-specific ways.
100-
///
101-
/// - Warning: This type supports early Xcode 16 betas and will be removed in a
102-
/// future update.
103-
struct EventAndContextSnapshot {
104-
/// A snapshot of the event.
105-
var event: Event.Snapshot
106-
107-
/// A snapshot of the event context.
108-
var eventContext: Event.Context.Snapshot
109-
}
110-
111-
extension EventAndContextSnapshot: Codable {}
75+
extension ABI.Xcode16Beta1 {
76+
static func eventHandler(
77+
encodeAsJSONLines: Bool,
78+
forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void
79+
) -> Event.Handler {
80+
return { event, context in
81+
if case .testDiscovered = event.kind {
82+
// Discard events of this kind rather than forwarding them to avoid a
83+
// crash in Xcode 16 Beta 1 (which does not expect any events to occur
84+
// before .runStarted.)
85+
return
86+
}
11287

113-
/// Create an event handler that encodes events as JSON and forwards them to an
114-
/// ABI-friendly event handler.
115-
///
116-
/// - Parameters:
117-
/// - eventHandler: The event handler to forward events to. See
118-
/// ``ABIv0/EntryPoint-swift.typealias`` for more information.
119-
///
120-
/// - Returns: An event handler.
121-
///
122-
/// The resulting event handler outputs data as JSON. For each event handled by
123-
/// the resulting event handler, a JSON object representing it and its
124-
/// associated context is created and is passed to `eventHandler`.
125-
///
126-
/// Note that ``configurationForEntryPoint(from:)`` calls this function and
127-
/// performs additional postprocessing before writing JSON data to ensure it
128-
/// does not contain any newline characters.
129-
///
130-
/// - Warning: This function supports early Xcode 16 betas and will be removed
131-
/// in a future update.
132-
func eventHandlerForStreamingEventSnapshots(
133-
to eventHandler: @escaping @Sendable (_ eventAndContextJSON: UnsafeRawBufferPointer) -> Void
134-
) -> Event.Handler {
135-
return { event, context in
136-
if case .testDiscovered = event.kind {
137-
// Discard events of this kind rather than forwarding them to avoid a
138-
// crash in Xcode 16 Beta 1 (which does not expect any events to occur
139-
// before .runStarted.)
140-
return
141-
}
142-
let snapshot = EventAndContextSnapshot(
143-
event: Event.Snapshot(snapshotting: event),
144-
eventContext: Event.Context.Snapshot(snapshotting: context)
145-
)
146-
try? JSON.withEncoding(of: snapshot) { eventAndContextJSON in
147-
eventAndContextJSON.withUnsafeBytes { eventAndContextJSON in
148-
eventHandler(eventAndContextJSON)
88+
struct EventAndContextSnapshot: Codable {
89+
var event: Event.Snapshot
90+
var eventContext: Event.Context.Snapshot
91+
}
92+
let snapshot = EventAndContextSnapshot(
93+
event: Event.Snapshot(snapshotting: event),
94+
eventContext: Event.Context.Snapshot(snapshotting: context)
95+
)
96+
try? JSON.withEncoding(of: snapshot) { eventAndContextJSON in
97+
eventAndContextJSON.withUnsafeBytes { eventAndContextJSON in
98+
eventHandler(eventAndContextJSON)
99+
}
149100
}
150101
}
151102
}

Sources/Testing/ABI/ABI.Record.swift

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,14 @@ extension ABI {
1515
/// This type is not part of the public interface of the testing library. It
1616
/// assists in converting values to JSON; clients that consume this JSON are
1717
/// expected to write their own decoders.
18-
struct Record: Sendable {
19-
/// The version of this record.
20-
///
21-
/// The value of this property corresponds to the ABI version i.e. `0`.
22-
var version = 0
23-
18+
struct Record<V>: Sendable where V: ABI.Version {
2419
/// An enumeration describing the various kinds of record.
2520
enum Kind: Sendable {
2621
/// A test record.
27-
case test(EncodedTest)
22+
case test(EncodedTest<V>)
2823

2924
/// An event record.
30-
case event(EncodedEvent)
25+
case event(EncodedEvent<V>)
3126
}
3227

3328
/// The kind of record.
@@ -38,7 +33,7 @@ extension ABI {
3833
}
3934

4035
init?(encoding event: borrowing Event, in eventContext: borrowing Event.Context, messages: borrowing [Event.HumanReadableOutputRecorder.Message]) {
41-
guard let event = EncodedEvent(encoding: event, in: eventContext, messages: messages) else {
36+
guard let event = EncodedEvent<V>(encoding: event, in: eventContext, messages: messages) else {
4237
return nil
4338
}
4439
kind = .event(event)
@@ -57,7 +52,7 @@ extension ABI.Record: Codable {
5752

5853
func encode(to encoder: any Encoder) throws {
5954
var container = encoder.container(keyedBy: CodingKeys.self)
60-
try container.encode(version, forKey: .version)
55+
try container.encode(V.versionNumber, forKey: .version)
6156
switch kind {
6257
case let .test(test):
6358
try container.encode("test", forKey: .kind)
@@ -70,16 +65,31 @@ extension ABI.Record: Codable {
7065

7166
init(from decoder: any Decoder) throws {
7267
let container = try decoder.container(keyedBy: CodingKeys.self)
73-
version = try container.decode(Int.self, forKey: .version)
68+
69+
let versionNumber = try container.decode(Int.self, forKey: .version)
70+
if versionNumber != V.versionNumber {
71+
throw DecodingError.dataCorrupted(
72+
DecodingError.Context(
73+
codingPath: decoder.codingPath + CollectionOfOne(CodingKeys.version as any CodingKey),
74+
debugDescription: "Unexpected record version \(versionNumber) (expected \(V.versionNumber))."
75+
)
76+
)
77+
}
78+
7479
switch try container.decode(String.self, forKey: .kind) {
7580
case "test":
76-
let test = try container.decode(ABI.EncodedTest.self, forKey: .payload)
81+
let test = try container.decode(ABI.EncodedTest<V>.self, forKey: .payload)
7782
kind = .test(test)
7883
case "event":
79-
let event = try container.decode(ABI.EncodedEvent.self, forKey: .payload)
84+
let event = try container.decode(ABI.EncodedEvent<V>.self, forKey: .payload)
8085
kind = .event(event)
8186
case let kind:
82-
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unrecognized record kind '\(kind)'"))
87+
throw DecodingError.dataCorrupted(
88+
DecodingError.Context(
89+
codingPath: decoder.codingPath + CollectionOfOne(CodingKeys.kind as any CodingKey),
90+
debugDescription: "Unrecognized record kind '\(kind)'"
91+
)
92+
)
8393
}
8494
}
8595
}

Sources/Testing/ABI/ABI.swift

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,70 @@
1212
@_spi(ForToolsIntegrationOnly)
1313
public enum ABI: Sendable {}
1414

15-
// MARK: -
15+
// MARK: - ABI version abstraction
16+
17+
extension ABI {
18+
/// A protocol describing the types that represent different ABI versions.
19+
protocol Version: Sendable {
20+
/// The numeric representation of this ABI version.
21+
static var versionNumber: Int { get }
22+
23+
/// Create an event handler that encodes events as JSON and forwards them to
24+
/// an ABI-friendly event handler.
25+
///
26+
/// - Parameters:
27+
/// - encodeAsJSONLines: Whether or not to ensure JSON passed to
28+
/// `eventHandler` is encoded as JSON Lines (i.e. that it does not
29+
/// contain extra newlines.)
30+
/// - eventHandler: The event handler to forward events to.
31+
///
32+
/// - Returns: An event handler.
33+
///
34+
/// The resulting event handler outputs data as JSON. For each event handled
35+
/// by the resulting event handler, a JSON object representing it and its
36+
/// associated context is created and is passed to `eventHandler`.
37+
static func eventHandler(
38+
encodeAsJSONLines: Bool,
39+
forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void
40+
) -> Event.Handler
41+
}
42+
43+
/// The current supported ABI version (ignoring any experimental versions.)
44+
typealias CurrentVersion = v0
45+
}
46+
47+
// MARK: - Concrete ABI versions
1648

17-
@_spi(ForToolsIntegrationOnly)
1849
extension ABI {
19-
/// A namespace for ABI version 0 symbols.
20-
public enum v0: Sendable {}
50+
#if !SWT_NO_SNAPSHOT_TYPES
51+
/// A namespace and version type for Xcode 16 Beta 1 compatibility.
52+
///
53+
/// - Warning: This type will be removed in a future update.
54+
enum Xcode16Beta1: Sendable, Version {
55+
static var versionNumber: Int {
56+
-1
57+
}
58+
}
59+
#endif
60+
61+
/// A namespace and type for ABI version 0 symbols.
62+
public enum v0: Sendable, Version {
63+
static var versionNumber: Int {
64+
0
65+
}
66+
}
2167

22-
/// A namespace for ABI version 1 symbols.
68+
/// A namespace and type for ABI version 1 symbols.
69+
///
70+
/// @Metadata {
71+
/// @Available("Swift Testing ABI", introduced: 1)
72+
/// }
2373
@_spi(Experimental)
24-
public enum v1: Sendable {}
74+
public enum v1: Sendable, Version {
75+
static var versionNumber: Int {
76+
1
77+
}
78+
}
2579
}
2680

2781
/// A namespace for ABI version 0 symbols.

Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ extension ABI {
1717
/// expected to write their own decoders.
1818
///
1919
/// - Warning: Attachments are not yet part of the JSON schema.
20-
struct EncodedAttachment: Sendable {
20+
struct EncodedAttachment<V>: Sendable where V: ABI.Version {
2121
/// The path where the attachment was written.
2222
var path: String?
2323

Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ extension ABI {
1717
/// expected to write their own decoders.
1818
///
1919
/// - Warning: Backtraces are not yet part of the JSON schema.
20-
struct EncodedBacktrace: Sendable {
20+
struct EncodedBacktrace<V>: Sendable where V: ABI.Version {
2121
/// The frames in the backtrace.
2222
var symbolicatedAddresses: [Backtrace.SymbolicatedAddress]
2323

Sources/Testing/ABI/Encoded/ABI.EncodedError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ extension ABI {
1717
/// expected to write their own decoders.
1818
///
1919
/// - Warning: Errors are not yet part of the JSON schema.
20-
struct EncodedError: Sendable {
20+
struct EncodedError<V>: Sendable where V: ABI.Version {
2121
/// The error's description
2222
var description: String
2323

Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ extension ABI {
1515
/// This type is not part of the public interface of the testing library. It
1616
/// assists in converting values to JSON; clients that consume this JSON are
1717
/// expected to write their own decoders.
18-
struct EncodedEvent: Sendable {
18+
struct EncodedEvent<V>: Sendable where V: ABI.Version {
1919
/// An enumeration describing the various kinds of event.
2020
///
2121
/// Note that the set of encodable events is a subset of all events
@@ -38,33 +38,33 @@ extension ABI {
3838
var kind: Kind
3939

4040
/// The instant at which the event occurred.
41-
var instant: EncodedInstant
41+
var instant: EncodedInstant<V>
4242

4343
/// The issue that occurred, if any.
4444
///
4545
/// The value of this property is `nil` unless the value of the
4646
/// ``kind-swift.property`` property is ``Kind-swift.enum/issueRecorded``.
47-
var issue: EncodedIssue?
47+
var issue: EncodedIssue<V>?
4848

4949
/// The value that was attached, if any.
5050
///
5151
/// The value of this property is `nil` unless the value of the
5252
/// ``kind-swift.property`` property is ``Kind-swift.enum/valueAttached``.
5353
///
5454
/// - Warning: Attachments are not yet part of the JSON schema.
55-
var _attachment: EncodedAttachment?
55+
var _attachment: EncodedAttachment<V>?
5656

5757
/// Human-readable messages associated with this event that can be presented
5858
/// to the user.
59-
var messages: [EncodedMessage]
59+
var messages: [EncodedMessage<V>]
6060

6161
/// The ID of the test associated with this event, if any.
62-
var testID: EncodedTest.ID?
62+
var testID: EncodedTest<V>.ID?
6363

6464
/// The ID of the test case associated with this event, if any.
6565
///
6666
/// - Warning: Test cases are not yet part of the JSON schema.
67-
var _testCase: EncodedTestCase?
67+
var _testCase: EncodedTestCase<V>?
6868

6969
init?(encoding event: borrowing Event, in eventContext: borrowing Event.Context, messages: borrowing [Event.HumanReadableOutputRecorder.Message]) {
7070
switch event.kind {

Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ extension ABI {
1515
/// This type is not part of the public interface of the testing library. It
1616
/// assists in converting values to JSON; clients that consume this JSON are
1717
/// expected to write their own decoders.
18-
struct EncodedInstant: Sendable {
18+
struct EncodedInstant<V>: Sendable where V: ABI.Version {
1919
/// The number of seconds since the system-defined suspending epoch.
2020
///
2121
/// For more information, see [`SuspendingClock`](https://developer.apple.com/documentation/swift/suspendingclock).

0 commit comments

Comments
 (0)