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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +18 -6
- data/.ruby-version +1 -0
- data/CHANGELOG.md +14 -1
- data/CLAUDE.md +8 -9
- data/CONTRIBUTING.md +5 -12
- data/Gemfile.lock +14 -17
- data/MATCHERS.md +28 -136
- data/README.md +56 -120
- data/backspin.gemspec +0 -3
- data/examples/match_on_example.rb +42 -71
- data/lib/backspin/command.rb +32 -21
- data/lib/backspin/command_diff.rb +14 -8
- data/lib/backspin/configuration.rb +1 -1
- data/lib/backspin/record.rb +9 -18
- data/lib/backspin/record_result.rb +0 -6
- data/lib/backspin/recorder.rb +46 -301
- data/lib/backspin/version.rb +1 -1
- data/lib/backspin.rb +136 -87
- metadata +4 -31
data/README.md
CHANGED
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
[](https://circleci.com/gh/rsanheim/backspin)
|
|
6
6
|
[](https://github.com/rsanheim/backspin/commits/main)
|
|
7
7
|
|
|
8
|
-
Backspin records and
|
|
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/
|
|
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
|
|
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
|
|
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")
|
|
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")
|
|
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")
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
###
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
104
|
+
If `env:` is not provided, it is not passed to `Open3.capture3` and is not recorded.
|
|
106
105
|
|
|
107
|
-
### Custom
|
|
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"]
|
|
116
|
+
recorded["stdout"] == actual["stdout"] && recorded["status"] == actual["status"]
|
|
119
117
|
}
|
|
120
118
|
|
|
121
|
-
result = Backspin.run("my_test", matcher: {
|
|
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: {
|
|
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")
|
|
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
|
|
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 #
|
|
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
|
|
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")
|
|
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
|
|
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
|
|
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")
|
|
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
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "bundler/inline"
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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")
|
data/lib/backspin/command.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
38
|
+
if recorded_hash["stdout"] != actual_hash["stdout"]
|
|
39
|
+
parts << stdout_diff(recorded_hash["stdout"], actual_hash["stdout"])
|
|
40
|
+
end
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
if recorded_hash["stderr"] != actual_hash["stderr"]
|
|
43
|
+
parts << stderr_diff(recorded_hash["stderr"], actual_hash["stderr"])
|
|
44
|
+
end
|
|
39
45
|
|
|
40
|
-
if
|
|
41
|
-
parts << "Exit status: expected #{
|
|
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
|
|
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
|
|
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`
|
|
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
|