backspin 0.3.0 → 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 +3 -1
- data/CHANGELOG.md +4 -0
- data/CLAUDE.md +4 -4
- data/CONTRIBUTING.md +1 -3
- data/Gemfile +1 -1
- data/Gemfile.lock +2 -2
- data/README.md +107 -33
- data/backspin.gemspec +2 -2
- data/bin/rake +27 -0
- data/bin/rspec +27 -0
- data/lib/backspin/command.rb +33 -14
- 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 +167 -285
- metadata +8 -4
@@ -0,0 +1,153 @@
|
|
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, :record_path, :commands, :mode, :command_diffs
|
8
|
+
|
9
|
+
def initialize(output:, mode:, record_path:, commands:, verified: nil, command_diffs: nil)
|
10
|
+
@output = output
|
11
|
+
@mode = mode
|
12
|
+
@record_path = record_path
|
13
|
+
@commands = commands
|
14
|
+
@verified = verified
|
15
|
+
@command_diffs = command_diffs || []
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Boolean] true if this result is from recording
|
19
|
+
def recorded?
|
20
|
+
mode == :record
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Boolean, nil] true/false for verification results, nil for recording
|
24
|
+
def verified?
|
25
|
+
@verified
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Boolean] true if this result is from playback mode
|
29
|
+
def playback?
|
30
|
+
mode == :playback
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [String, nil] Human-readable error message if verification failed
|
34
|
+
def error_message
|
35
|
+
return nil unless verified? == false
|
36
|
+
return "No commands to verify" if command_diffs.empty?
|
37
|
+
|
38
|
+
failed_diffs = command_diffs.reject(&:verified?)
|
39
|
+
return "All commands verified" if failed_diffs.empty?
|
40
|
+
|
41
|
+
msg = "Output verification failed for #{failed_diffs.size} command(s):\n\n"
|
42
|
+
|
43
|
+
command_diffs.each_with_index do |diff, idx|
|
44
|
+
next if diff.verified?
|
45
|
+
|
46
|
+
msg += "Command #{idx + 1}: #{diff.summary}\n"
|
47
|
+
msg += diff.diff
|
48
|
+
msg += "\n\n" if idx < command_diffs.size - 1
|
49
|
+
end
|
50
|
+
|
51
|
+
msg
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [String, nil] Combined diff from all failed commands
|
55
|
+
def diff
|
56
|
+
return nil if command_diffs.empty?
|
57
|
+
|
58
|
+
failed_diffs = command_diffs.reject(&:verified?)
|
59
|
+
return nil if failed_diffs.empty?
|
60
|
+
|
61
|
+
diff_parts = []
|
62
|
+
command_diffs.each_with_index do |cmd_diff, idx|
|
63
|
+
diff_parts << "Command #{idx + 1}:\n#{cmd_diff.diff}" unless cmd_diff.verified?
|
64
|
+
end
|
65
|
+
|
66
|
+
diff_parts.join("\n\n")
|
67
|
+
end
|
68
|
+
|
69
|
+
# Convenience accessors for command output
|
70
|
+
# For single command (common case), these provide direct access
|
71
|
+
# For multiple commands, use all_stdout, all_stderr, etc.
|
72
|
+
|
73
|
+
# @return [String, nil] stdout from the first command
|
74
|
+
def stdout
|
75
|
+
commands.first&.result&.stdout
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [String, nil] stderr from the first command
|
79
|
+
def stderr
|
80
|
+
commands.first&.result&.stderr
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [Integer, nil] exit status from the first command
|
84
|
+
def status
|
85
|
+
commands.first&.result&.status
|
86
|
+
end
|
87
|
+
|
88
|
+
# Multiple command accessors
|
89
|
+
|
90
|
+
# @return [Array<String>] stdout from all commands
|
91
|
+
def all_stdout
|
92
|
+
commands.map { |cmd| cmd.result.stdout }
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Array<String>] stderr from all commands
|
96
|
+
def all_stderr
|
97
|
+
commands.map { |cmd| cmd.result.stderr }
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [Array<Integer>] exit status from all commands
|
101
|
+
def all_status
|
102
|
+
commands.map { |cmd| cmd.result.status }
|
103
|
+
end
|
104
|
+
|
105
|
+
# @return [Boolean] true if this result contains multiple commands
|
106
|
+
def multiple_commands?
|
107
|
+
commands.size > 1
|
108
|
+
end
|
109
|
+
|
110
|
+
# @return [Boolean] true if all commands succeeded (exit status 0)
|
111
|
+
def success?
|
112
|
+
if multiple_commands?
|
113
|
+
# Check all commands - if any command has non-zero status, we're not successful
|
114
|
+
commands.all? { |cmd| cmd.result.status.zero? }
|
115
|
+
else
|
116
|
+
status&.zero? || false
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# @return [Boolean] true if any command failed (non-zero exit status)
|
121
|
+
def failure?
|
122
|
+
!success?
|
123
|
+
end
|
124
|
+
|
125
|
+
# @return [Hash] Summary of the result for debugging
|
126
|
+
def to_h
|
127
|
+
hash = {
|
128
|
+
mode: mode,
|
129
|
+
recorded: recorded?,
|
130
|
+
playback: playback?,
|
131
|
+
stdout: stdout,
|
132
|
+
stderr: stderr,
|
133
|
+
status: status,
|
134
|
+
record_path: record_path.to_s
|
135
|
+
}
|
136
|
+
|
137
|
+
# Only include verified if it's not nil
|
138
|
+
hash[:verified] = verified? unless verified?.nil?
|
139
|
+
|
140
|
+
# Only include diff if present
|
141
|
+
hash[:diff] = diff if diff
|
142
|
+
|
143
|
+
# Include number of failed commands if in verify mode
|
144
|
+
hash[:failed_commands] = command_diffs.count { |d| !d.verified? } if mode == :verify && command_diffs.any?
|
145
|
+
|
146
|
+
hash
|
147
|
+
end
|
148
|
+
|
149
|
+
def inspect
|
150
|
+
"#<Backspin::RecordResult mode=#{mode} verified=#{verified?.inspect} status=#{status}>"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/lib/backspin/recorder.rb
CHANGED
@@ -6,6 +6,7 @@ module Backspin
|
|
6
6
|
# Handles stubbing and recording of command executions
|
7
7
|
class Recorder
|
8
8
|
include RSpec::Mocks::ExampleMethods
|
9
|
+
SUPPORTED_COMMAND_TYPES = [:capture3, :system]
|
9
10
|
|
10
11
|
attr_reader :commands, :verification_data, :mode, :record
|
11
12
|
|
@@ -17,7 +18,7 @@ module Backspin
|
|
17
18
|
end
|
18
19
|
|
19
20
|
def record_calls(*command_types)
|
20
|
-
command_types =
|
21
|
+
command_types = SUPPORTED_COMMAND_TYPES if command_types.empty?
|
21
22
|
|
22
23
|
command_types.each do |command_type|
|
23
24
|
record_call(command_type)
|
@@ -31,7 +32,7 @@ module Backspin
|
|
31
32
|
when :capture3
|
32
33
|
setup_capture3_call_stub
|
33
34
|
else
|
34
|
-
raise ArgumentError, "
|
35
|
+
raise ArgumentError, "Unsupported command type: #{command_type} - currently supported types: #{SUPPORTED_COMMAND_TYPES.join(", ")}"
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
@@ -66,7 +67,6 @@ module Backspin
|
|
66
67
|
# For system calls, we only track the exit status
|
67
68
|
@verification_data["stdout"] = ""
|
68
69
|
@verification_data["stderr"] = ""
|
69
|
-
# Derive exit status from result: true = 0, false = non-zero
|
70
70
|
@verification_data["status"] = result ? 0 : 1
|
71
71
|
|
72
72
|
result
|
@@ -107,12 +107,10 @@ module Backspin
|
|
107
107
|
allow_any_instance_of(Object).to receive(:system) do |receiver, *args|
|
108
108
|
command = @record.next_command
|
109
109
|
|
110
|
-
# Make sure this is a system command
|
111
110
|
unless command.method_class == ::Kernel::System
|
112
111
|
raise RecordNotFoundError, "Expected Kernel::System command but got #{command.method_class.name}"
|
113
112
|
end
|
114
113
|
|
115
|
-
# Return true if exit status was 0, false otherwise
|
116
114
|
command.status == 0
|
117
115
|
rescue NoMoreRecordingsError => e
|
118
116
|
raise RecordNotFoundError, e.message
|
@@ -121,17 +119,14 @@ module Backspin
|
|
121
119
|
|
122
120
|
def setup_capture3_call_stub
|
123
121
|
allow(Open3).to receive(:capture3).and_wrap_original do |original_method, *args|
|
124
|
-
# Execute the real command
|
125
122
|
stdout, stderr, status = original_method.call(*args)
|
126
123
|
|
127
|
-
# Parse command args
|
128
124
|
cmd_args = if args.length == 1 && args.first.is_a?(String)
|
129
125
|
args.first.split(" ")
|
130
126
|
else
|
131
127
|
args
|
132
128
|
end
|
133
129
|
|
134
|
-
# Create command with interaction data
|
135
130
|
command = Command.new(
|
136
131
|
method_class: Open3::Capture3,
|
137
132
|
args: cmd_args,
|
@@ -142,17 +137,12 @@ module Backspin
|
|
142
137
|
)
|
143
138
|
@commands << command
|
144
139
|
|
145
|
-
# Store output for later access (last one wins)
|
146
|
-
Backspin.last_output = stdout
|
147
|
-
|
148
|
-
# Return original result
|
149
140
|
[stdout, stderr, status]
|
150
141
|
end
|
151
142
|
end
|
152
143
|
|
153
144
|
def setup_system_call_stub
|
154
145
|
allow_any_instance_of(Object).to receive(:system).and_wrap_original do |original_method, receiver, *args|
|
155
|
-
# Execute the real system call
|
156
146
|
result = original_method.call(receiver, *args)
|
157
147
|
|
158
148
|
# Parse command args based on how system was called
|
@@ -164,14 +154,9 @@ module Backspin
|
|
164
154
|
args
|
165
155
|
end
|
166
156
|
|
167
|
-
|
168
|
-
# The caller of system() doesn't have access to them
|
169
|
-
stdout = ""
|
170
|
-
stderr = ""
|
171
|
-
# Derive exit status from result: true = 0, false = non-zero, nil = command failed
|
157
|
+
stdout, stderr = "", ""
|
172
158
|
status = result ? 0 : 1
|
173
159
|
|
174
|
-
# Create command with interaction data
|
175
160
|
command = Command.new(
|
176
161
|
method_class: ::Kernel::System,
|
177
162
|
args: parsed_args,
|
@@ -182,10 +167,6 @@ module Backspin
|
|
182
167
|
)
|
183
168
|
@commands << command
|
184
169
|
|
185
|
-
# Store output for later access (for consistency with capture3)
|
186
|
-
Backspin.last_output = stdout
|
187
|
-
|
188
|
-
# Return the original result (true/false/nil)
|
189
170
|
result
|
190
171
|
end
|
191
172
|
end
|
data/lib/backspin/version.rb
CHANGED