backspin 0.7.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 803ecdad9628acd396e0d70b1e9e4081c612562f6872af8fa05108a820c61590
4
- data.tar.gz: 563d3b7e8c413742453971859cf4bb31aeff11dbeee38d5804922ffa150615bb
3
+ metadata.gz: c8a6c6a0ef97c99ace7fb068e7b85bc40c2f45594bc30ce5002920fd62fcd384
4
+ data.tar.gz: f62c15526c0a19a4ae876b8c737b172e2557c54a55f4d5045a0dbc499ccfcd51
5
5
  SHA512:
6
- metadata.gz: 17aff3cf0ab930e13da63fa427135cde5e41816341d75ca7712662d4feefb4df1d753db983668a57562c9922d2dda4ae5f54d8cc579ce720d0ac9009015740ff
7
- data.tar.gz: 203e8c9f1158712ae20e64864a20645cf5fad7e3bd79627bf6dfc7d7efa40cd51d5cbd2e5ac037ebe9a9f8c0408c33921f948400435aa023db32da9df9dfa8fc
6
+ metadata.gz: 117a79e8e448bae03c68b44e8a6de23671d746d2ea664094e7dc82792d8ff323af7c6faf73f85a9aa020bb259127660abc8d7687cf861b627049b6e3b88ff41f
7
+ data.tar.gz: dfeabf728ef8448d01889dbe2d62d9d2a8236da296d9f6ee21619f6e3f976218cd0af3da2ac8b7f4ab1ff9cbc19dd4afaef1ca64ac777f3f9215180a1349c1c0
data/.circleci/config.yml CHANGED
@@ -6,26 +6,38 @@ orbs:
6
6
  ruby: circleci/ruby@2.5.3
7
7
 
8
8
  jobs:
9
- build:
9
+ test:
10
10
  resource_class: medium
11
11
  parameters:
12
12
  ruby-version:
13
13
  type: string
14
14
  docker:
15
15
  - image: cimg/ruby:<< parameters.ruby-version >>
16
-
17
16
  steps:
18
17
  - checkout
19
18
  - ruby/install-deps:
20
19
  key: gems-v1-ruby<< parameters.ruby-version >>
21
20
  - run:
22
- name: Run specs and lint
23
- command: bundle exec rake
21
+ name: Run specs
22
+ command: bundle exec rake spec
23
+
24
+ lint:
25
+ resource_class: medium
26
+ docker:
27
+ - image: cimg/ruby:4.0
28
+ steps:
29
+ - checkout
30
+ - ruby/install-deps:
31
+ key: gems-v1-ruby4.0
32
+ - run:
33
+ name: Run Standard Ruby linter
34
+ command: bundle exec rake standard
24
35
 
25
36
  workflows:
26
37
  build:
27
38
  jobs:
28
- - build:
39
+ - test:
29
40
  matrix:
30
41
  parameters:
31
- ruby-version: ["3.2", "3.3", "3.4"]
42
+ ruby-version: ["3.2", "3.3", "3.4", "4.0"]
43
+ - lint
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
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
+
11
+ ## 0.8.0 - 2026-02-05
12
+ * Breaking: new `Backspin.run("command", name:, env:)` command API plus block capture via `Backspin.run(name:) { ... }` and `Backspin.capture("name") { ... }`
13
+ * Breaking: remove `run!` and `:playback`
14
+ * Breaking: drop RSpec dependency; verification failures raise `Backspin::VerificationError`
15
+ * Breaking: record format bumped to 3.0 and only `Open3::Capture3` / `Backspin::Capturer` records are accepted
16
+ * Scrub credential patterns apply to stdout, stderr, args, and env values
17
+
3
18
  ## 0.7.1 - 2025-12-02
4
19
  * Include result object on VerificationError to make it easier for callers to debug verification errors
5
20
 
@@ -36,4 +51,4 @@ Simpler, unified API: `Backspin.run` and `Backspin run!` methods that automatica
36
51
  - `use_cassette` method for VCR-style record/replay
37
52
  - Support for multiple verification modes (strict, playback, custom matcher)
38
53
  - Multi-command recording support
39
- - RSpec integration using RSpec's mocking framework
54
+ - RSpec integration using RSpec's mocking framework
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 replays CLI interactions by capturing stdout, stderr, and exit status from shell commands - similar to how VCR works for HTTP interactions. Backspin uses "records" (YAML files) to store recorded command outputs.
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
 
@@ -41,29 +41,33 @@ bin/rake standard # Alternative: Run via Rake task
41
41
  ### Core Components
42
42
 
43
43
  **Backspin Module** (`lib/backspin.rb`)
44
- - Main API: `run`, `run!` (both raise on verification failure by default)
45
- - Capture API: `capture` (raises `VerificationError` on verification failure by default)
46
- - Legacy API: `call`, `verify`, `verify!`, `use_record`
44
+ - Main API: `run` (direct command execution and block capture), `capture` (alias for block form)
47
45
  - Credential scrubbing logic
48
- - Configuration management (including `raise_on_verification_failure` which defaults to `true` and affects both `run` and `capture`)
46
+ - Configuration management (including `raise_on_verification_failure` which defaults to `true`)
49
47
 
50
- **Command Class** (`lib/backspin.rb`)
51
- - Represents a single CLI execution
52
- - 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
53
55
 
54
56
  **Record Class** (`lib/backspin/record.rb`)
55
57
  - Manages YAML record files
56
- - Handles recording/playback sequencing
58
+ - Handles record/verify sequencing
57
59
 
58
- **RSpecMetadata** (`lib/backspin/rspec_metadata.rb`)
59
- - Auto-generates record names from RSpec context
60
+ **Recorder Class** (`lib/backspin/recorder.rb`)
61
+ - Implements block capture recording and verification
62
+ - Restores stdout/stderr streams safely after capture
60
63
 
61
64
  ### Key Design Patterns
62
65
 
63
- - Uses RSpec mocking to intercept `Open3.capture3` calls
64
- - Records are stored as YAML arrays to support multiple commands
65
- - Automatic credential scrubbing for security (AWS keys, API tokens, passwords)
66
- - VCR-style recording modes: `:once`, `:all`, `:none`, `:new_episodes`
66
+ - Direct `Open3.capture3` execution for command runs
67
+ - Tempfile-based FD capture for block forms
68
+ - Single-command records stored as YAML
69
+ - Automatic credential scrubbing for security (AWS keys, API tokens, passwords, env values)
70
+ - Recording modes: `:auto`, `:record`, `:verify`
67
71
 
68
72
  ### Testing Approach
69
73
 
@@ -87,4 +91,4 @@ bin/rake standard # Alternative: Run via Rake task
87
91
 
88
92
  ### Updating Credential Patterns
89
93
  - Add patterns to `DEFAULT_CREDENTIAL_PATTERNS` in `lib/backspin.rb`
90
- - Test with appropriate fixtures in specs
94
+ - Test with appropriate fixtures in specs
data/CONTRIBUTING.md CHANGED
@@ -17,11 +17,11 @@ 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 replays CLI interactions by capturing stdout, stderr, and exit status from shell commands - similar to how VCR works for HTTP interactions.
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
 
24
- - Ruby 3.2, 3.3, or 3.4
24
+ - Ruby 3.2, 3.3, 3.4, or 4.0
25
25
  - Bundler
26
26
  - Git
27
27
 
@@ -71,17 +71,21 @@ Backspin is a Ruby gem for characterization testing of command-line interfaces.
71
71
  **Core Components:**
72
72
 
73
73
  - **Backspin Module** (`lib/backspin.rb`)
74
- - Main API: `call`, `verify`, `verify!`, `use_record`
74
+ - Main API: `run` (direct command execution and block capture), `capture` (alias for block form)
75
75
  - Credential scrubbing logic
76
76
  - Configuration management
77
77
 
78
- - **Command Class** (`lib/backspin/command.rb`)
79
- - Represents a single CLI execution
80
- - Stores: args, stdout, stderr, status, recorded_at, etc
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 recording/playback sequencing
88
+ - Handles record/verify sequencing
85
89
 
86
90
  ### Common Development Tasks
87
91
 
@@ -132,16 +136,9 @@ Example test structure:
132
136
  ```ruby
133
137
  RSpec.describe "Feature name" do
134
138
  it "does something specific" do
135
- # Setup
136
- record_name = "my_test_record"
137
-
138
- # Exercise
139
- result = Backspin.call(record_name) do
140
- Open3.capture3("echo", "hello")
141
- end
142
-
143
- # Verify
144
- expect(result.stdout).to eq("hello\n")
139
+ result = Backspin.run(["echo", "hello"], name: "my_test_record")
140
+
141
+ expect(result.actual.stdout).to eq("hello\n")
145
142
  end
146
143
  end
147
144
  ```
@@ -162,7 +159,7 @@ end
162
159
  - Keep changes focused and atomic
163
160
  - Include tests for new functionality
164
161
  - Update examples in README.md if changing public APIs
165
- - Ensure CI passes (tests against Ruby 3.2, 3.3, and 3.4)
162
+ - Ensure CI passes (tests against Ruby 3.2, 3.3, 3.4, and 4.0)
166
163
 
167
164
  ## Code Style
168
165
 
@@ -219,4 +216,4 @@ If you have questions about contributing, feel free to:
219
216
  - Check existing issues and pull requests
220
217
  - Review the test suite for examples
221
218
 
222
- Thank you for contributing to Backspin!
219
+ Thank you for contributing to Backspin!
data/Gemfile.lock CHANGED
@@ -1,9 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- backspin (0.7.1)
5
- ostruct
6
- rspec-mocks (~> 3)
4
+ backspin (0.9.0)
7
5
 
8
6
  GEM
9
7
  remote: https://rubygems.org/
@@ -13,7 +11,6 @@ GEM
13
11
  json (2.16.0)
14
12
  language_server-protocol (3.17.0.5)
15
13
  lint_roller (1.1.0)
16
- ostruct (0.6.1)
17
14
  parallel (1.27.0)
18
15
  parser (3.3.10.0)
19
16
  ast (~> 2.4.1)
@@ -70,7 +67,7 @@ GEM
70
67
  timecop (0.9.10)
71
68
  unicode-display_width (3.2.0)
72
69
  unicode-emoji (~> 4.1)
73
- unicode-emoji (4.1.0)
70
+ unicode-emoji (4.2.0)
74
71
 
75
72
  PLATFORMS
76
73
  arm64-darwin-24
@@ -84,4 +81,4 @@ DEPENDENCIES
84
81
  timecop (~> 0.9)
85
82
 
86
83
  BUNDLED WITH
87
- 4.0.0.beta2
84
+ 4.0.5
data/MATCHERS.md CHANGED
@@ -13,14 +13,11 @@ Backspin supports custom matchers for flexible verification of command outputs.
13
13
  A proc matcher receives full command hashes and can check any combination of fields:
14
14
 
15
15
  ```ruby
16
- # Check that output starts with expected prefix
17
- result = Backspin.run("version_test",
16
+ result = Backspin.run(["node", "--version"], name: "version_test",
18
17
  matcher: ->(recorded, actual) {
19
- recorded["stdout"].start_with?("v") &&
18
+ recorded["stdout"].start_with?("v") &&
20
19
  actual["stdout"].start_with?("v")
21
- }) do
22
- Open3.capture3("node --version")
23
- end
20
+ })
24
21
  ```
25
22
 
26
23
  ### 2. Field-Specific Hash Matchers
@@ -29,33 +26,13 @@ Use a hash to specify matchers for individual fields. Only specified fields are
29
26
 
30
27
  ```ruby
31
28
  # Only check stdout - stderr and status are ignored
32
- result = Backspin.run("timestamp_test",
29
+ result = Backspin.run(["date"], name: "timestamp_test",
33
30
  matcher: {
34
31
  stdout: ->(recorded, actual) {
35
- # Both should contain a timestamp
36
32
  recorded.match?(/\d{4}-\d{2}-\d{2}/) &&
37
33
  actual.match?(/\d{4}-\d{2}-\d{2}/)
38
34
  }
39
- }) do
40
- Open3.capture3("date")
41
- end
42
-
43
- # Check multiple specific fields
44
- result = Backspin.run("api_test",
45
- matcher: {
46
- stdout: ->(recorded, actual) {
47
- # Check JSON structure exists
48
- recorded.include?("\"data\":") &&
49
- actual.include?("\"data\":")
50
- },
51
- status: ->(recorded, actual) {
52
- # Both should succeed
53
- recorded == 0 && actual == 0
54
- }
55
- # Note: stderr is NOT checked
56
- }) do
57
- Open3.capture3("curl", "-s", "https://api.example.com")
58
- end
35
+ })
59
36
  ```
60
37
 
61
38
  ### 3. The :all Matcher
@@ -63,17 +40,13 @@ end
63
40
  The `:all` matcher receives complete command hashes with all fields:
64
41
 
65
42
  ```ruby
66
- # Cross-field validation
67
- result = Backspin.run("build_test",
43
+ result = Backspin.run(["./build.sh"], name: "build_test",
68
44
  matcher: {
69
45
  all: ->(recorded, actual) {
70
- # Check overall success: stdout has "BUILD SUCCESSFUL" AND status is 0
71
46
  actual["stdout"].include?("BUILD SUCCESSFUL") &&
72
47
  actual["status"] == 0
73
48
  }
74
- }) do
75
- Open3.capture3("./build.sh")
76
- end
49
+ })
77
50
  ```
78
51
 
79
52
  ### 4. Combining :all with Field Matchers
@@ -81,23 +54,12 @@ end
81
54
  When both `:all` and field matchers are present, all must pass:
82
55
 
83
56
  ```ruby
84
- result = Backspin.run("complex_test",
57
+ result = Backspin.run(["./process_data.sh"], name: "complex_test",
85
58
  matcher: {
86
- all: ->(recorded, actual) {
87
- # Overall check: command succeeded
88
- actual["status"] == 0
89
- },
90
- stdout: ->(recorded, actual) {
91
- # Specific check: output contains result
92
- actual.include?("Result:")
93
- },
94
- stderr: ->(recorded, actual) {
95
- # No errors expected
96
- actual.empty?
97
- }
98
- }) do
99
- Open3.capture3("./process_data.sh")
100
- end
59
+ all: ->(recorded, actual) { actual["status"] == 0 },
60
+ stdout: ->(recorded, actual) { actual.include?("Result:") },
61
+ stderr: ->(recorded, actual) { actual.empty? }
62
+ })
101
63
  ```
102
64
 
103
65
  ## Matcher Proc Arguments
@@ -108,10 +70,11 @@ Field-specific matchers receive field values:
108
70
 
109
71
  The `:all` matcher receives full hashes with these keys:
110
72
  - `"stdout"` - String output
111
- - `"stderr"` - String error output
112
- - `"status"` - Integer exit code
113
- - `"command_type"` - String like "Open3::Capture3"
114
- - `"args"` - Array of command arguments
73
+ - `"stderr"` - String error output
74
+ - `"status"` - Integer exit code (placeholder `0` for block capture)
75
+ - `"command_type"` - String like "Open3::Capture3" or "Backspin::Capturer"
76
+ - `"args"` - String or Array of command arguments
77
+ - `"env"` - Optional Hash of env vars (command runs only)
115
78
  - `"recorded_at"` - Timestamp string
116
79
 
117
80
  ## Examples
@@ -119,116 +82,45 @@ The `:all` matcher receives full hashes with these keys:
119
82
  ### Matching Version Numbers
120
83
 
121
84
  ```ruby
122
- # Match major version only
123
- matcher: {
85
+ matcher = {
124
86
  stdout: ->(recorded, actual) {
125
- recorded_major = recorded[/(\d+)\./, 1]
126
- actual_major = actual[/(\d+)\./, 1]
87
+ recorded_major = recorded[/\d+/, 0]
88
+ actual_major = actual[/\d+/, 0]
127
89
  recorded_major == actual_major
128
90
  }
129
91
  }
92
+
93
+ Backspin.run(["ruby", "--version"], name: "ruby_version", matcher: matcher)
130
94
  ```
131
95
 
132
96
  ### Ignoring Timestamps
133
97
 
134
98
  ```ruby
135
- # Strip timestamps before comparing
136
- matcher: {
99
+ matcher = {
137
100
  stdout: ->(recorded, actual) {
138
101
  pattern = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/
139
- recorded.gsub(pattern, '[TIME]') == actual.gsub(pattern, '[TIME]')
102
+ recorded.gsub(pattern, "[TIME]") == actual.gsub(pattern, "[TIME]")
140
103
  }
141
104
  }
142
- ```
143
105
 
144
- ### JSON Response Validation
145
-
146
- ```ruby
147
- # Validate JSON structure, ignore values
148
- matcher: {
149
- stdout: ->(recorded, actual) {
150
- begin
151
- recorded_data = JSON.parse(recorded)
152
- actual_data = JSON.parse(actual)
153
-
154
- # Same keys at top level
155
- recorded_data.keys.sort == actual_data.keys.sort
156
- rescue JSON::ParserError
157
- false
158
- end
159
- }
160
- }
106
+ Backspin.run(["date"], name: "timestamp_test", matcher: matcher)
161
107
  ```
162
108
 
163
109
  ### Logging/Debugging with :all
164
110
 
165
111
  ```ruby
166
- # Use :all for side effects while other matchers do validation
167
112
  logged_commands = []
168
113
 
169
- result = Backspin.run("debug_test",
114
+ Backspin.run(["./test.sh"], name: "debug_test",
170
115
  matcher: {
171
116
  all: ->(recorded, actual) {
172
- # Log for debugging
173
117
  logged_commands << {
174
118
  args: actual["args"],
175
119
  exit: actual["status"],
176
120
  output_size: actual["stdout"].size
177
121
  }
178
- true # Always pass
122
+ true
179
123
  },
180
- stdout: ->(recorded, actual) {
181
- # Actual validation
182
- actual.include?("SUCCESS")
183
- }
184
- }) do
185
- Open3.capture3("./test.sh")
186
- end
187
-
188
- # Can inspect logged_commands after run
189
- ```
190
-
191
- ## Using with run!
192
-
193
- The `run!` method automatically fails tests when matchers return false:
194
-
195
- ```ruby
196
- # This will raise an error if the matcher fails
197
- Backspin.run!("critical_test",
198
- matcher: {
199
- stdout: ->(r, a) { a.include?("OK") },
200
- status: ->(r, a) { a == 0 }
201
- }) do
202
- Open3.capture3("./health_check.sh")
203
- end
204
- ```
205
-
206
- ## Migration from match_on
207
-
208
- The `match_on` option is deprecated. To migrate:
209
-
210
- ```ruby
211
- # Old match_on style:
212
- Backspin.run("test",
213
- match_on: [:stdout, ->(r, a) { ... }])
214
-
215
- # New matcher style:
216
- Backspin.run("test",
217
- matcher: { stdout: ->(r, a) { ... } })
218
-
219
- # Old match_on with multiple fields:
220
- Backspin.run("test",
221
- match_on: [
222
- [:stdout, ->(r, a) { ... }],
223
- [:stderr, ->(r, a) { ... }]
224
- ])
225
-
226
- # New matcher style:
227
- Backspin.run("test",
228
- matcher: {
229
- stdout: ->(r, a) { ... },
230
- stderr: ->(r, a) { ... }
124
+ stdout: ->(recorded, actual) { actual.include?("SUCCESS") }
231
125
  })
232
126
  ```
233
-
234
- Key difference: `match_on` required other fields to match exactly, while the new `matcher` hash only checks specified fields.