backspin 0.7.0 → 0.8.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.
data/README.md CHANGED
@@ -5,19 +5,19 @@
5
5
  [![CircleCI](https://img.shields.io/circleci/build/github/rsanheim/backspin/main)](https://circleci.com/gh/rsanheim/backspin)
6
6
  [![Last Commit](https://img.shields.io/github/last-commit/rsanheim/backspin/main)](https://github.com/rsanheim/backspin/commits/main)
7
7
 
8
- Backspin records and replays CLI interactions in Ruby for easy snapshot testing of command-line interfaces. Currently supports `Open3.capture3` and `system` and requires `rspec`, as it uses `rspec-mocks` under the hood.
8
+ Backspin records command output and block output in Ruby for easy snapshot testing of command-line interfaces. It supports direct command runs via `Open3.capture3` and block capture for more complex scenarios.
9
9
 
10
10
  **NOTE:** Backspin should be considered alpha while pre version 1.0. It is in heavy development along-side some real-world CLI apps, so expect things to change and mature.
11
11
 
12
- Inspired by [VCR](https://github.com/vcr/vcr) and other [golden master](https://en.wikipedia.org/wiki/Golden_master_(software_development)) testing libraries.
12
+ Inspired by [VCR](https://github.com/vcr/vcr) and other [characterization (aka golden master)](https://en.wikipedia.org/wiki/Characterization_test) testing libraries.
13
13
 
14
14
  ## Overview
15
15
 
16
- Backspin is a Ruby library for snapshot testing (or characterization testing) of command-line interfaces. While VCR records and replays HTTP interactions, Backspin records and replays CLI interactions - capturing stdout, stderr, and exit status from shell commands.
16
+ Backspin is a Ruby library for snapshot testing (or characterization testing) of command-line interfaces. While VCR records and replays HTTP interactions, Backspin records stdout, stderr, and exit status from shell commands, or captures all output from a block.
17
17
 
18
18
  ## Installation
19
19
 
20
- Requires Ruby 3+ and will use rspec-mocks under the hood...Backspin has not been tested in other test frameworks.
20
+ Requires Ruby 3+.
21
21
 
22
22
  Add this line to your application's Gemfile in the `:test` group:
23
23
 
@@ -31,7 +31,7 @@ And then run `bundle install`.
31
31
 
32
32
  ## Usage
33
33
 
34
- ### Quick Start
34
+ ### Quick Start (Command Runs)
35
35
 
36
36
  The simplest way to use Backspin is with the `run` method, which automatically records on the first execution and verifies on subsequent runs.
37
37
 
@@ -39,23 +39,41 @@ The simplest way to use Backspin is with the `run` method, which automatically r
39
39
  require "backspin"
40
40
 
41
41
  # First run: records the output
42
- result = Backspin.run("my_command") do
43
- Open3.capture3("echo hello world")
44
- end
42
+ result = Backspin.run(["echo", "hello world"], name: "my_command")
45
43
 
46
44
  # Subsequent runs: verifies the output matches and raises on mismatch
47
- Backspin.run("my_command") do
48
- Open3.capture3("echo hello world") # Passes - output matches
49
- end
45
+ Backspin.run(["echo", "hello world"], name: "my_command")
50
46
 
51
47
  # This will raise an error automatically
52
- Backspin.run("my_command") do
53
- Open3.capture3("echo hello mars")
48
+ Backspin.run(["echo", "hello mars"], name: "my_command")
49
+ # Raises Backspin::VerificationError because output doesn't match
50
+ ```
51
+
52
+ You can also pass a string command (which invokes a shell):
53
+
54
+ ```ruby
55
+ Backspin.run("echo hello", name: "string_command")
56
+ ```
57
+
58
+ ### Block Capture
59
+
60
+ Use block capture when you need to run multiple commands or use APIs that already write to stdout/stderr:
61
+
62
+ ```ruby
63
+ # Capture all output from the block
64
+ result = Backspin.run(name: "block_capture") do
65
+ system("echo from system")
66
+ puts "from puts"
67
+ `echo from backticks`
68
+ end
69
+
70
+ # Alias form
71
+ Backspin.capture("block_capture") do
72
+ puts "from capture"
54
73
  end
55
- # Raises RSpec::Expectations::ExpectationNotMetError because output doesn't match
56
74
  ```
57
75
 
58
- By default, `Backspin.run` will raise an exception when verification fails, making your tests fail automatically. This is the recommended approach for most scenarios.
76
+ Block capture records a single combined stdout/stderr snapshot. Exit status is a placeholder (`0`) in this mode.
59
77
 
60
78
  ### Recording Modes
61
79
 
@@ -63,48 +81,29 @@ Backspin supports different modes for controlling how commands are recorded and
63
81
 
64
82
  ```ruby
65
83
  # Auto mode (default): Record on first run, verify on subsequent runs
66
- result = Backspin.run("my_command") do
67
- Open3.capture3("echo hello")
68
- end
84
+ Backspin.run(["echo", "hello"], name: "my_command")
69
85
 
70
86
  # Explicit record mode: Always record, overwriting existing recordings
71
- result = Backspin.run("echo_test", mode: :record) do
72
- Open3.capture3("echo hello")
73
- end
74
- # This will save the output to `fixtures/backspin/echo_test.yml`.
87
+ Backspin.run(["echo", "hello"], name: "echo_test", mode: :record)
75
88
 
76
89
  # Explicit verify mode: Always verify against existing recording
77
- result = Backspin.run("echo_test", mode: :verify) do
78
- Open3.capture3("echo hello")
79
- end
90
+ result = Backspin.run(["echo", "hello"], name: "echo_test", mode: :verify)
80
91
  expect(result.verified?).to be true
81
-
82
- # Playback mode: Return recorded output without running the command
83
- result = Backspin.run("slow_command", mode: :playback) do
84
- Open3.capture3("slow_command") # Not executed - returns recorded output
85
- end
86
92
  ```
87
93
 
88
- ### The run! method
89
-
90
- **NOTE:** This method is deprecated and will be removed soon.
91
-
92
- The `run!` method is maintained for backwards compatibility and works identically to `run`. Since `run` now raises on verification failure by default, both methods behave the same way:
94
+ ### Environment Variables
93
95
 
94
96
  ```ruby
95
- # Both of these are equivalent and will raise on verification failure
96
- Backspin.run("echo_test") do
97
- Open3.capture3("echo hello")
98
- end
99
-
100
- Backspin.run!("echo_test") do
101
- Open3.capture3("echo hello")
102
- end
97
+ Backspin.run(
98
+ ["ruby", "-e", "print ENV.fetch('MY_ENV_VAR')"],
99
+ name: "with_env",
100
+ env: {"MY_ENV_VAR" => "value"}
101
+ )
103
102
  ```
104
103
 
105
- For new code, we recommend using `run` as it's the primary API method.
104
+ If `env:` is not provided, it is not passed to `Open3.capture3` and is not recorded.
106
105
 
107
- ### Custom matchers
106
+ ### Custom Matchers
108
107
 
109
108
  For cases where full matching isn't suitable, you can override via `matcher:`. **NOTE**: If you provide
110
109
  custom matchers, that is the only matching that will be done. Default matching is skipped if user-provided
@@ -113,14 +112,11 @@ matchers are present.
113
112
  You can override the full match logic with a proc:
114
113
 
115
114
  ```ruby
116
- # Match stdout and status, ignore stderr
117
115
  my_matcher = ->(recorded, actual) {
118
- recorded["stdout"] == actual["stdout"] && recorded["status"] != actual["status"]
116
+ recorded["stdout"] == actual["stdout"] && recorded["status"] == actual["status"]
119
117
  }
120
118
 
121
- result = Backspin.run("my_test", matcher: { all: my_matcher }) do
122
- Open3.capture3("echo hello")
123
- end
119
+ result = Backspin.run(["echo", "hello"], name: "my_test", matcher: {all: my_matcher})
124
120
  ```
125
121
 
126
122
  Or you can override specific fields:
@@ -131,18 +127,7 @@ timestamp_matcher = ->(recorded, actual) {
131
127
  recorded.match?(/\d{4}-\d{2}-\d{2}/) && actual.match?(/\d{4}-\d{2}-\d{2}/)
132
128
  }
133
129
 
134
- result = Backspin.run("timestamp_test", matcher: { stdout: timestamp_matcher }) do
135
- Open3.capture3("date")
136
- end
137
-
138
- # Match version numbers in stderr
139
- version_matcher = ->(recorded, actual) {
140
- recorded[/v(\d+)\./, 1] == actual[/v(\d+)\./, 1]
141
- }
142
-
143
- result = Backspin.run("version_check", matcher: { stderr: version_matcher }) do
144
- Open3.capture3("node --version")
145
- end
130
+ result = Backspin.run(["date"], name: "timestamp_test", matcher: {stdout: timestamp_matcher})
146
131
  ```
147
132
 
148
133
  For more matcher examples and detailed documentation, see [MATCHERS.md](MATCHERS.md).
@@ -152,21 +137,18 @@ For more matcher examples and detailed documentation, see [MATCHERS.md](MATCHERS
152
137
  The API returns a `RecordResult` object with helpful methods:
153
138
 
154
139
  ```ruby
155
- result = Backspin.run("my_test") do
156
- Open3.capture3("echo out; echo err >&2; exit 42")
157
- end
140
+ result = Backspin.run(["sh", "-c", "echo out; echo err >&2; exit 42"], name: "my_test")
158
141
 
159
142
  # Check the mode
160
143
  result.recorded? # true on first run
161
144
  result.verified? # true/false on subsequent runs, nil when recording
162
- result.playback? # true in playback mode
163
145
 
164
- # Access output (first command for single commands)
146
+ # Access output (first command)
165
147
  result.stdout # "out\n"
166
- result.stderr # "err\n"
148
+ result.stderr # "err\n"
167
149
  result.status # 42
168
150
  result.success? # false (non-zero exit)
169
- result.output # The raw return value from the block
151
+ result.output # [stdout, stderr, status] for command runs
170
152
 
171
153
  # Debug information
172
154
  result.record_path # Path to the YAML file
@@ -174,47 +156,12 @@ result.error_message # Human-readable error if verification failed
174
156
  result.diff # Diff between expected and actual output
175
157
  ```
176
158
 
177
- ### Multiple Commands
178
-
179
- Backspin automatically records and verifies all commands executed in a block:
180
-
181
- ```ruby
182
- result = Backspin.run("multi_command_test") do
183
- # All of these commands will be recorded
184
- version, = Open3.capture3("ruby --version")
185
- files, = Open3.capture3("ls -la")
186
- system("echo 'Processing...'") # Note: system doesn't capture output
187
- data, stderr, = Open3.capture3("curl https://api.example.com/data")
188
-
189
- # Return whatever you need
190
- { version: version.strip, file_count: files.lines.count, data: data }
191
- end
192
-
193
- # Access individual command results
194
- result.commands.size # 4
195
- result.multiple_commands? # true
196
-
197
- # For multiple commands, use these accessors
198
- result.all_stdout # Array of stdout from each command
199
- result.all_stderr # Array of stderr from each command
200
- result.all_status # Array of exit statuses
201
-
202
- # Or access specific commands
203
- result.commands[0].stdout # Ruby version output
204
- result.commands[1].stdout # ls output
205
- result.commands[2].status # system call exit status (stdout is empty)
206
- result.commands[3].stderr # curl errors if any
207
- ```
208
-
209
- When verifying multiple commands, Backspin ensures all commands match in the exact order they were recorded. If any command differs, you'll get a detailed error showing which commands failed.
210
-
211
159
  ### Configuration
212
160
 
213
161
  You can configure Backspin's behavior globally:
214
162
 
215
163
  ```ruby
216
164
  Backspin.configure do |config|
217
- # Both run and capture methods will raise on verification failure by default
218
165
  config.raise_on_verification_failure = false # default is true
219
166
  config.backspin_dir = "spec/fixtures/cli_records" # default is "fixtures/backspin"
220
167
  config.scrub_credentials = false # default is true
@@ -222,42 +169,31 @@ end
222
169
  ```
223
170
 
224
171
  The `raise_on_verification_failure` setting affects both `Backspin.run` and `Backspin.capture`:
225
- - When `true` (default): Both methods raise exceptions on verification failure
226
- - `run` raises `RSpec::Expectations::ExpectationNotMetError`
227
- - `capture` raises `Backspin::VerificationError` (framework-agnostic)
172
+ - When `true` (default): Both methods raise `Backspin::VerificationError` on verification failure
228
173
  - When `false`: Both methods return a result with `verified?` set to false
229
174
 
230
175
  If you need to disable the raising behavior for a specific test, you can temporarily configure it:
231
176
 
232
177
  ```ruby
233
- # Temporarily disable raising for this block
234
178
  Backspin.configure do |config|
235
179
  config.raise_on_verification_failure = false
236
180
  end
237
181
 
238
- result = Backspin.run("my_test") do
239
- Open3.capture3("echo different")
240
- end
182
+ result = Backspin.run(["echo", "different"], name: "my_test")
241
183
  # result.verified? will be false but won't raise
242
184
 
243
- # Reset configuration
244
185
  Backspin.reset_configuration!
245
186
  ```
246
187
 
247
188
  ### Credential Scrubbing
248
189
 
249
- If the CLI interaction you are recording contains sensitive data in stdout or stderr, you should be careful to make sure it is not recorded to yaml!
190
+ If the CLI interaction you are recording contains sensitive data in stdout/stderr, you should be careful to make sure it is not recorded to YAML.
250
191
 
251
- By default, Backspin automatically tries to scrub [common credential patterns](https://github.com/rsanheim/backspin/blob/f8661f084aad0ae759cd971c4af31ccf9bdc6bba/lib/backspin.rb#L46-L65) from records, but this will only handle some common cases.
252
- Always review your record files before commiting them to source control.
253
-
254
- Use a tool like [trufflehog](https://github.com/trufflesecurity/trufflehog) or [gitleaks](https://github.com/gitleaks/gitleaks) run via a pre-commit to catch any sensitive data before commit.
192
+ By default, Backspin automatically tries to scrub common credential patterns from recorded stdout, stderr, args, and env values. Always review your record files before commiting them to source control.
255
193
 
256
194
  ```ruby
257
195
  # This will automatically scrub AWS keys, API tokens, passwords, etc.
258
- Backspin.run("aws_command") do
259
- Open3.capture3("aws s3 ls")
260
- end
196
+ Backspin.run(["aws", "s3", "ls"], name: "aws_command")
261
197
 
262
198
  # Add custom patterns to scrub
263
199
  Backspin.configure do |config|
data/backspin.gemspec CHANGED
@@ -24,7 +24,4 @@ Gem::Specification.new do |spec|
24
24
  spec.bindir = "exe"
25
25
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
26
26
  spec.require_paths = ["lib"]
27
-
28
- spec.add_dependency "ostruct"
29
- spec.add_dependency "rspec-mocks", "~> 3"
30
27
  end
@@ -2,7 +2,8 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "bundler/inline"
5
- require "open3"
5
+
6
+ # Example usage of Backspin matchers
6
7
 
7
8
  gemfile do
8
9
  source "https://rubygems.org"
@@ -13,24 +14,18 @@ end
13
14
  puts "Example 1: Matching timestamps with custom matcher"
14
15
  puts "-" * 50
15
16
 
16
- # First run: Record the output
17
- result = Backspin.run("timestamp_example") do
18
- Open3.capture3("date '+%Y-%m-%d %H:%M:%S'")
19
- end
17
+ result = Backspin.run(["date", "+%Y-%m-%d %H:%M:%S"], name: "timestamp_example")
20
18
  puts "Recorded: #{result.stdout.chomp}"
21
19
 
22
- # Sleep to ensure different timestamp
23
20
  sleep 1
24
21
 
25
- # Second run: Verify with custom matcher
26
- result = Backspin.run("timestamp_example",
27
- match_on: [:stdout, lambda { |recorded, actual|
28
- # Both should have the same date format
29
- recorded.match?(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/) &&
30
- actual.match?(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)
31
- }]) do
32
- Open3.capture3("date '+%Y-%m-%d %H:%M:%S'")
33
- end
22
+ result = Backspin.run(["date", "+%Y-%m-%d %H:%M:%S"], name: "timestamp_example",
23
+ matcher: {
24
+ stdout: ->(recorded, actual) {
25
+ recorded.match?(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/) &&
26
+ actual.match?(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)
27
+ }
28
+ })
34
29
 
35
30
  puts "Current: #{result.stdout.chomp}"
36
31
  puts "Verified: #{result.verified?}"
@@ -40,35 +35,23 @@ puts
40
35
  puts "Example 2: Matching multiple fields with different patterns"
41
36
  puts "-" * 50
42
37
 
43
- # Record a command with dynamic content
44
- Backspin.run("multi_field_example") do
45
- script = <<~BASH
46
- echo "PID: $$"
47
- echo "Error: Timeout at $(date '+%H:%M:%S')" >&2
48
- exit 1
49
- BASH
50
- Open3.capture3("bash", "-c", script)
51
- end
38
+ script = <<~SH
39
+ echo "PID: $$"
40
+ echo "Error: Timeout at $(date '+%H:%M:%S')" >&2
41
+ exit 1
42
+ SH
43
+
44
+ Backspin.run(["sh", "-c", script], name: "multi_field_example")
52
45
 
53
- # Verify with different PID and timestamp
54
- result = Backspin.run("multi_field_example",
55
- match_on: [
56
- [:stdout, lambda { |recorded, actual|
57
- # Both should have PID format
46
+ result = Backspin.run(["sh", "-c", script], name: "multi_field_example",
47
+ matcher: {
48
+ stdout: ->(recorded, actual) {
58
49
  recorded.match?(/PID: \d+/) && actual.match?(/PID: \d+/)
59
- }],
60
- [:stderr, lambda { |recorded, actual|
61
- # Both should have timeout error, ignore timestamp
50
+ },
51
+ stderr: ->(recorded, actual) {
62
52
  recorded.match?(/Error: Timeout at/) && actual.match?(/Error: Timeout at/)
63
- }]
64
- ]) do
65
- script = <<~BASH
66
- echo "PID: $$"
67
- echo "Error: Timeout at $(date '+%H:%M:%S')" >&2
68
- exit 1
69
- BASH
70
- Open3.capture3("bash", "-c", script)
71
- end
53
+ }
54
+ })
72
55
 
73
56
  puts "Stdout: #{result.stdout.chomp}"
74
57
  puts "Stderr: #{result.stderr.chomp}"
@@ -80,37 +63,25 @@ puts
80
63
  puts "Example 3: Mixed field matching"
81
64
  puts "-" * 50
82
65
 
83
- # Record with specific values
84
- Backspin.run("mixed_matching") do
85
- script = <<~BASH
86
- echo "Version: 1.2.3"
87
- echo "Build: $(date +%s)"
88
- echo "Status: OK"
89
- BASH
90
- Open3.capture3("bash", "-c", script)
91
- end
66
+ script = <<~SH
67
+ echo "Version: 1.2.3"
68
+ echo "Build: $(date +%s)"
69
+ echo "Status: OK"
70
+ SH
92
71
 
93
- # Verify - stdout uses custom matcher, stderr must match exactly
94
- result = Backspin.run("mixed_matching",
95
- match_on: [:stdout, lambda { |recorded, actual|
96
- # Version and Status must match, Build can differ
97
- recorded_lines = recorded.lines
98
- actual_lines = actual.lines
99
-
100
- recorded_lines[0] == actual_lines[0] && # Version line must match
101
- recorded_lines[1].start_with?("Build:") && actual_lines[1].start_with?("Build:") && # Build line format
102
- recorded_lines[2] == actual_lines[2] # Status line must match
103
- }]) do
104
- script = <<~BASH
105
- echo "Version: 1.2.3"
106
- echo "Build: $(date +%s)"
107
- echo "Status: OK"
108
- BASH
109
- Open3.capture3("bash", "-c", script)
110
- end
72
+ Backspin.run(["sh", "-c", script], name: "mixed_matching")
73
+
74
+ result = Backspin.run(["sh", "-c", script], name: "mixed_matching",
75
+ matcher: {
76
+ stdout: ->(recorded, actual) {
77
+ recorded_lines = recorded.lines
78
+ actual_lines = actual.lines
79
+
80
+ recorded_lines[0] == actual_lines[0] &&
81
+ recorded_lines[1].start_with?("Build:") && actual_lines[1].start_with?("Build:") &&
82
+ recorded_lines[2] == actual_lines[2]
83
+ }
84
+ })
111
85
 
112
86
  puts "Output:\n#{result.stdout}"
113
87
  puts "Verified: #{result.verified?}"
114
-
115
- # Cleanup
116
- FileUtils.rm_rf("fixtures/backspin")
@@ -4,11 +4,12 @@ require_relative "command_result"
4
4
 
5
5
  module Backspin
6
6
  class Command
7
- attr_reader :args, :result, :recorded_at, :method_class
7
+ attr_reader :args, :env, :result, :recorded_at, :method_class
8
8
 
9
- def initialize(method_class:, args:, stdout: nil, stderr: nil, status: nil, result: nil, recorded_at: nil)
9
+ def initialize(method_class:, args:, env: nil, stdout: nil, stderr: nil, status: nil, result: nil, recorded_at: nil)
10
10
  @method_class = method_class
11
11
  @args = args
12
+ @env = env
12
13
  @recorded_at = recorded_at
13
14
 
14
15
  # Accept either a CommandResult or individual stdout/stderr/status
@@ -38,6 +39,8 @@ module Backspin
38
39
  "recorded_at" => @recorded_at
39
40
  }
40
41
 
42
+ data["env"] = scrub_env(@env) if @env
43
+
41
44
  # Apply filter if provided
42
45
  data = filter.call(data) if filter
43
46
 
@@ -50,18 +53,16 @@ module Backspin
50
53
  method_class = case data["command_type"]
51
54
  when "Open3::Capture3"
52
55
  Open3::Capture3
53
- when "Kernel::System"
54
- ::Kernel::System
55
56
  when "Backspin::Capturer"
56
57
  Backspin::Capturer
57
58
  else
58
- # Default to capture3 for backwards compatibility
59
- Open3::Capture3
59
+ raise RecordFormatError, "Unknown command type: #{data["command_type"]}"
60
60
  end
61
61
 
62
62
  new(
63
63
  method_class: method_class,
64
64
  args: data["args"],
65
+ env: data["env"],
65
66
  stdout: data["stdout"],
66
67
  stderr: data["stderr"],
67
68
  status: data["status"],
@@ -74,19 +75,34 @@ module Backspin
74
75
  def scrub_args(args)
75
76
  return args unless Backspin.configuration.scrub_credentials && args
76
77
 
77
- args.map do |arg|
78
- case arg
79
- when String
80
- Backspin.scrub_text(arg)
81
- when Array
82
- scrub_args(arg)
83
- when Hash
84
- arg.transform_values { |v| v.is_a?(String) ? Backspin.scrub_text(v) : v }
85
- else
86
- arg
78
+ case args
79
+ when String
80
+ Backspin.scrub_text(args)
81
+ when Array
82
+ args.map do |arg|
83
+ case arg
84
+ when String
85
+ Backspin.scrub_text(arg)
86
+ when Array
87
+ scrub_args(arg)
88
+ when Hash
89
+ arg.transform_values { |v| v.is_a?(String) ? Backspin.scrub_text(v) : v }
90
+ else
91
+ arg
92
+ end
87
93
  end
94
+ when Hash
95
+ args.transform_values { |v| v.is_a?(String) ? Backspin.scrub_text(v) : v }
96
+ else
97
+ args
88
98
  end
89
99
  end
100
+
101
+ def scrub_env(env)
102
+ return env unless Backspin.configuration.scrub_credentials && env
103
+
104
+ env.transform_values { |value| value.is_a?(String) ? Backspin.scrub_text(value) : value }
105
+ end
90
106
  end
91
107
  end
92
108
 
@@ -95,11 +111,6 @@ module Open3
95
111
  class Capture3; end
96
112
  end
97
113
 
98
- # Define the Kernel::System class for identification
99
- module ::Kernel
100
- class System; end
101
- end
102
-
103
114
  # Define the Backspin::Capturer class for identification
104
115
  module Backspin
105
116
  class Capturer; end
@@ -28,17 +28,23 @@ module Backspin
28
28
  return nil if verified?
29
29
 
30
30
  parts = []
31
+ recorded_hash = recorded_command.to_h
32
+ actual_hash = actual_command.to_h
31
33
 
32
34
  unless method_classes_match?
33
35
  parts << "Command type mismatch: expected #{recorded_command.method_class.name}, got #{actual_command.method_class.name}"
34
36
  end
35
37
 
36
- parts << stdout_diff if recorded_command.stdout != actual_command.stdout
38
+ if recorded_hash["stdout"] != actual_hash["stdout"]
39
+ parts << stdout_diff(recorded_hash["stdout"], actual_hash["stdout"])
40
+ end
37
41
 
38
- parts << stderr_diff if recorded_command.stderr != actual_command.stderr
42
+ if recorded_hash["stderr"] != actual_hash["stderr"]
43
+ parts << stderr_diff(recorded_hash["stderr"], actual_hash["stderr"])
44
+ end
39
45
 
40
- if recorded_command.status != actual_command.status
41
- parts << "Exit status: expected #{recorded_command.status}, got #{actual_command.status}"
46
+ if recorded_hash["status"] != actual_hash["status"]
47
+ parts << "Exit status: expected #{recorded_hash["status"]}, got #{actual_hash["status"]}"
42
48
  end
43
49
 
44
50
  parts.join("\n\n")
@@ -67,12 +73,12 @@ module Backspin
67
73
  @matcher.failure_reason
68
74
  end
69
75
 
70
- def stdout_diff
71
- "stdout diff:\n#{generate_line_diff(recorded_command.stdout, actual_command.stdout)}"
76
+ def stdout_diff(expected, actual)
77
+ "[stdout]\n#{generate_line_diff(expected, actual)}"
72
78
  end
73
79
 
74
- def stderr_diff
75
- "stderr diff:\n#{generate_line_diff(recorded_command.stderr, actual_command.stderr)}"
80
+ def stderr_diff(expected, actual)
81
+ "[stderr]\n#{generate_line_diff(expected, actual)}"
76
82
  end
77
83
 
78
84
  def generate_line_diff(expected, actual)
@@ -8,7 +8,7 @@ module Backspin
8
8
  attr_accessor :scrub_credentials
9
9
  # The directory where backspin will store its files - defaults to fixtures/backspin
10
10
  attr_accessor :backspin_dir
11
- # Whether to raise an exception when verification fails in `run` method - defaults to true
11
+ # Whether to raise an exception when verification fails in `run`/`capture` - defaults to true
12
12
  attr_accessor :raise_on_verification_failure
13
13
  # Regex patterns to scrub from saved output
14
14
  attr_reader :credential_patterns