Skip to content

Commit 1f2893c

Browse files
authored
Adopt Span/RawSpan almost everywhere. (#1500)
This PR adopts `Span` and `RawSpan` throughout Swift Testing. Unsafe buffer pointers and the like are replaced with spans wherever possible. A few carve-outs: - `Attachment`/`Attachable` are untouched for now as we need to think through the correct API shape for them. - Code that interacts with C functions (including code that works with C strings) generally also must still use unsafe pointers, but the scope of those pointers is generally constrained to some local context. - The ABI entry point function and test content section must continue to use unsafe pointers as they must remain broadly compatible with C. ### 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 f37e1f5 commit 1f2893c

18 files changed

Lines changed: 145 additions & 171 deletions

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ private import Foundation
1414
extension ABI.Version {
1515
static func eventHandler(
1616
encodeAsJSONLines: Bool,
17-
forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void
17+
forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: borrowing RawSpan) -> Void
1818
) -> Event.Handler {
1919
// Encode as JSON Lines if requested.
2020
var eventHandlerCopy = eventHandler
@@ -44,7 +44,7 @@ extension ABI.Version {
4444
extension ABI.Xcode16 {
4545
static func eventHandler(
4646
encodeAsJSONLines: Bool,
47-
forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void
47+
forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: borrowing RawSpan) -> Void
4848
) -> Event.Handler {
4949
return { event, context in
5050
if case .testDiscovered = event.kind {
@@ -63,9 +63,7 @@ extension ABI.Xcode16 {
6363
eventContext: Event.Context.Snapshot(snapshotting: context)
6464
)
6565
try? JSON.withEncoding(of: snapshot) { eventAndContextJSON in
66-
eventAndContextJSON.withUnsafeBytes { eventAndContextJSON in
67-
eventHandler(eventAndContextJSON)
68-
}
66+
eventHandler(eventAndContextJSON)
6967
}
7068
}
7169
}

Sources/Testing/ABI/ABI.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ extension ABI {
3737
/// associated context is created and is passed to `eventHandler`.
3838
static func eventHandler(
3939
encodeAsJSONLines: Bool,
40-
forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void
40+
forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: borrowing RawSpan) -> Void
4141
) -> Event.Handler
4242
#endif
4343
}

Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ extension ABI.v0 {
4848
public static var entryPoint: EntryPoint {
4949
return { configurationJSON, recordHandler in
5050
let args = try configurationJSON.map { configurationJSON in
51-
try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON)
51+
try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON.bytes)
52+
}
53+
let eventHandler = try eventHandlerForStreamingEvents(withVersionNumber: args?.eventStreamVersionNumber, encodeAsJSONLines: false) { recordJSON in
54+
recordJSON.withUnsafeBytes(recordHandler)
5255
}
53-
let eventHandler = try eventHandlerForStreamingEvents(withVersionNumber: args?.eventStreamVersionNumber, encodeAsJSONLines: false, forwardingTo: recordHandler)
5456

5557
switch await Testing.entryPoint(passing: args, eventHandler: eventHandler) {
5658
case EXIT_SUCCESS, EXIT_NO_TESTS_FOUND:

Sources/Testing/ABI/EntryPoints/EntryPoint.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -425,9 +425,7 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
425425
if let path = args.argumentValue(forLabel: "--configuration-path") ?? args.argumentValue(forLabel: "--experimental-configuration-path") {
426426
let file = try FileHandle(forReadingAtPath: path)
427427
let configurationJSON = try file.readToEnd()
428-
result = try configurationJSON.withUnsafeBufferPointer { configurationJSON in
429-
try JSON.decode(__CommandLineArguments_v0.self, from: .init(configurationJSON))
430-
}
428+
result = try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON.span.bytes)
431429

432430
// NOTE: We don't return early or block other arguments here: a caller is
433431
// allowed to pass a configuration AND e.g. "--verbose" and they'll both be
@@ -706,7 +704,7 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr
706704
func eventHandlerForStreamingEvents(
707705
withVersionNumber versionNumber: VersionNumber?,
708706
encodeAsJSONLines: Bool,
709-
forwardingTo targetEventHandler: @escaping @Sendable (UnsafeRawBufferPointer) -> Void
707+
forwardingTo targetEventHandler: @escaping @Sendable (borrowing RawSpan) -> Void
710708
) throws -> Event.Handler {
711709
let versionNumber = versionNumber ?? ABI.CurrentVersion.versionNumber
712710
guard let abi = ABI.version(forVersionNumber: versionNumber) else {

Sources/Testing/Attachments/Attachment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ extension Attachment where AttachableValue: ~Copyable {
467467
// There should be no code path that leads to this call where the attachable
468468
// value is nil.
469469
try withUnsafeBytes { buffer in
470-
try file!.write(buffer)
470+
try file!.write(buffer.bytes)
471471
}
472472

473473
return result

Sources/Testing/Events/Event+FallbackHandler.swift

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,27 @@ private import _TestingInternals
1212

1313
extension Event {
1414
#if compiler(>=6.3) && !SWT_NO_INTEROP
15+
/// The installed fallback event handler.
1516
private static let _fallbackEventHandler: SWTFallbackEventHandler? = {
1617
_swift_testing_getFallbackEventHandler()
1718
}()
19+
20+
/// Encode an event and pass it to the installed fallback event handler.
21+
private static let _encodeAndInvoke: Event.Handler? = { [fallbackEventHandler = _fallbackEventHandler] in
22+
guard let fallbackEventHandler else {
23+
return nil
24+
}
25+
return ABI.CurrentVersion.eventHandler(encodeAsJSONLines: false) { recordJSON in
26+
recordJSON.withUnsafeBytes { recordJSON in
27+
fallbackEventHandler(
28+
String(describing: ABI.CurrentVersion.versionNumber),
29+
recordJSON.baseAddress!,
30+
recordJSON.count,
31+
nil
32+
)
33+
}
34+
}
35+
}()
1836
#endif
1937

2038
/// Post this event to the currently-installed fallback event handler.
@@ -27,23 +45,12 @@ extension Event {
2745
/// `false`.
2846
borrowing func postToFallbackHandler(in context: borrowing Context) -> Bool {
2947
#if compiler(>=6.3) && !SWT_NO_INTEROP
30-
guard let fallbackEventHandler = Self._fallbackEventHandler else {
31-
return false
32-
}
33-
3448
// Encode the event as JSON and pass it to the handler.
35-
let encodeAndInvoke = ABI.CurrentVersion.eventHandler(encodeAsJSONLines: false) { recordJSON in
36-
fallbackEventHandler(
37-
String(describing: ABI.CurrentVersion.versionNumber),
38-
recordJSON.baseAddress!,
39-
recordJSON.count,
40-
nil
41-
)
49+
if let encodeAndInvoke = Self._encodeAndInvoke {
50+
encodeAndInvoke(self, context)
51+
return true
4252
}
43-
encodeAndInvoke(self, context)
44-
return true
45-
#else
46-
return false
4753
#endif
54+
return false
4855
}
4956
}

Sources/Testing/ExitTests/ExitTest.swift

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -605,8 +605,8 @@ extension ExitTest {
605605
/// and standard error streams of the current process.
606606
private static func _writeBarrierValues() {
607607
let barrierValue = Self.barrierValue
608-
try? FileHandle.stdout.write(barrierValue)
609-
try? FileHandle.stderr.write(barrierValue)
608+
try? FileHandle.stdout.write(barrierValue.span.bytes)
609+
try? FileHandle.stderr.write(barrierValue.span.bytes)
610610
}
611611

612612
/// A handler that is invoked when an exit test starts.
@@ -712,13 +712,11 @@ extension ExitTest {
712712

713713
/// The ID of the exit test to run, if any, specified in the environment.
714714
static var environmentIDForEntryPoint: ID? {
715-
guard var idString = Environment.variable(named: Self._idEnvironmentVariableName) else {
715+
guard let idString = Environment.variable(named: Self._idEnvironmentVariableName) else {
716716
return nil
717717
}
718718

719-
return try? idString.withUTF8 { idBuffer in
720-
try JSON.decode(ExitTest.ID.self, from: UnsafeRawBufferPointer(idBuffer))
721-
}
719+
return try? JSON.decode(ExitTest.ID.self, from: idString.utf8.span.bytes)
722720
}
723721

724722
/// Find the exit test function specified in the environment of the current
@@ -870,7 +868,7 @@ extension ExitTest {
870868
// Insert a specific variable that tells the child process which exit test
871869
// to run.
872870
try JSON.withEncoding(of: exitTest.id) { json in
873-
childEnvironment[Self._idEnvironmentVariableName] = String(decoding: json, as: UTF8.self)
871+
childEnvironment[Self._idEnvironmentVariableName] = String(decoding: Array(json), as: UTF8.self)
874872
}
875873

876874
typealias ResultUpdater = @Sendable (inout ExitTest.Result) -> Void
@@ -1007,9 +1005,7 @@ extension ExitTest {
10071005

10081006
for recordJSON in bytes.split(whereSeparator: \.isASCIINewline) where !recordJSON.isEmpty {
10091007
do {
1010-
try recordJSON.withUnsafeBufferPointer { recordJSON in
1011-
try Self._processRecord(.init(recordJSON), fromBackChannel: backChannel)
1012-
}
1008+
try Self._processRecord(recordJSON.span.bytes, fromBackChannel: backChannel)
10131009
} catch {
10141010
// NOTE: an error caught here indicates a decoding problem.
10151011
// TODO: should we record these issues as systemic instead?
@@ -1026,7 +1022,7 @@ extension ExitTest {
10261022
/// - backChannel: The file handle that `recordJSON` was read from.
10271023
///
10281024
/// - Throws: Any error encountered attempting to decode or process the JSON.
1029-
private static func _processRecord(_ recordJSON: UnsafeRawBufferPointer, fromBackChannel backChannel: borrowing FileHandle) throws {
1025+
private static func _processRecord(_ recordJSON: borrowing RawSpan, fromBackChannel backChannel: borrowing FileHandle) throws {
10301026
let record = try JSON.decode(ABI.Record<ABI.BackChannelVersion>.self, from: recordJSON)
10311027
guard case let .event(event) = record.kind else {
10321028
return
@@ -1093,9 +1089,7 @@ extension ExitTest {
10931089
var capturedValue = capturedValue
10941090

10951091
func open<T>(_ type: T.Type) throws -> T where T: Codable & Sendable {
1096-
return try capturedValueJSON.withUnsafeBytes { capturedValueJSON in
1097-
try JSON.decode(type, from: capturedValueJSON)
1098-
}
1092+
return try JSON.decode(type, from: capturedValueJSON.span.bytes)
10991093
}
11001094
capturedValue.wrappedValue = try open(capturedValue.typeOfWrappedValue)
11011095

@@ -1118,7 +1112,7 @@ extension ExitTest {
11181112
/// This function should only be used when the process was started via the
11191113
/// `__swiftPMEntryPoint()` function. The effect of using it under other
11201114
/// configurations is undefined.
1121-
private borrowing func _withEncodedCapturedValuesForEntryPoint(_ body: (UnsafeRawBufferPointer) throws -> Void) throws -> Void {
1115+
private borrowing func _withEncodedCapturedValuesForEntryPoint(_ body: (borrowing RawSpan) throws -> Void) throws -> Void {
11221116
for capturedValue in capturedValues {
11231117
try JSON.withEncoding(of: capturedValue.wrappedValue!) { capturedValueJSON in
11241118
try JSON.asJSONLine(capturedValueJSON, body)

Sources/Testing/Support/Additions/ArrayAdditions.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,52 @@ extension Array {
6767
}
6868
}
6969

70+
// MARK: - Span/RawSpan support
71+
72+
extension Array where Element == UInt8 {
73+
init(_ bytes: borrowing RawSpan) {
74+
self = bytes.withUnsafeBytes { Array($0) }
75+
}
76+
}
77+
78+
#if SWT_TARGET_OS_APPLE
79+
extension Array {
80+
/// The elements of this array as a span.
81+
///
82+
/// This property is equivalent to the `span` property in the Swift standard
83+
/// library, but is available on earlier Apple platforms.
84+
///
85+
/// For arrays with contiguous storage, getting the value of this property is
86+
/// an _O_(1) operation. For arrays with non-contiguous storage (i.e. bridged
87+
/// from Objective-C), the operation may be up to _O_(_n_).
88+
var span: Span<Element> {
89+
@_lifetime(borrow self)
90+
_read {
91+
let slice = self[...]
92+
yield slice.span
93+
}
94+
}
95+
}
96+
97+
extension String.UTF8View {
98+
/// A raw span representing this string as UTF-8, not including a trailing
99+
/// null character.
100+
///
101+
/// This property is equivalent to the `span` property in the Swift standard
102+
/// library, but is available on earlier Apple platforms.
103+
var span: Span<Element> {
104+
@_lifetime(borrow self)
105+
_read {
106+
// This implementation incurs a copy even for native Swift strings. This
107+
// isn't currently a hot path in the testing library though.
108+
yield ContiguousArray(self).span
109+
}
110+
}
111+
}
112+
#endif
113+
114+
// MARK: - Parameter pack additions
115+
70116
/// Get the number of elements in a parameter pack.
71117
///
72118
/// - Parameters:

Sources/Testing/Support/FileHandle.swift

Lines changed: 10 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ extension FileHandle {
365365
// MARK: - Writing
366366

367367
extension FileHandle {
368-
/// Write a sequence of bytes to this file handle.
368+
/// Write a span of bytes to this file handle.
369369
///
370370
/// - Parameters:
371371
/// - bytes: The bytes to write. This untyped buffer is interpreted as a
@@ -376,57 +376,26 @@ extension FileHandle {
376376
///
377377
/// - Throws: Any error that occurred while writing `bytes`. If an error
378378
/// occurs while flushing the file, it is not thrown.
379-
func write(_ bytes: UnsafeBufferPointer<UInt8>, flushAfterward: Bool = true) throws {
379+
func write(_ bytes: borrowing RawSpan, flushAfterward: Bool = true) throws {
380380
try withUnsafeCFILEHandle { file in
381381
defer {
382382
if flushAfterward {
383383
_ = fflush(file)
384384
}
385385
}
386386

387-
let countWritten = fwrite(bytes.baseAddress!, MemoryLayout<UInt8>.stride, bytes.count, file)
388-
if countWritten < bytes.count {
387+
if bytes.isEmpty {
388+
return
389+
}
390+
let countWritten = bytes.withUnsafeBytes { bytes in
391+
fwrite(bytes.baseAddress!, MemoryLayout<UInt8>.stride, bytes.count, file)
392+
}
393+
if countWritten < bytes.byteCount {
389394
throw CError(rawValue: swt_errno())
390395
}
391396
}
392397
}
393398

394-
/// Write a sequence of bytes to this file handle.
395-
///
396-
/// - Parameters:
397-
/// - bytes: The bytes to write.
398-
/// - flushAfterward: Whether or not to flush the file (with `fflush()`)
399-
/// after writing. If `true`, `fflush()` is called even if an error
400-
/// occurred while writing.
401-
///
402-
/// - Throws: Any error that occurred while writing `bytes`. If an error
403-
/// occurs while flushing the file, it is not thrown.
404-
///
405-
/// - Precondition: `bytes` must provide contiguous storage.
406-
func write(_ bytes: some Sequence<UInt8>, flushAfterward: Bool = true) throws {
407-
let hasContiguousStorage: Void? = try bytes.withContiguousStorageIfAvailable { bytes in
408-
try write(bytes, flushAfterward: flushAfterward)
409-
}
410-
precondition(hasContiguousStorage != nil, "byte sequence must provide contiguous storage: \(bytes)")
411-
}
412-
413-
/// Write a sequence of bytes to this file handle.
414-
///
415-
/// - Parameters:
416-
/// - bytes: The bytes to write. This untyped buffer is interpreted as a
417-
/// sequence of `UInt8` values.
418-
/// - flushAfterward: Whether or not to flush the file (with `fflush()`)
419-
/// after writing. If `true`, `fflush()` is called even if an error
420-
/// occurred while writing.
421-
///
422-
/// - Throws: Any error that occurred while writing `bytes`. If an error
423-
/// occurs while flushing the file, it is not thrown.
424-
func write(_ bytes: UnsafeRawBufferPointer, flushAfterward: Bool = true) throws {
425-
try bytes.withMemoryRebound(to: UInt8.self) { bytes in
426-
try write(bytes, flushAfterward: flushAfterward)
427-
}
428-
}
429-
430399
/// Write a string to this file handle.
431400
///
432401
/// - Parameters:
@@ -441,19 +410,7 @@ extension FileHandle {
441410
/// `string` is converted to a UTF-8 C string (UTF-16 on Windows) and written
442411
/// to this file handle.
443412
func write(_ string: String, flushAfterward: Bool = true) throws {
444-
try withUnsafeCFILEHandle { file in
445-
defer {
446-
if flushAfterward {
447-
_ = fflush(file)
448-
}
449-
}
450-
451-
try string.withCString { string in
452-
if EOF == fputs(string, file) {
453-
throw CError(rawValue: swt_errno())
454-
}
455-
}
456-
}
413+
try write(string.utf8.span.bytes, flushAfterward: flushAfterward)
457414
}
458415
}
459416

0 commit comments

Comments
 (0)