Skip to content

Commit 28e5bba

Browse files
committed
Add support for attachments to the Foundation cross-import overlay.
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.
1 parent 35f2618 commit 28e5bba

13 files changed

+698
-18
lines changed

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ let package = Package(
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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
@_spi(Experimental)
16+
extension Data: Test.Attachable {
17+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
18+
try withUnsafeBytes(body)
19+
}
20+
}
21+
#endif
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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) import Testing
13+
import Foundation
14+
15+
#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
16+
private import UniformTypeIdentifiers
17+
#endif
18+
19+
/// An enumeration describing the encoding formats we support for `Encodable`
20+
/// and `NSSecureCoding` types that conform to `Test.Attachable`.
21+
enum EncodingFormat {
22+
/// A property list format.
23+
///
24+
/// - Parameters:
25+
/// - format: The corresponding property list format.
26+
case propertyListFormat(_ format: PropertyListSerialization.PropertyListFormat)
27+
28+
/// The JSON format.
29+
case json
30+
31+
/// The encoding format to use by default.
32+
///
33+
/// The specific format this case corresponds to depends on if we are encoding
34+
/// an `Encodable` value or an `NSSecureCoding` value.
35+
case `default`
36+
37+
/// Initialize an instance of this type representing the content type or media
38+
/// type of the specified attachment.
39+
///
40+
/// - Parameters:
41+
/// - attachment: The attachment that will be encoded.
42+
///
43+
/// - Throws: If the attachment's content type or media type is unsupported.
44+
init(for attachment: borrowing Test.Attachment<some Test.Attachable>) throws {
45+
let ext = (attachment.preferredName as NSString).pathExtension
46+
47+
#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
48+
// If the caller explicitly wants to encode their data as either XML or as a
49+
// property list, use PropertyListEncoder. Otherwise, we'll fall back to
50+
// JSONEncoder below.
51+
if #available(_uttypesAPI, *), let contentType = UTType(filenameExtension: ext) {
52+
if contentType == .data {
53+
self = .default
54+
} else if contentType.conforms(to: .json) {
55+
self = .json
56+
} else if contentType.conforms(to: .xml) {
57+
self = .propertyListFormat(.xml)
58+
} else if contentType.conforms(to: .binaryPropertyList) || contentType == .propertyList {
59+
self = .propertyListFormat(.binary)
60+
} else if contentType.conforms(to: .propertyList) {
61+
self = .propertyListFormat(.openStep)
62+
} else {
63+
let contentTypeDescription = contentType.localizedDescription ?? contentType.identifier
64+
throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "The content type '\(contentTypeDescription)' cannot be used to attach an instance of \(type(of: self)) to a test."])
65+
}
66+
return
67+
}
68+
#endif
69+
70+
if ext.isEmpty {
71+
// No path extension? No problem! Default data.
72+
self = .default
73+
} else if ext.caseInsensitiveCompare("plist") == .orderedSame {
74+
self = .propertyListFormat(.binary)
75+
} else if ext.caseInsensitiveCompare("xml") == .orderedSame {
76+
self = .propertyListFormat(.xml)
77+
} else if ext.caseInsensitiveCompare("json") == .orderedSame {
78+
self = .json
79+
} else {
80+
throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "The path extension '.\(ext)' cannot be used to attach an instance of \(type(of: self)) to a test."])
81+
}
82+
}
83+
}
84+
#endif
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
@_spi(Experimental)
16+
extension Test.Attachable where Self: Encodable & NSSecureCoding {
17+
@_documentation(visibility: private)
18+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
19+
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)
20+
}
21+
}
22+
#endif
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
/// - attachment: The attachment that is requesting a buffer (that is, the
21+
/// attachment containing this instance.)
22+
/// - body: A function to call. A temporary buffer containing a data
23+
/// representation of this instance is passed to it.
24+
///
25+
/// - Returns: Whatever is returned by `body`.
26+
///
27+
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
28+
/// creation of the buffer.
29+
func withUnsafeBufferPointer<E, R>(encoding attachableValue: borrowing E, for attachment: borrowing Test.Attachment<E>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R where E: Test.Attachable & Encodable {
30+
let format = try EncodingFormat(for: attachment)
31+
32+
let data: Data
33+
switch format {
34+
case let .propertyListFormat(propertyListFormat):
35+
let plistEncoder = PropertyListEncoder()
36+
plistEncoder.outputFormat = propertyListFormat
37+
data = try plistEncoder.encode(attachableValue)
38+
case .default:
39+
// The default format is JSON.
40+
fallthrough
41+
case .json:
42+
// We cannot use our own JSON encoding wrapper here because that would
43+
// require it be exported with (at least) package visibility which would
44+
// create a visible external dependency on Foundation in the main testing
45+
// library target.
46+
data = try JSONEncoder().encode(attachableValue)
47+
}
48+
49+
return try data.withUnsafeBytes(body)
50+
}
51+
52+
// Implement the protocol requirements generically for any encodable value by
53+
// encoding to JSON. This lets developers provide trivial conformance to the
54+
// protocol for types that already support Codable.
55+
@_spi(Experimental)
56+
extension Test.Attachable where Self: Encodable {
57+
/// Encode this value into a buffer using either [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder)
58+
/// or [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder),
59+
/// then call a function and pass that buffer to it.
60+
///
61+
/// - Parameters:
62+
/// - attachment: The attachment that is requesting a buffer (that is, the
63+
/// attachment containing this instance.)
64+
/// - body: A function to call. A temporary buffer containing a data
65+
/// representation of this instance is passed to it.
66+
///
67+
/// - Returns: Whatever is returned by `body`.
68+
///
69+
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
70+
/// creation of the buffer.
71+
///
72+
/// The testing library uses this function when writing an attachment to a
73+
/// test report or to a file on disk. The encoding used depends on the path
74+
/// extension specified by the value of `attachment`'s ``Testing/Test/Attachment/preferredName``
75+
/// property:
76+
///
77+
/// | Extension | Encoding Used | Encoder Used |
78+
/// |-|-|-|
79+
/// | `".xml"` | XML property list | [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder) |
80+
/// | `".plist"` | Binary property list | [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder) |
81+
/// | None, `".json"` | JSON | [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder) |
82+
///
83+
/// OpenStep-style property lists are not supported. If a value conforms to
84+
/// _both_ [`Encodable`](https://developer.apple.com/documentation/swift/encodable)
85+
/// _and_ [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding),
86+
/// the default implementation of this function uses the value's conformance
87+
/// to `Encodable`.
88+
///
89+
/// - Note: On Apple platforms, if the attachment's preferred name includes
90+
/// some other path extension, that path extension must represent a type
91+
/// that conforms to [`UTType.propertyList`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/propertylist)
92+
/// or to [`UTType.json`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/json).
93+
public func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
94+
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)
95+
}
96+
}
97+
#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 Test.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/Test/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 Test.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)