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.
@@ -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
@@ -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 = [:capture3, :system] if command_types.empty?
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, "Unknown command type: #{command_type}"
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
- # For system calls, stdout and stderr are not captured
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
@@ -1,3 +1,3 @@
1
1
  module Backspin
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end