backspin 0.4.0 → 0.4.2

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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.gem_release.yml +13 -0
  3. data/CHANGELOG.md +5 -7
  4. data/CLAUDE.md +7 -3
  5. data/CONTRIBUTING.md +2 -2
  6. data/Gemfile +3 -1
  7. data/Gemfile.lock +3 -1
  8. data/MATCH_ON_USAGE.md +110 -0
  9. data/README.md +7 -2
  10. data/Rakefile +5 -1
  11. data/backspin.gemspec +6 -3
  12. data/examples/match_on_example.rb +116 -0
  13. data/fixtures/backspin/all_and_fields.yml +15 -0
  14. data/fixtures/backspin/all_bypass_equality.yml +14 -0
  15. data/fixtures/backspin/all_checks_equality.yml +17 -0
  16. data/fixtures/backspin/all_for_logging.yml +13 -0
  17. data/fixtures/backspin/all_matcher_basic.yml +14 -0
  18. data/fixtures/backspin/all_matcher_custom.yml +17 -0
  19. data/fixtures/backspin/all_matcher_demo.yml +14 -0
  20. data/fixtures/backspin/all_matcher_test.yml +14 -0
  21. data/fixtures/backspin/all_mode_filter.yml +14 -0
  22. data/fixtures/backspin/all_no_short_circuit.yml +14 -0
  23. data/fixtures/backspin/all_pass_field_fail.yml +14 -0
  24. data/fixtures/backspin/all_short_circuit.yml +14 -0
  25. data/fixtures/backspin/all_skips_equality.yml +17 -0
  26. data/fixtures/backspin/all_with_equality.yml +17 -0
  27. data/fixtures/backspin/all_with_fields.yml +17 -0
  28. data/fixtures/backspin/combined_fail_demo.yml +14 -0
  29. data/fixtures/backspin/combined_matcher_demo.yml +14 -0
  30. data/fixtures/backspin/credential_filter.yml +18 -0
  31. data/fixtures/backspin/echo_hello.yml +14 -0
  32. data/fixtures/backspin/echo_verify.yml +14 -0
  33. data/fixtures/backspin/episodes_filter.yml +26 -0
  34. data/fixtures/backspin/failure_test.yml +14 -0
  35. data/fixtures/backspin/field_matcher_demo.yml +17 -0
  36. data/fixtures/backspin/field_matcher_values.yml +14 -0
  37. data/fixtures/backspin/full_data_filter.yml +17 -0
  38. data/fixtures/backspin/key_confusion_test.yml +14 -0
  39. data/fixtures/backspin/match_on_any_fail.yml +21 -0
  40. data/fixtures/backspin/match_on_bad_format.yml +14 -0
  41. data/fixtures/backspin/match_on_fail.yml +15 -0
  42. data/fixtures/backspin/match_on_invalid.yml +14 -0
  43. data/fixtures/backspin/match_on_multiple.yml +28 -0
  44. data/fixtures/backspin/match_on_nil.yml +14 -0
  45. data/fixtures/backspin/match_on_other_fields.yml +23 -0
  46. data/fixtures/backspin/match_on_run_bang.yml +16 -0
  47. data/fixtures/backspin/match_on_run_bang_fail.yml +15 -0
  48. data/fixtures/backspin/match_on_single.yml +17 -0
  49. data/fixtures/backspin/mixed_calls.yml +24 -0
  50. data/fixtures/backspin/multi_command.yml +34 -0
  51. data/fixtures/backspin/multi_command_filter.yml +26 -0
  52. data/fixtures/backspin/multi_field_filter.yml +13 -0
  53. data/fixtures/backspin/multi_system.yml +20 -0
  54. data/fixtures/backspin/nil_filter.yml +14 -0
  55. data/fixtures/backspin/none_mode_test.yml +14 -0
  56. data/fixtures/backspin/path_test.yml +17 -0
  57. data/fixtures/backspin/playback_system.yml +12 -0
  58. data/fixtures/backspin/playback_test.yml +14 -0
  59. data/fixtures/backspin/stderr_test.yml +19 -0
  60. data/fixtures/backspin/string_symbol_test.yml +14 -0
  61. data/fixtures/backspin/system_echo.yml +12 -0
  62. data/fixtures/backspin/system_false.yml +18 -0
  63. data/fixtures/backspin/timestamp_test.yml +18 -0
  64. data/fixtures/backspin/use_record_filter.yml +15 -0
  65. data/fixtures/backspin/verify_system.yml +12 -0
  66. data/fixtures/backspin/verify_system_diff.yml +11 -0
  67. data/fixtures/backspin/version_test.yml +14 -0
  68. data/lib/backspin/command.rb +1 -5
  69. data/lib/backspin/command_diff.rb +98 -16
  70. data/lib/backspin/command_result.rb +2 -4
  71. data/lib/backspin/record.rb +31 -10
  72. data/lib/backspin/record_result.rb +20 -14
  73. data/lib/backspin/recorder.rb +100 -55
  74. data/lib/backspin/version.rb +3 -1
  75. data/lib/backspin.rb +36 -175
  76. data/release.rake +97 -0
  77. data/script/lint +6 -0
  78. metadata +79 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59bc6e711951434fc84dd2c2694f69aa5b00ef3bf878b81c5b4e97beeb430ad6
4
- data.tar.gz: f9784962ef86ad73e0eb1c8388217700e477bc60ce0528b5dd16f08bc5c2f637
3
+ metadata.gz: 055d320685da2ca9e280c3aa8bcb15edb101d788c4d8c4df8ff203d4c2c34dac
4
+ data.tar.gz: cf21c90a588ff5590844ae2a9c50a78707a5c71d44b43ad0d3a571fed0bbe4b5
5
5
  SHA512:
6
- metadata.gz: e4151f4a28909e1a80706a30520b268f5114124230df9869a3603d5502194f4885ca8dff19e15a9a8ffe5f559b14695614f9f113c66e82ccc105de5b7ae17006
7
- data.tar.gz: a66f71d1a4b2e9404ca39178920d8b88686f5e8cec2fe62f282b1cda9de031c1041cdfa1007ee9dc725c24cfb1c4dad5445213091a20c0faf23165705a8d264e
6
+ metadata.gz: 7c3a0941042e885d5370c4287fdd88323bc1b95d15a7c795afde0d08aa5efe050f7e54838a5774de967d4d633b9e30df82b5700d17c431fa8ea1cce79777bee9
7
+ data.tar.gz: 86584a95060fda843934065c2d9a3ca59acd12a1e21f70dcf3d9bd648f4cd1cec97ff21a57f98f8aa7a133865ccbb38438bdb05402f19e8292bed02c56d51bfe
data/.gem_release.yml ADDED
@@ -0,0 +1,13 @@
1
+ # Configuration for gem-release
2
+ bump:
3
+ commit: true
4
+ message: "Bump version to %{version}"
5
+ skip_ci: false
6
+ push: true
7
+
8
+ tag:
9
+ tag: v%{version}
10
+ sign: false
11
+
12
+ release:
13
+ host: https://rubygems.org
data/CHANGELOG.md CHANGED
@@ -1,21 +1,19 @@
1
1
  # Changelog
2
2
 
3
- ## [0.4.0] - 2025-06-06
3
+ ## 0.4.0 - 2025-06-06
4
4
 
5
5
  Simpler, unified API: `Backspin.run` and `Backspin run!` methods that automatically record on first use and verify on subsequent runs. `run!` will raise an error if results differ, whereas `run` will return the result for the caller to decide what to do with
6
6
 
7
- ## [0.3.0] - 2025-06-05
7
+ ## 0.3.0 - 2025-06-05
8
8
  - Scrub credentials from command arguments
9
9
 
10
- ## [0.2.0] - 2025-06-05
10
+ ## 0.2.0 - 2025-06-05
11
11
  - First public release of Backspin, extracteed from `name-TBD` CLI tool
12
12
 
13
- ## [0.2.1] - 2025-06-04
13
+ ## 0.2.1 - 2025-06-04
14
14
  - major refactoring, add support for `system` calls
15
15
 
16
- ## [0.1.0] - 2025-06-02
17
-
18
- ### Added
16
+ ## 0.1.0 - 2025-06-02
19
17
  - Initial (internal) release of Backspin
20
18
  - `record` method to capture CLI command outputs
21
19
  - `verify` and `verify!` methods for output verification
data/CLAUDE.md CHANGED
@@ -29,9 +29,13 @@ bin/rake release # Release to RubyGems (updates version, tags, pushes)
29
29
 
30
30
  ### Code Quality
31
31
  ```bash
32
- bin/rake standard # Run Standard Ruby linter
32
+ script/lint # Run Standard Ruby linter
33
+ script/lint --fix # Auto-fix linting issues
34
+ bin/rake standard # Alternative: Run via Rake task
33
35
  ```
34
36
 
37
+ **Important**: Always use `standardrb` for linting, never use `rubocop` directly. The project uses Standard Ruby for consistent code style.
38
+
35
39
  ## Architecture
36
40
 
37
41
  ### Core Components
@@ -62,7 +66,7 @@ bin/rake standard # Run Standard Ruby linter
62
66
  ### Testing Approach
63
67
 
64
68
  - Integration-focused tests that exercise the full stack
65
- - Default record directory is `spec/backspin_data` (can be configured)
69
+ - Default record directory is `fixtures/backspin` (can be configured)
66
70
  - Tests use real shell commands (`echo`, `date`, etc.)
67
71
  - Configuration is reset between tests to avoid side effects
68
72
  - **Important**: Backspin specs MUST be as local and un-DRY as possible. Each spec should be self-contained with its own setup, expectations, and cleanup if needed. Avoid shared contexts or helpers that hide important test details.
@@ -76,7 +80,7 @@ bin/rake standard # Run Standard Ruby linter
76
80
  4. Run tests with `rake spec`
77
81
 
78
82
  ### Debugging Tests
79
- - Records are saved to `spec/backspin_data/` by default
83
+ - Records are saved to `fixtures/backspin/` by default
80
84
  - Check YAML files to see recorded command outputs
81
85
 
82
86
  ### Updating Credential Patterns
data/CONTRIBUTING.md CHANGED
@@ -99,7 +99,7 @@ Backspin is a Ruby gem for characterization testing of command-line interfaces.
99
99
 
100
100
  #### Debugging Tests
101
101
 
102
- - Records are saved to `spec/backspin_data/` by default
102
+ - Records are saved to `fixtures/backspin/` by default
103
103
  - Check YAML files to see recorded command outputs
104
104
 
105
105
  ## Testing
@@ -125,7 +125,7 @@ Backspin uses integration-focused tests that exercise the full stack. When writi
125
125
  - Avoid shared contexts or helpers that hide important test details
126
126
  - Use real shell commands (`echo`, `date`, etc.) for testing
127
127
  - Ensure configuration is reset between tests to avoid side effects
128
- - Verify new or updated test records in `spec/backspin_data/`
128
+ - Verify new or updated test records in `fixtures/backspin/`
129
129
 
130
130
  Example test structure:
131
131
 
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  # Specify your gem's dependencies in backspin.gemspec
@@ -6,6 +8,6 @@ gemspec
6
8
  group :development do
7
9
  gem "rake", "~> 13.0"
8
10
  gem "rspec", "~> 3.0"
9
- gem "timecop", "~> 0.9"
10
11
  gem "standard"
12
+ gem "timecop", "~> 0.9"
11
13
  end
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- backspin (0.4.0)
4
+ backspin (0.4.2)
5
5
  ostruct (~> 0.5.0)
6
6
  rspec-mocks (~> 3.0)
7
7
 
@@ -10,6 +10,7 @@ GEM
10
10
  specs:
11
11
  ast (2.4.3)
12
12
  diff-lcs (1.6.2)
13
+ gem-release (2.2.4)
13
14
  json (2.12.2)
14
15
  language_server-protocol (3.17.0.5)
15
16
  lint_roller (1.1.0)
@@ -78,6 +79,7 @@ PLATFORMS
78
79
 
79
80
  DEPENDENCIES
80
81
  backspin!
82
+ gem-release (~> 2.2)
81
83
  rake (~> 13.0)
82
84
  rspec (~> 3.0)
83
85
  standard
data/MATCH_ON_USAGE.md ADDED
@@ -0,0 +1,110 @@
1
+ # Using match_on for Field-Specific Verification
2
+
3
+ The `match_on` option allows you to use custom matchers for specific fields while maintaining exact equality checks for all other fields. This is useful when dealing with dynamic content like timestamps, process IDs, or version numbers.
4
+
5
+ ## Basic Usage
6
+
7
+ ### Single Field Matcher
8
+
9
+ ```ruby
10
+ # Record a command with a timestamp
11
+ Backspin.run("timestamp_test") do
12
+ Open3.capture3("date")
13
+ end
14
+
15
+ # Verify with a custom matcher for stdout
16
+ result = Backspin.run("timestamp_test",
17
+ match_on: [:stdout, ->(recorded, actual) {
18
+ # Both should contain a day of the week
19
+ recorded.match?(/Mon|Tue|Wed|Thu|Fri|Sat|Sun/) &&
20
+ actual.match?(/Mon|Tue|Wed|Thu|Fri|Sat|Sun/)
21
+ }]) do
22
+ Open3.capture3("date")
23
+ end
24
+ ```
25
+
26
+ ### Multiple Field Matchers
27
+
28
+ ```ruby
29
+ # Match different fields with different rules
30
+ result = Backspin.run("multi_field_test",
31
+ match_on: [
32
+ [:stdout, ->(recorded, actual) {
33
+ # Match process ID format
34
+ recorded.match?(/PID: \d+/) && actual.match?(/PID: \d+/)
35
+ }],
36
+ [:stderr, ->(recorded, actual) {
37
+ # Match error type, ignore details
38
+ recorded.include?("Error:") && actual.include?("Error:")
39
+ }]
40
+ ]) do
41
+ Open3.capture3("./my_script.sh")
42
+ end
43
+ ```
44
+
45
+ ## Matcher Format
46
+
47
+ The `match_on` option accepts two formats:
48
+
49
+ 1. **Single field**: `[:field_name, matcher_proc]`
50
+ 2. **Multiple fields**: `[[:field1, matcher1], [:field2, matcher2], ...]`
51
+
52
+ Valid field names are:
53
+ - `:stdout` - Standard output
54
+ - `:stderr` - Standard error
55
+ - `:status` - Exit status code
56
+
57
+ ## Matcher Proc
58
+
59
+ The matcher proc receives two arguments:
60
+ - `recorded_value` - The value from the saved recording
61
+ - `actual_value` - The value from the current execution
62
+
63
+ It should return `true` if the values match according to your criteria, `false` otherwise.
64
+
65
+ ## Examples
66
+
67
+ ### Matching Version Numbers
68
+
69
+ ```ruby
70
+ # Match major version only
71
+ match_on: [:stdout, ->(recorded, actual) {
72
+ recorded.match(/Version: (\d+)\./) &&
73
+ actual.match(/Version: (\d+)\./) &&
74
+ $1 == $1 # Major versions match
75
+ }]
76
+ ```
77
+
78
+ ### Ignoring Timestamps
79
+
80
+ ```ruby
81
+ # Match log format but ignore timestamp
82
+ match_on: [:stdout, ->(recorded, actual) {
83
+ # Remove timestamps before comparing
84
+ recorded.gsub(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/, '') ==
85
+ actual.gsub(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/, '')
86
+ }]
87
+ ```
88
+
89
+ ### Handling Dynamic IDs
90
+
91
+ ```ruby
92
+ # Match API response structure, ignore dynamic IDs
93
+ match_on: [:stdout, ->(recorded, actual) {
94
+ recorded_json = JSON.parse(recorded)
95
+ actual_json = JSON.parse(actual)
96
+
97
+ # Compare structure, not values
98
+ recorded_json.keys.sort == actual_json.keys.sort
99
+ }]
100
+ ```
101
+
102
+ ## Important Notes
103
+
104
+ 1. **Other fields must match exactly**: When using `match_on`, all fields not specified in the matcher list must match exactly. If stdout has a custom matcher but stderr doesn't, stderr must be identical to pass verification.
105
+
106
+ 2. **Precedence**: If both `matcher` and `match_on` options are provided, `matcher` takes precedence (for backward compatibility).
107
+
108
+ 3. **Error messages**: When verification fails with `match_on`, the error will indicate which fields failed and whether they failed exact matching or custom matching.
109
+
110
+ 4. **Works with run!**: The `match_on` option works with both `run` and `run!` methods.
data/README.md CHANGED
@@ -1,4 +1,9 @@
1
- # Backspin   [![Gem Version](https://badge.fury.io/rb/backspin.svg)](https://badge.fury.io/rb/backspin) [![CircleCI](https://dl.circleci.com/status-badge/img/gh/rsanheim/backspin/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/rsanheim/backspin/tree/main)
1
+ # Backspin
2
+
3
+ [![Ruby](https://img.shields.io/badge/ruby-%23CC342D.svg?style=flat&logo=ruby&logoColor=white)](https://www.ruby-lang.org/)
4
+ [![Gem Version](https://img.shields.io/gem/v/backspin)](https://rubygems.org/gems/backspin)
5
+ [![CircleCI](https://img.shields.io/circleci/build/github/rsanheim/backspin/main)](https://circleci.com/gh/rsanheim/backspin)
6
+ [![Last Commit](https://img.shields.io/github/last-commit/rsanheim/backspin/main)](https://github.com/rsanheim/backspin/commits/main)
2
7
 
3
8
  Backspin records and replays CLI interactions in Ruby for easy snapshot testing of command-line interfaces. Currently supports `Open3.capture3` and `system` and requires `rspec-mocks`. More system calls and test integrations are welcome - send a PR!
4
9
 
@@ -62,7 +67,7 @@ end
62
67
  result = Backspin.run("echo_test", mode: :record) do
63
68
  Open3.capture3("echo hello")
64
69
  end
65
- # This will save the output to `spec/backspin_data/echo_test.yml`.
70
+ # This will save the output to `fixtures/backspin/echo_test.yml`.
66
71
 
67
72
  # Explicit verify mode: Always verify against existing recording
68
73
  result = Backspin.run("echo_test", mode: :verify) do
data/Rakefile CHANGED
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rspec/core/rake_task"
3
5
  require "standard/rake"
4
6
 
5
7
  RSpec::Core::RakeTask.new(:spec)
6
8
 
7
- task default: [:spec, :standard]
9
+ task default: %i[spec standard]
10
+
11
+ load "release.rake" if File.exist?("release.rake")
data/backspin.gemspec CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "lib/backspin/version"
2
4
 
3
5
  Gem::Specification.new do |spec|
@@ -16,14 +18,15 @@ Gem::Specification.new do |spec|
16
18
  spec.metadata["source_code_uri"] = spec.homepage
17
19
  spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
18
20
 
19
- # Specify which files should be added to the gem when it is released.
20
- spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
22
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
23
  end
23
24
  spec.bindir = "exe"
24
25
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
25
26
  spec.require_paths = ["lib"]
26
27
 
27
- spec.add_dependency "rspec-mocks", "~> 3.0"
28
28
  spec.add_dependency "ostruct", "~> 0.5.0"
29
+ spec.add_dependency "rspec-mocks", "~> 3.0"
30
+
31
+ spec.add_development_dependency "gem-release", "~> 2.2"
29
32
  end
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/inline"
5
+ require "open3"
6
+
7
+ gemfile do
8
+ source "https://rubygems.org"
9
+ gem "backspin", path: ".."
10
+ end
11
+
12
+ # Example 1: Single field matcher
13
+ puts "Example 1: Matching timestamps with custom matcher"
14
+ puts "-" * 50
15
+
16
+ # First run: Record the output
17
+ result = Backspin.run("timestamp_example") do
18
+ Open3.capture3("date '+%Y-%m-%d %H:%M:%S'")
19
+ end
20
+ puts "Recorded: #{result.stdout.chomp}"
21
+
22
+ # Sleep to ensure different timestamp
23
+ sleep 1
24
+
25
+ # Second run: Verify with custom matcher
26
+ result = Backspin.run("timestamp_example",
27
+ match_on: [:stdout, lambda { |recorded, actual|
28
+ # Both should have the same date format
29
+ recorded.match?(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/) &&
30
+ actual.match?(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)
31
+ }]) do
32
+ Open3.capture3("date '+%Y-%m-%d %H:%M:%S'")
33
+ end
34
+
35
+ puts "Current: #{result.stdout.chomp}"
36
+ puts "Verified: #{result.verified?}"
37
+ puts
38
+
39
+ # Example 2: Multiple field matchers
40
+ puts "Example 2: Matching multiple fields with different patterns"
41
+ puts "-" * 50
42
+
43
+ # Record a command with dynamic content
44
+ Backspin.run("multi_field_example") do
45
+ script = <<~BASH
46
+ echo "PID: $$"
47
+ echo "Error: Timeout at $(date '+%H:%M:%S')" >&2
48
+ exit 1
49
+ BASH
50
+ Open3.capture3("bash", "-c", script)
51
+ end
52
+
53
+ # Verify with different PID and timestamp
54
+ result = Backspin.run("multi_field_example",
55
+ match_on: [
56
+ [:stdout, lambda { |recorded, actual|
57
+ # Both should have PID format
58
+ recorded.match?(/PID: \d+/) && actual.match?(/PID: \d+/)
59
+ }],
60
+ [:stderr, lambda { |recorded, actual|
61
+ # Both should have timeout error, ignore timestamp
62
+ recorded.match?(/Error: Timeout at/) && actual.match?(/Error: Timeout at/)
63
+ }]
64
+ ]) do
65
+ script = <<~BASH
66
+ echo "PID: $$"
67
+ echo "Error: Timeout at $(date '+%H:%M:%S')" >&2
68
+ exit 1
69
+ BASH
70
+ Open3.capture3("bash", "-c", script)
71
+ end
72
+
73
+ puts "Stdout: #{result.stdout.chomp}"
74
+ puts "Stderr: #{result.stderr.chomp}"
75
+ puts "Status: #{result.status}"
76
+ puts "Verified: #{result.verified?}"
77
+ puts
78
+
79
+ # Example 3: Mixed matching - some fields exact, some custom
80
+ puts "Example 3: Mixed field matching"
81
+ puts "-" * 50
82
+
83
+ # Record with specific values
84
+ Backspin.run("mixed_matching") do
85
+ script = <<~BASH
86
+ echo "Version: 1.2.3"
87
+ echo "Build: $(date +%s)"
88
+ echo "Status: OK"
89
+ BASH
90
+ Open3.capture3("bash", "-c", script)
91
+ end
92
+
93
+ # Verify - stdout uses custom matcher, stderr must match exactly
94
+ result = Backspin.run("mixed_matching",
95
+ match_on: [:stdout, lambda { |recorded, actual|
96
+ # Version and Status must match, Build can differ
97
+ recorded_lines = recorded.lines
98
+ actual_lines = actual.lines
99
+
100
+ recorded_lines[0] == actual_lines[0] && # Version line must match
101
+ recorded_lines[1].start_with?("Build:") && actual_lines[1].start_with?("Build:") && # Build line format
102
+ recorded_lines[2] == actual_lines[2] # Status line must match
103
+ }]) do
104
+ script = <<~BASH
105
+ echo "Version: 1.2.3"
106
+ echo "Build: $(date +%s)"
107
+ echo "Status: OK"
108
+ BASH
109
+ Open3.capture3("bash", "-c", script)
110
+ end
111
+
112
+ puts "Output:\n#{result.stdout}"
113
+ puts "Verified: #{result.verified?}"
114
+
115
+ # Cleanup
116
+ FileUtils.rm_rf("fixtures/backspin")
@@ -0,0 +1,15 @@
1
+ ---
2
+ first_recorded_at: '2025-06-10T11:27:40-05:00'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - sh
8
+ - "-c"
9
+ - echo 'valid'; exit 0
10
+ stdout: 'valid
11
+
12
+ '
13
+ stderr: ''
14
+ status: 0
15
+ recorded_at: '2025-06-10T11:27:40-05:00'
@@ -0,0 +1,14 @@
1
+ ---
2
+ first_recorded_at: '2025-06-10T11:02:03-05:00'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - echo
8
+ - first output
9
+ stdout: 'first output
10
+
11
+ '
12
+ stderr: ''
13
+ status: 0
14
+ recorded_at: '2025-06-10T11:02:03-05:00'
@@ -0,0 +1,17 @@
1
+ ---
2
+ first_recorded_at: '2025-05-01T12:00:00Z'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - sh
8
+ - "-c"
9
+ - echo 'original'; echo 'original error' >&2
10
+ stdout: 'original
11
+
12
+ '
13
+ stderr: 'original error
14
+
15
+ '
16
+ status: 0
17
+ recorded_at: '2025-05-01T12:00:00Z'
@@ -0,0 +1,13 @@
1
+ ---
2
+ first_recorded_at: '2025-05-01T12:00:00Z'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - date
8
+ stdout: 'Tue Jun 10 22:50:14 CDT 2025
9
+
10
+ '
11
+ stderr: ''
12
+ status: 0
13
+ recorded_at: '2025-05-01T12:00:00Z'
@@ -0,0 +1,14 @@
1
+ ---
2
+ first_recorded_at: '2025-05-01T12:00:00Z'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - echo
8
+ - hello world
9
+ stdout: 'hello world
10
+
11
+ '
12
+ stderr: ''
13
+ status: 0
14
+ recorded_at: '2025-05-01T12:00:00Z'
@@ -0,0 +1,17 @@
1
+ ---
2
+ first_recorded_at: '2025-05-01T12:00:00Z'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - sh
8
+ - "-c"
9
+ - 'echo ''PASS: test 1''; echo ''WARNING: minor issue'' >&2'
10
+ stdout: 'PASS: test 1
11
+
12
+ '
13
+ stderr: 'WARNING: minor issue
14
+
15
+ '
16
+ status: 0
17
+ recorded_at: '2025-05-01T12:00:00Z'
@@ -0,0 +1,14 @@
1
+ ---
2
+ first_recorded_at: '2025-06-10T11:02:02-05:00'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - echo
8
+ - test
9
+ stdout: 'test
10
+
11
+ '
12
+ stderr: ''
13
+ status: 0
14
+ recorded_at: '2025-06-10T11:02:02-05:00'
@@ -0,0 +1,14 @@
1
+ ---
2
+ first_recorded_at: '2025-06-10T11:00:57-05:00'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - echo
8
+ - test
9
+ stdout: 'test
10
+
11
+ '
12
+ stderr: ''
13
+ status: 0
14
+ recorded_at: '2025-06-10T11:00:57-05:00'
@@ -0,0 +1,14 @@
1
+ ---
2
+ first_recorded_at: '2025-05-01T12:00:00Z'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - echo
8
+ - "'second'"
9
+ stdout: 'filtered output 2
10
+
11
+ '
12
+ stderr: ''
13
+ status: 0
14
+ recorded_at: '2025-05-01T12:00:00Z'
@@ -0,0 +1,14 @@
1
+ ---
2
+ first_recorded_at: '2025-06-10T11:27:40-05:00'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - echo
8
+ - test
9
+ stdout: 'test
10
+
11
+ '
12
+ stderr: ''
13
+ status: 0
14
+ recorded_at: '2025-06-10T11:27:40-05:00'
@@ -0,0 +1,14 @@
1
+ ---
2
+ first_recorded_at: '2025-05-01T12:00:00Z'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - echo
8
+ - hello
9
+ stdout: 'hello
10
+
11
+ '
12
+ stderr: ''
13
+ status: 0
14
+ recorded_at: '2025-05-01T12:00:00Z'
@@ -0,0 +1,14 @@
1
+ ---
2
+ first_recorded_at: '2025-05-01T12:00:00Z'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - echo
8
+ - test
9
+ stdout: 'test
10
+
11
+ '
12
+ stderr: ''
13
+ status: 0
14
+ recorded_at: '2025-05-01T12:00:00Z'
@@ -0,0 +1,17 @@
1
+ ---
2
+ first_recorded_at: '2025-05-01T12:00:00Z'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - sh
8
+ - "-c"
9
+ - echo 'original'; echo 'original error' >&2
10
+ stdout: 'original
11
+
12
+ '
13
+ stderr: 'original error
14
+
15
+ '
16
+ status: 0
17
+ recorded_at: '2025-05-01T12:00:00Z'
@@ -0,0 +1,17 @@
1
+ ---
2
+ first_recorded_at: '2025-06-10T11:27:40-05:00'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - sh
8
+ - "-c"
9
+ - echo 'output1'; echo 'error1' >&2
10
+ stdout: 'output1
11
+
12
+ '
13
+ stderr: 'error1
14
+
15
+ '
16
+ status: 0
17
+ recorded_at: '2025-06-10T11:27:40-05:00'
@@ -0,0 +1,17 @@
1
+ ---
2
+ first_recorded_at: '2025-05-01T12:00:00Z'
3
+ format_version: '2.0'
4
+ commands:
5
+ - command_type: Open3::Capture3
6
+ args:
7
+ - sh
8
+ - "-c"
9
+ - echo 'output'; echo 'error' >&2
10
+ stdout: 'output
11
+
12
+ '
13
+ stderr: 'error
14
+
15
+ '
16
+ status: 0
17
+ recorded_at: '2025-05-01T12:00:00Z'