backspin 0.4.5 → 0.6.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 +4 -0
- data/Gemfile +2 -2
- data/Gemfile.lock +7 -9
- data/MATCHERS.md +234 -0
- data/README.md +43 -25
- data/backspin.gemspec +2 -4
- data/fixtures/backspin/all_for_logging.yml +1 -1
- data/lib/backspin/command.rb +7 -0
- data/lib/backspin/command_diff.rb +8 -80
- data/lib/backspin/configuration.rb +57 -0
- data/lib/backspin/matcher.rb +110 -0
- data/lib/backspin/recorder.rb +152 -7
- data/lib/backspin/version.rb +1 -1
- data/lib/backspin.rb +53 -65
- data/release.rake +6 -5
- metadata +11 -23
- data/MATCH_ON_USAGE.md +0 -110
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Backspin
|
4
|
+
# Handles matching logic between recorded and actual commands
|
5
|
+
class Matcher
|
6
|
+
attr_reader :config, :recorded_command, :actual_command
|
7
|
+
|
8
|
+
def initialize(config:, recorded_command:, actual_command:)
|
9
|
+
@config = normalize_config(config)
|
10
|
+
@recorded_command = recorded_command
|
11
|
+
@actual_command = actual_command
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Boolean] true if commands match according to the configured matcher
|
15
|
+
def match?
|
16
|
+
if config.nil?
|
17
|
+
# Default behavior: check all fields for equality
|
18
|
+
default_matcher.call(recorded_command.to_h, actual_command.to_h)
|
19
|
+
elsif config.is_a?(Proc)
|
20
|
+
config.call(recorded_command.to_h, actual_command.to_h)
|
21
|
+
elsif config.is_a?(Hash)
|
22
|
+
verify_with_hash_matcher
|
23
|
+
else
|
24
|
+
raise ArgumentError, "Invalid matcher type: #{config.class}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [String] reason why matching failed
|
29
|
+
def failure_reason
|
30
|
+
reasons = []
|
31
|
+
|
32
|
+
if config.nil?
|
33
|
+
# Default matcher checks all fields
|
34
|
+
recorded_hash = recorded_command.to_h
|
35
|
+
actual_hash = actual_command.to_h
|
36
|
+
|
37
|
+
reasons << "stdout differs" if recorded_hash["stdout"] != actual_hash["stdout"]
|
38
|
+
reasons << "stderr differs" if recorded_hash["stderr"] != actual_hash["stderr"]
|
39
|
+
reasons << "exit status differs" if recorded_hash["status"] != actual_hash["status"]
|
40
|
+
elsif config.is_a?(Hash)
|
41
|
+
recorded_hash = recorded_command.to_h
|
42
|
+
actual_hash = actual_command.to_h
|
43
|
+
|
44
|
+
# Only check matchers that were provided
|
45
|
+
config.each do |field, matcher_proc|
|
46
|
+
case field
|
47
|
+
when :all
|
48
|
+
reasons << ":all matcher failed" unless matcher_proc.call(recorded_hash, actual_hash)
|
49
|
+
when :stdout, :stderr, :status
|
50
|
+
unless matcher_proc.call(recorded_hash[field.to_s], actual_hash[field.to_s])
|
51
|
+
reasons << "#{field} custom matcher failed"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
else
|
56
|
+
# Proc matcher
|
57
|
+
reasons << "custom matcher failed"
|
58
|
+
end
|
59
|
+
|
60
|
+
reasons.join(", ")
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def normalize_config(config)
|
66
|
+
return nil if config.nil?
|
67
|
+
return config if config.is_a?(Proc)
|
68
|
+
|
69
|
+
raise ArgumentError, "Matcher must be a Proc or Hash, got #{config.class}" unless config.is_a?(Hash)
|
70
|
+
|
71
|
+
# Validate hash keys and values
|
72
|
+
config.each do |key, value|
|
73
|
+
unless %i[all stdout stderr status].include?(key)
|
74
|
+
raise ArgumentError, "Invalid matcher key: #{key}. Must be one of: :all, :stdout, :stderr, :status"
|
75
|
+
end
|
76
|
+
raise ArgumentError, "Matcher for #{key} must be callable (Proc/Lambda)" unless value.respond_to?(:call)
|
77
|
+
end
|
78
|
+
config
|
79
|
+
end
|
80
|
+
|
81
|
+
def verify_with_hash_matcher
|
82
|
+
recorded_hash = recorded_command.to_h
|
83
|
+
actual_hash = actual_command.to_h
|
84
|
+
|
85
|
+
# Override-based: only run matchers that are explicitly provided
|
86
|
+
# Use map to ensure all matchers run, then check if all passed
|
87
|
+
results = config.map do |field, matcher_proc|
|
88
|
+
case field
|
89
|
+
when :all
|
90
|
+
matcher_proc.call(recorded_hash, actual_hash)
|
91
|
+
when :stdout, :stderr, :status
|
92
|
+
matcher_proc.call(recorded_hash[field.to_s], actual_hash[field.to_s])
|
93
|
+
else
|
94
|
+
# This should never happen due to normalize_config validation
|
95
|
+
raise ArgumentError, "Unknown field: #{field}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
results.all?
|
100
|
+
end
|
101
|
+
|
102
|
+
def default_matcher
|
103
|
+
@default_matcher ||= lambda do |recorded, actual|
|
104
|
+
recorded["stdout"] == actual["stdout"] &&
|
105
|
+
recorded["stderr"] == actual["stderr"] &&
|
106
|
+
recorded["status"] == actual["status"]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/backspin/recorder.rb
CHANGED
@@ -12,12 +12,13 @@ module Backspin
|
|
12
12
|
include RSpec::Mocks::ExampleMethods
|
13
13
|
SUPPORTED_COMMAND_TYPES = %i[capture3 system].freeze
|
14
14
|
|
15
|
-
attr_reader :commands, :mode, :record, :
|
15
|
+
attr_reader :commands, :mode, :record, :matcher, :filter
|
16
16
|
|
17
|
-
def initialize(mode: :record, record: nil,
|
17
|
+
def initialize(mode: :record, record: nil, matcher: nil, filter: nil)
|
18
18
|
@mode = mode
|
19
19
|
@record = record
|
20
|
-
@
|
20
|
+
@matcher = matcher
|
21
|
+
@filter = filter
|
21
22
|
@commands = []
|
22
23
|
@playback_index = 0
|
23
24
|
@command_diffs = []
|
@@ -45,14 +46,14 @@ module Backspin
|
|
45
46
|
# Records registered commands, adds them to the record, saves the record, and returns the overall RecordResult
|
46
47
|
def perform_recording
|
47
48
|
result = yield
|
48
|
-
record.save(filter:
|
49
|
+
record.save(filter: @filter)
|
49
50
|
RecordResult.new(output: result, mode: :record, record: record)
|
50
51
|
end
|
51
52
|
|
52
53
|
# Performs verification by executing commands and comparing with recorded values
|
53
54
|
def perform_verification
|
54
55
|
raise RecordNotFoundError, "Record not found: #{record.path}" unless record.exists?
|
55
|
-
raise RecordNotFoundError, "No commands found in record" if record.empty?
|
56
|
+
raise RecordNotFoundError, "No commands found in record #{record.path}" if record.empty?
|
56
57
|
|
57
58
|
# Initialize tracking variables
|
58
59
|
@command_diffs = []
|
@@ -76,7 +77,7 @@ module Backspin
|
|
76
77
|
status: status.exitstatus
|
77
78
|
)
|
78
79
|
|
79
|
-
@command_diffs << CommandDiff.new(recorded_command: recorded_command, actual_command: actual_command, matcher:
|
80
|
+
@command_diffs << CommandDiff.new(recorded_command: recorded_command, actual_command: actual_command, matcher: @matcher)
|
80
81
|
@command_index += 1
|
81
82
|
[stdout, stderr, status]
|
82
83
|
end
|
@@ -96,7 +97,7 @@ module Backspin
|
|
96
97
|
)
|
97
98
|
|
98
99
|
# Create CommandDiff to track the comparison
|
99
|
-
@command_diffs << CommandDiff.new(recorded_command: recorded_command, actual_command: actual_command, matcher:
|
100
|
+
@command_diffs << CommandDiff.new(recorded_command: recorded_command, actual_command: actual_command, matcher: @matcher)
|
100
101
|
|
101
102
|
@command_index += 1
|
102
103
|
result
|
@@ -135,6 +136,150 @@ module Backspin
|
|
135
136
|
)
|
136
137
|
end
|
137
138
|
|
139
|
+
# Performs capture recording by intercepting all stdout/stderr output
|
140
|
+
def perform_capture_recording
|
141
|
+
require "tempfile"
|
142
|
+
|
143
|
+
# Create temporary files for capturing output
|
144
|
+
stdout_tempfile = Tempfile.new("backspin_stdout")
|
145
|
+
stderr_tempfile = Tempfile.new("backspin_stderr")
|
146
|
+
|
147
|
+
begin
|
148
|
+
# Save original file descriptors
|
149
|
+
original_stdout_fd = $stdout.dup
|
150
|
+
original_stderr_fd = $stderr.dup
|
151
|
+
|
152
|
+
# Redirect both Ruby IO and file descriptors
|
153
|
+
$stdout.reopen(stdout_tempfile)
|
154
|
+
$stderr.reopen(stderr_tempfile)
|
155
|
+
|
156
|
+
# Execute the block
|
157
|
+
result = yield
|
158
|
+
|
159
|
+
# Flush and read captured output
|
160
|
+
$stdout.flush
|
161
|
+
$stderr.flush
|
162
|
+
stdout_tempfile.rewind
|
163
|
+
stderr_tempfile.rewind
|
164
|
+
|
165
|
+
captured_stdout = stdout_tempfile.read
|
166
|
+
captured_stderr = stderr_tempfile.read
|
167
|
+
|
168
|
+
# Create a single command representing all captured output
|
169
|
+
command = Command.new(
|
170
|
+
method_class: Backspin::Capturer,
|
171
|
+
args: ["<captured block>"],
|
172
|
+
stdout: captured_stdout,
|
173
|
+
stderr: captured_stderr,
|
174
|
+
status: 0,
|
175
|
+
recorded_at: Time.now.iso8601
|
176
|
+
)
|
177
|
+
|
178
|
+
record.add_command(command)
|
179
|
+
record.save(filter: @filter)
|
180
|
+
|
181
|
+
RecordResult.new(output: result, mode: :record, record: record)
|
182
|
+
ensure
|
183
|
+
# Restore original file descriptors
|
184
|
+
$stdout.reopen(original_stdout_fd)
|
185
|
+
$stderr.reopen(original_stderr_fd)
|
186
|
+
original_stdout_fd.close
|
187
|
+
original_stderr_fd.close
|
188
|
+
|
189
|
+
# Clean up temp files
|
190
|
+
stdout_tempfile.close!
|
191
|
+
stderr_tempfile.close!
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Performs capture verification by capturing output and comparing with recorded values
|
196
|
+
def perform_capture_verification
|
197
|
+
raise RecordNotFoundError, "Record not found: #{record.path}" unless record.exists?
|
198
|
+
raise RecordNotFoundError, "No commands found in record #{record.path}" if record.empty?
|
199
|
+
|
200
|
+
require "tempfile"
|
201
|
+
|
202
|
+
# Create temporary files for capturing output
|
203
|
+
stdout_tempfile = Tempfile.new("backspin_stdout")
|
204
|
+
stderr_tempfile = Tempfile.new("backspin_stderr")
|
205
|
+
|
206
|
+
begin
|
207
|
+
# Save original file descriptors
|
208
|
+
original_stdout_fd = $stdout.dup
|
209
|
+
original_stderr_fd = $stderr.dup
|
210
|
+
|
211
|
+
# Redirect both Ruby IO and file descriptors
|
212
|
+
$stdout.reopen(stdout_tempfile)
|
213
|
+
$stderr.reopen(stderr_tempfile)
|
214
|
+
|
215
|
+
# Execute the block
|
216
|
+
output = yield
|
217
|
+
|
218
|
+
# Flush and read captured output
|
219
|
+
$stdout.flush
|
220
|
+
$stderr.flush
|
221
|
+
stdout_tempfile.rewind
|
222
|
+
stderr_tempfile.rewind
|
223
|
+
|
224
|
+
captured_stdout = stdout_tempfile.read
|
225
|
+
captured_stderr = stderr_tempfile.read
|
226
|
+
|
227
|
+
# Get the recorded command (should be only one for capture)
|
228
|
+
recorded_command = record.commands.first
|
229
|
+
|
230
|
+
# Create actual command from captured output
|
231
|
+
actual_command = Command.new(
|
232
|
+
method_class: Backspin::Capturer,
|
233
|
+
args: ["<captured block>"],
|
234
|
+
stdout: captured_stdout,
|
235
|
+
stderr: captured_stderr,
|
236
|
+
status: 0
|
237
|
+
)
|
238
|
+
|
239
|
+
# Create CommandDiff for comparison
|
240
|
+
command_diff = CommandDiff.new(
|
241
|
+
recorded_command: recorded_command,
|
242
|
+
actual_command: actual_command,
|
243
|
+
matcher: @matcher
|
244
|
+
)
|
245
|
+
|
246
|
+
RecordResult.new(
|
247
|
+
output: output,
|
248
|
+
mode: :verify,
|
249
|
+
verified: command_diff.verified?,
|
250
|
+
record: record,
|
251
|
+
command_diffs: [command_diff]
|
252
|
+
)
|
253
|
+
ensure
|
254
|
+
# Restore original file descriptors
|
255
|
+
$stdout.reopen(original_stdout_fd)
|
256
|
+
$stderr.reopen(original_stderr_fd)
|
257
|
+
original_stdout_fd.close
|
258
|
+
original_stderr_fd.close
|
259
|
+
|
260
|
+
# Clean up temp files
|
261
|
+
stdout_tempfile.close!
|
262
|
+
stderr_tempfile.close!
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Performs capture playback - executes block normally but could optionally suppress output
|
267
|
+
def perform_capture_playback
|
268
|
+
raise RecordNotFoundError, "Record not found: #{record.path}" unless record.exists?
|
269
|
+
raise RecordNotFoundError, "No commands found in record" if record.empty?
|
270
|
+
|
271
|
+
# For now, just execute the block normally
|
272
|
+
# In the future, we could optionally suppress output or return recorded output
|
273
|
+
output = yield
|
274
|
+
|
275
|
+
RecordResult.new(
|
276
|
+
output: output,
|
277
|
+
mode: :playback,
|
278
|
+
verified: true,
|
279
|
+
record: record
|
280
|
+
)
|
281
|
+
end
|
282
|
+
|
138
283
|
private
|
139
284
|
|
140
285
|
def setup_capture3_replay_stub
|
data/lib/backspin/version.rb
CHANGED
data/lib/backspin.rb
CHANGED
@@ -7,8 +7,10 @@ require "pathname"
|
|
7
7
|
require "ostruct"
|
8
8
|
require "rspec/mocks"
|
9
9
|
require "backspin/version"
|
10
|
+
require "backspin/configuration"
|
10
11
|
require "backspin/command_result"
|
11
12
|
require "backspin/command"
|
13
|
+
require "backspin/matcher"
|
12
14
|
require "backspin/command_diff"
|
13
15
|
require "backspin/record"
|
14
16
|
require "backspin/recorder"
|
@@ -20,58 +22,6 @@ module Backspin
|
|
20
22
|
# Include RSpec mocks methods
|
21
23
|
extend RSpec::Mocks::ExampleMethods
|
22
24
|
|
23
|
-
# Configuration for Backspin
|
24
|
-
class Configuration
|
25
|
-
attr_accessor :scrub_credentials
|
26
|
-
# The directory where backspin will store its files - defaults to fixtures/backspin
|
27
|
-
attr_accessor :backspin_dir
|
28
|
-
# Regex patterns to scrub from saved output
|
29
|
-
attr_reader :credential_patterns
|
30
|
-
|
31
|
-
def initialize
|
32
|
-
@scrub_credentials = true
|
33
|
-
@credential_patterns = default_credential_patterns
|
34
|
-
@backspin_dir = Pathname(Dir.pwd).join("fixtures", "backspin")
|
35
|
-
end
|
36
|
-
|
37
|
-
def add_credential_pattern(pattern)
|
38
|
-
@credential_patterns << pattern
|
39
|
-
end
|
40
|
-
|
41
|
-
def clear_credential_patterns
|
42
|
-
@credential_patterns = []
|
43
|
-
end
|
44
|
-
|
45
|
-
def reset_credential_patterns
|
46
|
-
@credential_patterns = default_credential_patterns
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
# Some default patterns for common credential types
|
52
|
-
def default_credential_patterns
|
53
|
-
[
|
54
|
-
# AWS credentials
|
55
|
-
/AKIA[0-9A-Z]{16}/, # AWS Access Key ID
|
56
|
-
%r{aws_secret_access_key\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})["']?}i, # AWS Secret Key
|
57
|
-
%r{aws_session_token\s*[:=]\s*["']?([A-Za-z0-9/+=]+)["']?}i, # AWS Session Token
|
58
|
-
|
59
|
-
# Google Cloud credentials
|
60
|
-
/AIza[0-9A-Za-z\-_]{35}/, # Google API Key
|
61
|
-
/[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com/, # Google OAuth2 client ID
|
62
|
-
/-----BEGIN (RSA )?PRIVATE KEY-----/, # Private keys
|
63
|
-
|
64
|
-
# Generic patterns
|
65
|
-
/api[_-]?key\s*[:=]\s*["']?([A-Za-z0-9\-_]{20,})["']?/i, # Generic API keys
|
66
|
-
/auth[_-]?token\s*[:=]\s*["']?([A-Za-z0-9\-_]{20,})["']?/i, # Auth tokens
|
67
|
-
/Bearer\s+([A-Za-z0-9\-_]+)/, # Bearer tokens
|
68
|
-
/password\s*[:=]\s*["']?([^"'\s]{8,})["']?/i, # Passwords
|
69
|
-
/-p([^"'\s]{8,})/, # MySQL-style password args
|
70
|
-
/secret\s*[:=]\s*["']?([A-Za-z0-9\-_]{20,})["']?/i # Generic secrets
|
71
|
-
]
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
25
|
class << self
|
76
26
|
def configuration
|
77
27
|
return @configuration if @configuration
|
@@ -102,22 +52,21 @@ module Backspin
|
|
102
52
|
# Primary API - records on first run, verifies on subsequent runs
|
103
53
|
#
|
104
54
|
# @param record_name [String] Name for the record file
|
105
|
-
# @param
|
106
|
-
# @
|
107
|
-
# @option options [Proc] :filter Custom filter for recorded data
|
108
|
-
# @option options [Proc, Hash] :matcher Custom matcher for verification
|
55
|
+
# @param mode [Symbol] Recording mode - :auto, :record, :verify, :playback
|
56
|
+
# @param matcher [Proc, Hash] Custom matcher for verification
|
109
57
|
# - Proc: ->(recorded, actual) { ... } for full command matching
|
110
58
|
# - Hash: { stdout: ->(recorded, actual) { ... }, stderr: ->(recorded, actual) { ... } } for field-specific matching
|
111
|
-
#
|
112
|
-
#
|
113
|
-
#
|
59
|
+
# Only specified fields are checked - fields without matchers are ignored
|
60
|
+
# - Hash with :all key: { all: ->(recorded, actual) { ... } } receives full command hashes
|
61
|
+
# Can be combined with field matchers - all specified matchers must pass
|
62
|
+
# @param filter [Proc] Custom filter for recorded data
|
114
63
|
# @return [RecordResult] Result object with output and status
|
115
|
-
def run(record_name,
|
64
|
+
def run(record_name, mode: :auto, matcher: nil, filter: nil, &block)
|
116
65
|
raise ArgumentError, "record_name is required" if record_name.nil? || record_name.empty?
|
117
66
|
raise ArgumentError, "block is required" unless block_given?
|
118
67
|
|
119
68
|
record_path = Record.build_record_path(record_name)
|
120
|
-
mode = determine_mode(
|
69
|
+
mode = determine_mode(mode, record_path)
|
121
70
|
|
122
71
|
# Create or load the record based on mode
|
123
72
|
record = if mode == :record
|
@@ -127,7 +76,7 @@ module Backspin
|
|
127
76
|
end
|
128
77
|
|
129
78
|
# Create recorder with all needed context
|
130
|
-
recorder = Recorder.new(record: record,
|
79
|
+
recorder = Recorder.new(record: record, mode: mode, matcher: matcher, filter: filter)
|
131
80
|
|
132
81
|
# Execute the appropriate mode
|
133
82
|
case mode
|
@@ -146,11 +95,13 @@ module Backspin
|
|
146
95
|
# Strict version of run that raises on verification failure
|
147
96
|
#
|
148
97
|
# @param record_name [String] Name for the record file
|
149
|
-
# @param
|
98
|
+
# @param mode [Symbol] Recording mode - :auto, :record, :verify, :playback
|
99
|
+
# @param matcher [Proc, Hash] Custom matcher for verification
|
100
|
+
# @param filter [Proc] Custom filter for recorded data
|
150
101
|
# @return [RecordResult] Result object with output and status
|
151
102
|
# @raise [RSpec::Expectations::ExpectationNotMetError] If verification fails
|
152
|
-
def run!(record_name,
|
153
|
-
result = run(record_name,
|
103
|
+
def run!(record_name, mode: :auto, matcher: nil, filter: nil, &block)
|
104
|
+
result = run(record_name, mode: mode, matcher: matcher, filter: filter, &block)
|
154
105
|
|
155
106
|
if result.verified? == false
|
156
107
|
error_message = "Backspin verification failed!\n"
|
@@ -165,6 +116,43 @@ module Backspin
|
|
165
116
|
result
|
166
117
|
end
|
167
118
|
|
119
|
+
# Captures all stdout/stderr output from a block
|
120
|
+
#
|
121
|
+
# @param record_name [String] Name for the record file
|
122
|
+
# @param mode [Symbol] Recording mode - :auto, :record, :verify, :playback
|
123
|
+
# @param matcher [Proc, Hash] Custom matcher for verification
|
124
|
+
# @param filter [Proc] Custom filter for recorded data
|
125
|
+
# @return [RecordResult] Result object with captured output
|
126
|
+
def capture(record_name, mode: :auto, matcher: nil, filter: nil, &block)
|
127
|
+
raise ArgumentError, "record_name is required" if record_name.nil? || record_name.empty?
|
128
|
+
raise ArgumentError, "block is required" unless block_given?
|
129
|
+
|
130
|
+
record_path = Record.build_record_path(record_name)
|
131
|
+
mode = determine_mode(mode, record_path)
|
132
|
+
|
133
|
+
# Create or load the record based on mode
|
134
|
+
record = if mode == :record
|
135
|
+
Record.create(record_name)
|
136
|
+
else
|
137
|
+
Record.load_or_create(record_path)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Create recorder with all needed context
|
141
|
+
recorder = Recorder.new(record: record, mode: mode, matcher: matcher, filter: filter)
|
142
|
+
|
143
|
+
# Execute the appropriate mode
|
144
|
+
case mode
|
145
|
+
when :record
|
146
|
+
recorder.perform_capture_recording(&block)
|
147
|
+
when :verify
|
148
|
+
recorder.perform_capture_verification(&block)
|
149
|
+
when :playback
|
150
|
+
recorder.perform_capture_playback(&block)
|
151
|
+
else
|
152
|
+
raise ArgumentError, "Unknown mode: #{mode}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
168
156
|
private
|
169
157
|
|
170
158
|
def determine_mode(mode_option, record_path)
|
data/release.rake
CHANGED
@@ -4,7 +4,7 @@ require "bundler/gem_tasks"
|
|
4
4
|
|
5
5
|
# Simplified release tasks using gem-release
|
6
6
|
# Install with: gem install gem-release
|
7
|
-
|
7
|
+
# https://github.com/svenfuchs/gem-release
|
8
8
|
namespace :release do
|
9
9
|
desc "Release a new version (bump, tag, release)"
|
10
10
|
task :version, [:level] do |t, args|
|
@@ -23,12 +23,13 @@ namespace :release do
|
|
23
23
|
sh "git commit -am 'Bump version to #{new_version}'"
|
24
24
|
sh "git push"
|
25
25
|
|
26
|
-
sh "gem release --tag --
|
26
|
+
sh "gem release --tag --push"
|
27
|
+
Rake::Task["release:github"].invoke(new_version)
|
27
28
|
end
|
28
29
|
|
29
|
-
desc "Create GitHub release for current version"
|
30
|
-
task :github do
|
31
|
-
version = Backspin::VERSION
|
30
|
+
desc "Create GitHub release for specified version or current version"
|
31
|
+
task :github, [:version] do |t, args|
|
32
|
+
version = args[:version] || Backspin::VERSION
|
32
33
|
|
33
34
|
if system("which gh > /dev/null 2>&1")
|
34
35
|
puts "\nCreating GitHub release for v#{version}..."
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: backspin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rob Sanheim
|
@@ -13,44 +13,30 @@ dependencies:
|
|
13
13
|
name: ostruct
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
15
15
|
requirements:
|
16
|
-
- - "
|
16
|
+
- - ">="
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: 0
|
18
|
+
version: '0'
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
|
-
- - "
|
23
|
+
- - ">="
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version: 0
|
25
|
+
version: '0'
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: rspec-mocks
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
29
29
|
requirements:
|
30
30
|
- - "~>"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '3
|
32
|
+
version: '3'
|
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: '3
|
40
|
-
- !ruby/object:Gem::Dependency
|
41
|
-
name: gem-release
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
43
|
-
requirements:
|
44
|
-
- - "~>"
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: '2'
|
47
|
-
type: :development
|
48
|
-
prerelease: false
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
50
|
-
requirements:
|
51
|
-
- - "~>"
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: '2'
|
39
|
+
version: '3'
|
54
40
|
description: Backspin is a Ruby library for characterization testing of command-line
|
55
41
|
interfaces. Inspired by VCR's cassette-based approach, it records and replays CLI
|
56
42
|
interactions to make testing faster and more deterministic.
|
@@ -71,7 +57,7 @@ files:
|
|
71
57
|
- Gemfile
|
72
58
|
- Gemfile.lock
|
73
59
|
- LICENSE.txt
|
74
|
-
-
|
60
|
+
- MATCHERS.md
|
75
61
|
- README.md
|
76
62
|
- Rakefile
|
77
63
|
- backspin.gemspec
|
@@ -138,6 +124,8 @@ files:
|
|
138
124
|
- lib/backspin/command.rb
|
139
125
|
- lib/backspin/command_diff.rb
|
140
126
|
- lib/backspin/command_result.rb
|
127
|
+
- lib/backspin/configuration.rb
|
128
|
+
- lib/backspin/matcher.rb
|
141
129
|
- lib/backspin/record.rb
|
142
130
|
- lib/backspin/record_result.rb
|
143
131
|
- lib/backspin/recorder.rb
|
@@ -165,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
165
153
|
- !ruby/object:Gem::Version
|
166
154
|
version: '0'
|
167
155
|
requirements: []
|
168
|
-
rubygems_version: 3.6.
|
156
|
+
rubygems_version: 3.6.7
|
169
157
|
specification_version: 4
|
170
158
|
summary: Record and replay CLI interactions for testing
|
171
159
|
test_files: []
|