backspin 0.5.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9c7a5cfbb0da377abb251f0f53d3bef24f131e72664fde1de6372f02024a506
4
- data.tar.gz: 3835b7201b3d9ca7d4b399b7f7e58fdfa8e81838e22a45b1f4d7225da2fca09b
3
+ metadata.gz: e49507f2d196ebd4d7b4d39a6013d840224a357272fb5ba4880702e26d28c8d9
4
+ data.tar.gz: cfee20f6ff8b65c622bfc9482d85e49c78f78e1e9a2c91b5f09f71e2b1d61663
5
5
  SHA512:
6
- metadata.gz: e533519b22a5e4a83a3511eb3ffbfd0429432b75179988435806d006ff57aa7a91e7c3f73c3bb6d1fe2b587f90bef97febdc5952a82f69e46c1b6f7bc26f6087
7
- data.tar.gz: ddffab372459f5c8ff6f8246cc5ffd988c3a28c5041d56f9961e13f7d5bd0a2c9bfde3bf6d844792e708b332e5acd836afd4d71d041ec1435e7a1644716d2156
6
+ metadata.gz: 660d3af239a57512fbd74eecf2f7fc81c91a6fc42c0a95957952f01931bcd2b5f5387af2004557af985c8c3ef6f37c6b284e41b757af2919a241d5408060b25e
7
+ data.tar.gz: 6a0b7f7e2bc4533e80e8011fb17c1f851c31ddd6e831300efec0589423f541640e1ace953c75f066e8a9efc26c6e3fef886cc87941d204e09d1a2302d58adcbc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 - 2025-06-11
4
+ * Simplify matcher API so user provided matchers override defaults - [#14](https://github.com/rsanheim/backspin/pull/14)
5
+ * Also extract a proper `Matcher` object
6
+
3
7
  ## 0.4.2 - 2025-06-10
4
8
  Unified `:match` API for customizing how actual commands are matched against recorded commands. - [#11](https://github.com/rsanheim/backspin/pull/11)
5
9
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- backspin (0.5.0)
4
+ backspin (0.6.0)
5
5
  ostruct
6
6
  rspec-mocks (~> 3)
7
7
 
@@ -52,6 +52,8 @@ module Backspin
52
52
  Open3::Capture3
53
53
  when "Kernel::System"
54
54
  ::Kernel::System
55
+ when "Backspin::Capturer"
56
+ Backspin::Capturer
55
57
  else
56
58
  # Default to capture3 for backwards compatibility
57
59
  Open3::Capture3
@@ -97,3 +99,8 @@ end
97
99
  module ::Kernel
98
100
  class System; end
99
101
  end
102
+
103
+ # Define the Backspin::Capturer class for identification
104
+ module Backspin
105
+ class Capturer; end
106
+ end
@@ -136,6 +136,150 @@ module Backspin
136
136
  )
137
137
  end
138
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
+
139
283
  private
140
284
 
141
285
  def setup_capture3_replay_stub
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Backspin
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/backspin.rb CHANGED
@@ -116,6 +116,43 @@ module Backspin
116
116
  result
117
117
  end
118
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
+
119
156
  private
120
157
 
121
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|
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Sanheim
@@ -153,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
153
  - !ruby/object:Gem::Version
154
154
  version: '0'
155
155
  requirements: []
156
- rubygems_version: 3.6.9
156
+ rubygems_version: 3.6.7
157
157
  specification_version: 4
158
158
  summary: Record and replay CLI interactions for testing
159
159
  test_files: []