Skip to content

Commit

Permalink
Increase test coverage, add estimatedAttachmentByteCount property
Browse files Browse the repository at this point in the history
  • Loading branch information
grynspan committed Oct 25, 2024
1 parent 097944b commit ad35d3e
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 21 deletions.
40 changes: 40 additions & 0 deletions Sources/Testing/Attachments/Test.Attachable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@ extension Test {
// TODO: write more about this protocol, how it works, and list conforming
// types (including discussion of the Foundation cross-import overlay.)
public protocol Attachable: ~Copyable {
/// An estimate of the number of bytes of memory needed to store this value
/// as an attachment.
///
/// The testing library uses this property to determine if an attachment
/// should be held in memory or should be immediately persisted to storage.
/// Larger attachments are more likely to be persisted, but the algorithm
/// the testing library uses is an implementation detail and is subject to
/// change.
///
/// The value of this property is approximately equal to the number of bytes
/// that will actually be needed, or `nil` if the value cannot be computed
/// efficiently. The default implementation of this property returns a value
/// of `nil`.
///
/// - Complexity: O(1) unless `Self` conforms to `Collection`, in which case
/// up to O(_n_).
var estimatedAttachmentByteCount: Int? { get }

/// Call a function and pass a buffer representing this instance to it.
///
/// - Parameters:
Expand All @@ -48,6 +66,28 @@ extension Test {

// MARK: - Default implementations

extension Test.Attachable where Self: ~Copyable {
public var estimatedAttachmentByteCount: Int? {
nil
}
}

extension Test.Attachable where Self: Collection, Element == UInt8 {
public var estimatedAttachmentByteCount: Int? {
count
}
}

extension Test.Attachable where Self: StringProtocol {
public var estimatedAttachmentByteCount: Int? {
// NOTE: utf8.count may be O(n) for foreign strings.
// SEE: https://github.com/swiftlang/swift/blob/main/stdlib/public/core/StringUTF8View.swift
utf8.count
}
}

// MARK: - Default conformances

// Implement the protocol requirements for byte arrays and buffers so that
// developers can attach raw data when needed.
@_spi(Experimental)
Expand Down
20 changes: 18 additions & 2 deletions Sources/Testing/Attachments/Test.Attachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ private struct _AttachableProxy: Test.Attachable, Sendable {
/// attachable value.
var encodedValue = [UInt8]()

var estimatedAttachmentByteCount: Int?

func withUnsafeBufferPointer<R>(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try encodedValue.withUnsafeBufferPointer(for: attachment, body)
}
Expand Down Expand Up @@ -128,6 +130,7 @@ extension Test.Attachment {
sourceLocation: SourceLocation = #_sourceLocation
) {
var proxyAttachable = _AttachableProxy()
proxyAttachable.estimatedAttachmentByteCount = attachableValue.estimatedAttachmentByteCount

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

let preferredName = usingPreferredName ? preferredName : Self.defaultPreferredName

var file: FileHandle?
do {
// First, attempt to create the file with the exact preferred name. If a
Expand All @@ -217,7 +227,13 @@ extension Test.Attachment {
file = try FileHandle(atPath: preferredPath, mode: "wxb")
result = preferredPath
break
} catch let error as CError where error.rawValue == EEXIST {}
} catch let error as CError where error.rawValue == EEXIST {
// Try again with a new suffix.
continue
} catch where usingPreferredName {
// Try again with the default name before giving up.
return try write(toFileInDirectoryAtPath: directoryPath, usingPreferredName: false, appending: suffix())
}
}
}

Expand Down
140 changes: 121 additions & 19 deletions Tests/TestingTests/AttachmentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,12 @@ private import _TestingInternals
struct AttachmentTests {
@Test func saveValue() {
let attachableValue = MyAttachable(string: "<!doctype html>")
Test.Attachment(attachableValue, named: "AttachmentTests.saveValue.html").attach()
let attachment = Test.Attachment(attachableValue, named: "AttachmentTests.saveValue.html")
attachment.attach()
}

#if !SWT_NO_FILE_IO
@Test func writeAttachment() throws {
let attachableValue = MyAttachable(string: "<!doctype html>")
let attachment = Test.Attachment(attachableValue, named: "loremipsum.html")

// Write the attachment to disk, then read it back.
let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectoryPath())
defer {
remove(filePath)
}
func compare(_ attachableValue: borrowing MyAttachable, toContentsOfFileAtPath filePath: String) throws {
let file = try FileHandle(forReadingAtPath: filePath)
let bytes = try file.readToEnd()

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

@Test func writeAttachment() throws {
let attachableValue = MyAttachable(string: "<!doctype html>")
let attachment = Test.Attachment(attachableValue, named: "loremipsum.html")

// Write the attachment to disk, then read it back.
let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectoryPath())
defer {
remove(filePath)
}
try compare(attachableValue, toContentsOfFileAtPath: filePath)
}

@Test func writeAttachmentWithNameConflict() throws {
// A sequence of suffixes that are guaranteed to cause conflict.
let randomBaseValue = UInt64.random(in: 0 ..< (.max - 10))
Expand Down Expand Up @@ -67,15 +72,7 @@ struct AttachmentTests {
} else {
#expect(fileName != baseFileName)
}
let file = try FileHandle(forReadingAtPath: filePath)
let bytes = try file.readToEnd()

let decodedValue = if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) {
try #require(String(validating: bytes, as: UTF8.self))
} else {
String(decoding: bytes, as: UTF8.self)
}
#expect(decodedValue == attachableValue.string)
try compare(attachableValue, toContentsOfFileAtPath: filePath)
}
}

Expand All @@ -98,6 +95,32 @@ struct AttachmentTests {
}
let fileName = try #require(filePath.split { $0 == "/" || $0 == #"\"# }.last)
#expect(fileName == "loremipsum-\(suffix).tar.gz.gif.jpeg.html")
try compare(attachableValue, toContentsOfFileAtPath: filePath)
}

#if os(Windows)
static let maximumNameCount = Int(_MAX_FNAME)
static let reservedNames = ["CON", "COM0", "LPT2"]
#else
static let maximumNameCount = Int(NAME_MAX)
static let reservedNames: [String] = []
#endif

@Test(arguments: [
#"/\:"#,
String(repeating: "a", count: maximumNameCount),
String(repeating: "a", count: maximumNameCount + 1),
String(repeating: "a", count: maximumNameCount + 2),
] + reservedNames) func writeAttachmentWithBadName(name: String) throws {
let attachableValue = MyAttachable(string: "<!doctype html>")
let attachment = Test.Attachment(attachableValue, named: name)

// Write the attachment to disk, then read it back.
let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectoryPath())
defer {
remove(filePath)
}
try compare(attachableValue, toContentsOfFileAtPath: filePath)
}
#endif

Expand Down Expand Up @@ -132,14 +155,82 @@ struct AttachmentTests {
}
}
}

@Test func issueRecordedWhenAttachingNonSendableValueThatThrows() async {
await confirmation("Attachment detected") { valueAttached in
await confirmation("Issue recorded") { issueRecorded in
await Test {
var attachableValue = MyAttachable(string: "<!doctype html>")
attachableValue.errorToThrow = MyError()
Test.Attachment(attachableValue, named: "loremipsum").attach()
}.run { event, _ in
if case .valueAttached = event.kind {
valueAttached()
} else if case let .issueRecorded(issue) = event.kind,
case let .errorCaught(error) = issue.kind,
error is MyError {
issueRecorded()
}
}
}
}
}
}

extension AttachmentTests {
@Suite("Built-in conformances")
struct BuiltInConformances {
func test(_ value: borrowing some Test.Attachable & ~Copyable) throws {
#expect(value.estimatedAttachmentByteCount == 6)
let attachment = Test.Attachment(value)
try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { buffer in
#expect(buffer.elementsEqual("abc123".utf8))
#expect(buffer.count == 6)
}
}

@Test func uint8Array() throws {
let value: [UInt8] = Array("abc123".utf8)
try test(value)
}

@Test func uint8UnsafeBufferPointer() throws {
let value: [UInt8] = Array("abc123".utf8)
try value.withUnsafeBufferPointer { value in
try test(value)
}
}

@Test func unsafeRawBufferPointer() throws {
let value: [UInt8] = Array("abc123".utf8)
try value.withUnsafeBytes { value in
try test(value)
}
}

@Test func string() throws {
let value = "abc123"
try test(value)
}

@Test func substring() throws {
let value: Substring = "abc123"[...]
try test(value)
}
}
}

// MARK: - Fixtures

struct MyAttachable: Test.Attachable, ~Copyable {
var string: String
var errorToThrow: (any Error)?

func withUnsafeBufferPointer<R>(for attachment: borrowing Testing.Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
if let errorToThrow {
throw errorToThrow
}

var string = string
return try string.withUTF8 { buffer in
try body(.init(buffer))
Expand All @@ -160,3 +251,14 @@ struct MySendableAttachable: Test.Attachable, Sendable {
}
}
}

struct MySendableAttachableWithDefaultByteCount: Test.Attachable, Sendable {
var string: String

func withUnsafeBufferPointer<R>(for attachment: borrowing Testing.Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
var string = string
return try string.withUTF8 { buffer in
try body(.init(buffer))
}
}
}

0 comments on commit ad35d3e

Please sign in to comment.