backspin 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +18 -6
- data/.ruby-version +1 -0
- data/CHANGELOG.md +16 -1
- data/CLAUDE.md +20 -16
- data/CONTRIBUTING.md +16 -19
- data/Gemfile.lock +3 -6
- data/MATCHERS.md +28 -136
- data/README.md +61 -124
- data/backspin.gemspec +0 -3
- data/docs/backspin-result-api-sketch.md +203 -0
- data/examples/match_on_example.rb +42 -71
- data/lib/backspin/backspin_result.rb +66 -0
- data/lib/backspin/command_diff.rb +28 -23
- data/lib/backspin/configuration.rb +1 -1
- data/lib/backspin/matcher.rb +21 -27
- data/lib/backspin/record.rb +23 -36
- data/lib/backspin/recorder.rb +54 -305
- data/lib/backspin/snapshot.rb +96 -0
- data/lib/backspin/version.rb +1 -1
- data/lib/backspin.rb +127 -94
- metadata +7 -34
- data/lib/backspin/command.rb +0 -106
- data/lib/backspin/command_result.rb +0 -58
- data/lib/backspin/record_result.rb +0 -159
data/lib/backspin.rb
CHANGED
|
@@ -4,17 +4,14 @@ require "yaml"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "open3"
|
|
6
6
|
require "pathname"
|
|
7
|
-
require "ostruct"
|
|
8
|
-
require "rspec/mocks"
|
|
9
7
|
require "backspin/version"
|
|
10
8
|
require "backspin/configuration"
|
|
11
|
-
require "backspin/
|
|
12
|
-
require "backspin/command"
|
|
9
|
+
require "backspin/snapshot"
|
|
13
10
|
require "backspin/matcher"
|
|
14
11
|
require "backspin/command_diff"
|
|
15
12
|
require "backspin/record"
|
|
13
|
+
require "backspin/backspin_result"
|
|
16
14
|
require "backspin/recorder"
|
|
17
|
-
require "backspin/record_result"
|
|
18
15
|
|
|
19
16
|
module Backspin
|
|
20
17
|
class RecordNotFoundError < StandardError; end
|
|
@@ -31,18 +28,15 @@ module Backspin
|
|
|
31
28
|
result.diff
|
|
32
29
|
end
|
|
33
30
|
|
|
34
|
-
def
|
|
35
|
-
result.
|
|
31
|
+
def expected_snapshot
|
|
32
|
+
result.expected
|
|
36
33
|
end
|
|
37
34
|
|
|
38
|
-
def
|
|
39
|
-
result.
|
|
35
|
+
def actual_snapshot
|
|
36
|
+
result.actual
|
|
40
37
|
end
|
|
41
38
|
end
|
|
42
39
|
|
|
43
|
-
# Include RSpec mocks methods
|
|
44
|
-
extend RSpec::Mocks::ExampleMethods
|
|
45
|
-
|
|
46
40
|
class << self
|
|
47
41
|
def configuration
|
|
48
42
|
return @configuration if @configuration
|
|
@@ -72,136 +66,167 @@ module Backspin
|
|
|
72
66
|
|
|
73
67
|
# Primary API - records on first run, verifies on subsequent runs
|
|
74
68
|
#
|
|
69
|
+
# @param command [String, Array] Command to execute via Open3.capture3
|
|
70
|
+
# @param name [String] Name for the record file
|
|
71
|
+
# @param env [Hash] Environment variables to pass to Open3.capture3
|
|
72
|
+
# @param mode [Symbol] Recording mode - :auto, :record, :verify
|
|
73
|
+
# @param matcher [Proc, Hash] Custom matcher for verification
|
|
74
|
+
# @param filter [Proc] Custom filter for recorded data
|
|
75
|
+
# @return [BackspinResult] Aggregate result for this run
|
|
76
|
+
def run(command = nil, name:, env: nil, mode: :auto, matcher: nil, filter: nil, &block)
|
|
77
|
+
if block_given?
|
|
78
|
+
raise ArgumentError, "command must be omitted when using a block" unless command.nil?
|
|
79
|
+
raise ArgumentError, "env is not supported when using a block" unless env.nil?
|
|
80
|
+
|
|
81
|
+
return perform_capture(name, mode: mode, matcher: matcher, filter: filter, &block)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
raise ArgumentError, "command is required" if command.nil?
|
|
85
|
+
|
|
86
|
+
perform_command_run(command, name: name, env: env, mode: mode, matcher: matcher, filter: filter)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Captures all stdout/stderr output from a block
|
|
90
|
+
#
|
|
75
91
|
# @param record_name [String] Name for the record file
|
|
76
|
-
# @param mode [Symbol] Recording mode - :auto, :record, :verify
|
|
92
|
+
# @param mode [Symbol] Recording mode - :auto, :record, :verify
|
|
77
93
|
# @param matcher [Proc, Hash] Custom matcher for verification
|
|
78
|
-
# - Proc: ->(recorded, actual) { ... } for full command matching
|
|
79
|
-
# - Hash: { stdout: ->(recorded, actual) { ... }, stderr: ->(recorded, actual) { ... } } for field-specific matching
|
|
80
|
-
# Only specified fields are checked - fields without matchers are ignored
|
|
81
|
-
# - Hash with :all key: { all: ->(recorded, actual) { ... } } receives full command hashes
|
|
82
|
-
# Can be combined with field matchers - all specified matchers must pass
|
|
83
94
|
# @param filter [Proc] Custom filter for recorded data
|
|
84
|
-
# @return [
|
|
85
|
-
def
|
|
95
|
+
# @return [BackspinResult] Aggregate result for this run
|
|
96
|
+
def capture(record_name, mode: :auto, matcher: nil, filter: nil, &block)
|
|
86
97
|
raise ArgumentError, "record_name is required" if record_name.nil? || record_name.empty?
|
|
87
98
|
raise ArgumentError, "block is required" unless block_given?
|
|
88
99
|
|
|
100
|
+
perform_capture(record_name, mode: mode, matcher: matcher, filter: filter, &block)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def perform_capture(record_name, mode:, matcher:, filter:, &block)
|
|
89
106
|
record_path = Record.build_record_path(record_name)
|
|
90
107
|
mode = determine_mode(mode, record_path)
|
|
108
|
+
validate_mode!(mode)
|
|
91
109
|
|
|
92
|
-
# Create or load the record based on mode
|
|
93
110
|
record = if mode == :record
|
|
94
111
|
Record.create(record_name)
|
|
95
112
|
else
|
|
96
113
|
Record.load_or_create(record_path)
|
|
97
114
|
end
|
|
98
115
|
|
|
99
|
-
# Create recorder with all needed context
|
|
100
116
|
recorder = Recorder.new(record: record, mode: mode, matcher: matcher, filter: filter)
|
|
101
117
|
|
|
102
|
-
# Execute the appropriate mode
|
|
103
118
|
result = case mode
|
|
104
119
|
when :record
|
|
105
|
-
recorder.
|
|
106
|
-
recorder.perform_recording(&block)
|
|
120
|
+
recorder.perform_capture_recording(&block)
|
|
107
121
|
when :verify
|
|
108
|
-
recorder.
|
|
109
|
-
when :playback
|
|
110
|
-
recorder.perform_playback(&block)
|
|
122
|
+
recorder.perform_capture_verification(&block)
|
|
111
123
|
else
|
|
112
124
|
raise ArgumentError, "Unknown mode: #{mode}"
|
|
113
125
|
end
|
|
114
126
|
|
|
115
|
-
|
|
116
|
-
if configuration.raise_on_verification_failure && result.verified? == false
|
|
117
|
-
error_message = "Backspin verification failed!\n"
|
|
118
|
-
error_message += "Record: #{result.record.path}\n"
|
|
119
|
-
error_message += "\n#{result.error_message}" if result.error_message
|
|
120
|
-
|
|
121
|
-
raise RSpec::Expectations::ExpectationNotMetError, error_message
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
result
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Strict version of run that raises on verification failure
|
|
128
|
-
#
|
|
129
|
-
# @param record_name [String] Name for the record file
|
|
130
|
-
# @param mode [Symbol] Recording mode - :auto, :record, :verify, :playback
|
|
131
|
-
# @param matcher [Proc, Hash] Custom matcher for verification
|
|
132
|
-
# @param filter [Proc] Custom filter for recorded data
|
|
133
|
-
# @return [RecordResult] Result object with output and status
|
|
134
|
-
# @raise [RSpec::Expectations::ExpectationNotMetError] If verification fails
|
|
135
|
-
def run!(record_name, mode: :auto, matcher: nil, filter: nil, &block)
|
|
136
|
-
result = run(record_name, mode: mode, matcher: matcher, filter: filter, &block)
|
|
137
|
-
|
|
138
|
-
if result.verified? == false
|
|
139
|
-
error_message = "Backspin verification failed!\n"
|
|
140
|
-
error_message += "Record: #{result.record.path}\n"
|
|
141
|
-
|
|
142
|
-
# Use the error_message from the result which is now properly formatted
|
|
143
|
-
error_message += "\n#{result.error_message}" if result.error_message
|
|
144
|
-
|
|
145
|
-
raise RSpec::Expectations::ExpectationNotMetError, error_message
|
|
146
|
-
end
|
|
127
|
+
raise_on_verification_failure!(result)
|
|
147
128
|
|
|
148
129
|
result
|
|
149
130
|
end
|
|
150
131
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# @param record_name [String] Name for the record file
|
|
154
|
-
# @param mode [Symbol] Recording mode - :auto, :record, :verify, :playback
|
|
155
|
-
# @param matcher [Proc, Hash] Custom matcher for verification
|
|
156
|
-
# @param filter [Proc] Custom filter for recorded data
|
|
157
|
-
# @return [RecordResult] Result object with captured output
|
|
158
|
-
def capture(record_name, mode: :auto, matcher: nil, filter: nil, &block)
|
|
159
|
-
raise ArgumentError, "record_name is required" if record_name.nil? || record_name.empty?
|
|
160
|
-
raise ArgumentError, "block is required" unless block_given?
|
|
161
|
-
|
|
162
|
-
record_path = Record.build_record_path(record_name)
|
|
132
|
+
def perform_command_run(command, name:, env:, mode:, matcher:, filter:)
|
|
133
|
+
record_path = Record.build_record_path(name)
|
|
163
134
|
mode = determine_mode(mode, record_path)
|
|
135
|
+
validate_mode!(mode)
|
|
164
136
|
|
|
165
|
-
# Create or load the record based on mode
|
|
166
137
|
record = if mode == :record
|
|
167
|
-
Record.create(
|
|
138
|
+
Record.create(name)
|
|
168
139
|
else
|
|
169
140
|
Record.load_or_create(record_path)
|
|
170
141
|
end
|
|
171
142
|
|
|
172
|
-
|
|
173
|
-
recorder = Recorder.new(record: record, mode: mode, matcher: matcher, filter: filter)
|
|
143
|
+
normalized_env = env.nil? ? nil : normalize_env(env)
|
|
174
144
|
|
|
175
|
-
# Execute the appropriate mode
|
|
176
145
|
result = case mode
|
|
177
146
|
when :record
|
|
178
|
-
|
|
147
|
+
stdout, stderr, status = execute_command(command, normalized_env)
|
|
148
|
+
actual_snapshot = Snapshot.new(
|
|
149
|
+
command_type: Open3::Capture3,
|
|
150
|
+
args: command,
|
|
151
|
+
env: normalized_env,
|
|
152
|
+
stdout: stdout,
|
|
153
|
+
stderr: stderr,
|
|
154
|
+
status: status.exitstatus,
|
|
155
|
+
recorded_at: Time.now.iso8601
|
|
156
|
+
)
|
|
157
|
+
record.set_snapshot(actual_snapshot)
|
|
158
|
+
record.save(filter: filter)
|
|
159
|
+
BackspinResult.new(
|
|
160
|
+
mode: :record,
|
|
161
|
+
record_path: record.path,
|
|
162
|
+
actual: actual_snapshot,
|
|
163
|
+
output: [stdout, stderr, status]
|
|
164
|
+
)
|
|
179
165
|
when :verify
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
166
|
+
raise RecordNotFoundError, "Record not found: #{record.path}" unless record.exists?
|
|
167
|
+
raise RecordNotFoundError, "No snapshot found in record #{record.path}" if record.empty?
|
|
168
|
+
|
|
169
|
+
expected_snapshot = record.snapshot
|
|
170
|
+
unless expected_snapshot.command_type == Open3::Capture3
|
|
171
|
+
raise RecordFormatError, "Invalid record format: expected Open3::Capture3 for run"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
stdout, stderr, status = execute_command(command, normalized_env)
|
|
175
|
+
actual_snapshot = Snapshot.new(
|
|
176
|
+
command_type: Open3::Capture3,
|
|
177
|
+
args: command,
|
|
178
|
+
env: normalized_env,
|
|
179
|
+
stdout: stdout,
|
|
180
|
+
stderr: stderr,
|
|
181
|
+
status: status.exitstatus
|
|
182
|
+
)
|
|
183
|
+
command_diff = CommandDiff.new(expected: expected_snapshot, actual: actual_snapshot, matcher: matcher)
|
|
184
|
+
BackspinResult.new(
|
|
185
|
+
mode: :verify,
|
|
186
|
+
record_path: record.path,
|
|
187
|
+
actual: actual_snapshot,
|
|
188
|
+
expected: expected_snapshot,
|
|
189
|
+
verified: command_diff.verified?,
|
|
190
|
+
command_diff: command_diff,
|
|
191
|
+
output: [stdout, stderr, status]
|
|
192
|
+
)
|
|
183
193
|
else
|
|
184
194
|
raise ArgumentError, "Unknown mode: #{mode}"
|
|
185
195
|
end
|
|
186
196
|
|
|
187
|
-
|
|
188
|
-
if configuration.raise_on_verification_failure && result.verified? == false
|
|
189
|
-
error_message = "Backspin verification failed!\n"
|
|
190
|
-
error_message += "Record: #{result.record.path}\n"
|
|
191
|
-
error_message += result.error_message || "Output verification failed"
|
|
197
|
+
raise_on_verification_failure!(result)
|
|
192
198
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
error_message += "\n\nDiff:\n#{result.diff}"
|
|
196
|
-
end
|
|
199
|
+
result
|
|
200
|
+
end
|
|
197
201
|
|
|
198
|
-
|
|
199
|
-
|
|
202
|
+
def normalize_env(env)
|
|
203
|
+
raise ArgumentError, "env must be a Hash" unless env.is_a?(Hash)
|
|
200
204
|
|
|
201
|
-
|
|
205
|
+
env.empty? ? nil : env
|
|
202
206
|
end
|
|
203
207
|
|
|
204
|
-
|
|
208
|
+
def execute_command(command, env)
|
|
209
|
+
case command
|
|
210
|
+
when String
|
|
211
|
+
env ? Open3.capture3(env, command) : Open3.capture3(command)
|
|
212
|
+
when Array
|
|
213
|
+
raise ArgumentError, "command array cannot be empty" if command.empty?
|
|
214
|
+
env ? Open3.capture3(env, *command) : Open3.capture3(*command)
|
|
215
|
+
else
|
|
216
|
+
raise ArgumentError, "command must be a String or Array"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def raise_on_verification_failure!(result)
|
|
221
|
+
return unless configuration.raise_on_verification_failure && result.verified? == false
|
|
222
|
+
|
|
223
|
+
error_message = "Backspin verification failed!\n"
|
|
224
|
+
error_message += "Record: #{result.record_path}\n"
|
|
225
|
+
details = result.error_message || result.diff
|
|
226
|
+
error_message += "\n#{details}" if details
|
|
227
|
+
|
|
228
|
+
raise VerificationError.new(error_message, result: result)
|
|
229
|
+
end
|
|
205
230
|
|
|
206
231
|
def determine_mode(mode_option, record_path)
|
|
207
232
|
return mode_option if mode_option && mode_option != :auto
|
|
@@ -209,5 +234,13 @@ module Backspin
|
|
|
209
234
|
# Auto mode: record if file doesn't exist, verify if it does
|
|
210
235
|
File.exist?(record_path) ? :verify : :record
|
|
211
236
|
end
|
|
237
|
+
|
|
238
|
+
def validate_mode!(mode)
|
|
239
|
+
return if %i[record verify].include?(mode)
|
|
240
|
+
|
|
241
|
+
raise ArgumentError, "Playback mode is not supported" if mode == :playback
|
|
242
|
+
|
|
243
|
+
raise ArgumentError, "Unknown mode: #{mode}"
|
|
244
|
+
end
|
|
212
245
|
end
|
|
213
246
|
end
|
metadata
CHANGED
|
@@ -1,42 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: backspin
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rob Sanheim
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: ostruct
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - ">="
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0'
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - ">="
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: rspec-mocks
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '3'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '3'
|
|
11
|
+
dependencies: []
|
|
40
12
|
description: Backspin is a Ruby library for characterization testing of command-line
|
|
41
13
|
interfaces. Inspired by VCR's cassette-based approach, it records and replays CLI
|
|
42
14
|
interactions to make testing faster and more deterministic.
|
|
@@ -50,6 +22,7 @@ files:
|
|
|
50
22
|
- ".gem_release.yml"
|
|
51
23
|
- ".gitignore"
|
|
52
24
|
- ".rspec"
|
|
25
|
+
- ".ruby-version"
|
|
53
26
|
- ".standard.yml"
|
|
54
27
|
- CHANGELOG.md
|
|
55
28
|
- CLAUDE.md
|
|
@@ -64,6 +37,7 @@ files:
|
|
|
64
37
|
- bin/rake
|
|
65
38
|
- bin/rspec
|
|
66
39
|
- bin/setup
|
|
40
|
+
- docs/backspin-result-api-sketch.md
|
|
67
41
|
- examples/match_on_example.rb
|
|
68
42
|
- fixtures/backspin/all_and_fields.yml
|
|
69
43
|
- fixtures/backspin/all_bypass_equality.yml
|
|
@@ -121,14 +95,13 @@ files:
|
|
|
121
95
|
- fixtures/backspin/verify_system_diff.yml
|
|
122
96
|
- fixtures/backspin/version_test.yml
|
|
123
97
|
- lib/backspin.rb
|
|
124
|
-
- lib/backspin/
|
|
98
|
+
- lib/backspin/backspin_result.rb
|
|
125
99
|
- lib/backspin/command_diff.rb
|
|
126
|
-
- lib/backspin/command_result.rb
|
|
127
100
|
- lib/backspin/configuration.rb
|
|
128
101
|
- lib/backspin/matcher.rb
|
|
129
102
|
- lib/backspin/record.rb
|
|
130
|
-
- lib/backspin/record_result.rb
|
|
131
103
|
- lib/backspin/recorder.rb
|
|
104
|
+
- lib/backspin/snapshot.rb
|
|
132
105
|
- lib/backspin/version.rb
|
|
133
106
|
- release.rake
|
|
134
107
|
- script/lint
|
|
@@ -154,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
154
127
|
- !ruby/object:Gem::Version
|
|
155
128
|
version: '0'
|
|
156
129
|
requirements: []
|
|
157
|
-
rubygems_version:
|
|
130
|
+
rubygems_version: 4.0.3
|
|
158
131
|
specification_version: 4
|
|
159
132
|
summary: Record and replay CLI interactions for testing
|
|
160
133
|
test_files: []
|
data/lib/backspin/command.rb
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "command_result"
|
|
4
|
-
|
|
5
|
-
module Backspin
|
|
6
|
-
class Command
|
|
7
|
-
attr_reader :args, :result, :recorded_at, :method_class
|
|
8
|
-
|
|
9
|
-
def initialize(method_class:, args:, stdout: nil, stderr: nil, status: nil, result: nil, recorded_at: nil)
|
|
10
|
-
@method_class = method_class
|
|
11
|
-
@args = args
|
|
12
|
-
@recorded_at = recorded_at
|
|
13
|
-
|
|
14
|
-
# Accept either a CommandResult or individual stdout/stderr/status
|
|
15
|
-
@result = result || CommandResult.new(stdout: stdout || "", stderr: stderr || "", status: status || 0)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def stdout
|
|
19
|
-
@result.stdout
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def stderr
|
|
23
|
-
@result.stderr
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def status
|
|
27
|
-
@result.status
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Convert to hash for YAML serialization
|
|
31
|
-
def to_h(filter: nil)
|
|
32
|
-
data = {
|
|
33
|
-
"command_type" => @method_class.name,
|
|
34
|
-
"args" => scrub_args(@args),
|
|
35
|
-
"stdout" => Backspin.scrub_text(@result.stdout),
|
|
36
|
-
"stderr" => Backspin.scrub_text(@result.stderr),
|
|
37
|
-
"status" => @result.status,
|
|
38
|
-
"recorded_at" => @recorded_at
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
# Apply filter if provided
|
|
42
|
-
data = filter.call(data) if filter
|
|
43
|
-
|
|
44
|
-
data
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Create from hash (for loading from YAML)
|
|
48
|
-
def self.from_h(data)
|
|
49
|
-
# Determine method class from command_type
|
|
50
|
-
method_class = case data["command_type"]
|
|
51
|
-
when "Open3::Capture3"
|
|
52
|
-
Open3::Capture3
|
|
53
|
-
when "Kernel::System"
|
|
54
|
-
::Kernel::System
|
|
55
|
-
when "Backspin::Capturer"
|
|
56
|
-
Backspin::Capturer
|
|
57
|
-
else
|
|
58
|
-
# Default to capture3 for backwards compatibility
|
|
59
|
-
Open3::Capture3
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
new(
|
|
63
|
-
method_class: method_class,
|
|
64
|
-
args: data["args"],
|
|
65
|
-
stdout: data["stdout"],
|
|
66
|
-
stderr: data["stderr"],
|
|
67
|
-
status: data["status"],
|
|
68
|
-
recorded_at: data["recorded_at"]
|
|
69
|
-
)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
def scrub_args(args)
|
|
75
|
-
return args unless Backspin.configuration.scrub_credentials && args
|
|
76
|
-
|
|
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
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Define the Open3::Capture3 class for identification
|
|
94
|
-
module Open3
|
|
95
|
-
class Capture3; end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Define the Kernel::System class for identification
|
|
99
|
-
module ::Kernel
|
|
100
|
-
class System; end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Define the Backspin::Capturer class for identification
|
|
104
|
-
module Backspin
|
|
105
|
-
class Capturer; end
|
|
106
|
-
end
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Backspin
|
|
4
|
-
# Represents the result of executing a command
|
|
5
|
-
# Stores stdout, stderr, and exit status
|
|
6
|
-
class CommandResult
|
|
7
|
-
attr_reader :stdout, :stderr, :status
|
|
8
|
-
|
|
9
|
-
def initialize(stdout:, stderr:, status:)
|
|
10
|
-
@stdout = stdout
|
|
11
|
-
@stderr = stderr
|
|
12
|
-
@status = normalize_status(status)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# @return [Boolean] true if the command succeeded (exit status 0)
|
|
16
|
-
def success?
|
|
17
|
-
status.zero?
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# @return [Boolean] true if the command failed (non-zero exit status)
|
|
21
|
-
def failure?
|
|
22
|
-
!success?
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# @return [Hash] Hash representation of the result
|
|
26
|
-
def to_h
|
|
27
|
-
{
|
|
28
|
-
"stdout" => stdout,
|
|
29
|
-
"stderr" => stderr,
|
|
30
|
-
"status" => status
|
|
31
|
-
}
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Compare two results for equality
|
|
35
|
-
def ==(other)
|
|
36
|
-
return false unless other.is_a?(CommandResult)
|
|
37
|
-
|
|
38
|
-
stdout == other.stdout && stderr == other.stderr && status == other.status
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def inspect
|
|
42
|
-
"#<Backspin::CommandResult status=#{status} stdout=#{stdout} stderr=#{stderr}>"
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
def normalize_status(status)
|
|
48
|
-
case status
|
|
49
|
-
when Integer
|
|
50
|
-
status
|
|
51
|
-
when Process::Status
|
|
52
|
-
status.exitstatus
|
|
53
|
-
else
|
|
54
|
-
status.respond_to?(:exitstatus) ? status.exitstatus : status.to_i
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|