backspin 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/CLAUDE.md +13 -8
- data/CONTRIBUTING.md +11 -7
- data/Gemfile.lock +1 -1
- data/README.md +7 -6
- data/docs/backspin-result-api-sketch.md +203 -0
- data/lib/backspin/backspin_result.rb +66 -0
- data/lib/backspin/command_diff.rb +22 -23
- data/lib/backspin/matcher.rb +21 -27
- data/lib/backspin/record.rb +19 -23
- data/lib/backspin/recorder.rb +22 -18
- data/lib/backspin/snapshot.rb +96 -0
- data/lib/backspin/version.rb +1 -1
- data/lib/backspin.rb +30 -27
- metadata +4 -4
- data/lib/backspin/command.rb +0 -117
- data/lib/backspin/command_result.rb +0 -58
- data/lib/backspin/record_result.rb +0 -153
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8a6c6a0ef97c99ace7fb068e7b85bc40c2f45594bc30ce5002920fd62fcd384
|
|
4
|
+
data.tar.gz: f62c15526c0a19a4ae876b8c737b172e2557c54a55f4d5045a0dbc499ccfcd51
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 117a79e8e448bae03c68b44e8a6de23671d746d2ea664094e7dc82792d8ff323af7c6faf73f85a9aa020bb259127660abc8d7687cf861b627049b6e3b88ff41f
|
|
7
|
+
data.tar.gz: dfeabf728ef8448d01889dbe2d62d9d2a8236da296d9f6ee21619f6e3f976218cd0af3da2ac8b7f4ab1ff9cbc19dd4afaef1ca64ac777f3f9215180a1349c1c0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 - 2026-02-11
|
|
4
|
+
* Breaking: `Backspin.run` and `Backspin.capture` now return `Backspin::BackspinResult` with explicit `result.actual` / `result.expected` snapshots.
|
|
5
|
+
* Breaking: result convenience accessors (`result.stdout`, `result.stderr`, `result.status`) were removed in favor of snapshot access.
|
|
6
|
+
* Breaking: record format bumped to 4.0 and now persists a single `snapshot` object (v3 records are rejected).
|
|
7
|
+
* Simplification: removed legacy `Command`, `CommandResult`, and `RecordResult` layers; matcher/diff now operate directly on snapshots.
|
|
8
|
+
* Added focused coverage for the new result contract and capture stream restoration behavior.
|
|
9
|
+
* Updated project docs to reflect the BackspinResult + Snapshot API surface.
|
|
10
|
+
|
|
3
11
|
## 0.8.0 - 2026-02-05
|
|
4
12
|
* Breaking: new `Backspin.run("command", name:, env:)` command API plus block capture via `Backspin.run(name:) { ... }` and `Backspin.capture("name") { ... }`
|
|
5
13
|
* Breaking: remove `run!` and `:playback`
|
data/CLAUDE.md
CHANGED
|
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Project Overview
|
|
6
6
|
|
|
7
|
-
Backspin is a Ruby gem for characterization testing of command-line interfaces. It records and
|
|
7
|
+
Backspin is a Ruby gem for characterization testing of command-line interfaces. It records and verifies CLI interactions by capturing stdout, stderr, and exit status from shell commands, similar to how VCR works for HTTP interactions. Backspin uses YAML "records" to store snapshots.
|
|
8
8
|
|
|
9
9
|
## Development Commands
|
|
10
10
|
|
|
@@ -45,16 +45,21 @@ bin/rake standard # Alternative: Run via Rake task
|
|
|
45
45
|
- Credential scrubbing logic
|
|
46
46
|
- Configuration management (including `raise_on_verification_failure` which defaults to `true`)
|
|
47
47
|
|
|
48
|
-
**
|
|
49
|
-
- Represents a single
|
|
50
|
-
- Stores: args, stdout, stderr, status, recorded_at
|
|
48
|
+
**Snapshot Class** (`lib/backspin/snapshot.rb`)
|
|
49
|
+
- Represents a single captured execution snapshot
|
|
50
|
+
- Stores: command type, args, env, stdout, stderr, status, recorded_at
|
|
51
|
+
|
|
52
|
+
**BackspinResult Class** (`lib/backspin/backspin_result.rb`)
|
|
53
|
+
- Return object from `run` and `capture`
|
|
54
|
+
- Exposes `actual` and `expected` snapshots plus verification metadata
|
|
51
55
|
|
|
52
56
|
**Record Class** (`lib/backspin/record.rb`)
|
|
53
57
|
- Manages YAML record files
|
|
54
|
-
- Handles
|
|
58
|
+
- Handles record/verify sequencing
|
|
55
59
|
|
|
56
|
-
**
|
|
57
|
-
-
|
|
60
|
+
**Recorder Class** (`lib/backspin/recorder.rb`)
|
|
61
|
+
- Implements block capture recording and verification
|
|
62
|
+
- Restores stdout/stderr streams safely after capture
|
|
58
63
|
|
|
59
64
|
### Key Design Patterns
|
|
60
65
|
|
|
@@ -86,4 +91,4 @@ bin/rake standard # Alternative: Run via Rake task
|
|
|
86
91
|
|
|
87
92
|
### Updating Credential Patterns
|
|
88
93
|
- Add patterns to `DEFAULT_CREDENTIAL_PATTERNS` in `lib/backspin.rb`
|
|
89
|
-
- Test with appropriate fixtures in specs
|
|
94
|
+
- Test with appropriate fixtures in specs
|
data/CONTRIBUTING.md
CHANGED
|
@@ -17,7 +17,7 @@ Note that Backspin is in early development and the API _will_ change before stab
|
|
|
17
17
|
|
|
18
18
|
## Getting Started
|
|
19
19
|
|
|
20
|
-
Backspin is a Ruby gem for characterization testing of command-line interfaces. It records and
|
|
20
|
+
Backspin is a Ruby gem for characterization testing of command-line interfaces. It records and verifies CLI interactions by capturing stdout, stderr, and exit status from shell commands, similar to how VCR works for HTTP interactions.
|
|
21
21
|
|
|
22
22
|
### Prerequisites
|
|
23
23
|
|
|
@@ -75,13 +75,17 @@ Backspin is a Ruby gem for characterization testing of command-line interfaces.
|
|
|
75
75
|
- Credential scrubbing logic
|
|
76
76
|
- Configuration management
|
|
77
77
|
|
|
78
|
-
- **
|
|
79
|
-
- Represents a single
|
|
80
|
-
- Stores: args, stdout, stderr, status, recorded_at
|
|
78
|
+
- **Snapshot Class** (`lib/backspin/snapshot.rb`)
|
|
79
|
+
- Represents a single execution snapshot
|
|
80
|
+
- Stores: command type, args, env, stdout, stderr, status, recorded_at
|
|
81
|
+
|
|
82
|
+
- **BackspinResult Class** (`lib/backspin/backspin_result.rb`)
|
|
83
|
+
- Return object from `Backspin.run` / `Backspin.capture`
|
|
84
|
+
- Exposes `actual` and `expected` snapshots plus verify details
|
|
81
85
|
|
|
82
86
|
- **Record Class** (`lib/backspin/record.rb`)
|
|
83
87
|
- Manages YAML record files
|
|
84
|
-
- Handles
|
|
88
|
+
- Handles record/verify sequencing
|
|
85
89
|
|
|
86
90
|
### Common Development Tasks
|
|
87
91
|
|
|
@@ -134,7 +138,7 @@ RSpec.describe "Feature name" do
|
|
|
134
138
|
it "does something specific" do
|
|
135
139
|
result = Backspin.run(["echo", "hello"], name: "my_test_record")
|
|
136
140
|
|
|
137
|
-
expect(result.stdout).to eq("hello\n")
|
|
141
|
+
expect(result.actual.stdout).to eq("hello\n")
|
|
138
142
|
end
|
|
139
143
|
end
|
|
140
144
|
```
|
|
@@ -212,4 +216,4 @@ If you have questions about contributing, feel free to:
|
|
|
212
216
|
- Check existing issues and pull requests
|
|
213
217
|
- Review the test suite for examples
|
|
214
218
|
|
|
215
|
-
Thank you for contributing to Backspin!
|
|
219
|
+
Thank you for contributing to Backspin!
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -134,7 +134,7 @@ For more matcher examples and detailed documentation, see [MATCHERS.md](MATCHERS
|
|
|
134
134
|
|
|
135
135
|
### Working with the Result Object
|
|
136
136
|
|
|
137
|
-
The API returns a `
|
|
137
|
+
The API returns a `Backspin::BackspinResult` object with helpful methods:
|
|
138
138
|
|
|
139
139
|
```ruby
|
|
140
140
|
result = Backspin.run(["sh", "-c", "echo out; echo err >&2; exit 42"], name: "my_test")
|
|
@@ -143,11 +143,12 @@ result = Backspin.run(["sh", "-c", "echo out; echo err >&2; exit 42"], name: "my
|
|
|
143
143
|
result.recorded? # true on first run
|
|
144
144
|
result.verified? # true/false on subsequent runs, nil when recording
|
|
145
145
|
|
|
146
|
-
# Access output
|
|
147
|
-
result.stdout
|
|
148
|
-
result.stderr
|
|
149
|
-
result.status
|
|
150
|
-
result.
|
|
146
|
+
# Access output snapshots
|
|
147
|
+
result.actual.stdout # "out\n"
|
|
148
|
+
result.actual.stderr # "err\n"
|
|
149
|
+
result.actual.status # 42
|
|
150
|
+
result.expected # nil in :record mode, populated in :verify mode
|
|
151
|
+
result.success? # false (non-zero exit)
|
|
151
152
|
result.output # [stdout, stderr, status] for command runs
|
|
152
153
|
|
|
153
154
|
# Debug information
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Backspin Result API Sketch
|
|
2
|
+
|
|
3
|
+
Date: 2026-02-11
|
|
4
|
+
Branch: `spike-backspin-result-api`
|
|
5
|
+
|
|
6
|
+
## Goals
|
|
7
|
+
|
|
8
|
+
- Keep the public API small and predictable.
|
|
9
|
+
- Make runtime output and baseline output explicit.
|
|
10
|
+
- Remove multi-command semantics from the result object.
|
|
11
|
+
- Keep command run and block capture under one consistent return type.
|
|
12
|
+
|
|
13
|
+
## Public API
|
|
14
|
+
|
|
15
|
+
### Entry points
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
Backspin.run(command = nil, name:, env: nil, mode: :auto, matcher: nil, filter: nil, &block)
|
|
19
|
+
Backspin.capture(name, mode: :auto, matcher: nil, filter: nil, &block)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Both return `BackspinResult`.
|
|
23
|
+
|
|
24
|
+
### `BackspinResult`
|
|
25
|
+
|
|
26
|
+
Top-level aggregate with one responsibility: represent this run and its comparison.
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
class BackspinResult
|
|
30
|
+
attr_reader :mode, :record_path, :actual, :expected
|
|
31
|
+
|
|
32
|
+
def recorded?; end
|
|
33
|
+
def verified?; end
|
|
34
|
+
def diff; end
|
|
35
|
+
def error_message; end
|
|
36
|
+
def success?; end
|
|
37
|
+
def failure?; end
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Rules:
|
|
42
|
+
|
|
43
|
+
- `actual` is always present and represents what just ran.
|
|
44
|
+
- `expected` is baseline snapshot when one exists.
|
|
45
|
+
- In `:record` mode, `expected` is `nil`, `verified?` is `nil`.
|
|
46
|
+
- In `:verify` mode, `expected` is present, `verified?` is boolean.
|
|
47
|
+
|
|
48
|
+
### `Snapshot`
|
|
49
|
+
|
|
50
|
+
Value object for one recorded/captured execution.
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class Snapshot
|
|
54
|
+
attr_reader :command_type, :args, :env, :stdout, :stderr, :status, :recorded_at
|
|
55
|
+
|
|
56
|
+
def success?; end
|
|
57
|
+
def failure?; end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Notes:
|
|
62
|
+
|
|
63
|
+
- `command_type` is `Open3::Capture3` for command runs.
|
|
64
|
+
- `command_type` is `Backspin::Capturer` for block capture.
|
|
65
|
+
- Capture status remains placeholder `0`.
|
|
66
|
+
|
|
67
|
+
## Usage Examples
|
|
68
|
+
|
|
69
|
+
### Command verify mismatch
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
result = Backspin.run(["echo", "changed"], name: "echo_case", mode: :verify)
|
|
73
|
+
|
|
74
|
+
result.actual.stdout # "changed\n"
|
|
75
|
+
result.expected.stdout # "original\n"
|
|
76
|
+
result.verified? # false
|
|
77
|
+
result.diff # unified-ish stdout/stderr/status diff
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### First record
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
result = Backspin.run(["echo", "hello"], name: "hello_case")
|
|
84
|
+
|
|
85
|
+
result.mode # :record
|
|
86
|
+
result.actual.stdout # "hello\n"
|
|
87
|
+
result.expected # nil
|
|
88
|
+
result.verified? # nil
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Capture verify
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
result = Backspin.capture("capture_case", mode: :verify) do
|
|
95
|
+
puts "runtime output"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
result.actual.stdout
|
|
99
|
+
result.expected.stdout
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Unix CLI examples
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# 1) Record + verify a simple command
|
|
106
|
+
Backspin.run(["echo", "hello"], name: "echo_hello")
|
|
107
|
+
result = Backspin.run(["echo", "hello"], name: "echo_hello")
|
|
108
|
+
result.verified? # true
|
|
109
|
+
result.actual.stdout # "hello\n"
|
|
110
|
+
result.expected.stdout # "hello\n"
|
|
111
|
+
|
|
112
|
+
# 2) Verify mismatch with a common command
|
|
113
|
+
Backspin.run(["date", "+%Y-%m-%d"], name: "today", mode: :record)
|
|
114
|
+
result = Backspin.run(["date", "+%Y-%m-%d"], name: "today", mode: :verify)
|
|
115
|
+
result.verified? # true/false depending on day change
|
|
116
|
+
|
|
117
|
+
# 3) Capture a small shell pipeline output
|
|
118
|
+
result = Backspin.capture("grep_wc") do
|
|
119
|
+
system("printf 'alpha\\nbeta\\nalpha\\n' | grep alpha | wc -l")
|
|
120
|
+
end
|
|
121
|
+
result.actual.stdout
|
|
122
|
+
|
|
123
|
+
# 4) Verify a directory listing snapshot
|
|
124
|
+
Backspin.run(["ls", "-1"], name: "project_listing", mode: :record)
|
|
125
|
+
result = Backspin.run(["ls", "-1"], name: "project_listing", mode: :verify)
|
|
126
|
+
result.actual.stdout
|
|
127
|
+
result.expected.stdout
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Matcher and Filter Semantics
|
|
131
|
+
|
|
132
|
+
- `matcher:` applies only during verify and compares `expected` vs `actual`.
|
|
133
|
+
- `filter:` applies only when writing snapshots to disk.
|
|
134
|
+
- Default match still compares stdout/stderr/status only.
|
|
135
|
+
|
|
136
|
+
## Error Semantics
|
|
137
|
+
|
|
138
|
+
- `Backspin::VerificationError` still raised by default when verification fails.
|
|
139
|
+
- Error message is generated from `BackspinResult#error_message`.
|
|
140
|
+
- Do not duplicate `diff` content in exception formatting.
|
|
141
|
+
|
|
142
|
+
## Record Format Sketch (v4)
|
|
143
|
+
|
|
144
|
+
Single-snapshot format to match single-snapshot runtime model:
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
---
|
|
148
|
+
format_version: "4.0"
|
|
149
|
+
recorded_at: "2026-02-11T00:00:00Z"
|
|
150
|
+
snapshot:
|
|
151
|
+
command_type: "Open3::Capture3"
|
|
152
|
+
args: ["echo", "hello"]
|
|
153
|
+
env:
|
|
154
|
+
MY_VAR: value
|
|
155
|
+
stdout: "hello\n"
|
|
156
|
+
stderr: ""
|
|
157
|
+
status: 0
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
For capture snapshots:
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
snapshot:
|
|
164
|
+
command_type: "Backspin::Capturer"
|
|
165
|
+
args: ["<captured block>"]
|
|
166
|
+
stdout: "..."
|
|
167
|
+
stderr: "..."
|
|
168
|
+
status: 0
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Implemented Simplifications
|
|
172
|
+
|
|
173
|
+
- Unified all run/capture return values under `BackspinResult`.
|
|
174
|
+
- Introduced `Snapshot` as the shared value object for `actual` and `expected`.
|
|
175
|
+
- Removed multi-command result semantics from the public return API.
|
|
176
|
+
- Kept `CommandDiff`, now operating directly on snapshots.
|
|
177
|
+
- Simplified persistence to one snapshot per record file.
|
|
178
|
+
|
|
179
|
+
## Current Status
|
|
180
|
+
|
|
181
|
+
Status date: 2026-02-11
|
|
182
|
+
|
|
183
|
+
1. `Snapshot` and `BackspinResult` classes are implemented and wired into runtime paths.
|
|
184
|
+
2. `Backspin.run` and `Backspin.capture` now return `BackspinResult`.
|
|
185
|
+
3. `Record` persistence moved to v4 single-snapshot format (`snapshot` key, no `commands` array).
|
|
186
|
+
4. `Matcher` and `CommandDiff` now operate on expected/actual snapshots.
|
|
187
|
+
5. Legacy result/command layering was removed from `lib/`.
|
|
188
|
+
6. Specs have been migrated to the new result contract and v4 format.
|
|
189
|
+
7. Validation is green: `66 examples, 0 failures` and Standard lint passes.
|
|
190
|
+
8. Public docs now use `result.actual` / `result.expected` terminology.
|
|
191
|
+
|
|
192
|
+
## Success Criteria
|
|
193
|
+
|
|
194
|
+
1. `Backspin.run` and `Backspin.capture` always return `BackspinResult` with `actual` populated.
|
|
195
|
+
2. In `:record` mode, `result.expected` is `nil` and `result.verified?` is `nil`.
|
|
196
|
+
3. In `:verify` mode, `result.expected` is present, `result.verified?` is boolean, and mismatch cases populate `result.diff` plus `result.error_message`.
|
|
197
|
+
4. No multi-command result API remains in the public result contract.
|
|
198
|
+
5. Snapshot object exposes a stable single-command shape: `stdout`, `stderr`, `status`, `args`, `env`, `command_type`.
|
|
199
|
+
6. Record format uses one snapshot (v4), not a commands array.
|
|
200
|
+
7. Existing strict verification behavior remains: default raises `Backspin::VerificationError`, while `raise_on_verification_failure = false` returns a failed result without raising.
|
|
201
|
+
8. End-to-end Unix command examples are covered in specs: `echo` record/verify, `ls -1` record/verify, `date` mismatch behavior (or matcher override), and captured `grep | wc` pipeline output via `Backspin.capture`.
|
|
202
|
+
9. Matcher behavior is preserved: default matching remains stdout/stderr/status, and custom `matcher:` contract (Proc, hash fields, `:all`) continues to work for both run and capture verification.
|
|
203
|
+
10. Credential scrubbing behavior is preserved: stdout/stderr/args/env are scrubbed on persistence, capture output is scrubbed, custom patterns still apply, and verification diffs/error messages do not re-expose scrubbed secrets.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Backspin
|
|
4
|
+
# Aggregate result returned by Backspin.run/capture.
|
|
5
|
+
class BackspinResult
|
|
6
|
+
attr_reader :mode, :record_path, :actual, :expected, :output
|
|
7
|
+
|
|
8
|
+
def initialize(mode:, record_path:, actual:, expected: nil, verified: nil, command_diff: nil, output: nil)
|
|
9
|
+
@mode = mode
|
|
10
|
+
@record_path = record_path
|
|
11
|
+
@actual = actual
|
|
12
|
+
@expected = expected
|
|
13
|
+
@verified = verified
|
|
14
|
+
@command_diff = command_diff
|
|
15
|
+
@output = output
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def recorded?
|
|
19
|
+
mode == :record
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# true/false for verify mode, nil for record mode
|
|
23
|
+
def verified?
|
|
24
|
+
return nil if mode == :record
|
|
25
|
+
|
|
26
|
+
@verified
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def diff
|
|
30
|
+
return nil unless verified? == false
|
|
31
|
+
|
|
32
|
+
@command_diff&.diff
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def error_message
|
|
36
|
+
return nil unless verified? == false
|
|
37
|
+
return "Output verification failed" unless @command_diff
|
|
38
|
+
|
|
39
|
+
msg = "Output verification failed:\n\n"
|
|
40
|
+
msg += @command_diff.summary
|
|
41
|
+
msg += "\n#{@command_diff.diff}" if @command_diff.diff
|
|
42
|
+
msg
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def success?
|
|
46
|
+
actual&.success? || false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def failure?
|
|
50
|
+
!success?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_h
|
|
54
|
+
hash = {
|
|
55
|
+
mode: mode,
|
|
56
|
+
record_path: record_path,
|
|
57
|
+
actual: actual&.to_h
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
hash[:expected] = expected&.to_h
|
|
61
|
+
hash[:verified] = verified? unless verified?.nil?
|
|
62
|
+
hash[:diff] = diff if diff
|
|
63
|
+
hash
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Backspin
|
|
4
|
-
# Represents the difference between
|
|
5
|
-
# Handles verification and diff generation for a single command
|
|
4
|
+
# Represents the difference between expected and actual snapshots.
|
|
6
5
|
class CommandDiff
|
|
7
|
-
attr_reader :
|
|
6
|
+
attr_reader :expected, :actual, :matcher
|
|
8
7
|
|
|
9
|
-
def initialize(
|
|
10
|
-
@
|
|
11
|
-
@
|
|
8
|
+
def initialize(expected:, actual:, matcher: nil)
|
|
9
|
+
@expected = expected
|
|
10
|
+
@actual = actual
|
|
12
11
|
@matcher = Matcher.new(
|
|
13
12
|
config: matcher,
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
expected: expected,
|
|
14
|
+
actual: actual
|
|
16
15
|
)
|
|
17
16
|
end
|
|
18
17
|
|
|
19
|
-
# @return [Boolean] true if the
|
|
18
|
+
# @return [Boolean] true if the snapshot output matches.
|
|
20
19
|
def verified?
|
|
21
|
-
return false unless
|
|
20
|
+
return false unless command_types_match?
|
|
22
21
|
|
|
23
22
|
@matcher.match?
|
|
24
23
|
end
|
|
@@ -28,23 +27,23 @@ module Backspin
|
|
|
28
27
|
return nil if verified?
|
|
29
28
|
|
|
30
29
|
parts = []
|
|
31
|
-
|
|
32
|
-
actual_hash =
|
|
30
|
+
expected_hash = expected.to_h
|
|
31
|
+
actual_hash = actual.to_h
|
|
33
32
|
|
|
34
|
-
unless
|
|
35
|
-
parts << "Command type mismatch: expected #{
|
|
33
|
+
unless command_types_match?
|
|
34
|
+
parts << "Command type mismatch: expected #{expected.command_type.name}, got #{actual.command_type.name}"
|
|
36
35
|
end
|
|
37
36
|
|
|
38
|
-
if
|
|
39
|
-
parts << stdout_diff(
|
|
37
|
+
if expected_hash["stdout"] != actual_hash["stdout"]
|
|
38
|
+
parts << stdout_diff(expected_hash["stdout"], actual_hash["stdout"])
|
|
40
39
|
end
|
|
41
40
|
|
|
42
|
-
if
|
|
43
|
-
parts << stderr_diff(
|
|
41
|
+
if expected_hash["stderr"] != actual_hash["stderr"]
|
|
42
|
+
parts << stderr_diff(expected_hash["stderr"], actual_hash["stderr"])
|
|
44
43
|
end
|
|
45
44
|
|
|
46
|
-
if
|
|
47
|
-
parts << "Exit status: expected #{
|
|
45
|
+
if expected_hash["status"] != actual_hash["status"]
|
|
46
|
+
parts << "Exit status: expected #{expected_hash["status"]}, got #{actual_hash["status"]}"
|
|
48
47
|
end
|
|
49
48
|
|
|
50
49
|
parts.join("\n\n")
|
|
@@ -61,12 +60,12 @@ module Backspin
|
|
|
61
60
|
|
|
62
61
|
private
|
|
63
62
|
|
|
64
|
-
def
|
|
65
|
-
|
|
63
|
+
def command_types_match?
|
|
64
|
+
expected.command_type == actual.command_type
|
|
66
65
|
end
|
|
67
66
|
|
|
68
67
|
def failure_reason
|
|
69
|
-
unless
|
|
68
|
+
unless command_types_match?
|
|
70
69
|
return "command type mismatch"
|
|
71
70
|
end
|
|
72
71
|
|
data/lib/backspin/matcher.rb
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Backspin
|
|
4
|
-
# Handles matching logic between
|
|
4
|
+
# Handles matching logic between expected and actual snapshots.
|
|
5
5
|
class Matcher
|
|
6
|
-
attr_reader :config, :
|
|
6
|
+
attr_reader :config, :expected, :actual
|
|
7
7
|
|
|
8
|
-
def initialize(config:,
|
|
8
|
+
def initialize(config:, expected:, actual:)
|
|
9
9
|
@config = normalize_config(config)
|
|
10
|
-
@
|
|
11
|
-
@
|
|
10
|
+
@expected = expected
|
|
11
|
+
@actual = actual
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
# @return [Boolean] true if
|
|
14
|
+
# @return [Boolean] true if snapshots match according to configured matcher
|
|
15
15
|
def match?
|
|
16
16
|
if config.nil?
|
|
17
|
-
|
|
18
|
-
default_matcher.call(recorded_command.to_h, actual_command.to_h)
|
|
17
|
+
default_matcher.call(expected.to_h, actual.to_h)
|
|
19
18
|
elsif config.is_a?(Proc)
|
|
20
|
-
config.call(
|
|
19
|
+
config.call(expected.to_h, actual.to_h)
|
|
21
20
|
elsif config.is_a?(Hash)
|
|
22
21
|
verify_with_hash_matcher
|
|
23
22
|
else
|
|
@@ -30,24 +29,22 @@ module Backspin
|
|
|
30
29
|
reasons = []
|
|
31
30
|
|
|
32
31
|
if config.nil?
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
actual_hash = actual_command.to_h
|
|
32
|
+
expected_hash = expected.to_h
|
|
33
|
+
actual_hash = actual.to_h
|
|
36
34
|
|
|
37
|
-
reasons << "stdout differs" if
|
|
38
|
-
reasons << "stderr differs" if
|
|
39
|
-
reasons << "exit status differs" if
|
|
35
|
+
reasons << "stdout differs" if expected_hash["stdout"] != actual_hash["stdout"]
|
|
36
|
+
reasons << "stderr differs" if expected_hash["stderr"] != actual_hash["stderr"]
|
|
37
|
+
reasons << "exit status differs" if expected_hash["status"] != actual_hash["status"]
|
|
40
38
|
elsif config.is_a?(Hash)
|
|
41
|
-
|
|
42
|
-
actual_hash =
|
|
39
|
+
expected_hash = expected.to_h
|
|
40
|
+
actual_hash = actual.to_h
|
|
43
41
|
|
|
44
|
-
# Only check matchers that were provided
|
|
45
42
|
config.each do |field, matcher_proc|
|
|
46
43
|
case field
|
|
47
44
|
when :all
|
|
48
|
-
reasons << ":all matcher failed" unless matcher_proc.call(
|
|
45
|
+
reasons << ":all matcher failed" unless matcher_proc.call(expected_hash, actual_hash)
|
|
49
46
|
when :stdout, :stderr, :status
|
|
50
|
-
unless matcher_proc.call(
|
|
47
|
+
unless matcher_proc.call(expected_hash[field.to_s], actual_hash[field.to_s])
|
|
51
48
|
reasons << "#{field} custom matcher failed"
|
|
52
49
|
end
|
|
53
50
|
end
|
|
@@ -79,19 +76,16 @@ module Backspin
|
|
|
79
76
|
end
|
|
80
77
|
|
|
81
78
|
def verify_with_hash_matcher
|
|
82
|
-
|
|
83
|
-
actual_hash =
|
|
79
|
+
expected_hash = expected.to_h
|
|
80
|
+
actual_hash = actual.to_h
|
|
84
81
|
|
|
85
|
-
# Override-based: only run matchers that are explicitly provided
|
|
86
|
-
# Use map to ensure all matchers run, then check if all passed
|
|
87
82
|
results = config.map do |field, matcher_proc|
|
|
88
83
|
case field
|
|
89
84
|
when :all
|
|
90
|
-
matcher_proc.call(
|
|
85
|
+
matcher_proc.call(expected_hash, actual_hash)
|
|
91
86
|
when :stdout, :stderr, :status
|
|
92
|
-
matcher_proc.call(
|
|
87
|
+
matcher_proc.call(expected_hash[field.to_s], actual_hash[field.to_s])
|
|
93
88
|
else
|
|
94
|
-
# This should never happen due to normalize_config validation
|
|
95
89
|
raise ArgumentError, "Unknown field: #{field}"
|
|
96
90
|
end
|
|
97
91
|
end
|