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 +4 -4
- data/CHANGELOG.md +21 -1
- data/README.md +6 -1
- data/lib/rspec/signal/backtrace/frame.rb +11 -3
- data/lib/rspec/signal/backtrace/parser.rb +1 -1
- data/lib/rspec/signal/backtrace/reducer.rb +41 -13
- data/lib/rspec/signal/cluster.rb +22 -6
- data/lib/rspec/signal/configuration.rb +10 -3
- data/lib/rspec/signal/failure_builder.rb +1 -1
- data/lib/rspec/signal/formatter.rb +10 -3
- data/lib/rspec/signal/group.rb +10 -3
- data/lib/rspec/signal/html_summary.rb +19 -13
- data/lib/rspec/signal/message.rb +4 -2
- data/lib/rspec/signal/parallel_merger.rb +1 -1
- data/lib/rspec/signal/project.rb +8 -3
- data/lib/rspec/signal/redactor.rb +4 -2
- data/lib/rspec/signal/report.rb +18 -5
- data/lib/rspec/signal/reporters/markdown.rb +27 -9
- data/lib/rspec/signal/reporters/related_failures.rb +12 -4
- data/lib/rspec/signal/symptom.rb +3 -1
- data/lib/rspec/signal/symptoms/http_status.rb +5 -3
- data/lib/rspec/signal/symptoms/record.rb +4 -4
- data/lib/rspec/signal/symptoms/route.rb +2 -2
- data/lib/rspec/signal/symptoms/ruby_error.rb +2 -2
- data/lib/rspec/signal/symptoms/selector.rb +6 -4
- data/lib/rspec/signal/symptoms.rb +3 -1
- data/lib/rspec/signal/version.rb +1 -1
- data/lib/rspec/signal/writer.rb +3 -1
- data/lib/rspec/signal.rb +7 -3
- metadata +9 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6cd660f06b5b7ecd54cdacbd562c06588c157139121debebe3a9ca2a58e6686e
|
|
4
|
+
data.tar.gz: fdd869cce815ac1b14733b54edf8eac6a3d233986aa316f340c60724c852998d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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?
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
@@ -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?
|
|
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
|
|
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?
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def
|
|
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
|
|
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
|
|
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
|
|
73
|
+
def primary_location
|
|
74
|
+
project_frames.first
|
|
75
|
+
end
|
|
50
76
|
|
|
51
|
-
def to_a
|
|
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
|
data/lib/rspec/signal/cluster.rb
CHANGED
|
@@ -24,11 +24,25 @@ module RSpec
|
|
|
24
24
|
self
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def failures
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def
|
|
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
|
|
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?
|
|
49
|
-
|
|
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
|
|
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
|
|
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
|
|
143
|
-
|
|
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
|
data/lib/rspec/signal/group.rb
CHANGED
|
@@ -17,7 +17,9 @@ module RSpec
|
|
|
17
17
|
self
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def 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
|
|
32
|
-
|
|
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)
|
|
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)
|
|
221
|
+
def number(value)
|
|
222
|
+
value.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
|
|
223
|
+
end
|
|
220
224
|
|
|
221
|
-
def plural(count, word)
|
|
225
|
+
def plural(count, word)
|
|
226
|
+
count == 1 ? word : "#{word}s"
|
|
227
|
+
end
|
|
222
228
|
end
|
|
223
229
|
end
|
|
224
230
|
end
|
data/lib/rspec/signal/message.rb
CHANGED
|
@@ -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?
|
|
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
|
|
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)
|
data/lib/rspec/signal/project.rb
CHANGED
|
@@ -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{
|
|
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)
|
|
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?
|
|
55
|
+
def enabled?
|
|
56
|
+
@enabled
|
|
57
|
+
end
|
|
56
58
|
|
|
57
59
|
# @param text [String, nil]
|
|
58
60
|
# @return [String, nil]
|
data/lib/rspec/signal/report.rb
CHANGED
|
@@ -31,10 +31,21 @@ module RSpec
|
|
|
31
31
|
[]
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def group_count
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
|
57
|
+
def outside_examples_count
|
|
58
|
+
"#{quantity(@report.errors_outside_examples, "error")} outside examples"
|
|
59
|
+
end
|
|
58
60
|
|
|
59
|
-
def quantity(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)
|
|
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)
|
|
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
|
|
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)
|
|
267
|
+
def one_line(text)
|
|
268
|
+
text.to_s.gsub(/\s+/, " ").strip
|
|
269
|
+
end
|
|
258
270
|
|
|
259
|
-
def escape(text)
|
|
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)
|
|
279
|
+
def plural(count, word)
|
|
280
|
+
count == 1 ? word : "#{word}s"
|
|
281
|
+
end
|
|
266
282
|
|
|
267
|
-
def number(value)
|
|
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
|
|
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)
|
|
95
|
+
def one_line(text)
|
|
96
|
+
text.to_s.gsub(/\s+/, " ").strip
|
|
97
|
+
end
|
|
94
98
|
|
|
95
|
-
def sentence(text)
|
|
99
|
+
def sentence(text)
|
|
100
|
+
text.to_s.sub(/\A[a-z]/, &:upcase)
|
|
101
|
+
end
|
|
96
102
|
|
|
97
|
-
def plural(count, word)
|
|
103
|
+
def plural(count, word)
|
|
104
|
+
count == 1 ? word : "#{word}s"
|
|
105
|
+
end
|
|
98
106
|
end
|
|
99
107
|
end
|
|
100
108
|
end
|
data/lib/rspec/signal/symptom.rb
CHANGED
|
@@ -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
|
|
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 = /
|
|
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)
|
|
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)
|
|
39
|
+
def one_line(text)
|
|
40
|
+
text.to_s.gsub(/\s+/, " ").strip
|
|
41
|
+
end
|
|
40
42
|
end
|
|
41
43
|
end
|
|
42
44
|
end
|
data/lib/rspec/signal/version.rb
CHANGED
data/lib/rspec/signal/writer.rb
CHANGED
data/lib/rspec/signal.rb
CHANGED
|
@@ -73,12 +73,16 @@ module RSpec
|
|
|
73
73
|
false
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
def 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?
|
|
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
|
|
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.
|
|
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-
|
|
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
|
|
35
|
-
coding
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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:
|
|
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:
|
|
116
|
-
agents.
|
|
115
|
+
summary: Token-efficient RSpec failure output for AI coding agents.
|
|
117
116
|
test_files: []
|