Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions Documentation/SPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ See https://swift.org/CONTRIBUTORS.txt for Swift project authors
<!-- Archived from
<https://forums.swift.org/t/spi-groups-in-swift-testing/70236> -->

This post describes the set of SPI groups used in Swift Testing. In general, two
groups of SPI exist in the testing library:
This post describes the set of SPI groups used in Swift Testing. In general,
three groups of SPI exist in the testing library:

1. Interfaces that aren't needed by test authors, but which may be needed by
tools that use the testing library such as Swift Package Manager; and
tools that use the testing library such as Swift Package Manager;
1. Interfaces that are available for test authors to use, but which are
experimental or under active development and which may be modified or removed
in the future.
in the future; and
1. Interfaces that are private to the testing library but need to be shared
across targets, but which for technical reasons cannot use `package`.

For interfaces used to integrate with external tools, the SPI group
`@_spi(ForToolsIntegrationOnly)` is used. The name is a hint to adopters that
Expand All @@ -37,6 +39,14 @@ external tools, _both_ groups are specified. Such SPI is not generally meant to
be promoted to public API, but is still experimental until tools authors have a
chance to evaluate it.

For interfaces internal to Swift Testing that must be available across targets,
the SPI group `@_spi(ForSwiftTestingOnly)` is used. They _should_ be marked
`package` and may be in the future, but are currently exported due to technical
constraints when Swift Testing is built using CMake.

> [!WARNING]
> Never use symbols marked `@_spi(ForSwiftTestingOnly)`.

## SPI stability

The testing library does **not** guarantee SPI stability for either group of
Expand All @@ -49,6 +59,12 @@ to newer interfaces.
SPI marked `@_spi(Experimental)` should be assumed to be unstable. It may be
modified or removed at any time.

SPI marked `@_spi(ForSwiftTestingOnly)` is unstable and subject to change at any
time.

> [!WARNING]
> Never use symbols marked `@_spi(ForSwiftTestingOnly)`.

## API and ABI stability

When Swift Testing reaches its 1.0 release, API changes will follow the same
Expand Down
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,13 @@ let package = Package(
cxxSettings: .packageSettings
),

// Cross-module overlays (unsupported)
// Cross-import overlays (not supported by Swift Package Manager)
.target(
name: "_Testing_Foundation",
dependencies: [
"Testing",
],
path: "Sources/Overlays/_Testing_Foundation",
swiftSettings: .packageSettings
),
],
Expand Down Expand Up @@ -147,6 +148,7 @@ extension Array where Element == PackageDescription.SwiftSetting {
private static var availabilityMacroSettings: Self {
[
.enableExperimentalFeature("AvailabilityMacro=_mangledTypeNameAPI:macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0"),
.enableExperimentalFeature("AvailabilityMacro=_uttypesAPI:macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0"),
.enableExperimentalFeature("AvailabilityMacro=_backtraceAsyncAPI:macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0"),
.enableExperimentalFeature("AvailabilityMacro=_clockAPI:macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0"),
.enableExperimentalFeature("AvailabilityMacro=_regexAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

#if canImport(Foundation)
@_spi(Experimental) public import Testing
public import Foundation

// This implementation is necessary to let the compiler disambiguate when a type
// conforms to both Encodable and NSSecureCoding. It is hidden from the DocC
// compiler because it appears redundant next to the other two implementations
// (which explicitly document what happens when a type conforms to both
// protocols.)

@_spi(Experimental)
extension Attachable where Self: Encodable & NSSecureCoding {
@_documentation(visibility: private)
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

#if canImport(Foundation)
@_spi(Experimental) public import Testing
private import Foundation

/// A common implementation of ``withUnsafeBufferPointer(for:_:)`` that is
/// used when a type conforms to `Encodable`, whether or not it also conforms
/// to `NSSecureCoding`.
///
/// - Parameters:
/// - attachableValue: The value to encode.
/// - attachment: The attachment that is requesting a buffer (that is, the
/// attachment containing this instance.)
/// - body: A function to call. A temporary buffer containing a data
/// representation of this instance is passed to it.
///
/// - Returns: Whatever is returned by `body`.
///
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
/// creation of the buffer.
func withUnsafeBufferPointer<E, R>(encoding attachableValue: borrowing E, for attachment: borrowing Attachment<E>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R where E: Attachable & Encodable {
let format = try EncodingFormat(for: attachment)

let data: Data
switch format {
case let .propertyListFormat(propertyListFormat):
let plistEncoder = PropertyListEncoder()
plistEncoder.outputFormat = propertyListFormat
data = try plistEncoder.encode(attachableValue)
case .default:
// The default format is JSON.
fallthrough
case .json:
// We cannot use our own JSON encoding wrapper here because that would
// require it be exported with (at least) package visibility which would
// create a visible external dependency on Foundation in the main testing
// library target.
data = try JSONEncoder().encode(attachableValue)
}

return try data.withUnsafeBytes(body)
}

// Implement the protocol requirements generically for any encodable value by
// encoding to JSON. This lets developers provide trivial conformance to the
// protocol for types that already support Codable.
@_spi(Experimental)
extension Attachable where Self: Encodable {
/// Encode this value into a buffer using either [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder)
/// or [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder),
/// then call a function and pass that buffer to it.
///
/// - Parameters:
/// - attachment: The attachment that is requesting a buffer (that is, the
/// attachment containing this instance.)
/// - body: A function to call. A temporary buffer containing a data
/// representation of this instance is passed to it.
///
/// - Returns: Whatever is returned by `body`.
///
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
/// creation of the buffer.
///
/// The testing library uses this function when writing an attachment to a
/// test report or to a file on disk. The encoding used depends on the path
/// extension specified by the value of `attachment`'s ``Testing/Attachment/preferredName``
/// property:
///
/// | Extension | Encoding Used | Encoder Used |
/// |-|-|-|
/// | `".xml"` | XML property list | [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder) |
/// | `".plist"` | Binary property list | [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder) |
/// | None, `".json"` | JSON | [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder) |
///
/// OpenStep-style property lists are not supported. If a value conforms to
/// _both_ [`Encodable`](https://developer.apple.com/documentation/swift/encodable)
/// _and_ [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding),
/// the default implementation of this function uses the value's conformance
/// to `Encodable`.
///
/// - Note: On Apple platforms, if the attachment's preferred name includes
/// some other path extension, that path extension must represent a type
/// that conforms to [`UTType.propertyList`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/propertylist)
/// or to [`UTType.json`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/json).
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

#if canImport(Foundation)
@_spi(Experimental) public import Testing
public import Foundation

// As with Encodable, implement the protocol requirements for
// NSSecureCoding-conformant classes by default. The implementation uses
// NSKeyedArchiver for encoding.
@_spi(Experimental)
extension Attachable where Self: NSSecureCoding {
/// Encode this object using [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver)
/// into a buffer, then call a function and pass that buffer to it.
///
/// - Parameters:
/// - attachment: The attachment that is requesting a buffer (that is, the
/// attachment containing this instance.)
/// - body: A function to call. A temporary buffer containing a data
/// representation of this instance is passed to it.
///
/// - Returns: Whatever is returned by `body`.
///
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
/// creation of the buffer.
///
/// The testing library uses this function when writing an attachment to a
/// test report or to a file on disk. The encoding used depends on the path
/// extension specified by the value of `attachment`'s ``Testing/Attachment/preferredName``
/// property:
///
/// | Extension | Encoding Used | Encoder Used |
/// |-|-|-|
/// | `".xml"` | XML property list | [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver) |
/// | None, `".plist"` | Binary property list | [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver) |
///
/// OpenStep-style property lists are not supported. If a value conforms to
/// _both_ [`Encodable`](https://developer.apple.com/documentation/swift/encodable)
/// _and_ [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding),
/// the default implementation of this function uses the value's conformance
/// to `Encodable`.
///
/// - Note: On Apple platforms, if the attachment's preferred name includes
/// some other path extension, that path extension must represent a type
/// that conforms to [`UTType.propertyList`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/propertylist).
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
let format = try EncodingFormat(for: attachment)

var data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true)
switch format {
case .default:
// The default format is just what NSKeyedArchiver produces.
break
case let .propertyListFormat(propertyListFormat):
// BUG: Foundation does not offer a variant of
// NSKeyedArchiver.archivedData(withRootObject:requiringSecureCoding:)
// that is Swift-safe (throws errors instead of exceptions) and lets the
// caller specify the output format. Work around this issue by decoding
// the archive re-encoding it manually.
if propertyListFormat != .binary {
let plist = try PropertyListSerialization.propertyList(from: data, format: nil)
data = try PropertyListSerialization.data(fromPropertyList: plist, format: propertyListFormat, options: 0)
}
case .json:
throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "An instance of \(type(of: self)) cannot be encoded as JSON. Specify a property list format instead."])
}

return try data.withUnsafeBytes(body)
}
}
#endif
Loading