Skip to content
Open
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
6 changes: 1 addition & 5 deletions Sources/Testing/Attachments/Attachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,7 @@ extension Attachment: CustomStringConvertible where AttachableValue: ~Copyable {
/// @Available(Xcode, introduced: 26.0)
/// }
public var description: String {
if #available(_castingWithNonCopyableGenerics, *), let attachableValue = boxCopyableValue(attachableValue) {
return #""\#(preferredName)": \#(String(describingForTest: attachableValue))"#
}
let typeInfo = TypeInfo(describing: AttachableValue.self)
return #""\#(preferredName)": instance of '\#(typeInfo.unqualifiedName)'"#
#""\#(preferredName)": \#(String(describingForTest: attachableValue))"#
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
/// ## See Also
///
/// - ``Swift/String/init(describingForTest:)``
public protocol CustomTestStringConvertible {
public protocol CustomTestStringConvertible: ~Copyable & ~Escapable {
/// A description of this instance to use when presenting it in a test's
/// output.
///
Expand All @@ -95,34 +95,55 @@ extension String {
/// ## See Also
///
/// - ``CustomTestStringConvertible``
public init(describingForTest value: some Any) {
// The mangled type name SPI doesn't handle generic types very well, so we
// ask for the dynamic type of `value` (type(of:)) instead of just T.self.
lazy var valueTypeInfo = TypeInfo(describingTypeOf: value)
if let value = value as? any CustomTestStringConvertible {
self = value.testDescription
} else if let value = value as? any CustomStringConvertible {
self.init(describing: value)
} else if let value = value as? any TextOutputStreamable {
self.init(describing: value)
} else if let value = value as? any CustomDebugStringConvertible {
self.init(reflecting: value)
} else if let value = value as? any Any.Type {
self = _testDescription(of: value)
} else if let value = value as? any RawRepresentable, let type = valueTypeInfo.type, valueTypeInfo.isImportedFromC {
// Present raw-representable C types, which we assume to be imported
// enumerations, in a consistent fashion. The case names of C enumerations
// are not statically visible, so instead present the enumeration type's
// name along with the raw value of `value`.
let typeName = String(describingForTest: type)
self = "\(typeName)(rawValue: \(String(describingForTest: value.rawValue)))"
} else if valueTypeInfo.isSwiftEnumeration {
// Add a leading period to enumeration cases to more closely match their
// source representation. SEE: _adHocPrint_unlocked() in the stdlib.
self = ".\(value)"
public init(describingForTest value: borrowing (some CustomTestStringConvertible & ~Copyable & ~Escapable)) {
self = value.testDescription
}

/// Initialize this instance so that it can be presented in a test's output.
///
/// - Parameters:
/// - value: The value to describe.
///
/// ## See Also
///
/// - ``CustomTestStringConvertible``
@_disfavoredOverload // prefer compile-time conformance check
public init<T>(describingForTest value: borrowing T) where T: ~Copyable & ~Escapable {
// TODO: when associated types with suppressed conformances land, check for
// conformance to `CustomTestStringConvertible` before casting to `Any` with
// `makeExistential()`. See SE-0503.
if #available(_castingWithNonCopyableGenerics, *), let value = makeExistential(value) {
// The mangled type name SPI doesn't handle generic types very well, so we
// ask for the dynamic type of `value` (type(of:)) instead of just T.self.
lazy var valueTypeInfo = TypeInfo(describingTypeOf: value)
if let value = value as? any CustomTestStringConvertible {
self = value.testDescription
} else if let value = value as? any CustomStringConvertible {
self.init(describing: value)
} else if let value = value as? any TextOutputStreamable {
self.init(describing: value)
} else if let value = value as? any CustomDebugStringConvertible {
self.init(reflecting: value)
} else if let value = value as? any Any.Type {
self = _testDescription(of: value)
} else if let value = value as? any RawRepresentable, let type = valueTypeInfo.type, valueTypeInfo.isImportedFromC {
// Present raw-representable C types, which we assume to be imported
// enumerations, in a consistent fashion. The case names of C enumerations
// are not statically visible, so instead present the enumeration type's
// name along with the raw value of `value`.
let typeName = String(describingForTest: type)
self = "\(typeName)(rawValue: \(String(describingForTest: value.rawValue)))"
} else if valueTypeInfo.isSwiftEnumeration {
// Add a leading period to enumeration cases to more closely match their
// source representation. SEE: _adHocPrint_unlocked() in the stdlib.
self = ".\(value)"
} else {
// Use the generic description of the value.
self.init(describing: value)
}
} else {
// Use the generic description of the value.
self.init(describing: value)
let typeInfo = TypeInfo(describing: T.self)
self = "instance of '\(typeInfo.unqualifiedName)'"
}
}
}
Expand All @@ -141,7 +162,8 @@ private func _testDescription(of type: any Any.Type) -> String {
TypeInfo(describing: type).unqualifiedName
}

extension Optional: CustomTestStringConvertible {
@_preInverseGenerics
extension Optional: CustomTestStringConvertible where Wrapped: ~Copyable & ~Escapable {
public var testDescription: String {
switch self {
case let .some(unwrappedValue):
Expand Down
32 changes: 20 additions & 12 deletions Sources/Testing/Support/Additions/CopyableAdditions.swift
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Copyright (c) 2025–2026 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 !hasFeature(Embedded)
/// A helper protocol for ``boxCopyableValue(_:)``.
private protocol _CopyablePointer {
/// A helper protocol for ``makeExistential(_:)``.
private protocol _CopierProtocol<Referent> {
/// The type of value that a conforming type can copy.
associatedtype Referent

/// Load the value at this address into an existential box.
///
/// - Returns: The value at this address.
func load() -> Any
static func load(from value: Referent) -> Any?
}

extension UnsafePointer: _CopyablePointer where Pointee: Copyable {
func load() -> Any {
pointee
/// A helper type for ``makeExistential(_:)``
private struct _Copier<Referent> where Referent: ~Copyable & ~Escapable {}

extension _Copier: _CopierProtocol where Referent: Copyable & Escapable {
static func load(from value: Referent) -> Any? {
value
}
}
#endif

/// Copy a value to an existential box if its type conforms to `Copyable`.
/// Copy a value to an existential box if its type conforms to `Copyable` and
/// `Escapable`.
///
/// - Parameters:
/// - value: The value to copy.
Expand All @@ -35,13 +42,14 @@ extension UnsafePointer: _CopyablePointer where Pointee: Copyable {
/// When using Embedded Swift, this function always returns `nil`.
#if !hasFeature(Embedded)
@available(_castingWithNonCopyableGenerics, *)
func boxCopyableValue(_ value: borrowing some ~Copyable) -> Any? {
withUnsafePointer(to: value) { address in
return (address as? any _CopyablePointer)?.load()
func makeExistential<T>(_ value: borrowing T) -> Any? where T: ~Copyable & ~Escapable {
if let type = _Copier<T>.self as? any _CopierProtocol<T>.Type {
return type.load(from: value)
}
return nil
}
#else
func boxCopyableValue(_ value: borrowing some ~Copyable) -> Void? {
func makeExistential<T>(_ value: borrowing T) -> Void? where T: ~Copyable & ~Escapable {
nil
}
#endif
10 changes: 10 additions & 0 deletions Sources/Testing/Traits/Comment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ extension Comment: ExpressibleByStringInterpolation {
rawValue += String(describingForTest: value)
}
}

@_disfavoredOverload
@inlinable public mutating func appendInterpolation(_ value: borrowing (some CustomTestStringConvertible & ~Copyable & ~Escapable)) {
rawValue += String(describingForTest: value)
}

@_disfavoredOverload
@inlinable public mutating func appendInterpolation(_ value: borrowing (some ~Copyable & ~Escapable)) {
rawValue += String(describingForTest: value)
}
}
}

Expand Down
29 changes: 27 additions & 2 deletions Tests/TestingTests/Traits/CommentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ struct CommentTests {
let value1: Int = 123
let value2: Int? = nil
let value3: Any.Type = Int.self
let comment: Comment = "abc\(value1)def\(value2)ghi\(value3)"
#expect(comment.rawValue == "abc123defnilghiInt")
let value4: String? = nil
let comment: Comment = "abc\(value1)def\(value2)ghi\(value3)\(value4)"
#expect(comment.rawValue == "abc123defnilghiIntnil")
}

@Test("String interpolation with a custom type")
Expand All @@ -82,6 +83,30 @@ struct CommentTests {
let comment: Comment = "abc\(S())def\(S() as S?)ghi\(S.self)jkl\("string")"
#expect(comment.rawValue == "abcright!defright!ghiSjklstring")
}

@Test("String interpolation with a move-only value")
func stringInterpolationWithMoveOnlyValue() {
struct S: ~Copyable {}

let s = S()
let comment: Comment = "abc\(s)"
#expect(comment.rawValue == "abcinstance of 'S'")
_ = s
}

@Test("String interpolation with a move-only value conforming to CustomTestStringConvertible")
func stringInterpolationWithMoveOnlyValueConformingToProtocol() {
struct S: CustomTestStringConvertible, ~Copyable {
var testDescription: String {
"right!"
}
}

let s = S()
let comment: Comment = "abc\(s)"
#expect(comment.rawValue == "abcright!")
_ = s
}
}

// MARK: - Fixtures
Expand Down
Loading