rspec-signal 0.1.0 → 0.2.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: 865646144fc7ea9cdb902ea7256972fd93b0e5282726195b1bfa60d333de0ac6
4
- data.tar.gz: d9a8a241a52e38b52d3b5b3f3f0d999323277a12f5b186bab8b04fdc41e36dee
3
+ metadata.gz: 6cd660f06b5b7ecd54cdacbd562c06588c157139121debebe3a9ca2a58e6686e
4
+ data.tar.gz: fdd869cce815ac1b14733b54edf8eac6a3d233986aa316f340c60724c852998d
5
5
  SHA512:
6
- metadata.gz: 96e69e65c7994aa49d186d3211b1d38783c65a261ef0ce065dc9fd2507b08be5fad1fa167faf413537d151cc48a07dddcae729a721961ea5ca297890e5af75e3
7
- data.tar.gz: 4f27334a9cf98604a33229f8c86253c855c92a91ad8f509fab93665a36a9c200340919d8c55337d10e861789e70026bfea094d88b76c26472c0281b789f56c38
6
+ metadata.gz: 819b4e310c972c189c1497ea25280651d3496c876f17e306a563413b4d2ab6bd03a79bb45595e4e840f2463a641bdd8890d3e4843037567196b2616ab6e5f8b0
7
+ data.tar.gz: d2f3245f7e28d91cb75414ce7c8e5dd361fcefcd8bb6f5ff44036f4b0022ebbc2020fac36c50487c5e9b2cbd587643946be035e073769ca9f4335f4bad99b2db
data/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-08-27
11
+
12
+ ### Added
13
+
14
+ - Ruby 2.7 support. `rspec-signal` now installs and runs on Ruby 2.7, 3.0, 3.1,
15
+ 3.2, 3.3 and 3.4; CI exercises the full matrix, including `rspec-signal-parallel`
16
+ under `parallel_tests` on Ruby 2.7 with the last `parallel_tests` release that
17
+ still supports it (4.7.1 — 4.7.2 and later, and all of the 5.x series, require
18
+ Ruby 3.0+ or 3.1+).
19
+
20
+ ### Changed
21
+
22
+ - `spec.required_ruby_version` lowered from `>= 3.1.0` to `>= 2.7.0`.
23
+ - Endless method definitions (a Ruby 3.0+ feature) were rewritten as ordinary
24
+ `def...end` methods throughout the library and spec suite, with no behavior
25
+ change.
26
+ - Gemspec summary and description now emphasize token-efficient RSpec output for
27
+ AI coding agents.
28
+
10
29
  ## [0.1.0] - 2026-08-26
11
30
 
12
31
  Initial release.
@@ -64,5 +83,6 @@ Initial release.
64
83
  - Diff hunk headers and HTML summary sizes are normalized out of the fingerprint,
65
84
  so a page one line longer is no longer a different failure.
66
85
 
67
- [Unreleased]: https://github.com/SilenceDogood1984/rspec-signal/compare/v0.1.0...HEAD
86
+ [Unreleased]: https://github.com/SilenceDogood1984/rspec-signal/compare/v0.2.0...HEAD
87
+ [0.2.0]: https://github.com/SilenceDogood1984/rspec-signal/compare/v0.1.0...v0.2.0
68
88
  [0.1.0]: https://github.com/SilenceDogood1984/rspec-signal/releases/tag/v0.1.0
data/README.md CHANGED
@@ -631,12 +631,17 @@ everything with `config.redaction_filter`, or turn scrubbing off with
631
631
 
632
632
  ## Compatibility
633
633
 
634
- - Ruby 3.1+
634
+ - Ruby 2.7, 3.0, 3.1, 3.2, 3.3, 3.4
635
635
  - RSpec 3.10+ (via `rspec-core`)
636
636
  - Rails, Capybara, ActiveRecord: optional
637
637
 
638
638
  The only runtime dependency is `rspec-core`.
639
639
 
640
+ If you use `rspec-signal-parallel`, note that `parallel_tests` itself dropped
641
+ Ruby 2.7 support after its 4.7.1 release; on Ruby 2.7, Bundler resolves it (and
642
+ `rubocop`) to the newest release that still supports your Ruby version. Ruby
643
+ 3.0+ can use any current `parallel_tests` release.
644
+
640
645
  ## Development
641
646
 
642
647
  ```bash
@@ -10,9 +10,17 @@ module RSpec
10
10
  # :external - third-party library code (may explain the failing operation)
11
11
  # :framework - test-runner / loader / CLI plumbing that never explains a bug
12
12
  Frame = Struct.new(:raw, :path, :line, :label, :kind, :display_path, :gem_name, keyword_init: true) do
13
- def project? = kind == :project
14
- def external? = kind == :external
15
- def framework? = kind == :framework
13
+ def project?
14
+ kind == :project
15
+ end
16
+
17
+ def external?
18
+ kind == :external
19
+ end
20
+
21
+ def framework?
22
+ kind == :framework
23
+ end
16
24
 
17
25
  def location
18
26
  line ? "#{display_path}:#{line}" : display_path
@@ -19,7 +19,7 @@ module RSpec
19
19
  (?::(?<line>\d+))?
20
20
  (?::in\s+[`'](?<label>.*)['`])?
21
21
  \s*\z
22
- /x
22
+ /x.freeze
23
23
 
24
24
  module_function
25
25
 
@@ -7,19 +7,25 @@ module RSpec
7
7
  module Backtrace
8
8
  # A gap standing in for frames that were dropped.
9
9
  Gap = Struct.new(:count, :kind, keyword_init: true) do # rubocop:disable Lint/StructNewOverride
10
- def frame? = false
10
+ def frame?
11
+ false
12
+ end
11
13
 
12
14
  def to_s
13
15
  noun = kind == :framework ? "framework/runtime" : "library"
14
16
  "[#{count} #{noun} frame#{"s" unless count == 1} omitted]"
15
17
  end
16
18
 
17
- def to_h = { omitted: count, kind: kind.to_s }
19
+ def to_h
20
+ { omitted: count, kind: kind.to_s }
21
+ end
18
22
  end
19
23
 
20
24
  # Frames and gaps are rendered side by side, so both answer `frame?`.
21
25
  class Frame
22
- def frame? = true
26
+ def frame?
27
+ true
28
+ end
23
29
  end
24
30
 
25
31
  # The result of reducing one backtrace.
@@ -33,22 +39,44 @@ module RSpec
33
39
  @fallback = fallback
34
40
  end
35
41
 
36
- def frames = entries.select(&:frame?)
37
- def project_frames = frames.select(&:project?)
38
- def kept_count = frames.size
39
- def fallback? = @fallback
40
- def empty? = frames.empty?
42
+ def frames
43
+ entries.select(&:frame?)
44
+ end
45
+
46
+ def project_frames
47
+ frames.select(&:project?)
48
+ end
49
+
50
+ def kept_count
51
+ frames.size
52
+ end
53
+
54
+ def fallback?
55
+ @fallback
56
+ end
57
+
58
+ def empty?
59
+ frames.empty?
60
+ end
41
61
 
42
- def omitted_count = omitted.values.sum
62
+ def omitted_count
63
+ omitted.values.sum
64
+ end
43
65
 
44
66
  # The frame that best identifies where this blew up: the innermost frame
45
67
  # that is not test-runner plumbing.
46
- def culprit = frames.first
68
+ def culprit
69
+ frames.first
70
+ end
47
71
 
48
72
  # The innermost first-party frame — what the agent should open first.
49
- def primary_location = project_frames.first
73
+ def primary_location
74
+ project_frames.first
75
+ end
50
76
 
51
- def to_a = entries.map(&:to_s)
77
+ def to_a
78
+ entries.map(&:to_s)
79
+ end
52
80
  end
53
81
 
54
82
  # Reduces a full backtrace to the frames that carry diagnostic value.
@@ -74,7 +102,7 @@ module RSpec
74
102
  SCORE_LIBRARY_CALLER = 60 # library frame that called into first-party code
75
103
 
76
104
  # Labels that name no method: delegation shims and block frames.
77
- ANONYMOUS_LABEL = /\A(?:block\s|<(?:top|module|class|main)\b|rescue in\b|ensure in\b)/
105
+ ANONYMOUS_LABEL = /\A(?:block\s|<(?:top|module|class|main)\b|rescue in\b|ensure in\b)/.freeze
78
106
 
79
107
  def initialize(max_frames: 12, max_external_context: 3, max_project_frames: 8, fallback_frames: 6)
80
108
  @max_frames = max_frames
@@ -24,11 +24,25 @@ module RSpec
24
24
  self
25
25
  end
26
26
 
27
- def failures = @members.map(&:failure)
28
- def size = @members.size
29
- def kind = @symptom.kind
30
- def key = @symptom.key
31
- def label = @symptom.label
27
+ def failures
28
+ @members.map(&:failure)
29
+ end
30
+
31
+ def size
32
+ @members.size
33
+ end
34
+
35
+ def kind
36
+ @symptom.kind
37
+ end
38
+
39
+ def key
40
+ @symptom.key
41
+ end
42
+
43
+ def label
44
+ @symptom.label
45
+ end
32
46
 
33
47
  # The distinct wordings the symptom took, most common first, ties broken
34
48
  # by the order they were seen so the list is stable.
@@ -46,7 +60,9 @@ module RSpec
46
60
  @signatures ||= failures.map { |failure| failure.fingerprint.digest }.uniq
47
61
  end
48
62
 
49
- def signature_count = signatures.size
63
+ def signature_count
64
+ signatures.size
65
+ end
50
66
 
51
67
  def spec_files
52
68
  @spec_files ||= failures.map { |failure| failure.spec_location.sub(/:\d+\z/, "") }.uniq
@@ -45,11 +45,18 @@ module RSpec
45
45
  @terminal_summary = true
46
46
  end
47
47
 
48
- def enabled? = !!@enabled
49
- def redact? = !!@redact
48
+ def enabled?
49
+ !!@enabled
50
+ end
51
+
52
+ def redact?
53
+ !!@redact
54
+ end
50
55
 
51
56
  # Nil means "leave HTML alone", which is what {Message} expects.
52
- def html_threshold = reduce_html ? max_html_chars : nil
57
+ def html_threshold
58
+ reduce_html ? max_html_chars : nil
59
+ end
53
60
 
54
61
  def root
55
62
  @root ||= File.expand_path(@project_root || default_root)
@@ -8,7 +8,7 @@ module RSpec
8
8
  # the reduction, grouping and rendering stages testable without booting a
9
9
  # suite.
10
10
  class FailureBuilder
11
- SCREENSHOT = /\[Screenshot(?:\s+Image)?\]:\s*(\S+)/i
11
+ SCREENSHOT = /\[Screenshot(?:\s+Image)?\]:\s*(\S+)/i.freeze
12
12
  MAX_CAUSE_DEPTH = 3
13
13
  MAX_CAUSE_MESSAGE_LINES = 5
14
14
  MAX_CAUSE_SCAN = 40
@@ -28,7 +28,9 @@ module RSpec
28
28
  @seed_used = false
29
29
  end
30
30
 
31
- def config = RSpec::Signal.configuration
31
+ def config
32
+ RSpec::Signal.configuration
33
+ end
32
34
 
33
35
  def start(notification)
34
36
  # Adding a formatter suppresses RSpec's default one. When rspec-signal
@@ -139,8 +141,13 @@ module RSpec
139
141
  @progress_total = nil
140
142
  end
141
143
 
142
- def builder = @builder ||= FailureBuilder.new(config)
143
- def writer = @writer ||= Writer.new(config)
144
+ def builder
145
+ @builder ||= FailureBuilder.new(config)
146
+ end
147
+
148
+ def writer
149
+ @writer ||= Writer.new(config)
150
+ end
144
151
 
145
152
  def print_summary(result)
146
153
  return unless config.terminal_summary
@@ -17,7 +17,9 @@ module RSpec
17
17
  self
18
18
  end
19
19
 
20
- def size = @failures.size
20
+ def size
21
+ @failures.size
22
+ end
21
23
 
22
24
  # The failure shown in full. We pick the one carrying the most first-party
23
25
  # frames, because that is the one whose trace is most useful; ties break on
@@ -28,8 +30,13 @@ module RSpec
28
30
  end.first
29
31
  end
30
32
 
31
- def exception_class = representative.exception_class
32
- def message = representative.message
33
+ def exception_class
34
+ representative.exception_class
35
+ end
36
+
37
+ def message
38
+ representative.message
39
+ end
33
40
 
34
41
  # Locations of every example in the group, in run order, deduplicated.
35
42
  def affected_locations
@@ -25,16 +25,16 @@ module RSpec
25
25
  MAX_FACT_CHARS = 160
26
26
  MAX_FACTS = 4
27
27
 
28
- DOCUMENT_START = /\A\s*(?:<!doctype\s+html|<html[\s>])/i
29
- TAG = %r{</?[a-z][a-z0-9]*(?:\s[^<>]*?)?/?>}im
30
- STRUCTURAL = /<(?:html|head|body|div|table|section|main|article|p)\b/i
31
- QUOTED = /"(?:[^"\\]|\\.)*"/m
32
- DIFF_LINE = /\A(\s*)([-+])(.*)\z/m
33
- NOISE = %r{<(script|style)\b[^>]*>.*?</\1>|<!--.*?-->}im
34
- TITLE = %r{<title[^>]*>(.*?)</title>}im
35
- H1 = %r{<h1[^>]*>(.*?)</h1>}im
36
- H2 = %r{<h2[^>]*>(.*?)</h2>}im
37
- PRE = %r{<pre[^>]*>(.*?)</pre>}im
28
+ DOCUMENT_START = /\A\s*(?:<!doctype\s+html|<html[\s>])/i.freeze
29
+ TAG = %r{</?[a-z][a-z0-9]*(?:\s[^<>]*?)?/?>}im.freeze
30
+ STRUCTURAL = /<(?:html|head|body|div|table|section|main|article|p)\b/i.freeze
31
+ QUOTED = /"(?:[^"\\]|\\.)*"/m.freeze
32
+ DIFF_LINE = /\A(\s*)([-+])(.*)\z/m.freeze
33
+ NOISE = %r{<(script|style)\b[^>]*>.*?</\1>|<!--.*?-->}im.freeze
34
+ TITLE = %r{<title[^>]*>(.*?)</title>}im.freeze
35
+ H1 = %r{<h1[^>]*>(.*?)</h1>}im.freeze
36
+ H2 = %r{<h2[^>]*>(.*?)</h2>}im.freeze
37
+ PRE = %r{<pre[^>]*>(.*?)</pre>}im.freeze
38
38
 
39
39
  ESCAPES = { "n" => "\n", "t" => "\t", "r" => "\r", "e" => "\e", '"' => '"', "\\" => "\\" }.freeze
40
40
  ENTITIES = { "amp" => "&", "lt" => "<", "gt" => ">", "quot" => '"',
@@ -195,7 +195,9 @@ module RSpec
195
195
  visible(match[1])
196
196
  end
197
197
 
198
- def excerpt(body) = visible(body)
198
+ def excerpt(body)
199
+ visible(body)
200
+ end
199
201
 
200
202
  def visible(fragment)
201
203
  text = fragment.to_s.gsub(NOISE, " ").gsub(TAG, " ")
@@ -216,9 +218,13 @@ module RSpec
216
218
  "#{(byte_size / (1024.0 * 1024)).round(1)} MB"
217
219
  end
218
220
 
219
- def number(value) = value.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
221
+ def number(value)
222
+ value.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
223
+ end
220
224
 
221
- def plural(count, word) = count == 1 ? word : "#{word}s"
225
+ def plural(count, word)
226
+ count == 1 ? word : "#{word}s"
227
+ end
222
228
  end
223
229
  end
224
230
  end
@@ -5,7 +5,7 @@ module RSpec
5
5
  # The human-readable failure message, plus a normalized form used for
6
6
  # grouping.
7
7
  class Message
8
- ANSI = /\e\[[0-9;]*[A-Za-z]/
8
+ ANSI = /\e\[[0-9;]*[A-Za-z]/.freeze
9
9
 
10
10
  # Volatile substrings that would otherwise split identical failures into
11
11
  # separate signatures.
@@ -50,7 +50,9 @@ module RSpec
50
50
  @cause_lines = trim(squeeze(reduce_html(normalize(cause_lines), html_threshold)))
51
51
  end
52
52
 
53
- def empty? = @lines.all?(&:empty?) && @cause_lines.all?(&:empty?)
53
+ def empty?
54
+ @lines.all?(&:empty?) && @cause_lines.all?(&:empty?)
55
+ end
54
56
 
55
57
  def text
56
58
  return @lines.join("\n") if @cause_lines.empty?
@@ -46,7 +46,7 @@ module RSpec
46
46
  end
47
47
 
48
48
  def run_directories(paths)
49
- paths.filter_map { |path| File.dirname(path, 2) if path.include?("/workers/") }.uniq
49
+ paths.filter_map { |path| File.dirname(File.dirname(path)) if path.include?("/workers/") }.uniq
50
50
  end
51
51
 
52
52
  def aggregate(payloads)
@@ -26,10 +26,13 @@ module RSpec
26
26
  # A release version, or the short SHA Bundler uses for git sources.
27
27
  (?:-(?<version>\d[\w.]*(?:-[a-z0-9]+)?|[0-9a-f]{12,40}))?
28
28
  /(?<rest>.*)\z
29
- }x
29
+ }x.freeze
30
30
 
31
31
  # Matches ".../lib/ruby/3.3.0/json/common.rb" and ".../ruby/3.3.0/x86_64-linux/..."
32
- STDLIB_PATH = %r{/lib/ruby/(?:\d+\.\d+\.\d+|site_ruby|vendor_ruby)/(?:[a-z0-9_]+-[a-z0-9_-]+/)?(?<rest>.*)\z}
32
+ STDLIB_PATH = %r{
33
+ /lib/ruby/(?:\d+\.\d+\.\d+|site_ruby|vendor_ruby)/
34
+ (?:[a-z0-9_]+-[a-z0-9_-]+/)?(?<rest>.*)\z
35
+ }x.freeze
33
36
 
34
37
  attr_reader :root
35
38
 
@@ -118,7 +121,9 @@ module RSpec
118
121
  rest
119
122
  end
120
123
 
121
- def normalize_gem_name(name) = name.to_s.delete("-_")
124
+ def normalize_gem_name(name)
125
+ name.to_s.delete("-_")
126
+ end
122
127
 
123
128
  def compute_first_party(path)
124
129
  absolute = absolutize(path)
@@ -19,7 +19,7 @@ module RSpec
19
19
  (?:api[_-]?key|secret[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key|
20
20
  secret|password|passwd|pwd|token|auth[_-]?token|access[_-]?token|refresh[_-]?token|
21
21
  session[_-]?id|csrf|cookie|authorization|credentials?)
22
- /xi
22
+ /xi.freeze
23
23
 
24
24
  DEFAULT_PATTERNS = [
25
25
  # Authorization: Bearer xyz / Basic xyz
@@ -52,7 +52,9 @@ module RSpec
52
52
  @filter = filter
53
53
  end
54
54
 
55
- def enabled? = @enabled
55
+ def enabled?
56
+ @enabled
57
+ end
56
58
 
57
59
  # @param text [String, nil]
58
60
  # @return [String, nil]
@@ -31,10 +31,21 @@ module RSpec
31
31
  []
32
32
  end
33
33
 
34
- def group_count = groups.size
35
- def cluster_count = clusters.size
36
- def any_failures? = !failures.empty?
37
- def seed_used? = !!@seed_used
34
+ def group_count
35
+ groups.size
36
+ end
37
+
38
+ def cluster_count
39
+ clusters.size
40
+ end
41
+
42
+ def any_failures?
43
+ !failures.empty?
44
+ end
45
+
46
+ def seed_used?
47
+ !!@seed_used
48
+ end
38
49
 
39
50
  # Backtrace frames dropped across the whole run. This is the number that
40
51
  # makes the reduction visible.
@@ -46,7 +57,9 @@ module RSpec
46
57
  @total_frames ||= failures.sum { |failure| failure.reduced.total }
47
58
  end
48
59
 
49
- def kept_frames = total_frames - omitted_frames
60
+ def kept_frames
61
+ total_frames - omitted_frames
62
+ end
50
63
 
51
64
  def to_h
52
65
  {
@@ -54,9 +54,13 @@ module RSpec
54
54
  "**#{parts.join(" | ")}**"
55
55
  end
56
56
 
57
- def outside_examples_count = "#{quantity(@report.errors_outside_examples, "error")} outside examples"
57
+ def outside_examples_count
58
+ "#{quantity(@report.errors_outside_examples, "error")} outside examples"
59
+ end
58
60
 
59
- def quantity(value, word) = "#{number(value)} #{plural(value, word)}"
61
+ def quantity(value, word)
62
+ "#{number(value)} #{plural(value, word)}"
63
+ end
60
64
 
61
65
  def reduction_line
62
66
  "Backtraces reduced from #{number(@report.total_frames)} to " \
@@ -141,7 +145,9 @@ module RSpec
141
145
  "> #{one_line(group.representative.description)}"].join("\n")
142
146
  end
143
147
 
144
- def labelled(label, body) = "**#{label}**\n\n#{body}"
148
+ def labelled(label, body)
149
+ "**#{label}**\n\n#{body}"
150
+ end
145
151
 
146
152
  # The representative on its own, plus the whole signature when that is a
147
153
  # different command and short enough to be worth typing.
@@ -190,7 +196,9 @@ module RSpec
190
196
 
191
197
  # The same frame the fingerprint calls the culprit, so the gem label
192
198
  # beside a location always belongs to that location.
193
- def culprit_frame(failure) = failure.frames.reject(&:framework?).first
199
+ def culprit_frame(failure)
200
+ failure.frames.reject(&:framework?).first
201
+ end
194
202
 
195
203
  def trace_lines(failure)
196
204
  entries = failure.reduced.entries.map(&:to_s)
@@ -248,23 +256,33 @@ module RSpec
248
256
  @groups ||= @config.max_groups ? @report.groups.first(@config.max_groups) : @report.groups
249
257
  end
250
258
 
251
- def truncated_groups = @report.group_count - groups.size
259
+ def truncated_groups
260
+ @report.group_count - groups.size
261
+ end
252
262
 
253
263
  def fenced(lines, language = "text")
254
264
  [FENCE + language, *Array(lines), FENCE].join("\n")
255
265
  end
256
266
 
257
- def one_line(text) = text.to_s.gsub(/\s+/, " ").strip
267
+ def one_line(text)
268
+ text.to_s.gsub(/\s+/, " ").strip
269
+ end
258
270
 
259
- def escape(text) = text.to_s.gsub("|", "\\|")
271
+ def escape(text)
272
+ text.to_s.gsub("|", "\\|")
273
+ end
260
274
 
261
275
  def humanize(key)
262
276
  DIAGNOSTIC_LABELS.fetch(key.to_sym) { key.to_s.tr("_", " ").capitalize }
263
277
  end
264
278
 
265
- def plural(count, word) = count == 1 ? word : "#{word}s"
279
+ def plural(count, word)
280
+ count == 1 ? word : "#{word}s"
281
+ end
266
282
 
267
- def number(value) = value.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
283
+ def number(value)
284
+ value.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
285
+ end
268
286
  end
269
287
  end
270
288
  end
@@ -45,7 +45,9 @@ module RSpec
45
45
  @shown ||= @config.max_clusters ? @clusters.first(@config.max_clusters) : @clusters
46
46
  end
47
47
 
48
- def hidden = @clusters.size - shown.size
48
+ def hidden
49
+ @clusters.size - shown.size
50
+ end
49
51
 
50
52
  def truncation_note
51
53
  "_#{hidden} further related #{plural(hidden, "cluster")} not rendered (see `signal.json`)._"
@@ -90,11 +92,17 @@ module RSpec
90
92
  rendered.join(", ")
91
93
  end
92
94
 
93
- def one_line(text) = text.to_s.gsub(/\s+/, " ").strip
95
+ def one_line(text)
96
+ text.to_s.gsub(/\s+/, " ").strip
97
+ end
94
98
 
95
- def sentence(text) = text.to_s.sub(/\A[a-z]/, &:upcase)
99
+ def sentence(text)
100
+ text.to_s.sub(/\A[a-z]/, &:upcase)
101
+ end
96
102
 
97
- def plural(count, word) = count == 1 ? word : "#{word}s"
103
+ def plural(count, word)
104
+ count == 1 ? word : "#{word}s"
105
+ end
98
106
  end
99
107
  end
100
108
  end
@@ -16,7 +16,9 @@ module RSpec
16
16
  # label the cluster heading, already human-readable
17
17
  # detail this failure's own wording of the symptom, for the symptom list
18
18
  Symptom = Struct.new(:kind, :key, :label, :detail, keyword_init: true) do
19
- def to_h = { kind: kind, key: key, label: label, detail: detail }
19
+ def to_h
20
+ { kind: kind, key: key, label: label, detail: detail }
21
+ end
20
22
  end
21
23
  end
22
24
  end
@@ -12,7 +12,7 @@ module RSpec
12
12
  # examples wanted 200 and others wanted a redirect.
13
13
  module HttpStatus
14
14
  # One side of a status mismatch: `404`, `:not_found`, `:ok (200)`.
15
- CODE = /:?[\w-]+(?: \(\d{3}\))?/
15
+ CODE = /:?[\w-]+(?: \(\d{3}\))?/.freeze
16
16
 
17
17
  PATTERNS = [
18
18
  # rspec-rails `have_http_status(200)` / `have_http_status(:ok)`
@@ -45,8 +45,10 @@ module RSpec
45
45
 
46
46
  module_function
47
47
 
48
- GENERIC_EXPECTATION = /expected:\s*(?<expected>\d{3})\s+got:\s*(?<actual>\d{3})/i
49
- STATUS_EXPRESSION = /(?:response\s*\.\s*status|response\s*\.\s*status_code|response\s*\[\s*:status\s*\])/i
48
+ GENERIC_EXPECTATION = /expected:\s*(?<expected>\d{3})\s+got:\s*(?<actual>\d{3})/i.freeze
49
+ STATUS_EXPRESSION = /
50
+ (?:response\s*\.\s*status|response\s*\.\s*status_code|response\s*\[\s*:status\s*\])
51
+ /ix.freeze
50
52
 
51
53
  def call(failure, text)
52
54
  match = PATTERNS.filter_map { |pattern| pattern.match(text) }.first
@@ -10,10 +10,10 @@ module RSpec
10
10
  # cluster keys. A `RecordNotFound` for `User` and one for `Order` are two
11
11
  # different problems and stay apart.
12
12
  module Record
13
- NOT_FOUND = /Couldn't find (?<model>[A-Z]\w*(?:::\w+)*)/
14
- VALIDATION = %r{Validation failed: (?<detail>.{1,120}?)(?=\s{2,}|\s+Caused by\b|\s+Failure/Error\b|\z)}
15
- MISSING = /(?<what>column|relation|table)\s+["'`]?(?<name>[\w."]+?)["'`]?\s+does not exist/i
16
- SQLITE = /no such (?<what>column|table):\s*(?<name>[\w.]+)/i
13
+ NOT_FOUND = /Couldn't find (?<model>[A-Z]\w*(?:::\w+)*)/.freeze
14
+ VALIDATION = %r{Validation failed: (?<detail>.{1,120}?)(?=\s{2,}|\s+Caused by\b|\s+Failure/Error\b|\z)}.freeze
15
+ MISSING = /(?<what>column|relation|table)\s+["'`]?(?<name>[\w."]+?)["'`]?\s+does not exist/i.freeze
16
+ SQLITE = /no such (?<what>column|table):\s*(?<name>[\w.]+)/i.freeze
17
17
 
18
18
  module_function
19
19
 
@@ -6,9 +6,9 @@ module RSpec
6
6
  # A route that does not exist -- the usual thing sitting behind a suite
7
7
  # full of unexplained 404s.
8
8
  module Route
9
- NO_ROUTE = /No route matches (?<target>\[[A-Z]+\]\s*"[^"]*"|\{[^}]*\})/
9
+ NO_ROUTE = /No route matches (?<target>\[[A-Z]+\]\s*"[^"]*"|\{[^}]*\})/.freeze
10
10
  # `reader_progress_path` after the route was renamed or removed.
11
- HELPER = /undefined (?:local variable or method|method) [`'"](?<helper>\w+_(?:path|url))['"`]/
11
+ HELPER = /undefined (?:local variable or method|method) [`'"](?<helper>\w+_(?:path|url))['"`]/.freeze
12
12
 
13
13
  module_function
14
14
 
@@ -11,8 +11,8 @@ module RSpec
11
11
  # `#name for nil` and `#total for nil` stay apart.
12
12
  module RubyError
13
13
  # Ruby 3.4 quotes with `'x'`, earlier versions with `` `x' ``.
14
- UNDEFINED_METHOD = /undefined method [`'"](?<name>[^'"`]+)['"`] for (?:an instance of )?(?<receiver>\S+)/
15
- UNINITIALIZED = /uninitialized constant (?<constant>[A-Z]\w*(?:::\w+)*)/
14
+ UNDEFINED_METHOD = /undefined method [`'"](?<name>[^'"`]+)['"`] for (?:an instance of )?(?<receiver>\S+)/.freeze
15
+ UNINITIALIZED = /uninitialized constant (?<constant>[A-Z]\w*(?:::\w+)*)/.freeze
16
16
 
17
17
  module_function
18
18
 
@@ -18,17 +18,17 @@ module RSpec
18
18
  TYPES = "css|xpath|field|link or button|link|button|select box|checkbox|" \
19
19
  "radio button|file field|fillable field|element|selector|table"
20
20
  # The selector as Capybara printed it: inspected, or a bare token.
21
- TARGET = /"(?:[^"\\]|\\.)*"|\S+/
21
+ TARGET = /"(?:[^"\\]|\\.)*"|\S+/.freeze
22
22
  VISIBILITY = "(?:visible |invisible )?"
23
23
 
24
24
  # `find(...)` and friends raise Capybara::ElementNotFound.
25
- UNABLE = /Unable to find #{VISIBILITY}(?<type>#{TYPES})\s+(?<target>#{TARGET})/i
25
+ UNABLE = /Unable to find #{VISIBILITY}(?<type>#{TYPES})\s+(?<target>#{TARGET})/i.freeze
26
26
  # `expect(page).to have_css(...)` fails the matcher instead, with a
27
27
  # different class and a different sentence for the same missing node.
28
28
  NO_MATCHES = Regexp.new("expected to find #{VISIBILITY}(?<type>#{TYPES})\\s+(?<target>#{TARGET})" \
29
29
  ".{0,200}?but there were no matches", Regexp::IGNORECASE)
30
30
  # Page text is not a selector, but it goes missing for the same reasons.
31
- TEXT = /(?:Unable to find|expected to find) text (?<target>#{TARGET})/i
31
+ TEXT = /(?:Unable to find|expected to find) text (?<target>#{TARGET})/i.freeze
32
32
 
33
33
  module_function
34
34
 
@@ -65,7 +65,9 @@ module RSpec
65
65
  text.gsub(/\s+/, " ").strip
66
66
  end
67
67
 
68
- def safe(target) = target.tr("`", "'")
68
+ def safe(target)
69
+ target.tr("`", "'")
70
+ end
69
71
  end
70
72
  end
71
73
  end
@@ -36,7 +36,9 @@ module RSpec
36
36
 
37
37
  # Extractor patterns are written against a single line: RSpec wraps the
38
38
  # same sentence differently depending on matcher and terminal width.
39
- def one_line(text) = text.to_s.gsub(/\s+/, " ").strip
39
+ def one_line(text)
40
+ text.to_s.gsub(/\s+/, " ").strip
41
+ end
40
42
  end
41
43
  end
42
44
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module Signal
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
@@ -21,7 +21,9 @@ module RSpec
21
21
  @config = config
22
22
  end
23
23
 
24
- def dir = @config.output_path
24
+ def dir
25
+ @config.output_path
26
+ end
25
27
 
26
28
  def write(report)
27
29
  return clean if report.failures.empty? && report.errors_outside_examples.zero?
data/lib/rspec/signal.rb CHANGED
@@ -73,12 +73,16 @@ module RSpec
73
73
  false
74
74
  end
75
75
 
76
- def installed? = !!@installed
76
+ def installed?
77
+ !!@installed
78
+ end
77
79
 
78
80
  # True when we added the formatter ourselves rather than the user asking
79
81
  # for it with `--format`. Only then do we put RSpec's default formatter
80
82
  # back, because only then did we displace it.
81
- def auto_installed? = !!@auto_installed
83
+ def auto_installed?
84
+ !!@auto_installed
85
+ end
82
86
 
83
87
  # Explicit formatter selection happens after --require files are loaded,
84
88
  # so install! may initially look automatic. Detect the user's intent from
@@ -88,7 +92,7 @@ module RSpec
88
92
  return true if ENV["RSPEC_SIGNAL_QUIET"] == "1"
89
93
  return @quiet_mode_requested if defined?(@quiet_mode_requested)
90
94
 
91
- @quiet_mode_requested = formatter_arguments.intersect?(["RSpec::Signal::Formatter", "signal"])
95
+ @quiet_mode_requested = !(formatter_arguments & ["RSpec::Signal::Formatter", "signal"]).empty?
92
96
  end
93
97
 
94
98
  # Adding any formatter suppresses RSpec's default one. When rspec-signal
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-signal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chad Snow
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-26 00:00:00.000000000 Z
11
+ date: 2026-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec-core
@@ -31,11 +31,11 @@ dependencies:
31
31
  - !ruby/object:Gem::Version
32
32
  version: '4.0'
33
33
  description: |
34
- rspec-signal is a deterministic context-reduction layer between RSpec and an AI
35
- coding agent. It collapses framework and runtime backtrace plumbing, keeps
36
- first-party frames plus the small amount of library context that explains the
37
- failing operation, groups repeated failures into distinct signatures, and writes
38
- a compact Markdown report you can hand straight to a coding agent.
34
+ rspec-signal turns noisy RSpec failures into compact, token-efficient diagnostic
35
+ reports for AI coding agents. It removes repetitive framework backtraces, preserves
36
+ the application and library context that matters, groups related failures, and
37
+ writes a focused Markdown report instead of forcing an agent to consume thousands
38
+ of lines of raw RSpec output.
39
39
  email:
40
40
  - chaddsnow@gmail.com
41
41
  executables:
@@ -102,7 +102,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
102
102
  requirements:
103
103
  - - ">="
104
104
  - !ruby/object:Gem::Version
105
- version: 3.1.0
105
+ version: 2.7.0
106
106
  required_rubygems_version: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - ">="
@@ -112,6 +112,5 @@ requirements: []
112
112
  rubygems_version: 3.5.22
113
113
  signing_key:
114
114
  specification_version: 4
115
- summary: Turn noisy RSpec failures into compact, high-signal reports for AI coding
116
- agents.
115
+ summary: Token-efficient RSpec failure output for AI coding agents.
117
116
  test_files: []