rspec-signal 0.1.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 +7 -0
- data/CHANGELOG.md +68 -0
- data/LICENSE +21 -0
- data/README.md +672 -0
- data/exe/rspec-signal +11 -0
- data/exe/rspec-signal-parallel +52 -0
- data/lib/rspec/signal/backtrace/classifier.rb +88 -0
- data/lib/rspec/signal/backtrace/frame.rb +31 -0
- data/lib/rspec/signal/backtrace/parser.rb +61 -0
- data/lib/rspec/signal/backtrace/reducer.rb +244 -0
- data/lib/rspec/signal/cluster.rb +68 -0
- data/lib/rspec/signal/clusterer.rb +49 -0
- data/lib/rspec/signal/configuration.rb +144 -0
- data/lib/rspec/signal/failure.rb +62 -0
- data/lib/rspec/signal/failure_builder.rb +224 -0
- data/lib/rspec/signal/fingerprint.rb +56 -0
- data/lib/rspec/signal/formatter.rb +207 -0
- data/lib/rspec/signal/group.rb +61 -0
- data/lib/rspec/signal/grouper.rb +28 -0
- data/lib/rspec/signal/html_summary.rb +224 -0
- data/lib/rspec/signal/integrations/capybara.rb +102 -0
- data/lib/rspec/signal/message.rb +161 -0
- data/lib/rspec/signal/parallel_merger.rb +125 -0
- data/lib/rspec/signal/parallel_run.rb +56 -0
- data/lib/rspec/signal/project.rb +158 -0
- data/lib/rspec/signal/redactor.rb +91 -0
- data/lib/rspec/signal/report.rb +88 -0
- data/lib/rspec/signal/reporters/full_output.rb +38 -0
- data/lib/rspec/signal/reporters/json_report.rb +20 -0
- data/lib/rspec/signal/reporters/markdown.rb +271 -0
- data/lib/rspec/signal/reporters/related_failures.rb +101 -0
- data/lib/rspec/signal/symptom.rb +22 -0
- data/lib/rspec/signal/symptoms/exception_class.rb +46 -0
- data/lib/rspec/signal/symptoms/http_status.rb +98 -0
- data/lib/rspec/signal/symptoms/record.rb +56 -0
- data/lib/rspec/signal/symptoms/route.rb +45 -0
- data/lib/rspec/signal/symptoms/ruby_error.rb +55 -0
- data/lib/rspec/signal/symptoms/selector.rb +72 -0
- data/lib/rspec/signal/symptoms.rb +42 -0
- data/lib/rspec/signal/version.rb +7 -0
- data/lib/rspec/signal/writer.rb +88 -0
- data/lib/rspec/signal.rb +156 -0
- data/lib/rspec-signal.rb +3 -0
- metadata +117 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpec
|
|
4
|
+
module Signal
|
|
5
|
+
module Symptoms
|
|
6
|
+
# Two Ruby-level messages specific enough to cluster on: a method called
|
|
7
|
+
# on the wrong thing, and a constant that was never defined.
|
|
8
|
+
#
|
|
9
|
+
# `undefined method 'progress' for nil` in six specs is one nil, not six
|
|
10
|
+
# bugs. Both the method and the receiver are part of the key, so
|
|
11
|
+
# `#name for nil` and `#total for nil` stay apart.
|
|
12
|
+
module RubyError
|
|
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+)*)/
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def call(_failure, text)
|
|
20
|
+
undefined_method(text) || uninitialized_constant(text)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def undefined_method(text)
|
|
24
|
+
match = UNDEFINED_METHOD.match(text)
|
|
25
|
+
return nil unless match
|
|
26
|
+
|
|
27
|
+
name = match[:name]
|
|
28
|
+
receiver = receiver_name(match[:receiver])
|
|
29
|
+
Symptom.new(kind: :undefined_method, key: "undefined-method:#{name}:#{receiver}",
|
|
30
|
+
label: "undefined method `#{name}` on #{receiver}",
|
|
31
|
+
detail: "undefined `#{name}` for #{receiver}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def uninitialized_constant(text)
|
|
35
|
+
match = UNINITIALIZED.match(text)
|
|
36
|
+
return nil unless match
|
|
37
|
+
|
|
38
|
+
constant = match[:constant]
|
|
39
|
+
Symptom.new(kind: :missing_constant, key: "missing-constant:#{constant}",
|
|
40
|
+
label: "uninitialized constant `#{constant}`", detail: "uninitialized #{constant}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# `nil:NilClass`, `#<User:0x00007f9a>` and `an instance of User` all
|
|
44
|
+
# name a class; use it, so two receivers of the same class cluster.
|
|
45
|
+
def receiver_name(raw)
|
|
46
|
+
text = raw.to_s
|
|
47
|
+
return "nil" if text.start_with?("nil")
|
|
48
|
+
return ::Regexp.last_match(1) if text =~ /\A#<([A-Z]\w*(?:::\w+)*)/
|
|
49
|
+
|
|
50
|
+
text.split(":").first.to_s
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpec
|
|
4
|
+
module Signal
|
|
5
|
+
module Symptoms
|
|
6
|
+
# A DOM node or piece of page text Capybara could not find.
|
|
7
|
+
#
|
|
8
|
+
# The same selector missing from four pages is one missing partial, one
|
|
9
|
+
# renamed `data-testid`, one component that never mounted. Capybara says
|
|
10
|
+
# so two different ways depending on whether you used `find` or
|
|
11
|
+
# `have_css`, and those two produce different exception classes and
|
|
12
|
+
# different messages, so they are correctly different signatures -- but
|
|
13
|
+
# they are obviously the same symptom.
|
|
14
|
+
#
|
|
15
|
+
# The selector itself is the cluster key, compared exactly. Two different
|
|
16
|
+
# selectors never meet.
|
|
17
|
+
module Selector
|
|
18
|
+
TYPES = "css|xpath|field|link or button|link|button|select box|checkbox|" \
|
|
19
|
+
"radio button|file field|fillable field|element|selector|table"
|
|
20
|
+
# The selector as Capybara printed it: inspected, or a bare token.
|
|
21
|
+
TARGET = /"(?:[^"\\]|\\.)*"|\S+/
|
|
22
|
+
VISIBILITY = "(?:visible |invisible )?"
|
|
23
|
+
|
|
24
|
+
# `find(...)` and friends raise Capybara::ElementNotFound.
|
|
25
|
+
UNABLE = /Unable to find #{VISIBILITY}(?<type>#{TYPES})\s+(?<target>#{TARGET})/i
|
|
26
|
+
# `expect(page).to have_css(...)` fails the matcher instead, with a
|
|
27
|
+
# different class and a different sentence for the same missing node.
|
|
28
|
+
NO_MATCHES = Regexp.new("expected to find #{VISIBILITY}(?<type>#{TYPES})\\s+(?<target>#{TARGET})" \
|
|
29
|
+
".{0,200}?but there were no matches", Regexp::IGNORECASE)
|
|
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
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
def call(_failure, text)
|
|
36
|
+
match = NO_MATCHES.match(text) || UNABLE.match(text)
|
|
37
|
+
return missing_text(text) unless match
|
|
38
|
+
|
|
39
|
+
target = unquote(match[:target])
|
|
40
|
+
return nil if target.empty?
|
|
41
|
+
|
|
42
|
+
type = match[:type].to_s.downcase
|
|
43
|
+
Symptom.new(kind: :selector, key: "selector:#{type}:#{target}",
|
|
44
|
+
label: "missing #{type} selector `#{safe(target)}`",
|
|
45
|
+
detail: "no match for #{type} #{target}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def missing_text(text)
|
|
49
|
+
match = TEXT.match(text)
|
|
50
|
+
return nil unless match
|
|
51
|
+
|
|
52
|
+
target = unquote(match[:target])
|
|
53
|
+
return nil if target.empty?
|
|
54
|
+
|
|
55
|
+
Symptom.new(kind: :missing_text, key: "text:#{target}",
|
|
56
|
+
label: "missing page text `#{safe(target)}`",
|
|
57
|
+
detail: "page did not contain \"#{target}\"")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Capybara prints the selector with `inspect`, so a `[data-testid="x"]`
|
|
61
|
+
# arrives with its inner quotes escaped.
|
|
62
|
+
def unquote(raw)
|
|
63
|
+
text = raw.to_s
|
|
64
|
+
text = text[1..-2].to_s.gsub('\\"', '"').gsub("\\\\", "\\") if text.start_with?('"')
|
|
65
|
+
text.gsub(/\s+/, " ").strip
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def safe(target) = target.tr("`", "'")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "symptoms/http_status"
|
|
4
|
+
require_relative "symptoms/route"
|
|
5
|
+
require_relative "symptoms/selector"
|
|
6
|
+
require_relative "symptoms/record"
|
|
7
|
+
require_relative "symptoms/ruby_error"
|
|
8
|
+
require_relative "symptoms/exception_class"
|
|
9
|
+
|
|
10
|
+
module RSpec
|
|
11
|
+
module Signal
|
|
12
|
+
# The symptom extractors, in the order they are tried.
|
|
13
|
+
#
|
|
14
|
+
# A failure takes the first symptom that matches and no more, so it belongs
|
|
15
|
+
# to at most one related cluster. The order is significance, not
|
|
16
|
+
# convenience: a 404 in a request spec is a better organising fact than the
|
|
17
|
+
# exception class that carried it, and the exception class is a worse one
|
|
18
|
+
# than everything above it, which is why it is last.
|
|
19
|
+
module Symptoms
|
|
20
|
+
EXTRACTORS = [HttpStatus, Route, Selector, Record, RubyError, ExceptionClass].freeze
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# @param failure [Failure]
|
|
25
|
+
# @return [Symptom, nil]
|
|
26
|
+
def for(failure)
|
|
27
|
+
text = one_line(failure.message.text)
|
|
28
|
+
EXTRACTORS.each do |extractor|
|
|
29
|
+
symptom = extractor.call(failure, text)
|
|
30
|
+
return symptom if symptom
|
|
31
|
+
end
|
|
32
|
+
nil
|
|
33
|
+
rescue StandardError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Extractor patterns are written against a single line: RSpec wraps the
|
|
38
|
+
# same sentence differently depending on matcher and terminal width.
|
|
39
|
+
def one_line(text) = text.to_s.gsub(/\s+/, " ").strip
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module RSpec
|
|
6
|
+
module Signal
|
|
7
|
+
# Puts artifacts on disk.
|
|
8
|
+
#
|
|
9
|
+
# A run with no failures removes artifacts from previous runs, so an agent
|
|
10
|
+
# can never be handed a stale report that describes failures you already
|
|
11
|
+
# fixed.
|
|
12
|
+
class Writer
|
|
13
|
+
SIGNAL = "signal.md"
|
|
14
|
+
JSON = "signal.json"
|
|
15
|
+
FULL = "full.txt"
|
|
16
|
+
MANAGED = [SIGNAL, JSON, FULL].freeze
|
|
17
|
+
|
|
18
|
+
Result = Struct.new(:summary_path, :written, :cleaned, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
def initialize(config)
|
|
21
|
+
@config = config
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def dir = @config.output_path
|
|
25
|
+
|
|
26
|
+
def write(report)
|
|
27
|
+
return clean if report.failures.empty? && report.errors_outside_examples.zero?
|
|
28
|
+
|
|
29
|
+
FileUtils.mkdir_p(dir)
|
|
30
|
+
write_gitignore
|
|
31
|
+
|
|
32
|
+
markdown = Reporters::Markdown.new(report, @config).render
|
|
33
|
+
written = [write_file(SIGNAL, markdown)]
|
|
34
|
+
written.concat(optional_artifacts(report))
|
|
35
|
+
|
|
36
|
+
stale = MANAGED - written.map { |path| File.basename(path) }
|
|
37
|
+
Result.new(summary_path: File.join(dir, SIGNAL), written: written, cleaned: remove(stale))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Relative to the project root when possible, because that is what you
|
|
41
|
+
# type and what an agent resolves.
|
|
42
|
+
def relative(path)
|
|
43
|
+
root = "#{@config.root}/"
|
|
44
|
+
path.start_with?(root) ? path[root.length..] : path
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def optional_artifacts(report)
|
|
50
|
+
written = []
|
|
51
|
+
written << write_file(JSON, Reporters::JsonReport.new(report, @config).render) if @config.write_json
|
|
52
|
+
written << write_file(FULL, Reporters::FullOutput.new(report, @config).render) if @config.write_full
|
|
53
|
+
written
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def clean
|
|
57
|
+
Result.new(summary_path: nil, written: [], cleaned: remove(MANAGED))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def remove(names)
|
|
61
|
+
names.filter_map do |name|
|
|
62
|
+
path = File.join(dir, name)
|
|
63
|
+
next unless File.file?(path)
|
|
64
|
+
|
|
65
|
+
File.delete(path)
|
|
66
|
+
path
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def write_file(name, contents)
|
|
71
|
+
path = File.join(dir, name)
|
|
72
|
+
File.write(path, contents)
|
|
73
|
+
path
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Failure artifacts routinely contain application data. Keeping them out of
|
|
77
|
+
# version control by default is cheap insurance.
|
|
78
|
+
def write_gitignore
|
|
79
|
+
return unless @config.write_gitignore
|
|
80
|
+
|
|
81
|
+
path = File.join(dir, ".gitignore")
|
|
82
|
+
return if File.exist?(path)
|
|
83
|
+
|
|
84
|
+
File.write(path, "# Written by rspec-signal. Artifacts can contain application data.\n*\n")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/rspec/signal.rb
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rspec/core"
|
|
4
|
+
|
|
5
|
+
require_relative "signal/version"
|
|
6
|
+
require_relative "signal/project"
|
|
7
|
+
require_relative "signal/backtrace/frame"
|
|
8
|
+
require_relative "signal/backtrace/parser"
|
|
9
|
+
require_relative "signal/backtrace/classifier"
|
|
10
|
+
require_relative "signal/backtrace/reducer"
|
|
11
|
+
require_relative "signal/redactor"
|
|
12
|
+
require_relative "signal/html_summary"
|
|
13
|
+
require_relative "signal/message"
|
|
14
|
+
require_relative "signal/fingerprint"
|
|
15
|
+
require_relative "signal/symptom"
|
|
16
|
+
require_relative "signal/symptoms"
|
|
17
|
+
require_relative "signal/failure"
|
|
18
|
+
require_relative "signal/group"
|
|
19
|
+
require_relative "signal/grouper"
|
|
20
|
+
require_relative "signal/cluster"
|
|
21
|
+
require_relative "signal/clusterer"
|
|
22
|
+
require_relative "signal/report"
|
|
23
|
+
require_relative "signal/configuration"
|
|
24
|
+
require_relative "signal/reporters/related_failures"
|
|
25
|
+
require_relative "signal/reporters/markdown"
|
|
26
|
+
require_relative "signal/reporters/json_report"
|
|
27
|
+
require_relative "signal/reporters/full_output"
|
|
28
|
+
require_relative "signal/writer"
|
|
29
|
+
require_relative "signal/parallel_run"
|
|
30
|
+
require_relative "signal/parallel_merger"
|
|
31
|
+
require_relative "signal/failure_builder"
|
|
32
|
+
require_relative "signal/formatter"
|
|
33
|
+
require_relative "signal/integrations/capybara"
|
|
34
|
+
|
|
35
|
+
module RSpec
|
|
36
|
+
# Turns noisy RSpec failures into compact, high-signal diagnostic artifacts
|
|
37
|
+
# designed to be handed to an AI coding agent.
|
|
38
|
+
module Signal
|
|
39
|
+
class << self
|
|
40
|
+
def configuration
|
|
41
|
+
@configuration ||= Configuration.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def configure
|
|
45
|
+
yield configuration if block_given?
|
|
46
|
+
configuration.reset_memoized!
|
|
47
|
+
configuration
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Registers the formatter and optional integrations.
|
|
51
|
+
#
|
|
52
|
+
# Called automatically when you `require "rspec/signal"` inside an
|
|
53
|
+
# `RSpec.configure` block or from `spec_helper.rb`. Safe to call twice.
|
|
54
|
+
#
|
|
55
|
+
# @return [Boolean] whether anything was installed
|
|
56
|
+
def install!(rspec_config = ::RSpec.configuration)
|
|
57
|
+
return false if @installed
|
|
58
|
+
|
|
59
|
+
@installed = true
|
|
60
|
+
return false unless configuration.enabled?
|
|
61
|
+
|
|
62
|
+
quiet_mode? # Capture CLI intent before RSpec finishes parsing ARGV.
|
|
63
|
+
loader = rspec_config.formatter_loader
|
|
64
|
+
unless loader.formatters.any?(Formatter)
|
|
65
|
+
rspec_config.add_formatter(Formatter)
|
|
66
|
+
@auto_installed = true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
install_integrations!(rspec_config)
|
|
70
|
+
true
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
warn "rspec-signal: could not install (#{e.class}: #{e.message})"
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def installed? = !!@installed
|
|
77
|
+
|
|
78
|
+
# True when we added the formatter ourselves rather than the user asking
|
|
79
|
+
# for it with `--format`. Only then do we put RSpec's default formatter
|
|
80
|
+
# back, because only then did we displace it.
|
|
81
|
+
def auto_installed? = !!@auto_installed
|
|
82
|
+
|
|
83
|
+
# Explicit formatter selection happens after --require files are loaded,
|
|
84
|
+
# so install! may initially look automatic. Detect the user's intent from
|
|
85
|
+
# the original CLI arguments (or the wrapper's explicit marker) at start
|
|
86
|
+
# time, after RSpec has finished configuring its formatter loader.
|
|
87
|
+
def quiet_mode?
|
|
88
|
+
return true if ENV["RSPEC_SIGNAL_QUIET"] == "1"
|
|
89
|
+
return @quiet_mode_requested if defined?(@quiet_mode_requested)
|
|
90
|
+
|
|
91
|
+
@quiet_mode_requested = formatter_arguments.intersect?(["RSpec::Signal::Formatter", "signal"])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Adding any formatter suppresses RSpec's default one. When rspec-signal
|
|
95
|
+
# installed itself the user did not ask for that, so restore normal
|
|
96
|
+
# terminal feedback.
|
|
97
|
+
def restore_default_formatter!(rspec_config = ::RSpec.configuration)
|
|
98
|
+
loader = rspec_config.formatter_loader
|
|
99
|
+
return if loader.formatters.any? { |formatter| primary_output_formatter?(formatter) }
|
|
100
|
+
|
|
101
|
+
before = loader.formatters.dup
|
|
102
|
+
loader.add(loader.default_formatter, rspec_config.output_stream)
|
|
103
|
+
(loader.formatters - before).each do |formatter|
|
|
104
|
+
formatter.start(::RSpec::Core::Notifications::StartNotification.new(0, 0)) if formatter.respond_to?(:start)
|
|
105
|
+
end
|
|
106
|
+
rescue StandardError
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Versions worth recording in the report header.
|
|
111
|
+
def environment
|
|
112
|
+
versions = { "ruby" => RUBY_VERSION, "rspec" => ::RSpec::Core::Version::STRING }
|
|
113
|
+
versions["rails"] = ::Rails::VERSION::STRING if defined?(::Rails::VERSION::STRING)
|
|
114
|
+
versions["capybara"] = ::Capybara::VERSION if defined?(::Capybara::VERSION)
|
|
115
|
+
versions
|
|
116
|
+
rescue StandardError
|
|
117
|
+
{ "ruby" => RUBY_VERSION }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @api private Test support.
|
|
121
|
+
def reset!
|
|
122
|
+
@installed = false
|
|
123
|
+
@auto_installed = false
|
|
124
|
+
remove_instance_variable(:@quiet_mode_requested) if defined?(@quiet_mode_requested)
|
|
125
|
+
@configuration = nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def formatter_arguments
|
|
131
|
+
ARGV.each_with_index.filter_map do |argument, index|
|
|
132
|
+
next ARGV[index + 1] if ["--format", "-f"].include?(argument)
|
|
133
|
+
next ::Regexp.last_match(1) if argument =~ /\A--format=(.+)\z/
|
|
134
|
+
|
|
135
|
+
argument[2..] if argument.start_with?("-f") && argument.length > 2
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def install_integrations!(rspec_config)
|
|
140
|
+
return unless configuration.capture_capybara
|
|
141
|
+
|
|
142
|
+
Integrations::Capybara.install!(rspec_config, configuration)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Formatters that give the developer their normal terminal feedback.
|
|
146
|
+
def primary_output_formatter?(formatter)
|
|
147
|
+
return false if formatter.is_a?(Formatter)
|
|
148
|
+
|
|
149
|
+
name = formatter.class.name.to_s
|
|
150
|
+
!name.end_with?("DeprecationFormatter", "FallbackMessageFormatter", "ProfileFormatter")
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
RSpec::Signal.install! if defined?(RSpec) && RSpec.respond_to?(:configuration)
|
data/lib/rspec-signal.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rspec-signal
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Chad Snow
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-26 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rspec-core
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.10'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '4.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '3.10'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '4.0'
|
|
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.
|
|
39
|
+
email:
|
|
40
|
+
- chaddsnow@gmail.com
|
|
41
|
+
executables:
|
|
42
|
+
- rspec-signal
|
|
43
|
+
- rspec-signal-parallel
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- CHANGELOG.md
|
|
48
|
+
- LICENSE
|
|
49
|
+
- README.md
|
|
50
|
+
- exe/rspec-signal
|
|
51
|
+
- exe/rspec-signal-parallel
|
|
52
|
+
- lib/rspec-signal.rb
|
|
53
|
+
- lib/rspec/signal.rb
|
|
54
|
+
- lib/rspec/signal/backtrace/classifier.rb
|
|
55
|
+
- lib/rspec/signal/backtrace/frame.rb
|
|
56
|
+
- lib/rspec/signal/backtrace/parser.rb
|
|
57
|
+
- lib/rspec/signal/backtrace/reducer.rb
|
|
58
|
+
- lib/rspec/signal/cluster.rb
|
|
59
|
+
- lib/rspec/signal/clusterer.rb
|
|
60
|
+
- lib/rspec/signal/configuration.rb
|
|
61
|
+
- lib/rspec/signal/failure.rb
|
|
62
|
+
- lib/rspec/signal/failure_builder.rb
|
|
63
|
+
- lib/rspec/signal/fingerprint.rb
|
|
64
|
+
- lib/rspec/signal/formatter.rb
|
|
65
|
+
- lib/rspec/signal/group.rb
|
|
66
|
+
- lib/rspec/signal/grouper.rb
|
|
67
|
+
- lib/rspec/signal/html_summary.rb
|
|
68
|
+
- lib/rspec/signal/integrations/capybara.rb
|
|
69
|
+
- lib/rspec/signal/message.rb
|
|
70
|
+
- lib/rspec/signal/parallel_merger.rb
|
|
71
|
+
- lib/rspec/signal/parallel_run.rb
|
|
72
|
+
- lib/rspec/signal/project.rb
|
|
73
|
+
- lib/rspec/signal/redactor.rb
|
|
74
|
+
- lib/rspec/signal/report.rb
|
|
75
|
+
- lib/rspec/signal/reporters/full_output.rb
|
|
76
|
+
- lib/rspec/signal/reporters/json_report.rb
|
|
77
|
+
- lib/rspec/signal/reporters/markdown.rb
|
|
78
|
+
- lib/rspec/signal/reporters/related_failures.rb
|
|
79
|
+
- lib/rspec/signal/symptom.rb
|
|
80
|
+
- lib/rspec/signal/symptoms.rb
|
|
81
|
+
- lib/rspec/signal/symptoms/exception_class.rb
|
|
82
|
+
- lib/rspec/signal/symptoms/http_status.rb
|
|
83
|
+
- lib/rspec/signal/symptoms/record.rb
|
|
84
|
+
- lib/rspec/signal/symptoms/route.rb
|
|
85
|
+
- lib/rspec/signal/symptoms/ruby_error.rb
|
|
86
|
+
- lib/rspec/signal/symptoms/selector.rb
|
|
87
|
+
- lib/rspec/signal/version.rb
|
|
88
|
+
- lib/rspec/signal/writer.rb
|
|
89
|
+
homepage: https://github.com/SilenceDogood1984/rspec-signal
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata:
|
|
93
|
+
source_code_uri: https://github.com/SilenceDogood1984/rspec-signal
|
|
94
|
+
changelog_uri: https://github.com/SilenceDogood1984/rspec-signal/blob/main/CHANGELOG.md
|
|
95
|
+
bug_tracker_uri: https://github.com/SilenceDogood1984/rspec-signal/issues
|
|
96
|
+
rubygems_mfa_required: 'true'
|
|
97
|
+
post_install_message:
|
|
98
|
+
rdoc_options: []
|
|
99
|
+
require_paths:
|
|
100
|
+
- lib
|
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: 3.1.0
|
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
requirements: []
|
|
112
|
+
rubygems_version: 3.5.22
|
|
113
|
+
signing_key:
|
|
114
|
+
specification_version: 4
|
|
115
|
+
summary: Turn noisy RSpec failures into compact, high-signal reports for AI coding
|
|
116
|
+
agents.
|
|
117
|
+
test_files: []
|