Skip to content

Commit c0e4f59

Browse files
authored
Make value capture of optionals more robust. (#277)
When an expression is optional and is passed through `#require()`, and that requirement fails, we capture the expression's subexpressions and record them as an expectation-failed event that is eventually propagated to `stderr`. Yay. The underlying value-capturing machinery accepts optional values as input so that they can be lazily evaluated: if a value is `nil` at this layer, that means that it wasn't evaluated and we present `"<not evaluated>"` instead of a description of it or `"nil"`. In the case of `#require()` unwrapping optionals, these values need to be boxed in a second optional (i.e. `T??` instead of `T?`, or `Optional<Optional<T>>` instead of `Optional<T>`) so that if the value is `nil`, it isn't confused with "no value". See the following table: | Expression | Value of `x as String??` | Presented As | |-|-|-| | `try #require(x)` | `.some(.some("Hello World"))` | `"Hello World"` | | `try #require(x)` | `.some(.none)` | `nil` | | `try #require("Goodbye Venus" ?? x)` | `.none` | `"<not evaluated>"` | (This maybe suggests we should use a different enum than `Optional` here, but that's a debate for another PR I think…) This PR fixes a couple of spots where we weren't casting our values, known to be optionals, to double-optionals, resulting in incorrect output that incorrectly included `"<not evaluated>"`. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 3a77507 commit c0e4f59

File tree

2 files changed

+24
-2
lines changed

2 files changed

+24
-2
lines changed

Sources/Testing/Expectations/ExpectationChecking+Macro.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ public func __checkPropertyAccess<T, U>(
557557
return __checkValue(
558558
optionalValue,
559559
expression: expression,
560-
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, optionalValue),
560+
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, optionalValue as U??),
561561
comments: comments(),
562562
isRequired: isRequired,
563563
sourceLocation: sourceLocation
@@ -731,7 +731,7 @@ public func __checkBinaryOperation<T>(
731731
return __checkValue(
732732
optionalValue,
733733
expression: expression,
734-
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, rhs),
734+
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs as T??, rhs as T??),
735735
comments: comments(),
736736
isRequired: isRequired,
737737
sourceLocation: sourceLocation

Tests/TestingTests/IssueTests.swift

+22
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,28 @@ final class IssueTests: XCTestCase {
11611161
await fulfillment(of: [rhsCalled], timeout: 0.0)
11621162
}
11631163

1164+
func testRequireOptionalMemberAccessEvaluatesToNil() async {
1165+
var configuration = Configuration()
1166+
configuration.eventHandler = { event, _ in
1167+
guard case let .issueRecorded(issue) = event.kind else {
1168+
return
1169+
}
1170+
XCTAssertFalse(issue.isKnown)
1171+
guard case let .expectationFailed(expectation) = issue.kind else {
1172+
XCTFail("Unexpected issue kind \(issue.kind)")
1173+
return
1174+
}
1175+
let expression = expectation.evaluatedExpression
1176+
XCTAssertTrue(expression.expandedDescription().contains("nil"))
1177+
XCTAssertFalse(expression.expandedDescription().contains("<not evaluated>"))
1178+
}
1179+
1180+
await Test {
1181+
let array = [String]()
1182+
_ = try #require(array.first)
1183+
}.run(configuration: configuration)
1184+
}
1185+
11641186
func testOptionalOperand() async {
11651187
let expectationFailed = expectation(description: "Expectation failed")
11661188

0 commit comments

Comments
 (0)