Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX Update to latest, including Swift Testing support #6

Merged
merged 11 commits into from
Dec 10, 2024
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ jobs:
strategy:
matrix:
xcode:
- "14.3.1"
- 15.4
- '16.0'

name: macOS 13 (Xcode ${{ matrix.xcode }})
runs-on: macos-13
name: macOS
runs-on: macos-14
steps:
- uses: actions/checkout@v3
- name: Select Xcode ${{ matrix.xcode }}
Expand Down
58 changes: 53 additions & 5 deletions Sources/SnapshotTesting/AssertSnapshot.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import XCTest

#if canImport(Testing)
import Testing
#endif

/// Enhances failure messages with a command line diff tool expression that can be copied and pasted
/// into a terminal.
@available(
Expand All @@ -9,12 +13,33 @@ import XCTest
"Use 'withSnapshotTesting' to customize the diff tool. See the documentation for more information."
)
public var diffTool: SnapshotTestingConfiguration.DiffTool {
get { _diffTool }
get {
_diffTool
}
set { _diffTool = newValue }
}

@_spi(Internals)
public var _diffTool: SnapshotTestingConfiguration.DiffTool = .default
public var _diffTool: SnapshotTestingConfiguration.DiffTool {
get {
#if canImport(Testing)
if let test = Test.current {
for trait in test.traits.reversed() {
if let diffTool = (trait as? _SnapshotsTestTrait)?.configuration.diffTool {
return diffTool
}
}
}
#endif
return __diffTool
}
set {
__diffTool = newValue
}
}

@_spi(Internals)
public var __diffTool: SnapshotTestingConfiguration.DiffTool = .default

/// Whether or not to record all new references.
@available(
Expand All @@ -28,7 +53,26 @@ public var isRecording: Bool {
}

@_spi(Internals)
public var _record: SnapshotTestingConfiguration.Record = {
public var _record: SnapshotTestingConfiguration.Record {
get {
#if canImport(Testing)
if let test = Test.current {
for trait in test.traits.reversed() {
if let record = (trait as? _SnapshotsTestTrait)?.configuration.record {
return record
}
}
}
#endif
return __record
}
set {
__record = newValue
}
}

@_spi(Internals)
public var __record: SnapshotTestingConfiguration.Record = {
if let value = ProcessInfo.processInfo.environment["SNAPSHOT_TESTING_RECORD"],
let record = SnapshotTestingConfiguration.Record(rawValue: value)
{
Expand Down Expand Up @@ -311,7 +355,9 @@ public func verifySnapshot<Value, Format>(
func recordSnapshot() throws {
try snapshotting.diffing.toData(diffable).write(to: snapshotFileUrl)
#if !os(Linux) && !os(Windows)
if ProcessInfo.processInfo.environment.keys.contains("__XCODE_BUILT_PRODUCTS_DIR_PATHS") {
if !isSwiftTesting,
ProcessInfo.processInfo.environment.keys.contains("__XCODE_BUILT_PRODUCTS_DIR_PATHS")
{
XCTContext.runActivity(named: "Attached Recorded Snapshot") { activity in
let attachment = XCTAttachment(contentsOfFile: snapshotFileUrl)
activity.add(attachment)
Expand Down Expand Up @@ -373,7 +419,9 @@ public func verifySnapshot<Value, Format>(

if !attachments.isEmpty {
#if !os(Linux) && !os(Windows)
if ProcessInfo.processInfo.environment.keys.contains("__XCODE_BUILT_PRODUCTS_DIR_PATHS") {
if ProcessInfo.processInfo.environment.keys.contains("__XCODE_BUILT_PRODUCTS_DIR_PATHS"),
!isSwiftTesting
{
XCTContext.runActivity(named: "Attached Failure Diff") { activity in
attachments.forEach {
activity.add($0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,14 @@ class FeatureTests: XCTestCase {

This will override the `diffTool` and `record` properties for each test function.

Swift's new testing framework does not currently have a public API for this kind of customization.
There is an experimental feature, called `CustomExecutionTrait`, that does gives us this ability,
and the library provides such a trait called ``Testing/Trait/snapshots(diffTool:record:)``. It can
be attached to any `@Test` or `@Suite` to configure snapshot testing:
Swift's new testing framework also allows for this kind of configuration, both for a single test
and an entire test suite. This is done via what are known as "test traits":

```swift
@_spi(Experimental) import SnapshotTesting
import SnapshotTesting

@Suite(.snapshots(record: .all, diffTool: .ksdiff))
struct FeatureTests {
}
```

> Important: You must import SnapshotTesting with the `@_spi(Experimental)` attribute to get access
to this functionality because Swift Testing's own `CustomExecutionTrait` is hidden behind the same
SPI flag. This means this API is subject to change in the future, but hopefully Apple will
publicize this tool soon.
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,12 @@ in an XCTest context or a Swift Testing context, and will determine if it should
`Issue.record` to trigger a test failure.

For the most part you can write tests for Swift Testing exactly as you would for XCTest. However,
there is one major difference. Swift Testing does not (yet) have a substitute for `invokeTest`,
which we used alongside `withSnapshotTesting` to customize snapshotting for a full test class.

There is an experimental version of this tool in Swift Testing, called `CustomExecutionTrait`, and
this library provides such a trait called ``Testing/Trait/snapshots(diffTool:record:)``. It allows
you to customize snapshots for a `@Test` or `@Suite`, but to get access to it you must perform an
`@_spi(Experimental)` import of snapshot testing:
there is one major difference. In order to override a snapshot's
[configuration](<doc:SnapshotTestingConfiguration>) for a particular test or an entire suite you
must use what are known as "test traits":

```swift
@_spi(Experimental) import SnapshotTesting
import SnapshotTesting

@Suite(.snapshots(record: .all, diffTool: .ksdiff))
struct FeatureTests {
Expand All @@ -169,7 +165,4 @@ struct FeatureTests {
```

That will override the `diffTool` and `record` options for the entire `FeatureTests` suite.

> Important: As evident by the usage of `@_spi(Experimental)` this API is subject to change. As
soon as the Swift Testing library finalizes its API for `CustomExecutionTrait` we will update
the library accordingly and remove the `@_spi` annotation.
These traits can also be used on individual `@Test`s too.
8 changes: 8 additions & 0 deletions Sources/SnapshotTesting/Internal/RecordIssue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ import XCTest
import Testing
#endif

var isSwiftTesting: Bool {
#if canImport(Testing)
return Test.current != nil
#else
return false
#endif
}

@_spi(Internals)
public func recordIssue(
_ message: @autoclosure () -> String,
Expand Down
30 changes: 14 additions & 16 deletions Sources/SnapshotTesting/SnapshotsTestTrait.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#if canImport(Testing)
@_spi(Experimental) import Testing
import Testing

@_spi(Experimental)
extension Trait where Self == _SnapshotsTestTrait {
/// Configure snapshot testing in a suite or test.
///
Expand Down Expand Up @@ -31,22 +30,21 @@
}

/// A type representing the configuration of snapshot testing.
@_spi(Experimental)
public struct _SnapshotsTestTrait: CustomExecutionTrait, SuiteTrait, TestTrait {
public struct _SnapshotsTestTrait: SuiteTrait, TestTrait {
public let isRecursive = true
let configuration: SnapshotTestingConfiguration

public func execute(
_ function: @escaping () async throws -> Void,
for test: Test,
testCase: Test.Case?
) async throws {
try await withSnapshotTesting(
record: configuration.record,
diffTool: configuration.diffTool
) {
try await function()
}
}
// public func execute(
// _ function: @escaping () async throws -> Void,
// for test: Test,
// testCase: Test.Case?
// ) async throws {
// try await withSnapshotTesting(
// record: configuration.record,
// diffTool: configuration.diffTool
// ) {
// try await function()
// }
// }
}
#endif
1 change: 1 addition & 0 deletions Sources/SnapshotTesting/Snapshotting/CGPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@

private let defaultNumberFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.decimalSeparator = "."
numberFormatter.minimumFractionDigits = 1
numberFormatter.maximumFractionDigits = 3
return numberFormatter
Expand Down
1 change: 1 addition & 0 deletions Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@

private let defaultNumberFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.decimalSeparator = "."
numberFormatter.minimumFractionDigits = 1
numberFormatter.maximumFractionDigits = 3
return numberFormatter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import Testing
import Foundation
import InlineSnapshotTesting
@_spi(Experimental) import SnapshotTesting
import SnapshotTesting

@Suite(
.snapshots(
Expand All @@ -21,6 +21,28 @@
}
}

@Test func inlineSnapshotFailure() {
withKnownIssue {
assertInlineSnapshot(of: ["Hello", "World"], as: .dump) {
"""
▿ 2 elements
- "Hello"

"""
}
} matching: { issue in
issue.description == """
Issue recorded: Snapshot did not match. Difference: …

@@ −1,3 +1,4 @@
 ▿ 2 elements
  - "Hello"
+ - "World"

"""
}
}

@Test func inlineSnapshot_NamedTrailingClosure() {
assertInlineSnapshot(
of: ["Hello", "World"], as: .dump,
Expand Down
18 changes: 18 additions & 0 deletions Tests/SnapshotTestingTests/AssertSnapshotSwiftTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#if canImport(Testing)
import Testing
import Foundation
import SnapshotTesting

@Suite(
.snapshots(
record: .missing
)
)
struct AssertSnapshotTests {
@Test func dump() {
struct User { let id: Int, name: String, bio: String }
let user = User(id: 1, name: "Blobby", bio: "Blobbed around the world.")
assertSnapshot(of: user, as: .dump)
}
}
#endif
20 changes: 8 additions & 12 deletions Tests/SnapshotTestingTests/SnapshotsTraitTests.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
#if compiler(>=6) && canImport(Testing)
@_spi(Experimental) import Testing
@_spi(Experimental) @_spi(Internals) import SnapshotTesting
import Testing
@_spi(Internals) import SnapshotTesting

struct SnapshotsTraitTests {
@Test(.snapshots(diffTool: "ksdiff"))
func testDiffTool() {
#expect(
SnapshotTestingConfiguration.current?
.diffTool?(currentFilePath: "old.png", failedFilePath: "new.png")
_diffTool(currentFilePath: "old.png", failedFilePath: "new.png")
== "ksdiff old.png new.png"
)
}
Expand All @@ -17,8 +16,7 @@
@Test(.snapshots(diffTool: "difftool"))
func testDiffToolOverride() {
#expect(
SnapshotTestingConfiguration.current?
.diffTool?(currentFilePath: "old.png", failedFilePath: "new.png")
_diffTool(currentFilePath: "old.png", failedFilePath: "new.png")
== "difftool old.png new.png"
)
}
Expand All @@ -28,23 +26,21 @@
@Test
func config() {
#expect(
SnapshotTestingConfiguration.current?
.diffTool?(currentFilePath: "old.png", failedFilePath: "new.png")
_diffTool(currentFilePath: "old.png", failedFilePath: "new.png")
== "ksdiff old.png new.png"
)
#expect(SnapshotTestingConfiguration.current?.record == .all)
#expect(_record == .all)
}

@Suite(.snapshots(record: .failed, diffTool: "diff"))
struct OverrideDiffToolAndRecord {
@Test
func config() {
#expect(
SnapshotTestingConfiguration.current?
.diffTool?(currentFilePath: "old.png", failedFilePath: "new.png")
_diffTool(currentFilePath: "old.png", failedFilePath: "new.png")
== "diff old.png new.png"
)
#expect(SnapshotTestingConfiguration.current?.record == .failed)
#expect(_record == .failed)
}
}
}
Expand Down
26 changes: 26 additions & 0 deletions Tests/SnapshotTestingTests/SwiftTestingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#if compiler(>=6) && canImport(Testing)
import Testing
import SnapshotTesting

@Suite(.snapshots(diffTool: "ksdiff"))
struct SwiftTestingTests {
@Test func testSnapshot() {
assertSnapshot(of: ["Hello", "World"], as: .dump)
}

@Test func testSnapshotFailure() {
withKnownIssue {
assertSnapshot(of: ["Goodbye", "World"], as: .dump)
} matching: { issue in
issue.description.hasSuffix(
"""
@@ −1,4 +1,4 @@
 ▿ 2 elements
− - "Hello"
+ - "Goodbye"
  - "World"
""")
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
▿ User
- bio: "Blobbed around the world."
- id: 1
- name: "Blobby"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
▿ 2 elements
- "Hello"
- "World"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
▿ 2 elements
- "Hello"
- "World"