backspin 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8a6c6a0ef97c99ace7fb068e7b85bc40c2f45594bc30ce5002920fd62fcd384
4
- data.tar.gz: f62c15526c0a19a4ae876b8c737b172e2557c54a55f4d5045a0dbc499ccfcd51
3
+ metadata.gz: edf2daa2123a270ca28d593b594188db57913b2ca5905a45b6a9e0d1221c0da4
4
+ data.tar.gz: 3ec4407d3396e3e8390f62b3c0e7d2124bdf026e4b02e74c036ee37fab8430a4
5
5
  SHA512:
6
- metadata.gz: 117a79e8e448bae03c68b44e8a6de23671d746d2ea664094e7dc82792d8ff323af7c6faf73f85a9aa020bb259127660abc8d7687cf861b627049b6e3b88ff41f
7
- data.tar.gz: dfeabf728ef8448d01889dbe2d62d9d2a8236da296d9f6ee21619f6e3f976218cd0af3da2ac8b7f4ab1ff9cbc19dd4afaef1ca64ac777f3f9215180a1349c1c0
6
+ metadata.gz: a851d194aa4f400c9104643b5521e7b5bd5e159708a0d6114c70cfd39907fb6174041a3bccffdc824edb3e224bab7db453b906c35adc7f0a2908d2b034a9b11a
7
+ data.tar.gz: 01b1c9b4288f978d9946090afda49e17001732ec0b4f5e47d3831767975d731e5c940cc0fbdd7ccfca4a2bdf54bc8d281eba7d3dc4cbbd137e3f4edb576d9378
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 - 2026-02-11
4
+ * Added `filter_on` to `Backspin.run` and `Backspin.capture` (`:both` default, `:record` opt-out).
5
+ * Changed default filter behavior: `filter` now applies during verify comparisons/diffs when `filter_on: :both`.
6
+ * Matcher callbacks now receive mutable copies of comparison data so in-place mutations do not mutate snapshots.
7
+ * Snapshot serialization is now immutable: `Snapshot#to_h` returns a frozen representation built at initialization.
8
+
3
9
  ## 0.9.0 - 2026-02-11
4
10
  * Breaking: `Backspin.run` and `Backspin.capture` now return `Backspin::BackspinResult` with explicit `result.actual` / `result.expected` snapshots.
5
11
  * Breaking: result convenience accessors (`result.stdout`, `result.stderr`, `result.status`) were removed in favor of snapshot access.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- backspin (0.9.0)
4
+ backspin (0.10.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/MATCHERS.md CHANGED
@@ -77,6 +77,24 @@ The `:all` matcher receives full hashes with these keys:
77
77
  - `"env"` - Optional Hash of env vars (command runs only)
78
78
  - `"recorded_at"` - Timestamp string
79
79
 
80
+ Matcher inputs are copies of comparison data so in-place mutation inside matcher callbacks
81
+ does not mutate Backspin's stored snapshots.
82
+
83
+ Matcher callbacks should return a boolean. Any truthy return value is treated as pass,
84
+ and false/nil is treated as failure.
85
+
86
+ Example with safe in-place mutation:
87
+
88
+ ```ruby
89
+ matcher = {
90
+ all: ->(recorded, actual) {
91
+ recorded["stdout"].gsub!(/id=\d+/, "id=[ID]")
92
+ actual["stdout"].gsub!(/id=\d+/, "id=[ID]")
93
+ recorded["stdout"] == actual["stdout"]
94
+ }
95
+ }
96
+ ```
97
+
80
98
  ## Examples
81
99
 
82
100
  ### Matching Version Numbers
data/README.md CHANGED
@@ -132,6 +132,41 @@ result = Backspin.run(["date"], name: "timestamp_test", matcher: {stdout: timest
132
132
 
133
133
  For more matcher examples and detailed documentation, see [MATCHERS.md](MATCHERS.md).
134
134
 
135
+ ### Filters and Canonicalization
136
+
137
+ Use `filter:` to normalize snapshot data (timestamps, random IDs, absolute paths).
138
+
139
+ By default (`filter_on: :both`), Backspin applies `filter`:
140
+ - when writing record snapshots
141
+ - during verify for both expected and actual, before matcher and diff
142
+
143
+ If you only want record-time filtering, use `filter_on: :record`.
144
+
145
+ Migration note: older behavior applied `filter` only at record write. To preserve that behavior, set `filter_on: :record`.
146
+
147
+ ```ruby
148
+ normalize_filter = ->(snapshot) do
149
+ snapshot.merge(
150
+ "stdout" => snapshot["stdout"].gsub(/id=\d+/, "id=[ID]")
151
+ )
152
+ end
153
+
154
+ # default: filter_on :both
155
+ Backspin.run(["echo", "id=123"], name: "canonicalized", filter: normalize_filter)
156
+ Backspin.run(["echo", "id=999"], name: "canonicalized", filter: normalize_filter) # verifies
157
+
158
+ # capture also supports verify-time canonicalization
159
+ Backspin.capture("capture_canonicalized", filter: normalize_filter) do
160
+ puts "id=123"
161
+ end
162
+ Backspin.capture("capture_canonicalized", filter: normalize_filter) do
163
+ puts "id=999"
164
+ end
165
+
166
+ # record-only filtering
167
+ Backspin.run(["echo", "id=123"], name: "record_only", filter: normalize_filter, filter_on: :record)
168
+ ```
169
+
135
170
  ### Working with the Result Object
136
171
 
137
172
  The API returns a `Backspin::BackspinResult` object with helpful methods:
@@ -15,8 +15,8 @@ Branch: `spike-backspin-result-api`
15
15
  ### Entry points
16
16
 
17
17
  ```ruby
18
- Backspin.run(command = nil, name:, env: nil, mode: :auto, matcher: nil, filter: nil, &block)
19
- Backspin.capture(name, mode: :auto, matcher: nil, filter: nil, &block)
18
+ Backspin.run(command = nil, name:, env: nil, mode: :auto, matcher: nil, filter: nil, filter_on: :both, &block)
19
+ Backspin.capture(name, mode: :auto, matcher: nil, filter: nil, filter_on: :both, &block)
20
20
  ```
21
21
 
22
22
  Both return `BackspinResult`.
@@ -130,7 +130,9 @@ result.expected.stdout
130
130
  ## Matcher and Filter Semantics
131
131
 
132
132
  - `matcher:` applies only during verify and compares `expected` vs `actual`.
133
- - `filter:` applies only when writing snapshots to disk.
133
+ - `filter:` applies during record writes, and during verify when `filter_on: :both`.
134
+ - `filter_on:` supports `:both` (default) and `:record`.
135
+ - `Snapshot` serializes once at initialization and returns a frozen hash from `to_h`.
134
136
  - Default match still compares stdout/stderr/status only.
135
137
 
136
138
  ## Error Semantics
@@ -5,21 +5,25 @@ module Backspin
5
5
  class CommandDiff
6
6
  attr_reader :expected, :actual, :matcher
7
7
 
8
- def initialize(expected:, actual:, matcher: nil)
8
+ def initialize(expected:, actual:, matcher: nil, filter: nil, filter_on: :both)
9
9
  @expected = expected
10
10
  @actual = actual
11
+ @expected_compare = build_comparison_snapshot(expected, filter: filter, filter_on: filter_on)
12
+ @actual_compare = build_comparison_snapshot(actual, filter: filter, filter_on: filter_on)
11
13
  @matcher = Matcher.new(
12
14
  config: matcher,
13
- expected: expected,
14
- actual: actual
15
+ expected: @expected_compare,
16
+ actual: @actual_compare
15
17
  )
18
+ @verified = nil
16
19
  end
17
20
 
18
21
  # @return [Boolean] true if the snapshot output matches.
19
22
  def verified?
20
- return false unless command_types_match?
23
+ return @verified unless @verified.nil?
24
+ return @verified = false unless command_types_match?
21
25
 
22
- @matcher.match?
26
+ @verified = @matcher.match?
23
27
  end
24
28
 
25
29
  # @return [String, nil] Human-readable diff if not verified
@@ -27,23 +31,21 @@ module Backspin
27
31
  return nil if verified?
28
32
 
29
33
  parts = []
30
- expected_hash = expected.to_h
31
- actual_hash = actual.to_h
32
34
 
33
35
  unless command_types_match?
34
36
  parts << "Command type mismatch: expected #{expected.command_type.name}, got #{actual.command_type.name}"
35
37
  end
36
38
 
37
- if expected_hash["stdout"] != actual_hash["stdout"]
38
- parts << stdout_diff(expected_hash["stdout"], actual_hash["stdout"])
39
+ if expected_compare.stdout != actual_compare.stdout
40
+ parts << stdout_diff(expected_compare.stdout, actual_compare.stdout)
39
41
  end
40
42
 
41
- if expected_hash["stderr"] != actual_hash["stderr"]
42
- parts << stderr_diff(expected_hash["stderr"], actual_hash["stderr"])
43
+ if expected_compare.stderr != actual_compare.stderr
44
+ parts << stderr_diff(expected_compare.stderr, actual_compare.stderr)
43
45
  end
44
46
 
45
- if expected_hash["status"] != actual_hash["status"]
46
- parts << "Exit status: expected #{expected_hash["status"]}, got #{actual_hash["status"]}"
47
+ if expected_compare.status != actual_compare.status
48
+ parts << "Exit status: expected #{expected_compare.status}, got #{actual_compare.status}"
47
49
  end
48
50
 
49
51
  parts.join("\n\n")
@@ -99,5 +101,60 @@ module Backspin
99
101
 
100
102
  diff_lines.join("\n")
101
103
  end
104
+
105
+ attr_reader :expected_compare
106
+
107
+ attr_reader :actual_compare
108
+
109
+ def build_comparison_snapshot(snapshot, filter:, filter_on:)
110
+ data = deep_dup(snapshot.to_h)
111
+ if filter && filter_on == :both
112
+ data = filter.call(data)
113
+ end
114
+
115
+ ComparisonSnapshot.new(
116
+ command_type: snapshot.command_type,
117
+ data: deep_freeze(data)
118
+ )
119
+ end
120
+
121
+ def deep_dup(value)
122
+ case value
123
+ when Hash
124
+ value.transform_values { |entry| deep_dup(entry) }
125
+ when Array
126
+ value.map { |entry| deep_dup(entry) }
127
+ when String
128
+ value.dup
129
+ else
130
+ value
131
+ end
132
+ end
133
+
134
+ def deep_freeze(value)
135
+ case value
136
+ when Hash
137
+ value.each_value { |entry| deep_freeze(entry) }
138
+ when Array
139
+ value.each { |entry| deep_freeze(entry) }
140
+ end
141
+ value.freeze
142
+ end
143
+
144
+ class ComparisonSnapshot
145
+ attr_reader :command_type, :stdout, :stderr, :status
146
+
147
+ def initialize(command_type:, data:)
148
+ @command_type = command_type
149
+ @data = data
150
+ @stdout = data["stdout"]
151
+ @stderr = data["stderr"]
152
+ @status = data["status"]
153
+ end
154
+
155
+ def to_h
156
+ @data
157
+ end
158
+ end
102
159
  end
103
160
  end
@@ -13,48 +13,12 @@ module Backspin
13
13
 
14
14
  # @return [Boolean] true if snapshots match according to configured matcher
15
15
  def match?
16
- if config.nil?
17
- default_matcher.call(expected.to_h, actual.to_h)
18
- elsif config.is_a?(Proc)
19
- config.call(expected.to_h, actual.to_h)
20
- elsif config.is_a?(Hash)
21
- verify_with_hash_matcher
22
- else
23
- raise ArgumentError, "Invalid matcher type: #{config.class}"
24
- end
16
+ evaluation[:match]
25
17
  end
26
18
 
27
19
  # @return [String] reason why matching failed
28
20
  def failure_reason
29
- reasons = []
30
-
31
- if config.nil?
32
- expected_hash = expected.to_h
33
- actual_hash = actual.to_h
34
-
35
- reasons << "stdout differs" if expected_hash["stdout"] != actual_hash["stdout"]
36
- reasons << "stderr differs" if expected_hash["stderr"] != actual_hash["stderr"]
37
- reasons << "exit status differs" if expected_hash["status"] != actual_hash["status"]
38
- elsif config.is_a?(Hash)
39
- expected_hash = expected.to_h
40
- actual_hash = actual.to_h
41
-
42
- config.each do |field, matcher_proc|
43
- case field
44
- when :all
45
- reasons << ":all matcher failed" unless matcher_proc.call(expected_hash, actual_hash)
46
- when :stdout, :stderr, :status
47
- unless matcher_proc.call(expected_hash[field.to_s], actual_hash[field.to_s])
48
- reasons << "#{field} custom matcher failed"
49
- end
50
- end
51
- end
52
- else
53
- # Proc matcher
54
- reasons << "custom matcher failed"
55
- end
56
-
57
- reasons.join(", ")
21
+ evaluation[:reason]
58
22
  end
59
23
 
60
24
  private
@@ -75,29 +39,73 @@ module Backspin
75
39
  config
76
40
  end
77
41
 
78
- def verify_with_hash_matcher
79
- expected_hash = expected.to_h
80
- actual_hash = actual.to_h
42
+ def evaluation
43
+ @evaluation ||= if config.nil?
44
+ evaluate_default
45
+ elsif config.is_a?(Proc)
46
+ evaluate_proc
47
+ elsif config.is_a?(Hash)
48
+ evaluate_hash
49
+ else
50
+ raise ArgumentError, "Invalid matcher type: #{config.class}"
51
+ end
52
+ end
53
+
54
+ def evaluate_default
55
+ reasons = []
56
+ reasons << "stdout differs" if expected.stdout != actual.stdout
57
+ reasons << "stderr differs" if expected.stderr != actual.stderr
58
+ reasons << "exit status differs" if expected.status != actual.status
81
59
 
82
- results = config.map do |field, matcher_proc|
83
- case field
60
+ {match: reasons.empty?, reason: reasons.join(", ")}
61
+ end
62
+
63
+ def evaluate_proc
64
+ match = !!config.call(deep_dup(expected_hash), deep_dup(actual_hash))
65
+ reason = match ? "" : "custom matcher failed"
66
+ {match: match, reason: reason}
67
+ end
68
+
69
+ def evaluate_hash
70
+ reasons = []
71
+
72
+ config.each do |field, matcher_proc|
73
+ passed = case field
84
74
  when :all
85
- matcher_proc.call(expected_hash, actual_hash)
75
+ matcher_proc.call(deep_dup(expected_hash), deep_dup(actual_hash))
86
76
  when :stdout, :stderr, :status
87
- matcher_proc.call(expected_hash[field.to_s], actual_hash[field.to_s])
77
+ matcher_proc.call(deep_dup(expected.public_send(field)), deep_dup(actual.public_send(field)))
88
78
  else
89
79
  raise ArgumentError, "Unknown field: #{field}"
90
80
  end
81
+
82
+ next if passed
83
+
84
+ reasons << ":all matcher failed" if field == :all
85
+ reasons << "#{field} custom matcher failed" if %i[stdout stderr status].include?(field)
91
86
  end
92
87
 
93
- results.all?
88
+ {match: reasons.empty?, reason: reasons.join(", ")}
89
+ end
90
+
91
+ def expected_hash
92
+ @expected_hash ||= expected.to_h
93
+ end
94
+
95
+ def actual_hash
96
+ @actual_hash ||= actual.to_h
94
97
  end
95
98
 
96
- def default_matcher
97
- @default_matcher ||= lambda do |recorded, actual|
98
- recorded["stdout"] == actual["stdout"] &&
99
- recorded["stderr"] == actual["stderr"] &&
100
- recorded["status"] == actual["status"]
99
+ def deep_dup(value)
100
+ case value
101
+ when Hash
102
+ value.transform_values { |entry| deep_dup(entry) }
103
+ when Array
104
+ value.map { |entry| deep_dup(entry) }
105
+ when String
106
+ value.dup
107
+ else
108
+ value
101
109
  end
102
110
  end
103
111
  end
@@ -47,10 +47,12 @@ module Backspin
47
47
 
48
48
  def save(filter: nil)
49
49
  FileUtils.mkdir_p(File.dirname(@path))
50
+ snapshot_data = @snapshot&.to_h
51
+ snapshot_data = filter.call(deep_dup(snapshot_data)) if snapshot_data && filter
50
52
  record_data = {
51
53
  "format_version" => FORMAT_VERSION,
52
54
  "recorded_at" => @recorded_at,
53
- "snapshot" => @snapshot&.to_h(filter: filter)
55
+ "snapshot" => snapshot_data
54
56
  }
55
57
  File.write(@path, record_data.to_yaml)
56
58
  end
@@ -91,5 +93,20 @@ module Backspin
91
93
  rescue Psych::SyntaxError => e
92
94
  raise RecordFormatError, "Invalid record format: #{e.message}"
93
95
  end
96
+
97
+ private
98
+
99
+ def deep_dup(value)
100
+ case value
101
+ when Hash
102
+ value.transform_values { |entry| deep_dup(entry) }
103
+ when Array
104
+ value.map { |entry| deep_dup(entry) }
105
+ when String
106
+ value.dup
107
+ else
108
+ value
109
+ end
110
+ end
94
111
  end
95
112
  end
@@ -6,13 +6,14 @@ require "backspin/command_diff"
6
6
  module Backspin
7
7
  # Handles capture-mode recording and verification
8
8
  class Recorder
9
- attr_reader :mode, :record, :matcher, :filter
9
+ attr_reader :mode, :record, :matcher, :filter, :filter_on
10
10
 
11
- def initialize(mode: :record, record: nil, matcher: nil, filter: nil)
11
+ def initialize(mode: :record, record: nil, matcher: nil, filter: nil, filter_on: :both)
12
12
  @mode = mode
13
13
  @record = record
14
14
  @matcher = matcher
15
15
  @filter = filter
16
+ @filter_on = filter_on
16
17
  end
17
18
 
18
19
  # Performs capture recording by intercepting all stdout/stderr output
@@ -62,7 +63,9 @@ module Backspin
62
63
  command_diff = CommandDiff.new(
63
64
  expected: expected_snapshot,
64
65
  actual: actual_snapshot,
65
- matcher: @matcher
66
+ matcher: @matcher,
67
+ filter: @filter,
68
+ filter_on: @filter_on
66
69
  )
67
70
 
68
71
  BackspinResult.new(
@@ -7,12 +7,13 @@ module Backspin
7
7
 
8
8
  def initialize(command_type:, args:, env: nil, stdout: "", stderr: "", status: 0, recorded_at: nil)
9
9
  @command_type = command_type
10
- @args = args
11
- @env = env
12
- @stdout = stdout || ""
13
- @stderr = stderr || ""
10
+ @args = sanitize_args(args)
11
+ @env = env.nil? ? nil : sanitize_env(env)
12
+ @stdout = Backspin.scrub_text((stdout || "").dup).freeze
13
+ @stderr = Backspin.scrub_text((stderr || "").dup).freeze
14
14
  @status = status || 0
15
- @recorded_at = recorded_at
15
+ @recorded_at = recorded_at.nil? ? nil : recorded_at.dup.freeze
16
+ @serialized_hash = build_serialized_hash
16
17
  end
17
18
 
18
19
  def success?
@@ -23,20 +24,8 @@ module Backspin
23
24
  !success?
24
25
  end
25
26
 
26
- def to_h(filter: nil)
27
- data = {
28
- "command_type" => command_type.name,
29
- "args" => scrub_args(args),
30
- "stdout" => Backspin.scrub_text(stdout),
31
- "stderr" => Backspin.scrub_text(stderr),
32
- "status" => status,
33
- "recorded_at" => recorded_at
34
- }
35
-
36
- data["env"] = scrub_env(env) if env
37
- data = filter.call(data) if filter
38
-
39
- data
27
+ def to_h
28
+ @serialized_hash
40
29
  end
41
30
 
42
31
  def self.from_h(data)
@@ -62,6 +51,19 @@ module Backspin
62
51
 
63
52
  private
64
53
 
54
+ def build_serialized_hash
55
+ data = {
56
+ "command_type" => command_type.name,
57
+ "args" => args,
58
+ "stdout" => stdout,
59
+ "stderr" => stderr,
60
+ "status" => status,
61
+ "recorded_at" => recorded_at
62
+ }
63
+ data["env"] = env if env
64
+ deep_freeze(data)
65
+ end
66
+
65
67
  def scrub_args(value)
66
68
  return value unless Backspin.configuration.scrub_credentials && value
67
69
 
@@ -82,6 +84,37 @@ module Backspin
82
84
 
83
85
  value.transform_values { |entry| entry.is_a?(String) ? Backspin.scrub_text(entry) : entry }
84
86
  end
87
+
88
+ def sanitize_args(value)
89
+ deep_freeze(scrub_args(deep_dup(value)))
90
+ end
91
+
92
+ def sanitize_env(value)
93
+ deep_freeze(scrub_env(deep_dup(value)))
94
+ end
95
+
96
+ def deep_freeze(value)
97
+ case value
98
+ when Hash
99
+ value.each_value { |v| deep_freeze(v) }
100
+ when Array
101
+ value.each { |v| deep_freeze(v) }
102
+ end
103
+ value.freeze
104
+ end
105
+
106
+ def deep_dup(value)
107
+ case value
108
+ when Hash
109
+ value.transform_values { |entry| deep_dup(entry) }
110
+ when Array
111
+ value.map { |entry| deep_dup(entry) }
112
+ when String
113
+ value.dup
114
+ else
115
+ value
116
+ end
117
+ end
85
118
  end
86
119
  end
87
120
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Backspin
4
- VERSION = "0.9.0"
4
+ VERSION = "0.10.0"
5
5
  end
data/lib/backspin.rb CHANGED
@@ -71,19 +71,30 @@ module Backspin
71
71
  # @param env [Hash] Environment variables to pass to Open3.capture3
72
72
  # @param mode [Symbol] Recording mode - :auto, :record, :verify
73
73
  # @param matcher [Proc, Hash] Custom matcher for verification
74
- # @param filter [Proc] Custom filter for recorded data
74
+ # @param filter [Proc] Custom filter for recorded data/canonicalization
75
+ # @param filter_on [Symbol] Filter application mode - :both (default), :record
75
76
  # @return [BackspinResult] Aggregate result for this run
76
- def run(command = nil, name:, env: nil, mode: :auto, matcher: nil, filter: nil, &block)
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
+
77
80
  if block_given?
78
81
  raise ArgumentError, "command must be omitted when using a block" unless command.nil?
79
82
  raise ArgumentError, "env is not supported when using a block" unless env.nil?
80
83
 
81
- 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)
82
85
  end
83
86
 
84
87
  raise ArgumentError, "command is required" if command.nil?
85
88
 
86
- perform_command_run(command, name: name, env: env, mode: mode, matcher: matcher, filter: filter)
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
+ )
87
98
  end
88
99
 
89
100
  # Captures all stdout/stderr output from a block
@@ -91,18 +102,20 @@ module Backspin
91
102
  # @param record_name [String] Name for the record file
92
103
  # @param mode [Symbol] Recording mode - :auto, :record, :verify
93
104
  # @param matcher [Proc, Hash] Custom matcher for verification
94
- # @param filter [Proc] Custom filter for recorded data
105
+ # @param filter [Proc] Custom filter for recorded data/canonicalization
106
+ # @param filter_on [Symbol] Filter application mode - :both (default), :record
95
107
  # @return [BackspinResult] Aggregate result for this run
96
- def capture(record_name, mode: :auto, matcher: nil, filter: nil, &block)
108
+ def capture(record_name, mode: :auto, matcher: nil, filter: nil, filter_on: :both, &block)
97
109
  raise ArgumentError, "record_name is required" if record_name.nil? || record_name.empty?
98
110
  raise ArgumentError, "block is required" unless block_given?
111
+ validate_filter_on!(filter_on)
99
112
 
100
- 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)
101
114
  end
102
115
 
103
116
  private
104
117
 
105
- def perform_capture(record_name, mode:, matcher:, filter:, &block)
118
+ def perform_capture(record_name, mode:, matcher:, filter:, filter_on:, &block)
106
119
  record_path = Record.build_record_path(record_name)
107
120
  mode = determine_mode(mode, record_path)
108
121
  validate_mode!(mode)
@@ -113,7 +126,7 @@ module Backspin
113
126
  Record.load_or_create(record_path)
114
127
  end
115
128
 
116
- 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)
117
130
 
118
131
  result = case mode
119
132
  when :record
@@ -129,7 +142,7 @@ module Backspin
129
142
  result
130
143
  end
131
144
 
132
- def perform_command_run(command, name:, env:, mode:, matcher:, filter:)
145
+ def perform_command_run(command, name:, env:, mode:, matcher:, filter:, filter_on:)
133
146
  record_path = Record.build_record_path(name)
134
147
  mode = determine_mode(mode, record_path)
135
148
  validate_mode!(mode)
@@ -180,7 +193,13 @@ module Backspin
180
193
  stderr: stderr,
181
194
  status: status.exitstatus
182
195
  )
183
- command_diff = CommandDiff.new(expected: expected_snapshot, actual: actual_snapshot, matcher: matcher)
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
+ )
184
203
  BackspinResult.new(
185
204
  mode: :verify,
186
205
  record_path: record.path,
@@ -242,5 +261,11 @@ module Backspin
242
261
 
243
262
  raise ArgumentError, "Unknown mode: #{mode}"
244
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
245
270
  end
246
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.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Sanheim