Module: RSpecTracer::CLI::BlastRadius Private
- Defined in:
- lib/rspec_tracer/cli/blast_radius.rb
Overview
This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.
rspec-tracer blast-radius <file> [<file> ...] -- show which
examples a change to each given file would re-run on the next
rspec invocation. Reads the persisted reverse-dependency map
(file -> examples) plus the whole-suite invalidator watch list
and the boot set, so whole-suite triggers (Gemfile.lock, boot
files) report "re-runs all N examples" instead of under-reporting
via a bare reverse-dependency lookup. Reports the file-change
trigger only; examples that always re-run for status reasons
(failed / flaky / pending / interrupted) are not included.
Backend-agnostic: dispatches through
Storage::Backend.build (via
SnapshotHelpers.load_snapshot) so storage_backend :sqlite
resolves the latest run from the meta table instead of the
JsonBackend-only last_run.json file.
Constant Summary collapse
- USAGE =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Internal constant.
'rspec-tracer blast-radius [--list|--json] <file> [<file> ...]'- WHOLE_SUITE_STATUSES =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Statuses whose blast radius is the entire suite (a change to the file invalidates every cached example, not just tracked dependents).
%w[whole_suite_invalidator boot_file].freeze
Class Method Summary collapse
-
.dependent_radius(path, file_name, snapshot) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.example_entries(ids, snapshot) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.example_entry(id, meta) ⇒ Object
private
Enrich one example_id from the persisted all_examples meta.
-
.execute(parsed, stdout, stderr) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.file_json(radius) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.json_payload(radii, snapshot) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.no_files(stderr) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.normalize_file_name(path) ⇒ Object
private
Convert a user-supplied path into the cache's file_name convention (project-relative with a leading
/, absolute when outside the project root). -
.parse_args(args) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.print_help(stdout) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.print_report(stdout, radii, snapshot, list:) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.print_total(stdout, radii, snapshot) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.radius_for(path, snapshot) ⇒ Object
private
Classify one input file and collect its affected examples.
-
.run(args, stdout: $stdout, stderr: $stderr) ⇒ Integer
private
Exit status (0 = radius printed, including untracked / zero-dependent files; 1 = no files given, unknown option, cache missing, or schema mismatch).
-
.spec_file_count(entries) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.summary_line(radius) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.union_entries(radii) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.unknown_option(arg, stderr) ⇒ Object
private
Internal helper for the tracer pipeline.
-
.whole_suite_radius(path, file_name, status, snapshot) ⇒ Object
private
Internal helper for the tracer pipeline.
Class Method Details
.dependent_radius(path, file_name, snapshot) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
183 184 185 186 187 188 189 190 191 192 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 183 def self.dependent_radius(path, file_name, snapshot) ids = (snapshot.reverse_dependency || {})[file_name] if ids.nil? || ids.empty? status = (snapshot.all_files || {}).key?(file_name) ? 'no_dependents' : 'untracked' { path: path, file_name: file_name, status: status, examples: [] } else { path: path, file_name: file_name, status: 'tracked', examples: example_entries(ids, snapshot) } end end |
.example_entries(ids, snapshot) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
196 197 198 199 200 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 196 def self.example_entries(ids, snapshot) all = snapshot.all_examples || {} ids.map { |id| example_entry(id.to_s, all[id]) } .sort_by { |entry| [entry[:location], entry[:description]] } end |
.example_entry(id, meta) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Enrich one example_id from the persisted all_examples meta.
A dangling id (present in reverse_dependency but missing from
all_examples -- stale or partially-written cache) degrades to
<unknown> fields instead of raising.
207 208 209 210 211 212 213 214 215 216 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 207 def self.example_entry(id, ) = {} unless .is_a?(::Hash) file, line = SnapshotHelpers.example_location() { example_id: id, spec_file: file || '<unknown>', location: file.nil? ? '<unknown>' : "#{file}:#{line}", description: SnapshotHelpers.example_description() || '<unknown>' } end |
.execute(parsed, stdout, stderr) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 65 def self.execute(parsed, stdout, stderr) loaded = SnapshotHelpers.load_snapshot(RSpecTracer.cache_path, command: 'blast-radius', stderr: stderr) return 1 if loaded.nil? snapshot, = loaded radii = parsed[:files].map { |file| radius_for(file, snapshot) } if parsed[:json] stdout.puts JSON.pretty_generate(json_payload(radii, snapshot)) else print_report(stdout, radii, snapshot, list: parsed[:list]) end 0 end |
.file_json(radius) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 300 def self.file_json(radius) { 'path' => radius[:path], 'file_name' => radius[:file_name], 'status' => radius[:status], 'example_count' => radius[:examples].size, 'spec_file_count' => spec_file_count(radius[:examples]), 'examples' => radius[:examples].map do |entry| { 'example_id' => entry[:example_id], 'location' => entry[:location], 'description' => entry[:description] } end } end |
.json_payload(radii, snapshot) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
285 286 287 288 289 290 291 292 293 294 295 296 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 285 def self.json_payload(radii, snapshot) union = union_entries(radii) { 'files' => radii.map { |radius| file_json(radius) }, 'total' => { 'example_count' => union.size, 'spec_file_count' => spec_file_count(union), 'whole_suite' => radii.any? { |radius| WHOLE_SUITE_STATUSES.include?(radius[:status]) }, 'all_examples_count' => (snapshot.all_examples || {}).size } } end |
.no_files(stderr) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
104 105 106 107 108 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 104 def self.no_files(stderr) stderr.puts 'blast-radius: no file paths given' stderr.puts " usage: #{USAGE}" 1 end |
.normalize_file_name(path) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert a user-supplied path into the cache's file_name
convention (project-relative with a leading /, absolute when
outside the project root). Computed at command runtime against
RSpecTracer.root -- SourceFile.file_name is unsuitable here
because its PROJECT_ROOT_REGEX freezes the root at gem-require
time, before the CLI loads the project's .rspec-tracer config.
144 145 146 147 148 149 150 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 144 def self.normalize_file_name(path) root = RSpecTracer.root abs = File.(path, root) return abs unless abs.start_with?("#{root}/") "/#{abs[(root.length + 1)..]}" end |
.parse_args(args) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 81 def self.parse_args(args) parsed = { list: false, json: false, files: [], unknown: nil } args.each do |arg| case arg when '--list' then parsed[:list] = true when '--json' then parsed[:json] = true when /\A-/ then parsed[:unknown] ||= arg else parsed[:files] << arg end end parsed end |
.print_help(stdout) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 112 def self.print_help(stdout) stdout.puts <<~HELP Usage: #{USAGE} Show which examples a change to each given file would re-run on the next rspec invocation. Reports the file-change trigger only; examples that always re-run for status reasons (failed / flaky / pending / interrupted) are not included. Paths may be relative to the project root or absolute. Whole-suite invalidators (Gemfile.lock, .ruby-version, .rspec-tracer) and boot files re-run every example. Backend-aware: works under `storage_backend :json` (default) and `storage_backend :sqlite`. Requires a prior rspec run. Multi-file input composes with git: git diff --name-only main | xargs bundle exec rspec-tracer blast-radius Options: --list Enumerate affected examples (location + description). --json Emit one machine-readable JSON document to stdout. HELP 0 end |
.print_report(stdout, radii, snapshot, list:) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 220 def self.print_report(stdout, radii, snapshot, list:) radii.each do |radius| stdout.puts summary_line(radius) next unless list radius[:examples].each do |entry| stdout.puts " #{entry[:location]} #{entry[:description]}" end end print_total(stdout, radii, snapshot) if radii.size > 1 end |
.print_total(stdout, radii, snapshot) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
261 262 263 264 265 266 267 268 269 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 261 def self.print_total(stdout, radii, snapshot) if radii.any? { |radius| WHOLE_SUITE_STATUSES.include?(radius[:status]) } total = (snapshot.all_examples || {}).size stdout.puts "total: all #{total} examples (whole-suite invalidator)" else union = union_entries(radii) stdout.puts "total: #{union.size} unique examples across #{spec_file_count(union)} spec files" end end |
.radius_for(path, snapshot) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Classify one input file and collect its affected examples.
Precedence: whole-suite invalidator watch file, then boot file
(boot-set changes OR into whole-suite invalidation), then
tracked reverse dependents, then untracked / no-dependents.
Note the key conventions: boot_set keys are project-relative
(no leading slash) while reverse_dependency / all_files
keys carry a leading slash.
160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 160 def self.radius_for(path, snapshot) file_name = normalize_file_name(path) relative = file_name.delete_prefix('/') if Tracker::WholeSuiteInvalidators::WATCH_FILES.include?(relative) whole_suite_radius(path, file_name, 'whole_suite_invalidator', snapshot) elsif (snapshot.boot_set || {}).key?(relative) whole_suite_radius(path, file_name, 'boot_file', snapshot) else dependent_radius(path, file_name, snapshot) end end |
.run(args, stdout: $stdout, stderr: $stderr) ⇒ Integer
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns exit status (0 = radius printed, including untracked / zero-dependent files; 1 = no files given, unknown option, cache missing, or schema mismatch).
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 45 def self.run(args, stdout: $stdout, stderr: $stderr) return print_help(stdout) if SnapshotHelpers.help_requested?(args) parsed = parse_args(args) return unknown_option(parsed[:unknown], stderr) unless parsed[:unknown].nil? return no_files(stderr) if parsed[:files].empty? execute(parsed, stdout, stderr) rescue Errno::EPIPE # Downstream pipe (`... | head`, `... | jq`) closed early -- # routine in shell pipelines (the help text promotes them), # not a failure. Exit 0 silently. 0 rescue StandardError => e stderr.puts "blast-radius: #{e.class}: #{e.}" 1 end |
.spec_file_count(entries) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
279 280 281 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 279 def self.spec_file_count(entries) entries.map { |entry| entry[:spec_file] }.uniq.size end |
.summary_line(radius) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline. The untracked wording sticks to what the tracer can actually observe: absence of the file from the cache. It must NOT claim the file "never loaded" -- files consumed outside the hooked surface (spec_helper.rb itself, which executes before coverage tracking starts; reads via unhooked APIs, C extensions, or other threads) load fine yet never appear in the cache. See the soundness model in ARCHITECTURE.md.
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 241 def self.summary_line(radius) file_name = radius[:file_name] count = radius[:examples].size case radius[:status] when 'whole_suite_invalidator' "#{file_name}: whole-suite invalidator -- re-runs all #{count} examples" when 'boot_file' "#{file_name}: boot file -- re-runs all #{count} examples" when 'untracked' "#{file_name}: not tracked in cache (no recorded dependents -- the tracer never " \ 'observed it as an input; see the soundness model in ARCHITECTURE.md)' when 'no_dependents' "#{file_name}: 0 examples (no tracked dependents)" else "#{file_name}: #{count} examples across #{spec_file_count(radius[:examples])} spec files" end end |
.union_entries(radii) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
273 274 275 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 273 def self.union_entries(radii) radii.flat_map { |radius| radius[:examples] }.uniq { |entry| entry[:example_id] } end |
.unknown_option(arg, stderr) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
96 97 98 99 100 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 96 def self.unknown_option(arg, stderr) stderr.puts "blast-radius: unknown option #{arg.inspect}" stderr.puts " usage: #{USAGE}" 1 end |
.whole_suite_radius(path, file_name, status, snapshot) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Internal helper for the tracer pipeline.
175 176 177 178 179 |
# File 'lib/rspec_tracer/cli/blast_radius.rb', line 175 def self.whole_suite_radius(path, file_name, status, snapshot) ids = (snapshot.all_examples || {}).keys { path: path, file_name: file_name, status: status, examples: example_entries(ids, snapshot) } end |