Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6894115
Rethink how we capture expectation conditions and their subexpressions.
grynspan Sep 20, 2024
d917c94
Borrow/consume some args in the macro expansion
grynspan Mar 20, 2025
9fd24cc
Delete some dead code
grynspan Apr 24, 2025
b33a9d6
Remove invalid test
grynspan Apr 24, 2025
5a83ae0
Suppress ExpectationFailedError in withKnownIssue
grynspan Apr 24, 2025
f168971
Fix up some tests
grynspan May 23, 2025
4a5f7e5
Hook up effectful keywords discovery from lexical context
grynspan May 23, 2025
301f3f9
Merge branch 'main' into jgrynspan/162-redesign-value-capture
grynspan Sep 5, 2025
be162e5
Merge branch 'main' into jgrynspan/162-redesign-value-capture
grynspan Sep 22, 2025
c8c37be
Do __cmp() typechecking at runtime instead of compile time
grynspan Sep 22, 2025
488a82d
Fix DocC references
grynspan Sep 22, 2025
39a64d4
Merge branch 'main' into jgrynspan/162-redesign-value-capture
grynspan Sep 22, 2025
894fa8b
Remove mismatchedExitConditionDescription (obsolete)
grynspan Sep 22, 2025
da3ebda
Merge branch 'main' into jgrynspan/162-redesign-value-capture
grynspan Sep 24, 2025
b92851b
Reduce the number of (explicit) copies we need
grynspan Sep 24, 2025
60f5c7e
Moar
grynspan Sep 24, 2025
c749eed
Merge branch 'main' into jgrynspan/162-redesign-value-capture
grynspan Oct 20, 2025
7f0d347
Avoid move-only value
grynspan Oct 20, 2025
6b6453d
Merge branch 'main' into jgrynspan/162-redesign-value-capture
grynspan Oct 23, 2025
72cb0cf
Enable __cmp for move-only values
grynspan Oct 23, 2025
a39f5d5
Merge branch 'main' into jgrynspan/162-redesign-value-capture
grynspan Oct 29, 2025
233d674
Fix typo in comment
grynspan Oct 29, 2025
45307b5
Simplify after https://github.com/swiftlang/swift/pull/84907 lands
grynspan Oct 29, 2025
28a7e0d
Make __EC generic over the type of expression
grynspan Oct 29, 2025
ab6b153
Simplify ExpressionID-to-keypath code
grynspan Oct 29, 2025
1fbe4e3
Fix typo
grynspan Oct 29, 2025
3d4a858
Remove some unsafe bitcasts
grynspan Oct 29, 2025
c749cdd
Bail on variadic generic expressions
grynspan Oct 30, 2025
21d179c
Disable VariadicGenericTests.swift until rdar://161205293 is fixed
grynspan Oct 30, 2025
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
7 changes: 7 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,13 @@ let package = Package(
path: "Tests/_MemorySafeTestingTests",
swiftSettings: .packageSettings + [.strictMemorySafety()]
),
.testTarget(
name: "SubexpressionShowcase",
dependencies: [
"Testing",
],
swiftSettings: .packageSettings
),

.macro(
name: "TestingMacros",
Expand Down
34 changes: 34 additions & 0 deletions Sources/Testing/ABI/Encoded/ABI.EncodedExpectation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 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
//

extension ABI {
/// A type implementing the JSON encoding of ``Expectation`` for the ABI entry
/// point and event stream output.
///
/// This type is not part of the public interface of the testing library. It
/// assists in converting values to JSON; clients that consume this JSON are
/// expected to write their own decoders.
///
/// - Warning: Expectations are not yet part of the JSON schema.
struct EncodedExpectation<V>: Sendable where V: ABI.Version {
/// The expression evaluated by this expectation.
///
/// - Warning: Expressions are not yet part of the JSON schema.
var _expression: EncodedExpression<V>

init(encoding expectation: borrowing Expectation, in eventContext: borrowing Event.Context) {
_expression = EncodedExpression<V>(encoding: expectation.evaluatedExpression, in: eventContext)
}
}
}

// MARK: - Codable

extension ABI.EncodedExpectation: Codable {}
52 changes: 52 additions & 0 deletions Sources/Testing/ABI/Encoded/ABI.EncodedExpression.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 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
//

extension ABI {
/// A type implementing the JSON encoding of ``Expression`` for the ABI entry
/// point and event stream output.
///
/// This type is not part of the public interface of the testing library. It
/// assists in converting values to JSON; clients that consume this JSON are
/// expected to write their own decoders.
///
/// - Warning: Expressions are not yet part of the JSON schema.
struct EncodedExpression<V>: Sendable where V: ABI.Version {
/// The source code of the original captured expression.
var sourceCode: String

/// A string representation of the runtime value of this expression.
///
/// If the runtime value of this expression has not been evaluated, the
/// value of this property is `nil`.
var runtimeValue: String?

/// The fully-qualified name of the type of value represented by
/// `runtimeValue`, or `nil` if that value has not been captured.
var runtimeTypeName: String?

/// Any child expressions within this expression.
var children: [EncodedExpression]?

init(encoding expression: borrowing __Expression, in eventContext: borrowing Event.Context) {
sourceCode = expression.sourceCode
runtimeValue = expression.runtimeValue.map(String.init(describingForTest:))
runtimeTypeName = expression.runtimeValue.map(\.typeInfo.fullyQualifiedName)
if !expression.subexpressions.isEmpty {
children = expression.subexpressions.map { [eventContext = copy eventContext] subexpression in
Self(encoding: subexpression, in: eventContext)
}
}
}
}
}

// MARK: - Codable

extension ABI.EncodedExpression: Codable {}
8 changes: 8 additions & 0 deletions Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ extension ABI {
/// - Warning: Errors are not yet part of the JSON schema.
var _error: EncodedError<V>?

/// The expectation associated with this issue, if applicable.
///
/// - Warning: Expectations are not yet part of the JSON schema.
var _expectation: EncodedExpectation<V>?

init(encoding issue: borrowing Issue, in eventContext: borrowing Event.Context) {
// >= v0
isKnown = issue.isKnown
Expand All @@ -81,6 +86,9 @@ extension ABI {
_error = EncodedError(encoding: error, in: eventContext)
}
}
if case let .expectationFailed(expectation) = issue.kind {
_expectation = EncodedExpectation(encoding: expectation, in: eventContext)
}
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,19 @@ extension ABI {
/// The symbol associated with this message.
var symbol: Symbol

/// How much to indent this message when presenting it.
///
/// - Warning: This property is not yet part of the JSON schema.
var _indentation: Int?

/// The human-readable, unformatted text associated with this message.
var text: String

init(encoding message: borrowing Event.HumanReadableOutputRecorder.Message) {
symbol = Symbol(encoding: message.symbol ?? .default)
if message.indentation > 0 {
_indentation = message.indentation
}
text = message.conciseStringValue ?? message.stringValue
}
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/Testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ add_library(Testing
ABI/Encoded/ABI.EncodedBacktrace.swift
ABI/Encoded/ABI.EncodedError.swift
ABI/Encoded/ABI.EncodedEvent.swift
ABI/Encoded/ABI.EncodedExpectation.swift
ABI/Encoded/ABI.EncodedExpression.swift
ABI/Encoded/ABI.EncodedInstant.swift
ABI/Encoded/ABI.EncodedIssue.swift
ABI/Encoded/ABI.EncodedMessage.swift
Expand Down Expand Up @@ -47,6 +49,7 @@ add_library(Testing
Expectations/Expectation.swift
Expectations/Expectation+Macro.swift
Expectations/ExpectationChecking+Macro.swift
Expectations/ExpectationContext.swift
Issues/Confirmation.swift
Issues/ErrorSnapshot.swift
Issues/Issue.swift
Expand All @@ -69,7 +72,7 @@ add_library(Testing
SourceAttribution/Backtrace+Symbolication.swift
SourceAttribution/CustomTestStringConvertible.swift
SourceAttribution/Expression.swift
SourceAttribution/Expression+Macro.swift
SourceAttribution/ExpressionID.swift
SourceAttribution/SourceContext.swift
SourceAttribution/SourceLocation.swift
SourceAttribution/SourceLocation+Macro.swift
Expand Down
40 changes: 25 additions & 15 deletions Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,22 @@ private let _ansiEscapeCodePrefix = "\u{001B}["
private let _resetANSIEscapeCode = "\(_ansiEscapeCodePrefix)0m"

extension Event.Symbol {
/// Get the string value to use for a message with no associated symbol.
///
/// - Parameters:
/// - options: Options to use when writing the symbol.
///
/// - Returns: A string representation of "no symbol" appropriate for writing
/// to a stream.
fileprivate static func placeholderStringValue(options: Event.ConsoleOutputRecorder.Options) -> String {
#if os(macOS) || (os(iOS) && targetEnvironment(macCatalyst))
if options.useSFSymbols {
return " "
}
#endif
return " "
}

/// Get the string value for this symbol with the given write options.
///
/// - Parameters:
Expand Down Expand Up @@ -171,7 +187,7 @@ extension Event.Symbol {
case .attachment:
return "\(_ansiEscapeCodePrefix)94m\(symbolCharacter)\(_resetANSIEscapeCode)"
case .details:
return symbolCharacter
return "\(symbolCharacter)"
}
}
return "\(symbolCharacter)"
Expand Down Expand Up @@ -305,18 +321,12 @@ extension Event.ConsoleOutputRecorder {
/// - Returns: Whether any output was produced and written to this instance's
/// destination.
@discardableResult public func record(_ event: borrowing Event, in context: borrowing Event.Context) -> Bool {
let messages = _humanReadableOutputRecorder.record(event, in: context)

// Padding to use in place of a symbol for messages that don't have one.
var padding = " "
#if os(macOS) || (os(iOS) && targetEnvironment(macCatalyst))
if options.useSFSymbols {
padding = " "
}
#endif
let symbolPlaceholder = Event.Symbol.placeholderStringValue(options: options)

let messages = _humanReadableOutputRecorder.record(event, in: context)
let lines = messages.lazy.map { [test = context.test] message in
let symbol = message.symbol?.stringValue(options: options) ?? padding
let symbol = message.symbol?.stringValue(options: options) ?? symbolPlaceholder
let indentation = String(repeating: " ", count: message.indentation)

if case .details = message.symbol {
// Special-case the detail symbol to apply grey to the entire line of
Expand All @@ -325,17 +335,17 @@ extension Event.ConsoleOutputRecorder {
// to the indentation provided by the symbol.
var lines = message.stringValue.split(omittingEmptySubsequences: false, whereSeparator: \.isNewline)
lines = CollectionOfOne(lines[0]) + lines.dropFirst().map { line in
"\(padding) \(line)"
"\(indentation)\(symbolPlaceholder) \(line)"
}
let stringValue = lines.joined(separator: "\n")
if options.useANSIEscapeCodes, options.ansiColorBitDepth > 1 {
return "\(_ansiEscapeCodePrefix)90m\(symbol) \(stringValue)\(_resetANSIEscapeCode)\n"
return "\(_ansiEscapeCodePrefix)90m\(symbol) \(indentation)\(stringValue)\(_resetANSIEscapeCode)\n"
} else {
return "\(symbol) \(stringValue)\n"
return "\(symbol) \(indentation)\(stringValue)\n"
}
} else {
let colorDots = test.map { self.colorDots(for: $0.tags) } ?? ""
return "\(symbol) \(colorDots)\(message.stringValue)\n"
return "\(symbol) \(indentation)\(colorDots)\(message.stringValue)\n"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ extension Event {
/// The symbol associated with this message, if any.
var symbol: Symbol?

/// How much to indent this message when presenting it.
///
/// The way in which this additional indentation is rendered is
/// implementation-defined. Typically, the greater the value of this
/// property, the more whitespace characters are inserted.
///
/// Rendering of indentation is optional.
var indentation = 0

/// The human-readable message.
var stringValue: String

Expand Down Expand Up @@ -496,20 +505,18 @@ extension Event.HumanReadableOutputRecorder {
additionalMessages.append(_formattedComment(knownIssueComment))
}

if verbosity > 0, case let .expectationFailed(expectation) = issue.kind {
if verbosity >= 0, case let .expectationFailed(expectation) = issue.kind {
let expression = expectation.evaluatedExpression
func addMessage(about expression: __Expression) {
let description = expression.expandedDebugDescription()
additionalMessages.append(Message(symbol: .details, stringValue: description))
}
let subexpressions = expression.subexpressions
if subexpressions.isEmpty {
addMessage(about: expression)
} else {
for subexpression in subexpressions {
addMessage(about: subexpression)
func addMessage(about expression: __Expression, depth: Int) {
let description = expression.expandedDescription(verbose: verbosity > 0)
if description != expression.sourceCode {
additionalMessages.append(Message(symbol: .details, indentation: depth, stringValue: description))
}
for subexpression in expression.subexpressions {
addMessage(about: subexpression, depth: depth + 1)
}
}
addMessage(about: expression, depth: 0)
}

let atSourceLocation = issue.sourceLocation.map { " at \($0)" } ?? ""
Expand Down
13 changes: 8 additions & 5 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ func callExitTest(
encodingCapturedValues capturedValues: [ExitTest.CapturedValue],
processExitsWith expectedExitCondition: ExitTest.Condition,
observing observedValues: [any PartialKeyPath<ExitTest.Result> & Sendable],
expression: __Expression,
sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String],
comments: @autoclosure () -> [Comment],
isRequired: Bool,
isolation: isolated (any Actor)? = #isolation,
Expand Down Expand Up @@ -521,11 +521,14 @@ func callExitTest(
}

// Plumb the exit test's result through the general expectation machinery.
let expression = __Expression(String(describingForTest: expectedExitCondition))
return __checkValue(
let expectationContext = __ExpectationContext<Bool>(
sourceCode: [.root: String(describingForTest: expectedExitCondition)],
runtimeValues: [.root: { Expression.Value(reflecting: result.exitStatus) }]
)
return check(
expectedExitCondition.isApproximatelyEqual(to: result.exitStatus),
expression: expression,
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(result.exitStatus),
expectationContext: expectationContext,
mismatchedErrorDescription: nil,
comments: comments(),
isRequired: isRequired,
sourceLocation: sourceLocation
Expand Down
12 changes: 6 additions & 6 deletions Sources/Testing/Expectations/Expectation+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@
/// running in the current task and an instance of ``ExpectationFailedError`` is
/// thrown.
@freestanding(expression) public macro require<T>(
_ optionalValue: T?,
_ optionalValue: consuming T?,
_ comment: @autoclosure () -> Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) -> T = #externalMacro(module: "TestingMacros", type: "RequireMacro")
) -> T = #externalMacro(module: "TestingMacros", type: "UnwrapMacro") where T: ~Copyable

/// Unwrap an optional boolean value or, if it is `nil`, fail and throw an
/// error.
Expand All @@ -89,7 +89,7 @@
/// running in the current task and an instance of ``ExpectationFailedError`` is
/// thrown.
///
/// This overload of ``require(_:_:sourceLocation:)-6w9oo`` checks if
/// This overload of ``require(_:_:sourceLocation:)-5l63q`` checks if
/// `optionalValue` may be ambiguous (i.e. it is unclear if the developer
/// intended to check for a boolean value or unwrap an optional boolean value)
/// and provides additional compile-time diagnostics when it is.
Expand Down Expand Up @@ -118,16 +118,16 @@ public macro require(
/// running in the current task and an instance of ``ExpectationFailedError`` is
/// thrown.
///
/// This overload of ``require(_:_:sourceLocation:)-6w9oo`` is used when a
/// This overload of ``require(_:_:sourceLocation:)-5l63q`` is used when a
/// non-optional, non-`Bool` value is passed to `#require()`. It emits a warning
/// diagnostic indicating that the expectation is redundant.
@freestanding(expression)
@_documentation(visibility: private)
public macro require<T>(
_ optionalValue: T,
_ optionalValue: consuming T,
_ comment: @autoclosure () -> Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) -> T = #externalMacro(module: "TestingMacros", type: "NonOptionalRequireMacro")
) -> T = #externalMacro(module: "TestingMacros", type: "NonOptionalRequireMacro") where T: ~Copyable

// MARK: - Matching errors by type

Expand Down
Loading
Loading