Skip to content

Commit 3ac742f

Browse files
authored
Provide an ABI-friendly entry point function that takes/produces JSON. (#360)
This PR introduces an **experimental** function, intended to eventually become part of our stable ABI, named `swt_copyABIEntryPoint_v0()`. This function can be looked up at runtime and used to hook into the testing library when a test target is loaded into the current process. It takes a JSON-encoded structure as input and yields JSON-encoded events/event contexts via a callback. The input is encoded from a type we've tentatively named `__CommandLineArguments_v0`. This type conforms to `Codable` and represents the parsed command-line arguments that the package can understand. Normally, swift-testing will construct an instance of this type from the command-line arguments to the current process, but a caller can pass in a different instance to the ABI entry point instead. The output consists of `Event.Snapshot` and `Event.Context.Snapshot` values encoded in a single JSON object. The format of this JSON is identical to what is emitted when `--experimental-event-stream-output` is specified at the command line. `__swiftPMEntryPoint()` has also been modified to use the same `__CommandLineArguments_v0` type as input and as much of the same implementation as possible. Because trying to load a Swift function dynamically at runtime is Hard™, `swt_copyABIEntryPoint_v0()` is a C function that yields the "real" Swift function. This indirection also gives us the flexibility to produce a thick Swift closure rather than a thin function. > [!WARNING] > The signature of structure of `swt_copyABIEntryPoint_v0`, the structure of `__CommandLineArguments_v0`, how it encodes to/from JSON, and all other details about it are still subject to change. The hope is that it becomes a stable ABI surface _eventually_, but we're not there yet. :) ### 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 819a7a0 commit 3ac742f

File tree

8 files changed

+535
-162
lines changed

8 files changed

+535
-162
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
/// The type of the entry point to the testing library used by tools that want
13+
/// to remain version-agnostic regarding the testing library.
14+
///
15+
/// - Parameters:
16+
/// - argumentsJSON: A buffer to memory representing the JSON encoding of an
17+
/// instance of `__CommandLineArguments_v0`. If `nil`, a new instance is
18+
/// created from the command-line arguments to the current process.
19+
/// - eventHandler: An event handler to which is passed a buffer to memory
20+
/// representing each event and its context, as with ``Event/Handler``, but
21+
/// encoded as JSON.
22+
///
23+
/// - Returns: The result of invoking the testing library. The type of this
24+
/// value is subject to change.
25+
///
26+
/// This function examines the command-line arguments to the current process
27+
/// and then invokes available tests in the current process.
28+
///
29+
/// - Warning: This function's signature and the structure of its JSON inputs
30+
/// and outputs have not been finalized yet.
31+
@_spi(ForToolsIntegrationOnly)
32+
public typealias ABIEntryPoint_v0 = @Sendable (
33+
_ argumentsJSON: UnsafeRawBufferPointer?,
34+
_ eventHandler: @escaping @Sendable (_ eventAndContextJSON: UnsafeRawBufferPointer) -> Void
35+
) async -> CInt
36+
37+
/// Get the entry point to the testing library used by tools that want to remain
38+
/// version-agnostic regarding the testing library.
39+
///
40+
/// - Parameters:
41+
/// - outEntryPoint: Uninitialized memory large enough to hold an instance of
42+
/// ``ABIEntryPoint_v0``. On return, a pointer to an instance of that type
43+
/// representing the ABI-stable entry point to the testing library. The
44+
/// caller owns this memory and is responsible for deinitializing and
45+
/// deallocating it when done.
46+
///
47+
/// This function can be used by tools that do not link directly to the testing
48+
/// library and wish to invoke tests in a binary that has been loaded into the
49+
/// current process. The function is emitted into the binary under the name
50+
/// `"swt_copyABIEntryPoint_v0"` and can be dynamically looked up at runtime
51+
/// using `dlsym()` or a platform equivalent.
52+
///
53+
/// The function stored at `outEntryPoint` can be thought of as equivalent to
54+
/// `swift test --experimental-event-stream-output` except that, instead of
55+
/// streaming events to a named pipe or file, it streams them to a callback.
56+
///
57+
/// - Warning: This function's signature and the structure of its JSON inputs
58+
/// and outputs have not been finalized yet.
59+
@_cdecl("swt_copyABIEntryPoint_v0")
60+
@usableFromInline
61+
func abiEntryPoint_v0(_ outEntryPoint: UnsafeMutableRawPointer) {
62+
outEntryPoint.initializeMemory(as: ABIEntryPoint_v0.self) { argumentsJSON, eventHandler in
63+
let args = try! argumentsJSON.map { argumentsJSON in
64+
try JSON.decode(__CommandLineArguments_v0.self, from: argumentsJSON)
65+
}
66+
67+
let eventHandler = eventHandlerForStreamingEvents_v0(to: eventHandler)
68+
return await entryPoint(passing: args, eventHandler: eventHandler)
69+
}
70+
}
71+
#endif
72+
73+
// MARK: - Experimental event streaming
74+
75+
#if canImport(Foundation) && (!SWT_NO_FILE_IO || !SWT_NO_ABI_ENTRY_POINT)
76+
/// A type containing an event snapshot and snapshots of the contents of an
77+
/// event context suitable for streaming over JSON.
78+
///
79+
/// This function is not part of the public interface of the testing library.
80+
/// External adopters are not necessarily written in Swift and are expected to
81+
/// decode the JSON produced for this type in implementation-specific ways.
82+
struct EventAndContextSnapshot {
83+
/// A snapshot of the event.
84+
var event: Event.Snapshot
85+
86+
/// A snapshot of the event context.
87+
var eventContext: Event.Context.Snapshot
88+
}
89+
90+
extension EventAndContextSnapshot: Codable {}
91+
92+
/// Create an event handler that encodes events as JSON and forwards them to an
93+
/// ABI-friendly event handler.
94+
///
95+
/// - Parameters:
96+
/// - eventHandler: The event handler to forward events to. See
97+
/// ``ABIEntryPoint_v0`` for more information.
98+
///
99+
/// - Returns: An event handler.
100+
///
101+
/// The resulting event handler outputs data as JSON. For each event handled by
102+
/// the resulting event handler, a JSON object representing it and its
103+
/// associated context is created and is passed to `eventHandler`. These JSON
104+
/// objects are guaranteed not to contain any ASCII newline characters (`"\r"`
105+
/// or `"\n"`).
106+
///
107+
/// Note that `_eventHandlerForStreamingEvents_v0(toFileAtPath:)` calls this
108+
/// function and performs additional postprocessing before writing JSON data.
109+
func eventHandlerForStreamingEvents_v0(
110+
to eventHandler: @escaping @Sendable (_ eventAndContextJSON: UnsafeRawBufferPointer) -> Void
111+
) -> Event.Handler {
112+
return { event, context in
113+
let snapshot = EventAndContextSnapshot(
114+
event: Event.Snapshot(snapshotting: event),
115+
eventContext: Event.Context.Snapshot(snapshotting: context)
116+
)
117+
try? JSON.withEncoding(of: snapshot) { eventAndContextJSON in
118+
eventAndContextJSON.withUnsafeBytes { eventAndContextJSON in
119+
eventHandler(eventAndContextJSON)
120+
}
121+
}
122+
}
123+
}
124+
#endif

0 commit comments

Comments
 (0)