|
| 1 | +# Working with the JSON event stream in Swift |
| 2 | + |
| 3 | +<!-- |
| 4 | +This source file is part of the Swift.org open source project |
| 5 | +
|
| 6 | +Copyright (c) 2026 Apple Inc. and the Swift project authors |
| 7 | +Licensed under Apache License v2.0 with Runtime Library Exception |
| 8 | +
|
| 9 | +See https://swift.org/LICENSE.txt for license information |
| 10 | +See https://swift.org/CONTRIBUTORS.txt for Swift project authors |
| 11 | +--> |
| 12 | + |
| 13 | +This document outlines the `ABI.Record` and `ABI.Encoded*` Swift types and how |
| 14 | +to use them. |
| 15 | + |
| 16 | +> [!NOTE] |
| 17 | +> These types are marked `@_spi(ForToolsIntegrationOnly)`, so they are only |
| 18 | +> available when you build Swift Testing from source. |
| 19 | +
|
| 20 | +## The `ABI` namespace |
| 21 | + |
| 22 | +Swift Testing uses a Swift enumeration type named `ABI` to namespace types, |
| 23 | +constants, etc. that are related to Swift Testing's ABI-stable and -semi-stable |
| 24 | +interface but which are not exposed to test authors as API. These symbols are |
| 25 | +not meant to be used by test authors in typical workflows, but are useful for |
| 26 | +_tools_ authors who wish to integrate their tools with Swift Testing. |
| 27 | + |
| 28 | +### The `ABI.Version` and `ABI.VersionNumber` types |
| 29 | + |
| 30 | +Swift Testing uses the `ABI.Version` Swift protocol to define a particular ABI |
| 31 | +version, which as of Swift 6.3 is directly tied to a specific Swift toolchain |
| 32 | +release. We declare types under the `ABI` namespace that conform to this |
| 33 | +protocol. For example, `ABI.v6_3` is a type that conforms to `ABI.Version` and |
| 34 | +represents the Swift Testing ABI used in the Swift 6.3 toolchain. |
| 35 | + |
| 36 | +> [!NOTE] |
| 37 | +> Unless otherwise stated, Swift toolchain patch releases share an ABI version |
| 38 | +> with their previous minor Swift toolchain release. For instance, Swift Testing |
| 39 | +> in the Swift 6.3.1 toolchain uses the same ABI as does Swift Testing in the |
| 40 | +> Swift 6.3.0 toolchain. |
| 41 | +
|
| 42 | +Each type that conforms to `ABI.Version` has a static `versionNumber` property |
| 43 | +of type `ABI.VersionNumber`, which is a `Comparable` and `Codable` value that |
| 44 | +represents that version. You can use this property to, for example, check if an |
| 45 | +`ABI.Version`-conforming type is newer than another: |
| 46 | + |
| 47 | +```swift |
| 48 | +let abi: (some ABI.Version).Type |
| 49 | +let isNewerThan6_3 = abi.versionNumber > ABI.v6_3.versionNumber |
| 50 | +``` |
| 51 | + |
| 52 | +Swift Testing also defines `ABI.ExperimentalVersion` to represent experimental |
| 53 | +(and **unsupported!**) ABI variants. |
| 54 | + |
| 55 | +## The JSON event stream |
| 56 | + |
| 57 | +The JSON event stream Swift Testing produces at runtime is defined in |
| 58 | +[JSON.md](JSON.md). An instance of the Swift type `JSON.Record` represents an |
| 59 | +`<output-record>` value as defined in that file. `JSON.Record` itself is generic |
| 60 | +over some type conforming to `ABI.Version`; that type provides `JSON.Record` |
| 61 | +with the information it needs to correctly encode and decode JSON objects. |
| 62 | + |
| 63 | +> [!NOTE] |
| 64 | +> If you are writing your tools using a language other than Swift, you can |
| 65 | +> decode the JSON in this stream using your language's JSON decoder and access |
| 66 | +> its fields directly. Using languages other than Swift is beyond the scope of |
| 67 | +> this document. |
| 68 | +
|
| 69 | +### Decoding a JSON event record |
| 70 | + |
| 71 | +To decode an instance of `ABI.Record`, you'll first need to know what ABI |
| 72 | +version it was encoded with. We sometimes refer to this version as the JSON |
| 73 | +event stream's _schema version_. |
| 74 | + |
| 75 | +To get the appropriate `ABI.Version`-conforming type for a known version number, |
| 76 | +you can use `ABI.version(forVersionNumber:)`. If you have a JSON object from the |
| 77 | +JSON event stream and do not know what version number to use to decode it, you |
| 78 | +can use `ABI.VersionNumber.init(fromRecordJSON:)` to find out. |
| 79 | + |
| 80 | +You can use the following template to decode a JSON object from the JSON event |
| 81 | +stream: |
| 82 | + |
| 83 | +```swift |
| 84 | +func handleJSONRecord(_ json: Data) throws { |
| 85 | + // Get the ABI version number from the JSON object. Note that this initializer |
| 86 | + // takes an UnsafeRawBufferPointer rather than a Data. |
| 87 | + let versionNumber = try json.withUnsafeBytes { try ABI.VersionNumber(fromRecordJSON: $0) } |
| 88 | + |
| 89 | + guard let abi = ABI.version(forVersionNumber: versionNumber) else { |
| 90 | + // There is no Swift Testing ABI version associated with the version number |
| 91 | + // provided. Either the JSON object is malformed or the version number is |
| 92 | + // too new for the current version of Swift Testing to support. |
| 93 | + throw ... |
| 94 | + } |
| 95 | + try handleJSONRecord(json, using: abi) |
| 96 | +} |
| 97 | + |
| 98 | +func handleJSONRecord<V: ABI.Version>(_ json: Data, using _: V.Type) throws { |
| 99 | + let record = try JSONDecoder().decode(ABI.Record<V>.self, from: json) |
| 100 | + switch record.kind { |
| 101 | + case let .test(test): |
| 102 | + // This record declares the existence of a test. |
| 103 | + try cacheTest(test) |
| 104 | + case let .event(event): |
| 105 | + // This record represents some event that has occurred. |
| 106 | + try handleEvent(event) |
| 107 | + } |
| 108 | +} |
| 109 | +``` |
| 110 | + |
| 111 | +The associated values `test` and `event` in the template above are instances of |
| 112 | +the Swift Testing types `ABI.EncodedTest` and `ABI.EncodedEvent`, respectively. |
| 113 | +These types represent the JSON-codable subset of information contained in Swift |
| 114 | +Testing's `Test` API type and `Event` SPI type. If needed, you can convert them |
| 115 | +back to instances of `Test` and `Event` using the `init?(decoding:)` initializer |
| 116 | +on either type: |
| 117 | + |
| 118 | +```swift |
| 119 | +private let cachedTests = Mutex<[Test.ID: Test]>([:]) |
| 120 | + |
| 121 | +func cacheTest<V: ABI.Version>(_ test: ABI.EncodedTest<V>) throws { |
| 122 | + guard let test = Test(decoding: test) else { |
| 123 | + // Swift Testing could not recreate a copy of the original `Test` instance. |
| 124 | + throw ... |
| 125 | + } |
| 126 | + cachedTests.withLock { $0[test.id] = test } |
| 127 | + print("Discovered test '\(test.displayName ?? test.name)' at \(test.sourceLocation)") |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +> [!IMPORTANT] |
| 132 | +> When you convert an instance of `ABI.EncodedTest` to an instance of `Test` (or |
| 133 | +> `ABI.EncodedEvent` to `Event`, etc.), the conversion is lossy. Information |
| 134 | +> that was originally available in these values at runtime may not be available |
| 135 | +> in the copy you derive from the JSON event stream. In particular, you cannot |
| 136 | +> run a Swift Testing test function created in this manner as the body of the |
| 137 | +> test is not representable as JSON data. |
| 138 | +
|
| 139 | +<!-- TODO: document how to convert runtime Swift values to JSON objects (going the other direction) --> |
0 commit comments