backspin 0.2.1 → 0.4.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 +31 -0
- data/.gitignore +1 -4
- data/CHANGELOG.md +6 -0
- data/CLAUDE.md +4 -5
- data/CONTRIBUTING.md +222 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +87 -0
- data/README.md +108 -34
- data/backspin.gemspec +4 -4
- data/bin/rake +27 -0
- data/bin/rspec +27 -0
- data/bin/setup +1 -3
- data/lib/backspin/command.rb +49 -12
- data/lib/backspin/command_diff.rb +88 -0
- data/lib/backspin/command_result.rb +60 -0
- data/lib/backspin/record.rb +2 -2
- data/lib/backspin/record_result.rb +153 -0
- data/lib/backspin/recorder.rb +4 -23
- data/lib/backspin/version.rb +1 -1
- data/lib/backspin.rb +170 -285
- metadata +17 -10
data/lib/backspin.rb
CHANGED
@@ -5,9 +5,12 @@ require "pathname"
|
|
5
5
|
require "ostruct"
|
6
6
|
require "rspec/mocks"
|
7
7
|
require "backspin/version"
|
8
|
+
require "backspin/command_result"
|
8
9
|
require "backspin/command"
|
10
|
+
require "backspin/command_diff"
|
9
11
|
require "backspin/record"
|
10
12
|
require "backspin/recorder"
|
13
|
+
require "backspin/record_result"
|
11
14
|
|
12
15
|
module Backspin
|
13
16
|
class RecordNotFoundError < StandardError; end
|
@@ -43,6 +46,7 @@ module Backspin
|
|
43
46
|
|
44
47
|
private
|
45
48
|
|
49
|
+
# Some default patterns for common credential types
|
46
50
|
def default_credential_patterns
|
47
51
|
[
|
48
52
|
# AWS credentials
|
@@ -58,7 +62,9 @@ module Backspin
|
|
58
62
|
# Generic patterns
|
59
63
|
/api[_-]?key\s*[:=]\s*["']?([A-Za-z0-9\-_]{20,})["']?/i, # Generic API keys
|
60
64
|
/auth[_-]?token\s*[:=]\s*["']?([A-Za-z0-9\-_]{20,})["']?/i, # Auth tokens
|
65
|
+
/Bearer\s+([A-Za-z0-9\-_]+)/, # Bearer tokens
|
61
66
|
/password\s*[:=]\s*["']?([^"'\s]{8,})["']?/i, # Passwords
|
67
|
+
/-p([^"'\s]{8,})/, # MySQL-style password args
|
62
68
|
/secret\s*[:=]\s*["']?([A-Za-z0-9\-_]{20,})["']?/i # Generic secrets
|
63
69
|
]
|
64
70
|
end
|
@@ -66,7 +72,9 @@ module Backspin
|
|
66
72
|
|
67
73
|
class << self
|
68
74
|
def configuration
|
69
|
-
@configuration
|
75
|
+
return @configuration if @configuration
|
76
|
+
|
77
|
+
@configuration = Configuration.new
|
70
78
|
end
|
71
79
|
|
72
80
|
def configure
|
@@ -76,86 +84,6 @@ module Backspin
|
|
76
84
|
def reset_configuration!
|
77
85
|
@configuration = Configuration.new
|
78
86
|
end
|
79
|
-
end
|
80
|
-
|
81
|
-
class Result
|
82
|
-
attr_reader :commands, :record_path
|
83
|
-
|
84
|
-
def initialize(commands:, record_path:)
|
85
|
-
@commands = commands
|
86
|
-
@record_path = record_path
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
class VerifyResult
|
91
|
-
attr_reader :record_path, :expected_output, :actual_output, :diff, :stderr_diff
|
92
|
-
|
93
|
-
def initialize(verified:, record_path:, expected_output: nil, actual_output: nil,
|
94
|
-
expected_stderr: nil, actual_stderr: nil, expected_status: nil, actual_status: nil,
|
95
|
-
command_executed: true)
|
96
|
-
@verified = verified
|
97
|
-
@record_path = record_path
|
98
|
-
@expected_output = expected_output
|
99
|
-
@actual_output = actual_output
|
100
|
-
@expected_stderr = expected_stderr
|
101
|
-
@actual_stderr = actual_stderr
|
102
|
-
@expected_status = expected_status
|
103
|
-
@actual_status = actual_status
|
104
|
-
@command_executed = command_executed
|
105
|
-
|
106
|
-
if !verified && expected_output && actual_output
|
107
|
-
@diff = generate_diff(expected_output, actual_output)
|
108
|
-
end
|
109
|
-
|
110
|
-
if !verified && expected_stderr && actual_stderr && expected_stderr != actual_stderr
|
111
|
-
@stderr_diff = generate_diff(expected_stderr, actual_stderr)
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
def verified?
|
116
|
-
@verified
|
117
|
-
end
|
118
|
-
|
119
|
-
def output
|
120
|
-
@actual_output
|
121
|
-
end
|
122
|
-
|
123
|
-
def error_message
|
124
|
-
return nil if verified?
|
125
|
-
|
126
|
-
"Output verification failed\nExpected: #{@expected_output.chomp}\nActual: #{@actual_output.chomp}"
|
127
|
-
end
|
128
|
-
|
129
|
-
def command_executed?
|
130
|
-
@command_executed
|
131
|
-
end
|
132
|
-
|
133
|
-
private
|
134
|
-
|
135
|
-
def generate_diff(expected, actual)
|
136
|
-
expected_lines = expected.lines
|
137
|
-
actual_lines = actual.lines
|
138
|
-
diff = []
|
139
|
-
|
140
|
-
# Simple diff for now
|
141
|
-
expected_lines.each do |line|
|
142
|
-
unless actual_lines.include?(line)
|
143
|
-
diff << "-#{line.chomp}"
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
actual_lines.each do |line|
|
148
|
-
unless expected_lines.include?(line)
|
149
|
-
diff << "+#{line.chomp}"
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
diff.join("\n")
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
class << self
|
158
|
-
attr_accessor :last_output
|
159
87
|
|
160
88
|
def scrub_text(text)
|
161
89
|
return text unless configuration.scrub_credentials && text
|
@@ -170,262 +98,219 @@ module Backspin
|
|
170
98
|
scrubbed
|
171
99
|
end
|
172
100
|
|
173
|
-
|
101
|
+
# Primary API - records on first run, verifies on subsequent runs
|
102
|
+
#
|
103
|
+
# @param record_name [String] Name for the record file
|
104
|
+
# @param options [Hash] Options for recording/verification
|
105
|
+
# @option options [Symbol] :mode (:auto) Recording mode - :auto, :record, :verify, :playback
|
106
|
+
# @option options [Proc] :filter Custom filter for recorded data
|
107
|
+
# @option options [Proc] :matcher Custom matcher for verification
|
108
|
+
# @return [RecordResult] Result object with output and status
|
109
|
+
def run(record_name, options = {}, &block)
|
174
110
|
raise ArgumentError, "record_name is required" if record_name.nil? || record_name.empty?
|
111
|
+
raise ArgumentError, "block is required" unless block_given?
|
175
112
|
|
176
113
|
record_path = build_record_path(record_name)
|
114
|
+
mode = determine_mode(options[:mode], record_path)
|
115
|
+
|
116
|
+
case mode
|
117
|
+
when :record
|
118
|
+
perform_recording(record_name, record_path, options, &block)
|
119
|
+
when :verify
|
120
|
+
perform_verification(record_name, record_path, options, &block)
|
121
|
+
when :playback
|
122
|
+
perform_playback(record_name, record_path, options, &block)
|
123
|
+
else
|
124
|
+
raise ArgumentError, "Unknown mode: #{mode}"
|
125
|
+
end
|
126
|
+
end
|
177
127
|
|
178
|
-
|
179
|
-
|
180
|
-
|
128
|
+
# Strict version of run that raises on verification failure
|
129
|
+
#
|
130
|
+
# @param record_name [String] Name for the record file
|
131
|
+
# @param options [Hash] Options for recording/verification
|
132
|
+
# @return [RecordResult] Result object with output and status
|
133
|
+
# @raise [RSpec::Expectations::ExpectationNotMetError] If verification fails
|
134
|
+
def run!(record_name, options = {}, &block)
|
135
|
+
result = run(record_name, options, &block)
|
181
136
|
|
182
|
-
|
137
|
+
if result.verified? == false
|
138
|
+
error_message = "Backspin verification failed!\n"
|
139
|
+
error_message += "Record: #{result.record_path}\n"
|
183
140
|
|
184
|
-
|
185
|
-
|
186
|
-
# Don't load existing data when creating new record
|
187
|
-
record = Record.new(record_path)
|
188
|
-
record.clear # Clear any loaded data
|
189
|
-
recorder.commands.each { |cmd| record.add_command(cmd) }
|
190
|
-
record.save(filter: filter)
|
141
|
+
# Use the error_message from the result which is now properly formatted
|
142
|
+
error_message += "\n#{result.error_message}" if result.error_message
|
191
143
|
|
192
|
-
|
193
|
-
|
144
|
+
raise RSpec::Expectations::ExpectationNotMetError, error_message
|
145
|
+
end
|
194
146
|
|
195
|
-
|
196
|
-
last_output
|
147
|
+
result
|
197
148
|
end
|
198
149
|
|
199
|
-
|
200
|
-
raise ArgumentError, "record_name is required" if record_name.nil? || record_name.empty?
|
150
|
+
private
|
201
151
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
when :none
|
208
|
-
# Never record, only replay
|
209
|
-
unless File.exist?(record_path)
|
210
|
-
raise RecordNotFoundError, "Record not found: #{record_path}"
|
211
|
-
end
|
212
|
-
replay_record(record_path, &block)
|
213
|
-
when :all
|
214
|
-
# Always record
|
215
|
-
record_and_save_record(record_path, filter: filter, &block)
|
216
|
-
when :once
|
217
|
-
# Record if doesn't exist, replay if exists
|
218
|
-
if File.exist?(record_path)
|
219
|
-
replay_record(record_path, &block)
|
220
|
-
else
|
221
|
-
record_and_save_record(record_path, filter: filter, &block)
|
222
|
-
end
|
223
|
-
when :new_episodes
|
224
|
-
# Record new commands not in record
|
225
|
-
# For now, simplified: just append new recordings
|
226
|
-
record_new_episode(record_path, filter: filter, &block)
|
227
|
-
else
|
228
|
-
raise ArgumentError, "Unknown record mode: #{record_mode}"
|
229
|
-
end
|
152
|
+
def determine_mode(mode_option, record_path)
|
153
|
+
return mode_option if mode_option && mode_option != :auto
|
154
|
+
|
155
|
+
# Auto mode: record if file doesn't exist, verify if it does
|
156
|
+
File.exist?(record_path) ? :verify : :record
|
230
157
|
end
|
231
158
|
|
232
|
-
def
|
233
|
-
|
159
|
+
def perform_recording(_record_name, record_path, options)
|
160
|
+
recorder = Recorder.new
|
161
|
+
recorder.record_calls(:capture3, :system)
|
234
162
|
|
235
|
-
|
236
|
-
unless record.exists?
|
237
|
-
raise RecordNotFoundError, "Record not found: #{record_path}"
|
238
|
-
end
|
163
|
+
output = yield
|
239
164
|
|
240
|
-
if
|
241
|
-
|
165
|
+
if output.is_a?(Array) && output.size == 3
|
166
|
+
stdout, stderr, status = output
|
167
|
+
status_int = status.respond_to?(:exitstatus) ? status.exitstatus : status
|
168
|
+
output = [stdout, stderr, status_int]
|
242
169
|
end
|
243
170
|
|
244
|
-
#
|
245
|
-
|
246
|
-
|
171
|
+
# Save the recording
|
172
|
+
FileUtils.mkdir_p(File.dirname(record_path))
|
173
|
+
record = Record.new(record_path)
|
174
|
+
record.clear
|
175
|
+
recorder.commands.each { |cmd| record.add_command(cmd) }
|
176
|
+
record.save(filter: options[:filter])
|
177
|
+
|
178
|
+
# Return result
|
179
|
+
RecordResult.new(
|
180
|
+
output: output,
|
181
|
+
mode: :record,
|
182
|
+
record_path: Pathname.new(record_path),
|
183
|
+
commands: recorder.commands
|
184
|
+
)
|
185
|
+
end
|
247
186
|
|
248
|
-
|
249
|
-
|
187
|
+
def perform_verification(_record_name, record_path, options)
|
188
|
+
record = Record.load_or_create(record_path)
|
250
189
|
|
251
|
-
|
252
|
-
|
253
|
-
recorder.setup_playback_stub(command)
|
254
|
-
|
255
|
-
yield
|
256
|
-
|
257
|
-
# In playback mode, always verified
|
258
|
-
VerifyResult.new(
|
259
|
-
verified: true,
|
260
|
-
record_path: Pathname.new(record_path),
|
261
|
-
expected_output: command.stdout,
|
262
|
-
actual_output: command.stdout,
|
263
|
-
expected_stderr: command.stderr,
|
264
|
-
actual_stderr: command.stderr,
|
265
|
-
expected_status: command.status,
|
266
|
-
actual_status: command.status,
|
267
|
-
command_executed: false
|
268
|
-
)
|
269
|
-
elsif matcher
|
270
|
-
# Custom matcher verification
|
271
|
-
recorder.setup_verification_stub(command)
|
272
|
-
|
273
|
-
yield
|
274
|
-
|
275
|
-
# Call custom matcher - convert command back to hash format for matcher
|
276
|
-
recorded_data = command.to_h
|
277
|
-
verified = matcher.call(recorded_data, recorder.verification_data)
|
278
|
-
|
279
|
-
VerifyResult.new(
|
280
|
-
verified: verified,
|
281
|
-
record_path: Pathname.new(record_path),
|
282
|
-
expected_output: command.stdout,
|
283
|
-
actual_output: recorder.verification_data["stdout"],
|
284
|
-
expected_stderr: command.stderr,
|
285
|
-
actual_stderr: recorder.verification_data["stderr"],
|
286
|
-
expected_status: command.status,
|
287
|
-
actual_status: recorder.verification_data["status"]
|
288
|
-
)
|
289
|
-
else
|
290
|
-
# Default strict mode
|
291
|
-
recorder.setup_verification_stub(command)
|
292
|
-
|
293
|
-
yield
|
294
|
-
|
295
|
-
# Compare outputs
|
296
|
-
actual_stdout = recorder.verification_data["stdout"]
|
297
|
-
actual_stderr = recorder.verification_data["stderr"]
|
298
|
-
actual_status = recorder.verification_data["status"]
|
299
|
-
|
300
|
-
verified =
|
301
|
-
command.stdout == actual_stdout &&
|
302
|
-
command.stderr == actual_stderr &&
|
303
|
-
command.status == actual_status
|
304
|
-
|
305
|
-
VerifyResult.new(
|
306
|
-
verified: verified,
|
307
|
-
record_path: Pathname.new(record_path),
|
308
|
-
expected_output: command.stdout,
|
309
|
-
actual_output: actual_stdout,
|
310
|
-
expected_stderr: command.stderr,
|
311
|
-
actual_stderr: actual_stderr,
|
312
|
-
expected_status: command.status,
|
313
|
-
actual_status: actual_status
|
314
|
-
)
|
315
|
-
end
|
316
|
-
end
|
190
|
+
raise RecordNotFoundError, "Record not found: #{record_path}" unless record.exists?
|
191
|
+
raise RecordNotFoundError, "No commands found in record" if record.empty?
|
317
192
|
|
318
|
-
|
319
|
-
|
193
|
+
# For verification, we need to track all commands executed
|
194
|
+
recorder = Recorder.new(mode: :verify, record: record)
|
195
|
+
recorder.setup_replay_stubs
|
320
196
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
197
|
+
# Track verification results for each command
|
198
|
+
command_diffs = []
|
199
|
+
command_index = 0
|
200
|
+
|
201
|
+
# Override stubs to verify each command as it's executed
|
202
|
+
allow(Open3).to receive(:capture3).and_wrap_original do |original_method, *args|
|
203
|
+
recorded_command = record.commands[command_index]
|
326
204
|
|
327
|
-
if
|
328
|
-
|
205
|
+
if recorded_command.nil?
|
206
|
+
raise RecordNotFoundError, "No more recorded commands, but tried to execute: #{args.inspect}"
|
329
207
|
end
|
330
208
|
|
331
|
-
if
|
332
|
-
|
209
|
+
if recorded_command.method_class != Open3::Capture3
|
210
|
+
raise RecordNotFoundError, "Expected #{recorded_command.method_class.name} but got Open3.capture3"
|
333
211
|
end
|
334
212
|
|
335
|
-
#
|
336
|
-
|
337
|
-
end
|
213
|
+
# Execute the actual command
|
214
|
+
stdout, stderr, status = original_method.call(*args)
|
338
215
|
|
339
|
-
|
340
|
-
|
216
|
+
# Create verification result
|
217
|
+
actual_result = CommandResult.new(
|
218
|
+
stdout: stdout,
|
219
|
+
stderr: stderr,
|
220
|
+
status: status.exitstatus
|
221
|
+
)
|
341
222
|
|
342
|
-
|
223
|
+
# Create CommandDiff to track the comparison
|
224
|
+
command_diffs << CommandDiff.new(
|
225
|
+
recorded_command: recorded_command,
|
226
|
+
actual_result: actual_result,
|
227
|
+
matcher: options[:matcher]
|
228
|
+
)
|
343
229
|
|
344
|
-
|
345
|
-
|
346
|
-
unless record.exists?
|
347
|
-
raise RecordNotFoundError, "Record not found: #{record_path}"
|
230
|
+
command_index += 1
|
231
|
+
[stdout, stderr, status]
|
348
232
|
end
|
349
233
|
|
350
|
-
|
351
|
-
|
352
|
-
end
|
234
|
+
allow_any_instance_of(Object).to receive(:system).and_wrap_original do |original_method, receiver, *args|
|
235
|
+
recorded_command = record.commands[command_index]
|
353
236
|
|
354
|
-
|
355
|
-
|
356
|
-
|
237
|
+
if recorded_command.nil?
|
238
|
+
raise RecordNotFoundError, "No more recorded commands, but tried to execute: system #{args.inspect}"
|
239
|
+
end
|
357
240
|
|
358
|
-
|
241
|
+
if recorded_command.method_class != ::Kernel::System
|
242
|
+
raise RecordNotFoundError, "Expected #{recorded_command.method_class.name} but got system"
|
243
|
+
end
|
359
244
|
|
360
|
-
|
361
|
-
|
362
|
-
if block_return_value.is_a?(Array) && block_return_value.size == 3 &&
|
363
|
-
block_return_value[0].is_a?(String) && block_return_value[1].is_a?(String)
|
364
|
-
# Convert status to integer for consistency
|
365
|
-
stdout, stderr, status = block_return_value
|
366
|
-
status_int = status.respond_to?(:exitstatus) ? status.exitstatus : status
|
367
|
-
[stdout, stderr, status_int]
|
368
|
-
else
|
369
|
-
block_return_value
|
370
|
-
end
|
371
|
-
end
|
245
|
+
# Execute the actual command
|
246
|
+
result = original_method.call(receiver, *args)
|
372
247
|
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
248
|
+
# Create verification result (system only gives us exit status)
|
249
|
+
actual_result = CommandResult.new(
|
250
|
+
stdout: "",
|
251
|
+
stderr: "",
|
252
|
+
status: result ? 0 : 1
|
253
|
+
)
|
377
254
|
|
378
|
-
|
255
|
+
# Create CommandDiff to track the comparison
|
256
|
+
command_diffs << CommandDiff.new(
|
257
|
+
recorded_command: recorded_command,
|
258
|
+
actual_result: actual_result,
|
259
|
+
matcher: options[:matcher]
|
260
|
+
)
|
379
261
|
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
record = Record.new(record_path)
|
384
|
-
record.clear # Clear any loaded data
|
385
|
-
recorder.commands.each { |cmd| record.add_command(cmd) }
|
386
|
-
record.save(filter: filter)
|
262
|
+
command_index += 1
|
263
|
+
result
|
264
|
+
end
|
387
265
|
|
388
|
-
#
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
block_return_value
|
266
|
+
# Execute block
|
267
|
+
output = yield
|
268
|
+
|
269
|
+
# Check if all commands were executed
|
270
|
+
if command_index < record.commands.size
|
271
|
+
raise RecordNotFoundError, "Expected #{record.commands.size} commands but only #{command_index} were executed"
|
395
272
|
end
|
273
|
+
|
274
|
+
# Overall verification status
|
275
|
+
all_verified = command_diffs.all?(&:verified?)
|
276
|
+
|
277
|
+
RecordResult.new(
|
278
|
+
output: output,
|
279
|
+
mode: :verify,
|
280
|
+
verified: all_verified,
|
281
|
+
record_path: Pathname.new(record_path),
|
282
|
+
commands: record.commands,
|
283
|
+
command_diffs: command_diffs
|
284
|
+
)
|
396
285
|
end
|
397
286
|
|
398
|
-
def
|
399
|
-
# For new_episodes mode, we'd need to track which commands have been seen
|
400
|
-
# For now, simplified implementation that just appends
|
287
|
+
def perform_playback(_record_name, record_path, _options)
|
401
288
|
record = Record.load_or_create(record_path)
|
402
289
|
|
403
|
-
|
404
|
-
|
405
|
-
recorder.record_calls(:capture3, :system)
|
290
|
+
raise RecordNotFoundError, "Record not found: #{record_path}" unless record.exists?
|
291
|
+
raise RecordNotFoundError, "No commands found in record" if record.empty?
|
406
292
|
|
407
|
-
|
293
|
+
# Setup replay mode - this will handle returning values for all commands
|
294
|
+
recorder = Recorder.new(mode: :replay, record: record)
|
295
|
+
recorder.setup_replay_stubs
|
408
296
|
|
409
|
-
#
|
410
|
-
|
411
|
-
recorder.commands.each { |cmd| record.add_command(cmd) }
|
412
|
-
record.save(filter: filter)
|
413
|
-
end
|
297
|
+
# Execute block (all commands will be stubbed with recorded values)
|
298
|
+
output = yield
|
414
299
|
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
300
|
+
RecordResult.new(
|
301
|
+
output: output,
|
302
|
+
mode: :playback,
|
303
|
+
verified: true, # Always true for playback
|
304
|
+
record_path: Pathname.new(record_path),
|
305
|
+
commands: record.commands
|
306
|
+
)
|
422
307
|
end
|
423
308
|
|
424
309
|
def build_record_path(name)
|
425
310
|
backspin_dir = configuration.backspin_dir
|
426
311
|
backspin_dir.mkpath
|
427
312
|
|
428
|
-
File.join(backspin_dir, "#{name}.
|
313
|
+
File.join(backspin_dir, "#{name}.yml")
|
429
314
|
end
|
430
315
|
end
|
431
316
|
end
|
metadata
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: backspin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rob Sanheim
|
8
|
-
bindir:
|
8
|
+
bindir: exe
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
@@ -27,40 +27,47 @@ dependencies:
|
|
27
27
|
name: ostruct
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
29
29
|
requirements:
|
30
|
-
- - "
|
30
|
+
- - "~>"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version:
|
32
|
+
version: 0.5.0
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
|
-
- - "
|
37
|
+
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
39
|
+
version: 0.5.0
|
40
40
|
description: Backspin is a Ruby library for characterization testing of command-line
|
41
41
|
interfaces. Inspired by VCR's cassette-based approach, it records and replays CLI
|
42
42
|
interactions to make testing faster and more deterministic.
|
43
43
|
email:
|
44
44
|
- rsanheim@gmail.com
|
45
|
-
executables:
|
46
|
-
- setup
|
45
|
+
executables: []
|
47
46
|
extensions: []
|
48
47
|
extra_rdoc_files: []
|
49
48
|
files:
|
49
|
+
- ".circleci/config.yml"
|
50
50
|
- ".gitignore"
|
51
51
|
- ".rspec"
|
52
52
|
- ".standard.yml"
|
53
53
|
- CHANGELOG.md
|
54
54
|
- CLAUDE.md
|
55
|
+
- CONTRIBUTING.md
|
55
56
|
- Gemfile
|
57
|
+
- Gemfile.lock
|
56
58
|
- LICENSE.txt
|
57
59
|
- README.md
|
58
60
|
- Rakefile
|
59
61
|
- backspin.gemspec
|
62
|
+
- bin/rake
|
63
|
+
- bin/rspec
|
60
64
|
- bin/setup
|
61
65
|
- lib/backspin.rb
|
62
66
|
- lib/backspin/command.rb
|
67
|
+
- lib/backspin/command_diff.rb
|
68
|
+
- lib/backspin/command_result.rb
|
63
69
|
- lib/backspin/record.rb
|
70
|
+
- lib/backspin/record_result.rb
|
64
71
|
- lib/backspin/recorder.rb
|
65
72
|
- lib/backspin/version.rb
|
66
73
|
homepage: https://github.com/rsanheim/backspin
|
@@ -77,14 +84,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
84
|
requirements:
|
78
85
|
- - ">="
|
79
86
|
- !ruby/object:Gem::Version
|
80
|
-
version:
|
87
|
+
version: 3.1.0
|
81
88
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
89
|
requirements:
|
83
90
|
- - ">="
|
84
91
|
- !ruby/object:Gem::Version
|
85
92
|
version: '0'
|
86
93
|
requirements: []
|
87
|
-
rubygems_version: 3.6.
|
94
|
+
rubygems_version: 3.6.9
|
88
95
|
specification_version: 4
|
89
96
|
summary: Record and replay CLI interactions for testing
|
90
97
|
test_files: []
|