Skip to content

Commit ad35d3e

Browse files
committed
Increase test coverage, add estimatedAttachmentByteCount property
1 parent 097944b commit ad35d3e

File tree

3 files changed

+179
-21
lines changed

3 files changed

+179
-21
lines changed

Diff for: Sources/Testing/Attachments/Test.Attachable.swift

+40
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,24 @@ extension Test {
2222
// TODO: write more about this protocol, how it works, and list conforming
2323
// types (including discussion of the Foundation cross-import overlay.)
2424
public protocol Attachable: ~Copyable {
25+
/// An estimate of the number of bytes of memory needed to store this value
26+
/// as an attachment.
27+
///
28+
/// The testing library uses this property to determine if an attachment
29+
/// should be held in memory or should be immediately persisted to storage.
30+
/// Larger attachments are more likely to be persisted, but the algorithm
31+
/// the testing library uses is an implementation detail and is subject to
32+
/// change.
33+
///
34+
/// The value of this property is approximately equal to the number of bytes
35+
/// that will actually be needed, or `nil` if the value cannot be computed
36+
/// efficiently. The default implementation of this property returns a value
37+
/// of `nil`.
38+
///
39+
/// - Complexity: O(1) unless `Self` conforms to `Collection`, in which case
40+
/// up to O(_n_).
41+
var estimatedAttachmentByteCount: Int? { get }
42+
2543
/// Call a function and pass a buffer representing this instance to it.
2644
///
2745
/// - Parameters:
@@ -48,6 +66,28 @@ extension Test {
4866

4967
// MARK: - Default implementations
5068

69+
extension Test.Attachable where Self: ~Copyable {
70+
public var estimatedAttachmentByteCount: Int? {
71+
nil
72+
}
73+
}
74+
75+
extension Test.Attachable where Self: Collection, Element == UInt8 {
76+
public var estimatedAttachmentByteCount: Int? {
77+
count
78+
}
79+
}
80+
81+
extension Test.Attachable where Self: StringProtocol {
82+
public var estimatedAttachmentByteCount: Int? {
83+
// NOTE: utf8.count may be O(n) for foreign strings.
84+
// SEE: https://github.com/swiftlang/swift/blob/main/stdlib/public/core/StringUTF8View.swift
85+
utf8.count
86+
}
87+
}
88+
89+
// MARK: - Default conformances
90+
5191
// Implement the protocol requirements for byte arrays and buffers so that
5292
// developers can attach raw data when needed.
5393
@_spi(Experimental)

Diff for: Sources/Testing/Attachments/Test.Attachment.swift

+18-2
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ private struct _AttachableProxy: Test.Attachable, Sendable {
9999
/// attachable value.
100100
var encodedValue = [UInt8]()
101101

102+
var estimatedAttachmentByteCount: Int?
103+
102104
func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
103105
try encodedValue.withUnsafeBufferPointer(for: attachment, body)
104106
}
@@ -128,6 +130,7 @@ extension Test.Attachment {
128130
sourceLocation: SourceLocation = #_sourceLocation
129131
) {
130132
var proxyAttachable = _AttachableProxy()
133+
proxyAttachable.estimatedAttachmentByteCount = attachableValue.estimatedAttachmentByteCount
131134

132135
// BUG: the borrow checker thinks that withErrorRecording() is consuming
133136
// attachableValue, so get around it with an additional do/catch clause.
@@ -138,6 +141,9 @@ extension Test.Attachment {
138141
}
139142
} catch {
140143
Issue.withErrorRecording(at: sourceLocation) {
144+
// TODO: define new issue kind .valueAttachmentFailed(any Error)
145+
// (but only use it if the caught error isn't ExpectationFailedError,
146+
// SystemError, or APIMisuseError. We need a protocol for these things.)
141147
throw error
142148
}
143149
}
@@ -176,6 +182,8 @@ extension Test.Attachment {
176182
/// - Parameters:
177183
/// - directoryPath: The directory to which the attachment should be
178184
/// written.
185+
/// - usingPreferredName: Whether or not to use the attachment's preferred
186+
/// name. If `false`, ``defaultPreferredName`` is used instead.
179187
/// - suffix: A suffix to attach to the file name (instead of randomly
180188
/// generating one.) This value may be evaluated multiple times.
181189
///
@@ -189,9 +197,11 @@ extension Test.Attachment {
189197
///
190198
/// If the argument `suffix` always produces the same string, the result of
191199
/// this function is undefined.
192-
func write(toFileInDirectoryAtPath directoryPath: String, appending suffix: @autoclosure () -> String) throws -> String {
200+
func write(toFileInDirectoryAtPath directoryPath: String, usingPreferredName: Bool = true, appending suffix: @autoclosure () -> String) throws -> String {
193201
let result: String
194202

203+
let preferredName = usingPreferredName ? preferredName : Self.defaultPreferredName
204+
195205
var file: FileHandle?
196206
do {
197207
// First, attempt to create the file with the exact preferred name. If a
@@ -217,7 +227,13 @@ extension Test.Attachment {
217227
file = try FileHandle(atPath: preferredPath, mode: "wxb")
218228
result = preferredPath
219229
break
220-
} catch let error as CError where error.rawValue == EEXIST {}
230+
} catch let error as CError where error.rawValue == EEXIST {
231+
// Try again with a new suffix.
232+
continue
233+
} catch where usingPreferredName {
234+
// Try again with the default name before giving up.
235+
return try write(toFileInDirectoryAtPath: directoryPath, usingPreferredName: false, appending: suffix())
236+
}
221237
}
222238
}
223239

Diff for: Tests/TestingTests/AttachmentTests.swift

+121-19
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,12 @@ private import _TestingInternals
1515
struct AttachmentTests {
1616
@Test func saveValue() {
1717
let attachableValue = MyAttachable(string: "<!doctype html>")
18-
Test.Attachment(attachableValue, named: "AttachmentTests.saveValue.html").attach()
18+
let attachment = Test.Attachment(attachableValue, named: "AttachmentTests.saveValue.html")
19+
attachment.attach()
1920
}
2021

2122
#if !SWT_NO_FILE_IO
22-
@Test func writeAttachment() throws {
23-
let attachableValue = MyAttachable(string: "<!doctype html>")
24-
let attachment = Test.Attachment(attachableValue, named: "loremipsum.html")
25-
26-
// Write the attachment to disk, then read it back.
27-
let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectoryPath())
28-
defer {
29-
remove(filePath)
30-
}
23+
func compare(_ attachableValue: borrowing MyAttachable, toContentsOfFileAtPath filePath: String) throws {
3124
let file = try FileHandle(forReadingAtPath: filePath)
3225
let bytes = try file.readToEnd()
3326

@@ -39,6 +32,18 @@ struct AttachmentTests {
3932
#expect(decodedValue == attachableValue.string)
4033
}
4134

35+
@Test func writeAttachment() throws {
36+
let attachableValue = MyAttachable(string: "<!doctype html>")
37+
let attachment = Test.Attachment(attachableValue, named: "loremipsum.html")
38+
39+
// Write the attachment to disk, then read it back.
40+
let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectoryPath())
41+
defer {
42+
remove(filePath)
43+
}
44+
try compare(attachableValue, toContentsOfFileAtPath: filePath)
45+
}
46+
4247
@Test func writeAttachmentWithNameConflict() throws {
4348
// A sequence of suffixes that are guaranteed to cause conflict.
4449
let randomBaseValue = UInt64.random(in: 0 ..< (.max - 10))
@@ -67,15 +72,7 @@ struct AttachmentTests {
6772
} else {
6873
#expect(fileName != baseFileName)
6974
}
70-
let file = try FileHandle(forReadingAtPath: filePath)
71-
let bytes = try file.readToEnd()
72-
73-
let decodedValue = if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) {
74-
try #require(String(validating: bytes, as: UTF8.self))
75-
} else {
76-
String(decoding: bytes, as: UTF8.self)
77-
}
78-
#expect(decodedValue == attachableValue.string)
75+
try compare(attachableValue, toContentsOfFileAtPath: filePath)
7976
}
8077
}
8178

@@ -98,6 +95,32 @@ struct AttachmentTests {
9895
}
9996
let fileName = try #require(filePath.split { $0 == "/" || $0 == #"\"# }.last)
10097
#expect(fileName == "loremipsum-\(suffix).tar.gz.gif.jpeg.html")
98+
try compare(attachableValue, toContentsOfFileAtPath: filePath)
99+
}
100+
101+
#if os(Windows)
102+
static let maximumNameCount = Int(_MAX_FNAME)
103+
static let reservedNames = ["CON", "COM0", "LPT2"]
104+
#else
105+
static let maximumNameCount = Int(NAME_MAX)
106+
static let reservedNames: [String] = []
107+
#endif
108+
109+
@Test(arguments: [
110+
#"/\:"#,
111+
String(repeating: "a", count: maximumNameCount),
112+
String(repeating: "a", count: maximumNameCount + 1),
113+
String(repeating: "a", count: maximumNameCount + 2),
114+
] + reservedNames) func writeAttachmentWithBadName(name: String) throws {
115+
let attachableValue = MyAttachable(string: "<!doctype html>")
116+
let attachment = Test.Attachment(attachableValue, named: name)
117+
118+
// Write the attachment to disk, then read it back.
119+
let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectoryPath())
120+
defer {
121+
remove(filePath)
122+
}
123+
try compare(attachableValue, toContentsOfFileAtPath: filePath)
101124
}
102125
#endif
103126

@@ -132,14 +155,82 @@ struct AttachmentTests {
132155
}
133156
}
134157
}
158+
159+
@Test func issueRecordedWhenAttachingNonSendableValueThatThrows() async {
160+
await confirmation("Attachment detected") { valueAttached in
161+
await confirmation("Issue recorded") { issueRecorded in
162+
await Test {
163+
var attachableValue = MyAttachable(string: "<!doctype html>")
164+
attachableValue.errorToThrow = MyError()
165+
Test.Attachment(attachableValue, named: "loremipsum").attach()
166+
}.run { event, _ in
167+
if case .valueAttached = event.kind {
168+
valueAttached()
169+
} else if case let .issueRecorded(issue) = event.kind,
170+
case let .errorCaught(error) = issue.kind,
171+
error is MyError {
172+
issueRecorded()
173+
}
174+
}
175+
}
176+
}
177+
}
178+
}
179+
180+
extension AttachmentTests {
181+
@Suite("Built-in conformances")
182+
struct BuiltInConformances {
183+
func test(_ value: borrowing some Test.Attachable & ~Copyable) throws {
184+
#expect(value.estimatedAttachmentByteCount == 6)
185+
let attachment = Test.Attachment(value)
186+
try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { buffer in
187+
#expect(buffer.elementsEqual("abc123".utf8))
188+
#expect(buffer.count == 6)
189+
}
190+
}
191+
192+
@Test func uint8Array() throws {
193+
let value: [UInt8] = Array("abc123".utf8)
194+
try test(value)
195+
}
196+
197+
@Test func uint8UnsafeBufferPointer() throws {
198+
let value: [UInt8] = Array("abc123".utf8)
199+
try value.withUnsafeBufferPointer { value in
200+
try test(value)
201+
}
202+
}
203+
204+
@Test func unsafeRawBufferPointer() throws {
205+
let value: [UInt8] = Array("abc123".utf8)
206+
try value.withUnsafeBytes { value in
207+
try test(value)
208+
}
209+
}
210+
211+
@Test func string() throws {
212+
let value = "abc123"
213+
try test(value)
214+
}
215+
216+
@Test func substring() throws {
217+
let value: Substring = "abc123"[...]
218+
try test(value)
219+
}
220+
}
135221
}
136222

137223
// MARK: - Fixtures
138224

139225
struct MyAttachable: Test.Attachable, ~Copyable {
140226
var string: String
227+
var errorToThrow: (any Error)?
141228

142229
func withUnsafeBufferPointer<R>(for attachment: borrowing Testing.Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
230+
if let errorToThrow {
231+
throw errorToThrow
232+
}
233+
143234
var string = string
144235
return try string.withUTF8 { buffer in
145236
try body(.init(buffer))
@@ -160,3 +251,14 @@ struct MySendableAttachable: Test.Attachable, Sendable {
160251
}
161252
}
162253
}
254+
255+
struct MySendableAttachableWithDefaultByteCount: Test.Attachable, Sendable {
256+
var string: String
257+
258+
func withUnsafeBufferPointer<R>(for attachment: borrowing Testing.Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
259+
var string = string
260+
return try string.withUTF8 { buffer in
261+
try body(.init(buffer))
262+
}
263+
}
264+
}

0 commit comments

Comments
 (0)