Skip to content

Commit 44662e1

Browse files
committed
Property accesses in failed expectations sometimes include their value twice in expanded description
1 parent ace555d commit 44662e1

File tree

6 files changed

+76
-21
lines changed

6 files changed

+76
-21
lines changed

Sources/Testing/Expectations/ExpectationChecking+Macro.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,7 @@ public func __checkPropertyAccess<T>(
545545
return __checkValue(
546546
condition,
547547
expression: expression,
548-
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, condition),
548+
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs),
549549
comments: comments(),
550550
isRequired: isRequired,
551551
sourceLocation: sourceLocation
@@ -575,7 +575,7 @@ public func __checkPropertyAccess<T, U>(
575575
return __checkValue(
576576
optionalValue,
577577
expression: expression,
578-
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, optionalValue as U??),
578+
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs),
579579
comments: comments(),
580580
isRequired: isRequired,
581581
sourceLocation: sourceLocation

Sources/Testing/SourceAttribution/Expression+Macro.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ extension __Expression {
8282
///
8383
/// - Warning: This function is used to implement the `@Test`, `@Suite`,
8484
/// `#expect()` and `#require()` macros. Do not call it directly.
85-
public static func __fromPropertyAccess(_ value: Self, _ keyPath: Self) -> Self {
85+
public static func __fromPropertyAccess(_ value: Self, _ keyPath: String) -> Self {
8686
return Self(kind: .propertyAccess(value: value, keyPath: keyPath))
8787
}
8888

Sources/Testing/SourceAttribution/Expression.swift

+9-9
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public struct __Expression: Sendable {
8181
/// - value: The value whose property was accessed.
8282
/// - keyPath: The key path, relative to `value`, that was accessed, not
8383
/// including a leading backslash or period.
84-
indirect case propertyAccess(value: __Expression, keyPath: __Expression)
84+
indirect case propertyAccess(value: __Expression, keyPath: String)
8585

8686
/// The expression negates another expression.
8787
///
@@ -124,7 +124,7 @@ public struct __Expression: Sendable {
124124
}
125125
return "\(functionName)(\(argumentList))"
126126
case let .propertyAccess(value, keyPath):
127-
return "\(value.sourceCode).\(keyPath.sourceCode)"
127+
return "\(value.sourceCode).\(keyPath)"
128128
case let .negation(expression, isParenthetical):
129129
var sourceCode = expression.sourceCode
130130
if isParenthetical {
@@ -298,7 +298,9 @@ public struct __Expression: Sendable {
298298

299299
// Convert the variadic generic argument list to an array.
300300
var additionalValuesArray = [Any?]()
301-
repeat additionalValuesArray.append(each additionalValues)
301+
for additionalValue in repeat each additionalValues {
302+
additionalValuesArray.append(additionalValue)
303+
}
302304

303305
switch kind {
304306
case .generic, .stringLiteral:
@@ -320,7 +322,7 @@ public struct __Expression: Sendable {
320322
case let .propertyAccess(value, keyPath):
321323
result.kind = .propertyAccess(
322324
value: value.capturingRuntimeValues(firstValue),
323-
keyPath: keyPath.capturingRuntimeValues(additionalValuesArray.first ?? nil)
325+
keyPath: keyPath
324326
)
325327
case let .negation(expression, isParenthetical):
326328
result.kind = .negation(
@@ -421,9 +423,7 @@ public struct __Expression: Sendable {
421423
"\(functionName)(\(argumentList))"
422424
}
423425
case let .propertyAccess(value, keyPath):
424-
var keyPathContext = childContext
425-
keyPathContext.includeParenthesesIfNeeded = false
426-
result = "\(value._expandedDescription(in: childContext)).\(keyPath._expandedDescription(in: keyPathContext))"
426+
result = "\(value._expandedDescription(in: childContext)).\(keyPath)"
427427
case let .negation(expression, isParenthetical):
428428
childContext.includeParenthesesIfNeeded = !isParenthetical
429429
var expandedDescription = expression._expandedDescription(in: childContext)
@@ -475,8 +475,8 @@ public struct __Expression: Sendable {
475475
} else {
476476
arguments.lazy.map(\.value)
477477
}
478-
case let .propertyAccess(value: value, keyPath: keyPath):
479-
[value, keyPath]
478+
case let .propertyAccess(value, _):
479+
[value]
480480
case let .negation(expression, _):
481481
[expression]
482482
}

Sources/TestingMacros/Support/SourceCodeCapturing.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func createExpressionExprForFunctionCall(_ value: (any SyntaxProtocol)?, _ funct
9292
func createExpressionExprForPropertyAccess(_ value: ExprSyntax, _ keyPath: DeclReferenceExprSyntax) -> ExprSyntax {
9393
let arguments = LabeledExprListSyntax {
9494
LabeledExprSyntax(expression: createExpressionExpr(from: value))
95-
LabeledExprSyntax(expression: createExpressionExpr(from: keyPath.baseName))
95+
LabeledExprSyntax(expression: StringLiteralExprSyntax(content: keyPath.baseName.text))
9696
}
9797

9898
return ".__fromPropertyAccess(\(arguments))"

Tests/TestingMacrosTests/ConditionMacroTests.swift

+7-7
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,11 @@ struct ConditionMacroTests {
8383
##"#expect(a, sourceLocation: someValue)"##:
8484
##"Testing.__checkValue(a, expression: .__fromSyntaxNode("a"), comments: [], isRequired: false, sourceLocation: someValue).__expected()"##,
8585
##"#expect(a.isB)"##:
86-
##"Testing.__checkPropertyAccess(a.self, getting: { $0.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
86+
##"Testing.__checkPropertyAccess(a.self, getting: { $0.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), "isB"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
8787
##"#expect(a???.isB)"##:
88-
##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
88+
##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), "isB"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
8989
##"#expect(a?.b.isB)"##:
90-
##"Testing.__checkPropertyAccess(a?.b.self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
90+
##"Testing.__checkPropertyAccess(a?.b.self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b"), "isB"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
9191
]
9292
)
9393
func expectMacro(input: String, expectedOutput: String) throws {
@@ -159,11 +159,11 @@ struct ConditionMacroTests {
159159
##"#require(a, sourceLocation: someValue)"##:
160160
##"Testing.__checkValue(a, expression: .__fromSyntaxNode("a"), comments: [], isRequired: true, sourceLocation: someValue).__required()"##,
161161
##"#require(a.isB)"##:
162-
##"Testing.__checkPropertyAccess(a.self, getting: { $0.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
162+
##"Testing.__checkPropertyAccess(a.self, getting: { $0.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), "isB"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
163163
##"#require(a???.isB)"##:
164-
##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
164+
##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), "isB"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
165165
##"#require(a?.b.isB)"##:
166-
##"Testing.__checkPropertyAccess(a?.b.self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
166+
##"Testing.__checkPropertyAccess(a?.b.self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b"), "isB"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
167167
]
168168
)
169169
func requireMacro(input: String, expectedOutput: String) throws {
@@ -175,7 +175,7 @@ struct ConditionMacroTests {
175175
@Test("Unwrapping #require() macro",
176176
arguments: [
177177
##"#require(Optional<Int>.none)"##:
178-
##"Testing.__checkPropertyAccess(Optional<Int>.self, getting: { $0.none }, expression: .__fromPropertyAccess(.__fromSyntaxNode("Optional<Int>"), .__fromSyntaxNode("none")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
178+
##"Testing.__checkPropertyAccess(Optional<Int>.self, getting: { $0.none }, expression: .__fromPropertyAccess(.__fromSyntaxNode("Optional<Int>"), "none"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
179179
##"#require(nil ?? 123)"##:
180180
##"Testing.__checkBinaryOperation(nil, { $0 ?? $1() }, 123, expression: .__fromBinaryOperation(.__fromSyntaxNode("nil"), "??", .__fromSyntaxNode("123")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
181181
##"#require(123 ?? nil)"##:

Tests/TestingTests/IssueTests.swift

+56-1
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,60 @@ final class IssueTests: XCTestCase {
317317
await fulfillment(of: [expectationChecked], timeout: 0.0)
318318
}
319319

320+
func testPropertyAccessExpressionExpansion() async {
321+
let expectationFailed = expectation(description: "Expectation failed")
322+
323+
var configuration = Configuration()
324+
configuration.eventHandler = { event, _ in
325+
guard case let .issueRecorded(issue) = event.kind,
326+
case let .expectationFailed(expectation) = issue.kind
327+
else {
328+
return
329+
}
330+
331+
let desc = expectation.evaluatedExpression.expandedDescription()
332+
XCTAssertEqual(desc, "!([].isEmpty → true)")
333+
expectationFailed.fulfill()
334+
}
335+
336+
await Test {
337+
#expect(![].isEmpty)
338+
}.run(configuration: configuration)
339+
await fulfillment(of: [expectationFailed], timeout: 0.0)
340+
}
341+
342+
func testChainedOptionalPropertyAccessExpressionExpansion() async {
343+
let expectationFailed = expectation(description: "Expectation failed")
344+
345+
var configuration = Configuration()
346+
configuration.eventHandler = { event, _ in
347+
guard case let .issueRecorded(issue) = event.kind,
348+
case let .expectationFailed(expectation) = issue.kind
349+
else {
350+
return
351+
}
352+
353+
let desc = expectation.evaluatedExpression.expandedDescription()
354+
XCTAssertEqual(desc, "(outer.middle.inner → Inner(value: nil)).value → nil")
355+
expectationFailed.fulfill()
356+
}
357+
358+
await Test {
359+
struct Outer {
360+
struct Middle {
361+
struct Inner {
362+
var value: Int? = nil
363+
}
364+
var inner = Inner()
365+
}
366+
var middle = Middle()
367+
}
368+
let outer = Outer()
369+
_ = try #require(outer.middle.inner.value)
370+
}.run(configuration: configuration)
371+
await fulfillment(of: [expectationFailed], timeout: 0.0)
372+
}
373+
320374
func testExpressionLiterals() async {
321375
func expectIssue(containing content: String, in testFunction: @escaping @Sendable () async throws -> Void) async {
322376
let issueRecorded = expectation(description: "Issue recorded")
@@ -1174,7 +1228,8 @@ final class IssueTests: XCTestCase {
11741228
return
11751229
}
11761230
let expression = expectation.evaluatedExpression
1177-
XCTAssertTrue(expression.expandedDescription().contains("<not evaluated>"))
1231+
let expandedDescription = expression.expandedDescription()
1232+
XCTAssertTrue(expandedDescription.contains("<not evaluated>"), "expandedDescription: \(expandedDescription)")
11781233
}
11791234

11801235
@Sendable func rhs() -> Bool {

0 commit comments

Comments
 (0)