Skip to content

Commit 4960964

Browse files
committed
Property accesses in failed expectations sometimes include their value twice in expanded description
1 parent f2c8ee1 commit 4960964

File tree

6 files changed

+78
-23
lines changed

6 files changed

+78
-23
lines changed

Sources/Testing/Expectations/ExpectationChecking+Macro.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ public func __checkPropertyAccess<T>(
547547
return __checkValue(
548548
condition,
549549
expression: expression,
550-
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, condition),
550+
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs),
551551
comments: comments(),
552552
isRequired: isRequired,
553553
sourceLocation: sourceLocation
@@ -577,7 +577,7 @@ public func __checkPropertyAccess<T, U>(
577577
return __checkValue(
578578
optionalValue,
579579
expression: expression,
580-
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, optionalValue as U??),
580+
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs),
581581
comments: comments(),
582582
isRequired: isRequired,
583583
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

+9-9
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,13 @@ 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
##"#expect(a?.b().isB)"##:
92-
##"Testing.__checkPropertyAccess(a?.b().self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b()"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
92+
##"Testing.__checkPropertyAccess(a?.b().self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b()"), "isB"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
9393
##"#expect(isolation: somewhere) {}"##:
9494
##"Testing.__checkClosureCall(performing: {}, expression: .__fromSyntaxNode("{}"), comments: [], isRequired: false, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
9595
]
@@ -163,13 +163,13 @@ struct ConditionMacroTests {
163163
##"#require(a, sourceLocation: someValue)"##:
164164
##"Testing.__checkValue(a, expression: .__fromSyntaxNode("a"), comments: [], isRequired: true, sourceLocation: someValue).__required()"##,
165165
##"#require(a.isB)"##:
166-
##"Testing.__checkPropertyAccess(a.self, getting: { $0.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
166+
##"Testing.__checkPropertyAccess(a.self, getting: { $0.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), "isB"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
167167
##"#require(a???.isB)"##:
168-
##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
168+
##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), "isB"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
169169
##"#require(a?.b.isB)"##:
170-
##"Testing.__checkPropertyAccess(a?.b.self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
170+
##"Testing.__checkPropertyAccess(a?.b.self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b"), "isB"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
171171
##"#require(a?.b().isB)"##:
172-
##"Testing.__checkPropertyAccess(a?.b().self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b()"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
172+
##"Testing.__checkPropertyAccess(a?.b().self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b()"), "isB"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
173173
##"#require(isolation: somewhere) {}"##:
174174
##"Testing.__checkClosureCall(performing: {}, expression: .__fromSyntaxNode("{}"), comments: [], isRequired: true, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
175175
]
@@ -183,7 +183,7 @@ struct ConditionMacroTests {
183183
@Test("Unwrapping #require() macro",
184184
arguments: [
185185
##"#require(Optional<Int>.none)"##:
186-
##"Testing.__checkPropertyAccess(Optional<Int>.self, getting: { $0.none }, expression: .__fromPropertyAccess(.__fromSyntaxNode("Optional<Int>"), .__fromSyntaxNode("none")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
186+
##"Testing.__checkPropertyAccess(Optional<Int>.self, getting: { $0.none }, expression: .__fromPropertyAccess(.__fromSyntaxNode("Optional<Int>"), "none"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
187187
##"#require(nil ?? 123)"##:
188188
##"Testing.__checkBinaryOperation(nil, { $0 ?? $1() }, 123, expression: .__fromBinaryOperation(.__fromSyntaxNode("nil"), "??", .__fromSyntaxNode("123")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
189189
##"#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")
@@ -1206,7 +1260,8 @@ final class IssueTests: XCTestCase {
12061260
return
12071261
}
12081262
let expression = expectation.evaluatedExpression
1209-
XCTAssertTrue(expression.expandedDescription().contains("<not evaluated>"))
1263+
let expandedDescription = expression.expandedDescription()
1264+
XCTAssertTrue(expandedDescription.contains("<not evaluated>"), "expandedDescription: \(expandedDescription)")
12101265
}
12111266

12121267
@Sendable func rhs() -> Bool {

0 commit comments

Comments
 (0)