Skip to content

Commit

Permalink
Beta support for Swift Testing, and other improvements. (#867)
Browse files Browse the repository at this point in the history
* wip

* wip

* wip

* wip

* wip

* tests

* wip

* migration guide

* wip

* wip

* wip

* Update Sources/SnapshotTesting/AssertSnapshot.swift

* wip

* formatting

* wip

* format

* more

* wip

* fix

* Make record mode opaque.

* more docs

* wip

* wip

* Added new 'failed' record strategy, and wrote some tests.

* remove test artificats

* wip

* more docs

* fix linux tests

* more test fixes

* test clean up

* debugging

* debug

* wip

* fix

* fix tests

* wip

* wip

* wip

* make snapshot configuration optional

* make snapshot configuration optional

* clean up

* fix

* indent

* typo

* more clean up

* record before difftool

* more tests

* clean up test code

---------

Co-authored-by: Stephen Celis <[email protected]>
  • Loading branch information
mbrandonw and stephencelis authored Jul 8, 2024
1 parent 4742060 commit dc6d151
Show file tree
Hide file tree
Showing 39 changed files with 1,593 additions and 372 deletions.
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,28 @@ Repeat test runs will load this reference and compare it with the runtime value.
match, the test will fail and describe the difference. Failures can be inspected from Xcode's Report
Navigator or by inspecting the file URLs of the failure.

You can record a new reference by setting the `record` parameter to `true` on the assertion or
setting `isRecording` globally.
You can record a new reference by customizing snapshots inline with the assertion, or using the
`withSnapshotTesting` tool:

``` swift
assertSnapshot(of: vc, as: .image, record: true)

// or globally
```swift
// Record just this one snapshot
assertSnapshot(of: vc, as: .image, record: .all)

// Record all snapshots in a scope:
withSnapshotTesting(record: .all) {
assertSnapshot(of: vc1, as: .image)
assertSnapshot(of: vc2, as: .image)
assertSnapshot(of: vc3, as: .image)
}

isRecording = true
assertSnapshot(of: vc, as: .image)
// Record all snapshots in an XCTestCase subclass:
class FeatureTests: XCTestCase {
override func invokeTest() {
withSnapshotTesting(record: .all) {
super.invokeTest()
}
}
}
```

## Snapshot Anything
Expand Down
173 changes: 98 additions & 75 deletions Sources/InlineSnapshotTesting/AssertInlineSnapshot.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

#if canImport(SwiftSyntax509)
import SnapshotTesting
@_spi(Internals) import SnapshotTesting
import SwiftParser
import SwiftSyntax
import SwiftSyntaxBuilder
Expand Down Expand Up @@ -35,7 +35,7 @@ import Foundation
of value: @autoclosure () throws -> Value?,
as snapshotting: Snapshotting<Value, String>,
message: @autoclosure () -> String = "",
record isRecording: Bool = isRecording,
record isRecording: Bool? = nil,
timeout: TimeInterval = 5,
syntaxDescriptor: InlineSnapshotSyntaxDescriptor = InlineSnapshotSyntaxDescriptor(),
matches expected: (() -> String)? = nil,
Expand All @@ -44,95 +44,116 @@ import Foundation
line: UInt = #line,
column: UInt = #column
) {
let _: Void = installTestObserver
do {
var actual: String?
let expectation = XCTestExpectation()
if let value = try value() {
snapshotting.snapshot(value).run {
actual = $0
expectation.fulfill()
let record =
(isRecording == true ? .all : isRecording == false ? .missing : nil)
?? SnapshotTestingConfiguration.current?.record
?? _record
withSnapshotTesting(record: record) {
let _: Void = installTestObserver
do {
var actual: String?
let expectation = XCTestExpectation()
if let value = try value() {
snapshotting.snapshot(value).run {
actual = $0
expectation.fulfill()
}
switch XCTWaiter.wait(for: [expectation], timeout: timeout) {
case .completed:
break
case .timedOut:
recordIssue(
"""
Exceeded timeout of \(timeout) seconds waiting for snapshot.
This can happen when an asynchronously loaded value (like a network response) has not \
loaded. If a timeout is unavoidable, consider setting the "timeout" parameter of
"assertInlineSnapshot" to a higher value.
""",
file: file,
line: line
)
return
case .incorrectOrder, .interrupted, .invertedFulfillment:
recordIssue("Couldn't snapshot value", file: file, line: line)
return
@unknown default:
recordIssue("Couldn't snapshot value", file: file, line: line)
return
}
}
switch XCTWaiter.wait(for: [expectation], timeout: timeout) {
case .completed:
break
case .timedOut:
XCTFail(
let expected = expected?()
guard
record != .all,
record != .missing || expected != nil
else {
// NB: Write snapshot state before calling `XCTFail` in case `continueAfterFailure = false`
inlineSnapshotState[File(path: file), default: []].append(
InlineSnapshot(
expected: expected,
actual: actual,
wasRecording: record == .all,
syntaxDescriptor: syntaxDescriptor,
function: "\(function)",
line: line,
column: column
)
)

var failure: String
if syntaxDescriptor.trailingClosureLabel
== InlineSnapshotSyntaxDescriptor.defaultTrailingClosureLabel
{
failure = "Automatically recorded a new snapshot."
} else {
failure = """
Automatically recorded a new snapshot for "\(syntaxDescriptor.trailingClosureLabel)".
"""
}
if let difference = snapshotting.diffing.diff(expected ?? "", actual ?? "")?.0 {
failure += " Difference: …\n\n\(difference.indenting(by: 2))"
}
recordIssue(
"""
Exceeded timeout of \(timeout) seconds waiting for snapshot.
\(failure)
This can happen when an asynchronously loaded value (like a network response) has not \
loaded. If a timeout is unavoidable, consider setting the "timeout" parameter of
"assertInlineSnapshot" to a higher value.
Re-run "\(function)" to assert against the newly-recorded snapshot.
""",
file: file,
line: line
)
return
case .incorrectOrder, .interrupted, .invertedFulfillment:
XCTFail("Couldn't snapshot value", file: file, line: line)
return
@unknown default:
XCTFail("Couldn't snapshot value", file: file, line: line)
return
}
}
let expected = expected?()
guard !isRecording, let expected
else {
// NB: Write snapshot state before calling `XCTFail` in case `continueAfterFailure = false`
inlineSnapshotState[File(path: file), default: []].append(
InlineSnapshot(
expected: expected,
actual: actual,
wasRecording: isRecording,
syntaxDescriptor: syntaxDescriptor,
function: "\(function)",
line: line,
column: column
)
)

var failure: String
if syntaxDescriptor.trailingClosureLabel
== InlineSnapshotSyntaxDescriptor.defaultTrailingClosureLabel
{
failure = "Automatically recorded a new snapshot."
} else {
failure = """
Automatically recorded a new snapshot for "\(syntaxDescriptor.trailingClosureLabel)".
guard let expected
else {
recordIssue(
"""
No expected value to assert against.
""",
file: file,
line: line
)
return
}
if let difference = snapshotting.diffing.diff(expected ?? "", actual ?? "")?.0 {
failure += " Difference: …\n\n\(difference.indenting(by: 2))"
}
XCTFail(
guard
let difference = snapshotting.diffing.diff(expected, actual ?? "")?.0
else { return }

let message = message()
syntaxDescriptor.fail(
"""
\(failure)
\(message.isEmpty ? "Snapshot did not match. Difference: …" : message)
Re-run "\(function)" to assert against the newly-recorded snapshot.
\(difference.indenting(by: 2))
""",
file: file,
line: line
line: line,
column: column
)
return
} catch {
recordIssue("Threw error: \(error)", file: file, line: line)
}
guard let difference = snapshotting.diffing.diff(expected, actual ?? "")?.0
else { return }

let message = message()
syntaxDescriptor.fail(
"""
\(message.isEmpty ? "Snapshot did not match. Difference: …" : message)
\(difference.indenting(by: 2))
""",
file: file,
line: line,
column: column
)
} catch {
XCTFail("Threw error: \(error)", file: file, line: line)
}
}
#else
Expand Down Expand Up @@ -197,6 +218,8 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
/// Initializes an inline snapshot syntax descriptor.
///
/// - Parameters:
/// - deprecatedTrailingClosureLabels: An array of deprecated labels to consider for the inline
/// snapshot.
/// - trailingClosureLabel: The label of the trailing closure that returns the inline snapshot.
/// - trailingClosureOffset: The offset of the trailing closure that returns the inline
/// snapshot, relative to the first trailing closure.
Expand Down Expand Up @@ -242,7 +265,7 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
visitor.walk(testSource.sourceFile)
trailingClosureLine = visitor.trailingClosureLine
}
XCTFail(
recordIssue(
message(),
file: file,
line: trailingClosureLine.map(UInt.init) ?? line
Expand Down Expand Up @@ -386,7 +409,7 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
) {
self.file = file
self.line = snapshots.first?.line
self.wasRecording = snapshots.first?.wasRecording ?? isRecording
self.wasRecording = snapshots.first?.wasRecording ?? false
self.indent = String(
sourceLocationConverter.sourceLines
.first { $0.first?.isWhitespace == true && $0.contains { !$0.isWhitespace } }?
Expand Down
Loading

0 comments on commit dc6d151

Please sign in to comment.