Skip to content

Commit 94f8a22

Browse files
authored
Add support for attachments to the Foundation cross-import overlay. (#819)
This PR adds experimental support for attachments to some types in Foundation via the (non-functional) cross-import overlay. @stmontgomery is working on setting up said overlay so that it can actually be used; until then, the changes here are speculative only. Replaces #799. ### 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 b470365 commit 94f8a22

File tree

15 files changed

+730
-25
lines changed

15 files changed

+730
-25
lines changed

Documentation/SPI.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ See https://swift.org/CONTRIBUTORS.txt for Swift project authors
1313
<!-- Archived from
1414
<https://forums.swift.org/t/spi-groups-in-swift-testing/70236> -->
1515

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

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

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

42+
For interfaces internal to Swift Testing that must be available across targets,
43+
the SPI group `@_spi(ForSwiftTestingOnly)` is used. They _should_ be marked
44+
`package` and may be in the future, but are currently exported due to technical
45+
constraints when Swift Testing is built using CMake.
46+
47+
> [!WARNING]
48+
> Never use symbols marked `@_spi(ForSwiftTestingOnly)`.
49+
4050
## SPI stability
4151

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

62+
SPI marked `@_spi(ForSwiftTestingOnly)` is unstable and subject to change at any
63+
time.
64+
65+
> [!WARNING]
66+
> Never use symbols marked `@_spi(ForSwiftTestingOnly)`.
67+
5268
## API and ABI stability
5369

5470
When Swift Testing reaches its 1.0 release, API changes will follow the same

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,13 @@ let package = Package(
9090
cxxSettings: .packageSettings
9191
),
9292

93-
// Cross-module overlays (unsupported)
93+
// Cross-import overlays (not supported by Swift Package Manager)
9494
.target(
9595
name: "_Testing_Foundation",
9696
dependencies: [
9797
"Testing",
9898
],
99+
path: "Sources/Overlays/_Testing_Foundation",
99100
swiftSettings: .packageSettings
100101
),
101102
],
@@ -147,6 +148,7 @@ extension Array where Element == PackageDescription.SwiftSetting {
147148
private static var availabilityMacroSettings: Self {
148149
[
149150
.enableExperimentalFeature("AvailabilityMacro=_mangledTypeNameAPI:macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0"),
151+
.enableExperimentalFeature("AvailabilityMacro=_uttypesAPI:macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0"),
150152
.enableExperimentalFeature("AvailabilityMacro=_backtraceAsyncAPI:macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0"),
151153
.enableExperimentalFeature("AvailabilityMacro=_clockAPI:macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0"),
152154
.enableExperimentalFeature("AvailabilityMacro=_regexAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"),
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 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)
12+
@_spi(Experimental) public import Testing
13+
public import Foundation
14+
15+
// This implementation is necessary to let the compiler disambiguate when a type
16+
// conforms to both Encodable and NSSecureCoding. It is hidden from the DocC
17+
// compiler because it appears redundant next to the other two implementations
18+
// (which explicitly document what happens when a type conforms to both
19+
// protocols.)
20+
21+
@_spi(Experimental)
22+
extension Attachable where Self: Encodable & NSSecureCoding {
23+
@_documentation(visibility: private)
24+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
25+
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)
26+
}
27+
}
28+
#endif
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 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)
12+
@_spi(Experimental) public import Testing
13+
private import Foundation
14+
15+
/// A common implementation of ``withUnsafeBufferPointer(for:_:)`` that is
16+
/// used when a type conforms to `Encodable`, whether or not it also conforms
17+
/// to `NSSecureCoding`.
18+
///
19+
/// - Parameters:
20+
/// - attachableValue: The value to encode.
21+
/// - attachment: The attachment that is requesting a buffer (that is, the
22+
/// attachment containing this instance.)
23+
/// - body: A function to call. A temporary buffer containing a data
24+
/// representation of this instance is passed to it.
25+
///
26+
/// - Returns: Whatever is returned by `body`.
27+
///
28+
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
29+
/// creation of the buffer.
30+
func withUnsafeBufferPointer<E, R>(encoding attachableValue: borrowing E, for attachment: borrowing Attachment<E>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R where E: Attachable & Encodable {
31+
let format = try EncodingFormat(for: attachment)
32+
33+
let data: Data
34+
switch format {
35+
case let .propertyListFormat(propertyListFormat):
36+
let plistEncoder = PropertyListEncoder()
37+
plistEncoder.outputFormat = propertyListFormat
38+
data = try plistEncoder.encode(attachableValue)
39+
case .default:
40+
// The default format is JSON.
41+
fallthrough
42+
case .json:
43+
// We cannot use our own JSON encoding wrapper here because that would
44+
// require it be exported with (at least) package visibility which would
45+
// create a visible external dependency on Foundation in the main testing
46+
// library target.
47+
data = try JSONEncoder().encode(attachableValue)
48+
}
49+
50+
return try data.withUnsafeBytes(body)
51+
}
52+
53+
// Implement the protocol requirements generically for any encodable value by
54+
// encoding to JSON. This lets developers provide trivial conformance to the
55+
// protocol for types that already support Codable.
56+
@_spi(Experimental)
57+
extension Attachable where Self: Encodable {
58+
/// Encode this value into a buffer using either [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder)
59+
/// or [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder),
60+
/// then call a function and pass that buffer to it.
61+
///
62+
/// - Parameters:
63+
/// - attachment: The attachment that is requesting a buffer (that is, the
64+
/// attachment containing this instance.)
65+
/// - body: A function to call. A temporary buffer containing a data
66+
/// representation of this instance is passed to it.
67+
///
68+
/// - Returns: Whatever is returned by `body`.
69+
///
70+
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
71+
/// creation of the buffer.
72+
///
73+
/// The testing library uses this function when writing an attachment to a
74+
/// test report or to a file on disk. The encoding used depends on the path
75+
/// extension specified by the value of `attachment`'s ``Testing/Attachment/preferredName``
76+
/// property:
77+
///
78+
/// | Extension | Encoding Used | Encoder Used |
79+
/// |-|-|-|
80+
/// | `".xml"` | XML property list | [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder) |
81+
/// | `".plist"` | Binary property list | [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder) |
82+
/// | None, `".json"` | JSON | [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder) |
83+
///
84+
/// OpenStep-style property lists are not supported. If a value conforms to
85+
/// _both_ [`Encodable`](https://developer.apple.com/documentation/swift/encodable)
86+
/// _and_ [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding),
87+
/// the default implementation of this function uses the value's conformance
88+
/// to `Encodable`.
89+
///
90+
/// - Note: On Apple platforms, if the attachment's preferred name includes
91+
/// some other path extension, that path extension must represent a type
92+
/// that conforms to [`UTType.propertyList`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/propertylist)
93+
/// or to [`UTType.json`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/json).
94+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
95+
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)
96+
}
97+
}
98+
#endif
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 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)
12+
@_spi(Experimental) public import Testing
13+
public import Foundation
14+
15+
// As with Encodable, implement the protocol requirements for
16+
// NSSecureCoding-conformant classes by default. The implementation uses
17+
// NSKeyedArchiver for encoding.
18+
@_spi(Experimental)
19+
extension Attachable where Self: NSSecureCoding {
20+
/// Encode this object using [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver)
21+
/// into a buffer, then call a function and pass that buffer to it.
22+
///
23+
/// - Parameters:
24+
/// - attachment: The attachment that is requesting a buffer (that is, the
25+
/// attachment containing this instance.)
26+
/// - body: A function to call. A temporary buffer containing a data
27+
/// representation of this instance is passed to it.
28+
///
29+
/// - Returns: Whatever is returned by `body`.
30+
///
31+
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
32+
/// creation of the buffer.
33+
///
34+
/// The testing library uses this function when writing an attachment to a
35+
/// test report or to a file on disk. The encoding used depends on the path
36+
/// extension specified by the value of `attachment`'s ``Testing/Attachment/preferredName``
37+
/// property:
38+
///
39+
/// | Extension | Encoding Used | Encoder Used |
40+
/// |-|-|-|
41+
/// | `".xml"` | XML property list | [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver) |
42+
/// | None, `".plist"` | Binary property list | [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver) |
43+
///
44+
/// OpenStep-style property lists are not supported. If a value conforms to
45+
/// _both_ [`Encodable`](https://developer.apple.com/documentation/swift/encodable)
46+
/// _and_ [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding),
47+
/// the default implementation of this function uses the value's conformance
48+
/// to `Encodable`.
49+
///
50+
/// - Note: On Apple platforms, if the attachment's preferred name includes
51+
/// some other path extension, that path extension must represent a type
52+
/// that conforms to [`UTType.propertyList`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/propertylist).
53+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
54+
let format = try EncodingFormat(for: attachment)
55+
56+
var data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true)
57+
switch format {
58+
case .default:
59+
// The default format is just what NSKeyedArchiver produces.
60+
break
61+
case let .propertyListFormat(propertyListFormat):
62+
// BUG: Foundation does not offer a variant of
63+
// NSKeyedArchiver.archivedData(withRootObject:requiringSecureCoding:)
64+
// that is Swift-safe (throws errors instead of exceptions) and lets the
65+
// caller specify the output format. Work around this issue by decoding
66+
// the archive re-encoding it manually.
67+
if propertyListFormat != .binary {
68+
let plist = try PropertyListSerialization.propertyList(from: data, format: nil)
69+
data = try PropertyListSerialization.data(fromPropertyList: plist, format: propertyListFormat, options: 0)
70+
}
71+
case .json:
72+
throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "An instance of \(type(of: self)) cannot be encoded as JSON. Specify a property list format instead."])
73+
}
74+
75+
return try data.withUnsafeBytes(body)
76+
}
77+
}
78+
#endif

0 commit comments

Comments
 (0)