backspin 0.8.0 → 0.10.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 +14 -0
- data/CLAUDE.md +13 -8
- data/CONTRIBUTING.md +11 -7
- data/Gemfile.lock +1 -1
- data/MATCHERS.md +18 -0
- data/README.md +42 -6
- data/docs/backspin-result-api-sketch.md +205 -0
- data/lib/backspin/backspin_result.rb +66 -0
- data/lib/backspin/command_diff.rb +80 -24
- data/lib/backspin/matcher.rb +65 -63
- data/lib/backspin/record.rb +36 -23
- data/lib/backspin/recorder.rb +28 -21
- data/lib/backspin/snapshot.rb +129 -0
- data/lib/backspin/version.rb +1 -1
- data/lib/backspin.rb +65 -37
- metadata +4 -4
- data/lib/backspin/command.rb +0 -117
- data/lib/backspin/command_result.rb +0 -58
- data/lib/backspin/record_result.rb +0 -153
data/lib/backspin.rb
CHANGED
|
@@ -6,13 +6,12 @@ require "open3"
|
|
|
6
6
|
require "pathname"
|
|
7
7
|
require "backspin/version"
|
|
8
8
|
require "backspin/configuration"
|
|
9
|
-
require "backspin/
|
|
10
|
-
require "backspin/command"
|
|
9
|
+
require "backspin/snapshot"
|
|
11
10
|
require "backspin/matcher"
|
|
12
11
|
require "backspin/command_diff"
|
|
13
12
|
require "backspin/record"
|
|
13
|
+
require "backspin/backspin_result"
|
|
14
14
|
require "backspin/recorder"
|
|
15
|
-
require "backspin/record_result"
|
|
16
15
|
|
|
17
16
|
module Backspin
|
|
18
17
|
class RecordNotFoundError < StandardError; end
|
|
@@ -29,12 +28,12 @@ module Backspin
|
|
|
29
28
|
result.diff
|
|
30
29
|
end
|
|
31
30
|
|
|
32
|
-
def
|
|
33
|
-
result.
|
|
31
|
+
def expected_snapshot
|
|
32
|
+
result.expected
|
|
34
33
|
end
|
|
35
34
|
|
|
36
|
-
def
|
|
37
|
-
result.
|
|
35
|
+
def actual_snapshot
|
|
36
|
+
result.actual
|
|
38
37
|
end
|
|
39
38
|
end
|
|
40
39
|
|
|
@@ -72,19 +71,30 @@ module Backspin
|
|
|
72
71
|
# @param env [Hash] Environment variables to pass to Open3.capture3
|
|
73
72
|
# @param mode [Symbol] Recording mode - :auto, :record, :verify
|
|
74
73
|
# @param matcher [Proc, Hash] Custom matcher for verification
|
|
75
|
-
# @param filter [Proc] Custom filter for recorded data
|
|
76
|
-
# @
|
|
77
|
-
|
|
74
|
+
# @param filter [Proc] Custom filter for recorded data/canonicalization
|
|
75
|
+
# @param filter_on [Symbol] Filter application mode - :both (default), :record
|
|
76
|
+
# @return [BackspinResult] Aggregate result for this run
|
|
77
|
+
def run(command = nil, name:, env: nil, mode: :auto, matcher: nil, filter: nil, filter_on: :both, &block)
|
|
78
|
+
validate_filter_on!(filter_on)
|
|
79
|
+
|
|
78
80
|
if block_given?
|
|
79
81
|
raise ArgumentError, "command must be omitted when using a block" unless command.nil?
|
|
80
82
|
raise ArgumentError, "env is not supported when using a block" unless env.nil?
|
|
81
83
|
|
|
82
|
-
return perform_capture(name, mode: mode, matcher: matcher, filter: filter, &block)
|
|
84
|
+
return perform_capture(name, mode: mode, matcher: matcher, filter: filter, filter_on: filter_on, &block)
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
raise ArgumentError, "command is required" if command.nil?
|
|
86
88
|
|
|
87
|
-
perform_command_run(
|
|
89
|
+
perform_command_run(
|
|
90
|
+
command,
|
|
91
|
+
name: name,
|
|
92
|
+
env: env,
|
|
93
|
+
mode: mode,
|
|
94
|
+
matcher: matcher,
|
|
95
|
+
filter: filter,
|
|
96
|
+
filter_on: filter_on
|
|
97
|
+
)
|
|
88
98
|
end
|
|
89
99
|
|
|
90
100
|
# Captures all stdout/stderr output from a block
|
|
@@ -92,18 +102,20 @@ module Backspin
|
|
|
92
102
|
# @param record_name [String] Name for the record file
|
|
93
103
|
# @param mode [Symbol] Recording mode - :auto, :record, :verify
|
|
94
104
|
# @param matcher [Proc, Hash] Custom matcher for verification
|
|
95
|
-
# @param filter [Proc] Custom filter for recorded data
|
|
96
|
-
# @
|
|
97
|
-
|
|
105
|
+
# @param filter [Proc] Custom filter for recorded data/canonicalization
|
|
106
|
+
# @param filter_on [Symbol] Filter application mode - :both (default), :record
|
|
107
|
+
# @return [BackspinResult] Aggregate result for this run
|
|
108
|
+
def capture(record_name, mode: :auto, matcher: nil, filter: nil, filter_on: :both, &block)
|
|
98
109
|
raise ArgumentError, "record_name is required" if record_name.nil? || record_name.empty?
|
|
99
110
|
raise ArgumentError, "block is required" unless block_given?
|
|
111
|
+
validate_filter_on!(filter_on)
|
|
100
112
|
|
|
101
|
-
perform_capture(record_name, mode: mode, matcher: matcher, filter: filter, &block)
|
|
113
|
+
perform_capture(record_name, mode: mode, matcher: matcher, filter: filter, filter_on: filter_on, &block)
|
|
102
114
|
end
|
|
103
115
|
|
|
104
116
|
private
|
|
105
117
|
|
|
106
|
-
def perform_capture(record_name, mode:, matcher:, filter:, &block)
|
|
118
|
+
def perform_capture(record_name, mode:, matcher:, filter:, filter_on:, &block)
|
|
107
119
|
record_path = Record.build_record_path(record_name)
|
|
108
120
|
mode = determine_mode(mode, record_path)
|
|
109
121
|
validate_mode!(mode)
|
|
@@ -114,7 +126,7 @@ module Backspin
|
|
|
114
126
|
Record.load_or_create(record_path)
|
|
115
127
|
end
|
|
116
128
|
|
|
117
|
-
recorder = Recorder.new(record: record, mode: mode, matcher: matcher, filter: filter)
|
|
129
|
+
recorder = Recorder.new(record: record, mode: mode, matcher: matcher, filter: filter, filter_on: filter_on)
|
|
118
130
|
|
|
119
131
|
result = case mode
|
|
120
132
|
when :record
|
|
@@ -130,7 +142,7 @@ module Backspin
|
|
|
130
142
|
result
|
|
131
143
|
end
|
|
132
144
|
|
|
133
|
-
def perform_command_run(command, name:, env:, mode:, matcher:, filter:)
|
|
145
|
+
def perform_command_run(command, name:, env:, mode:, matcher:, filter:, filter_on:)
|
|
134
146
|
record_path = Record.build_record_path(name)
|
|
135
147
|
mode = determine_mode(mode, record_path)
|
|
136
148
|
validate_mode!(mode)
|
|
@@ -146,8 +158,8 @@ module Backspin
|
|
|
146
158
|
result = case mode
|
|
147
159
|
when :record
|
|
148
160
|
stdout, stderr, status = execute_command(command, normalized_env)
|
|
149
|
-
|
|
150
|
-
|
|
161
|
+
actual_snapshot = Snapshot.new(
|
|
162
|
+
command_type: Open3::Capture3,
|
|
151
163
|
args: command,
|
|
152
164
|
env: normalized_env,
|
|
153
165
|
stdout: stdout,
|
|
@@ -155,37 +167,47 @@ module Backspin
|
|
|
155
167
|
status: status.exitstatus,
|
|
156
168
|
recorded_at: Time.now.iso8601
|
|
157
169
|
)
|
|
158
|
-
record.
|
|
170
|
+
record.set_snapshot(actual_snapshot)
|
|
159
171
|
record.save(filter: filter)
|
|
160
|
-
|
|
172
|
+
BackspinResult.new(
|
|
173
|
+
mode: :record,
|
|
174
|
+
record_path: record.path,
|
|
175
|
+
actual: actual_snapshot,
|
|
176
|
+
output: [stdout, stderr, status]
|
|
177
|
+
)
|
|
161
178
|
when :verify
|
|
162
179
|
raise RecordNotFoundError, "Record not found: #{record.path}" unless record.exists?
|
|
163
|
-
raise RecordNotFoundError, "No
|
|
164
|
-
if record.commands.size != 1
|
|
165
|
-
raise RecordFormatError, "Invalid record format: expected 1 command for run, found #{record.commands.size}"
|
|
166
|
-
end
|
|
180
|
+
raise RecordNotFoundError, "No snapshot found in record #{record.path}" if record.empty?
|
|
167
181
|
|
|
168
|
-
|
|
169
|
-
unless
|
|
182
|
+
expected_snapshot = record.snapshot
|
|
183
|
+
unless expected_snapshot.command_type == Open3::Capture3
|
|
170
184
|
raise RecordFormatError, "Invalid record format: expected Open3::Capture3 for run"
|
|
171
185
|
end
|
|
172
186
|
|
|
173
187
|
stdout, stderr, status = execute_command(command, normalized_env)
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
actual_snapshot = Snapshot.new(
|
|
189
|
+
command_type: Open3::Capture3,
|
|
176
190
|
args: command,
|
|
177
191
|
env: normalized_env,
|
|
178
192
|
stdout: stdout,
|
|
179
193
|
stderr: stderr,
|
|
180
194
|
status: status.exitstatus
|
|
181
195
|
)
|
|
182
|
-
command_diff = CommandDiff.new(
|
|
183
|
-
|
|
184
|
-
|
|
196
|
+
command_diff = CommandDiff.new(
|
|
197
|
+
expected: expected_snapshot,
|
|
198
|
+
actual: actual_snapshot,
|
|
199
|
+
matcher: matcher,
|
|
200
|
+
filter: filter,
|
|
201
|
+
filter_on: filter_on
|
|
202
|
+
)
|
|
203
|
+
BackspinResult.new(
|
|
185
204
|
mode: :verify,
|
|
205
|
+
record_path: record.path,
|
|
206
|
+
actual: actual_snapshot,
|
|
207
|
+
expected: expected_snapshot,
|
|
186
208
|
verified: command_diff.verified?,
|
|
187
|
-
|
|
188
|
-
|
|
209
|
+
command_diff: command_diff,
|
|
210
|
+
output: [stdout, stderr, status]
|
|
189
211
|
)
|
|
190
212
|
else
|
|
191
213
|
raise ArgumentError, "Unknown mode: #{mode}"
|
|
@@ -218,7 +240,7 @@ module Backspin
|
|
|
218
240
|
return unless configuration.raise_on_verification_failure && result.verified? == false
|
|
219
241
|
|
|
220
242
|
error_message = "Backspin verification failed!\n"
|
|
221
|
-
error_message += "Record: #{result.
|
|
243
|
+
error_message += "Record: #{result.record_path}\n"
|
|
222
244
|
details = result.error_message || result.diff
|
|
223
245
|
error_message += "\n#{details}" if details
|
|
224
246
|
|
|
@@ -239,5 +261,11 @@ module Backspin
|
|
|
239
261
|
|
|
240
262
|
raise ArgumentError, "Unknown mode: #{mode}"
|
|
241
263
|
end
|
|
264
|
+
|
|
265
|
+
def validate_filter_on!(filter_on)
|
|
266
|
+
return if %i[both record].include?(filter_on)
|
|
267
|
+
|
|
268
|
+
raise ArgumentError, "Unknown filter_on: #{filter_on}. Must be :both or :record"
|
|
269
|
+
end
|
|
242
270
|
end
|
|
243
271
|
end
|
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.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rob Sanheim
|
|
@@ -37,6 +37,7 @@ files:
|
|
|
37
37
|
- bin/rake
|
|
38
38
|
- bin/rspec
|
|
39
39
|
- bin/setup
|
|
40
|
+
- docs/backspin-result-api-sketch.md
|
|
40
41
|
- examples/match_on_example.rb
|
|
41
42
|
- fixtures/backspin/all_and_fields.yml
|
|
42
43
|
- fixtures/backspin/all_bypass_equality.yml
|
|
@@ -94,14 +95,13 @@ files:
|
|
|
94
95
|
- fixtures/backspin/verify_system_diff.yml
|
|
95
96
|
- fixtures/backspin/version_test.yml
|
|
96
97
|
- lib/backspin.rb
|
|
97
|
-
- lib/backspin/
|
|
98
|
+
- lib/backspin/backspin_result.rb
|
|
98
99
|
- lib/backspin/command_diff.rb
|
|
99
|
-
- lib/backspin/command_result.rb
|
|
100
100
|
- lib/backspin/configuration.rb
|
|
101
101
|
- lib/backspin/matcher.rb
|
|
102
102
|
- lib/backspin/record.rb
|
|
103
|
-
- lib/backspin/record_result.rb
|
|
104
103
|
- lib/backspin/recorder.rb
|
|
104
|
+
- lib/backspin/snapshot.rb
|
|
105
105
|
- lib/backspin/version.rb
|
|
106
106
|
- release.rake
|
|
107
107
|
- script/lint
|
data/lib/backspin/command.rb
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "command_result"
|
|
4
|
-
|
|
5
|
-
module Backspin
|
|
6
|
-
class Command
|
|
7
|
-
attr_reader :args, :env, :result, :recorded_at, :method_class
|
|
8
|
-
|
|
9
|
-
def initialize(method_class:, args:, env: nil, stdout: nil, stderr: nil, status: nil, result: nil, recorded_at: nil)
|
|
10
|
-
@method_class = method_class
|
|
11
|
-
@args = args
|
|
12
|
-
@env = env
|
|
13
|
-
@recorded_at = recorded_at
|
|
14
|
-
|
|
15
|
-
# Accept either a CommandResult or individual stdout/stderr/status
|
|
16
|
-
@result = result || CommandResult.new(stdout: stdout || "", stderr: stderr || "", status: status || 0)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def stdout
|
|
20
|
-
@result.stdout
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def stderr
|
|
24
|
-
@result.stderr
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def status
|
|
28
|
-
@result.status
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Convert to hash for YAML serialization
|
|
32
|
-
def to_h(filter: nil)
|
|
33
|
-
data = {
|
|
34
|
-
"command_type" => @method_class.name,
|
|
35
|
-
"args" => scrub_args(@args),
|
|
36
|
-
"stdout" => Backspin.scrub_text(@result.stdout),
|
|
37
|
-
"stderr" => Backspin.scrub_text(@result.stderr),
|
|
38
|
-
"status" => @result.status,
|
|
39
|
-
"recorded_at" => @recorded_at
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
data["env"] = scrub_env(@env) if @env
|
|
43
|
-
|
|
44
|
-
# Apply filter if provided
|
|
45
|
-
data = filter.call(data) if filter
|
|
46
|
-
|
|
47
|
-
data
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Create from hash (for loading from YAML)
|
|
51
|
-
def self.from_h(data)
|
|
52
|
-
# Determine method class from command_type
|
|
53
|
-
method_class = case data["command_type"]
|
|
54
|
-
when "Open3::Capture3"
|
|
55
|
-
Open3::Capture3
|
|
56
|
-
when "Backspin::Capturer"
|
|
57
|
-
Backspin::Capturer
|
|
58
|
-
else
|
|
59
|
-
raise RecordFormatError, "Unknown command type: #{data["command_type"]}"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
new(
|
|
63
|
-
method_class: method_class,
|
|
64
|
-
args: data["args"],
|
|
65
|
-
env: data["env"],
|
|
66
|
-
stdout: data["stdout"],
|
|
67
|
-
stderr: data["stderr"],
|
|
68
|
-
status: data["status"],
|
|
69
|
-
recorded_at: data["recorded_at"]
|
|
70
|
-
)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
private
|
|
74
|
-
|
|
75
|
-
def scrub_args(args)
|
|
76
|
-
return args unless Backspin.configuration.scrub_credentials && args
|
|
77
|
-
|
|
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
|
|
93
|
-
end
|
|
94
|
-
when Hash
|
|
95
|
-
args.transform_values { |v| v.is_a?(String) ? Backspin.scrub_text(v) : v }
|
|
96
|
-
else
|
|
97
|
-
args
|
|
98
|
-
end
|
|
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
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Define the Open3::Capture3 class for identification
|
|
110
|
-
module Open3
|
|
111
|
-
class Capture3; end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Define the Backspin::Capturer class for identification
|
|
115
|
-
module Backspin
|
|
116
|
-
class Capturer; end
|
|
117
|
-
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
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Backspin
|
|
4
|
-
# Result object for all Backspin record operations
|
|
5
|
-
# Provides a consistent interface whether recording, verifying, or playing back
|
|
6
|
-
class RecordResult
|
|
7
|
-
attr_reader :output, :commands, :mode, :command_diffs
|
|
8
|
-
attr_reader :record
|
|
9
|
-
|
|
10
|
-
def initialize(output:, mode:, record:, verified: nil, command_diffs: nil)
|
|
11
|
-
@output = output
|
|
12
|
-
@mode = mode
|
|
13
|
-
@record = record
|
|
14
|
-
@commands = record.commands
|
|
15
|
-
@verified = verified
|
|
16
|
-
@command_diffs = command_diffs || []
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# @return [Boolean] true if this result is from recording
|
|
20
|
-
def recorded?
|
|
21
|
-
mode == :record
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def record_path
|
|
25
|
-
record.path
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# @return [Boolean, nil] true/false for verification results, nil for recording
|
|
29
|
-
def verified?
|
|
30
|
-
return @verified unless mode == :verify
|
|
31
|
-
|
|
32
|
-
return false if command_diffs.size < commands.size
|
|
33
|
-
|
|
34
|
-
@verified
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# @return [String, nil] Human-readable error message if verification failed
|
|
38
|
-
def error_message
|
|
39
|
-
return nil unless verified? == false
|
|
40
|
-
|
|
41
|
-
# Check for command count mismatch first
|
|
42
|
-
if command_diffs.size < commands.size
|
|
43
|
-
return "Expected #{commands.size} commands but only #{command_diffs.size} were executed"
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
return "No commands to verify" if command_diffs.empty?
|
|
47
|
-
|
|
48
|
-
failed_diffs = command_diffs.reject(&:verified?)
|
|
49
|
-
return "All commands verified" if failed_diffs.empty?
|
|
50
|
-
|
|
51
|
-
msg = "Output verification failed for #{failed_diffs.size} command(s):\n\n"
|
|
52
|
-
|
|
53
|
-
command_diffs.each_with_index do |diff, idx|
|
|
54
|
-
next if diff.verified?
|
|
55
|
-
|
|
56
|
-
msg += "Command #{idx + 1}: #{diff.summary}\n"
|
|
57
|
-
msg += diff.diff
|
|
58
|
-
msg += "\n\n" if idx < command_diffs.size - 1
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
msg
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# @return [String, nil] Combined diff from all failed commands
|
|
65
|
-
def diff
|
|
66
|
-
return nil if command_diffs.empty?
|
|
67
|
-
|
|
68
|
-
failed_diffs = command_diffs.reject(&:verified?)
|
|
69
|
-
return nil if failed_diffs.empty?
|
|
70
|
-
|
|
71
|
-
diff_parts = []
|
|
72
|
-
command_diffs.each_with_index do |cmd_diff, idx|
|
|
73
|
-
diff_parts << "Command #{idx + 1}:\n#{cmd_diff.diff}" unless cmd_diff.verified?
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
diff_parts.join("\n\n")
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Convenience accessors for command output
|
|
80
|
-
# For single command (common case), these provide direct access
|
|
81
|
-
# For multiple commands, use all_stdout, all_stderr, etc.
|
|
82
|
-
|
|
83
|
-
# @return [String, nil] stdout from the first command
|
|
84
|
-
def stdout
|
|
85
|
-
commands.first&.result&.stdout
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# @return [String, nil] stderr from the first command
|
|
89
|
-
def stderr
|
|
90
|
-
commands.first&.result&.stderr
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# @return [Integer, nil] exit status from the first command
|
|
94
|
-
def status
|
|
95
|
-
commands.first&.result&.status
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Multiple command accessors
|
|
99
|
-
|
|
100
|
-
# @return [Array<String>] stdout from all commands
|
|
101
|
-
def all_stdout
|
|
102
|
-
commands.map { |cmd| cmd.result.stdout }
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# @return [Array<String>] stderr from all commands
|
|
106
|
-
def all_stderr
|
|
107
|
-
commands.map { |cmd| cmd.result.stderr }
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# @return [Array<Integer>] exit status from all commands
|
|
111
|
-
def all_status
|
|
112
|
-
commands.map { |cmd| cmd.result.status }
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# @return [Boolean] true if this result contains multiple commands
|
|
116
|
-
def multiple_commands?
|
|
117
|
-
commands.size > 1
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# @return [Boolean] true if all commands succeeded (exit status 0)
|
|
121
|
-
def success?
|
|
122
|
-
if multiple_commands?
|
|
123
|
-
# Check all commands - if any command has non-zero status, we're not successful
|
|
124
|
-
commands.all? { |cmd| cmd.result.status.zero? }
|
|
125
|
-
else
|
|
126
|
-
status&.zero? || false
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# @return [Boolean] true if any command failed (non-zero exit status)
|
|
131
|
-
def failure?
|
|
132
|
-
!success?
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# @return [Hash] Summary of the result for debugging
|
|
136
|
-
def to_h
|
|
137
|
-
hash = {
|
|
138
|
-
mode: mode,
|
|
139
|
-
recorded: recorded?,
|
|
140
|
-
stdout: stdout,
|
|
141
|
-
stderr: stderr,
|
|
142
|
-
status: status
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
hash[:verified] = verified? unless verified?.nil?
|
|
146
|
-
hash[:diff] = diff if diff
|
|
147
|
-
# Include number of failed commands if in verify mode
|
|
148
|
-
hash[:failed_commands] = command_diffs.count { |d| !d.verified? } if mode == :verify && command_diffs.any?
|
|
149
|
-
|
|
150
|
-
hash
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|