Module: RSpecTracer::CLI::Explain Private

Defined in:
lib/rspec_tracer/cli/explain.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 explain <example> -- show why a given example is scheduled to run or skip on the next rspec invocation. 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.

Class Method Summary collapse

Class Method Details

.always_rerun_reason(id, snapshot) ⇒ String?

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.

First always-re-run status the id carries, in the filter's precedence order (interrupted > flaky > failed > pending), or nil when the example has no status-based re-run trigger.

Returns:

  • (String, nil)


236
237
238
239
240
241
242
243
# File 'lib/rspec_tracer/cli/explain.rb', line 236

def self.always_rerun_reason(id, snapshot)
  return 'interrupted' if in_id_set?(snapshot.interrupted_examples, id)
  return 'flaky' if in_id_set?(snapshot.flaky_examples, id)
  return 'failed' if in_id_set?(snapshot.failed_examples, id)
  return 'pending' if in_id_set?(snapshot.pending_examples, id)

  nil
end

.find_example(all_examples, query) ⇒ 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.



82
83
84
85
86
87
88
89
90
# File 'lib/rspec_tracer/cli/explain.rb', line 82

def self.find_example(all_examples, query)
  return all_examples[query] if all_examples.key?(query)

  all_examples.find do |id, meta|
    meta = {} unless meta.is_a?(::Hash)
    desc = SnapshotHelpers.example_description(meta) || ''
    id.include?(query) || desc.include?(query)
  end&.last
end

.format_lines(meta, skipped: false) ⇒ 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.



103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/rspec_tracer/cli/explain.rb', line 103

def self.format_lines(meta, skipped: false)
  id = SnapshotHelpers.fetch_meta(meta, 'example_id', 'id') || '<unknown>'
  file, line = SnapshotHelpers.example_location(meta)
  status = last_status(meta)
  [
    "id:           #{id}",
    "description:  #{SnapshotHelpers.example_description(meta)}",
    "location:     #{file}:#{line}",
    "last status:  #{status}",
    "run reason:   #{run_reason_field(meta, skipped)}"
  ]
end

.in_id_set?(id_set, id) ⇒ Boolean

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.

Returns:

  • (Boolean)


259
260
261
# File 'lib/rspec_tracer/cli/explain.rb', line 259

def self.in_id_set?(id_set, id)
  (id_set || []).include?(id)
end

.last_run_line(id, snapshot, filter_persisted:) ⇒ String

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.

One last run: line for the --not-run view, derived from per-run membership: skipped_examples (the filter skipped it), filtered_examples (it ran, with the recorded reason), or neither. The neither case splits on filter_persisted: a backend that persists filter decisions (json) with an empty decision set means the last run recorded no decision for this id (a cold run persists empty sets by design, and an id absent from the last run has no entry either) -- only a non-persisting backend (sqlite) earns the storage-backend wording.

Returns:

  • (String)


192
193
194
195
196
197
198
199
200
# File 'lib/rspec_tracer/cli/explain.rb', line 192

def self.last_run_line(id, snapshot, filter_persisted:)
  return 'last run:     skipped (cache hit)' if skipped?(id, snapshot)

  reason = ran_reason(id, snapshot)
  return "last run:     ran -- #{reason}" if reason
  return 'last run:     <not recorded by this storage backend>' unless filter_persisted

  'last run:     no filter decision recorded (cold run, or this example was not part of it)'
end

.last_status(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.

Internal helper for the tracer pipeline.



134
135
136
137
# File 'lib/rspec_tracer/cli/explain.rb', line 134

def self.last_status(meta)
  SnapshotHelpers.dig_meta(meta, 'execution_result', 'status') ||
    SnapshotHelpers.fetch_meta(meta, 'status') || 'unknown'
end

.next_run_line(id, snapshot) ⇒ String

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.

Prediction line for the NEXT run, from the persisted status sets (the inputs to the next run's always-re-run triggers).

Returns:

  • (String)


225
226
227
228
229
230
# File 'lib/rspec_tracer/cli/explain.rb', line 225

def self.next_run_line(id, snapshot)
  reason = always_rerun_reason(id, snapshot)
  return "will re-run regardless (#{reason} last run)" if reason

  'runs only if a dependency, whole-suite invalidator, boot file, or tracked env var changes'
end

.no_match(query, all_examples, 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.



53
54
55
56
57
# File 'lib/rspec_tracer/cli/explain.rb', line 53

def self.no_match(query, all_examples, stderr)
  stderr.puts "explain: no example matching #{query.inspect}"
  stderr.puts "  cache has #{all_examples.size} examples; pass an example_id or substring of description"
  1
end

.not_run_detail_lines(id, snapshot) ⇒ Array<String>

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.

Detail lines printed under last run:: the itemized skip derivation for a skipped id, a run-side hint for an id that actually ran, nothing for the no-decision fallbacks.

Returns:

  • (Array<String>)


171
172
173
174
175
176
177
178
179
# File 'lib/rspec_tracer/cli/explain.rb', line 171

def self.not_run_detail_lines(id, snapshot)
  return skip_reason_lines(id, snapshot) if skipped?(id, snapshot)

  if ran_reason(id, snapshot)
    return ["  it was not skipped; run 'rspec-tracer explain #{id}' for the run-side view"]
  end

  []
end

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.



265
266
267
268
269
270
271
272
# File 'lib/rspec_tracer/cli/explain.rb', line 265

def self.print_dependency_summary(stdout, meta, snapshot)
  id = SnapshotHelpers.fetch_meta(meta, 'example_id', 'id')
  deps = snapshot.dependency || {}
  files = Array(deps[id])
  stdout.puts "dependencies: #{files.size} files tracked"
  files.first(10).each { |f| stdout.puts "  - #{f}" }
  stdout.puts "  ... (#{files.size - 10} more)" if files.size > 10
end

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.



94
95
96
97
98
99
# File 'lib/rspec_tracer/cli/explain.rb', line 94

def self.print_explanation(stdout, meta, snapshot)
  meta = {} unless meta.is_a?(::Hash)
  id = SnapshotHelpers.fetch_meta(meta, 'example_id', 'id')
  format_lines(meta, skipped: skipped?(id, snapshot)).each { |line| stdout.puts line }
  print_dependency_summary(stdout, meta, snapshot)
end

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.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/rspec_tracer/cli/explain.rb', line 61

def self.print_help(stdout)
  stdout.puts <<~HELP
    Usage: rspec-tracer explain [--not-run] <example_id_or_substring>

    Show why an example is scheduled to run or skip. Matches against
    example_id exactly first, then falls back to a substring match
    on the example's full_description. Backend-aware: works under
    `storage_backend :json` (default) and `storage_backend :sqlite`.
    Requires a prior rspec run.

    --not-run flips to the skip-side view: whether the example was
    skipped on the last run (and why no run trigger fired), its last
    recorded status, and what would make it run on the next rspec
    invocation. The cache keeps only the most recent snapshot, so
    "last status" reflects the last run, not a run history.
  HELP
  0
end

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.

The --not-run view. Deliberately does NOT print the run reason: field from all_examples meta: for a skipped example that field is the carry-forward PRIOR-run reason (seeded from the previous snapshot), so echoing it here would misreport why the example did not run. Everything below is derived from the per-run membership sets instead (skipped_examples / filtered_examples / the status id-sets).



146
147
148
149
150
151
152
153
154
155
156
# File 'lib/rspec_tracer/cli/explain.rb', line 146

def self.print_not_run_explanation(stdout, meta, snapshot, backend)
  meta = {} unless meta.is_a?(::Hash)
  id = SnapshotHelpers.fetch_meta(meta, 'example_id', 'id') || '<unknown>'
  filter_persisted = SnapshotHelpers.filter_decisions_persisted?(backend)
  print_not_run_header(stdout, meta, id)
  stdout.puts last_run_line(id, snapshot, filter_persisted: filter_persisted)
  not_run_detail_lines(id, snapshot).each { |detail| stdout.puts detail }
  stdout.puts "last status:  #{last_status(meta)} (most recent snapshot; this cache keeps only the last run)"
  stdout.puts "next run:     #{next_run_line(id, snapshot)}"
  print_dependency_summary(stdout, meta, snapshot)
end

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.



160
161
162
163
164
165
# File 'lib/rspec_tracer/cli/explain.rb', line 160

def self.print_not_run_header(stdout, meta, id)
  file, line = SnapshotHelpers.example_location(meta)
  stdout.puts "id:           #{id}"
  stdout.puts "description:  #{SnapshotHelpers.example_description(meta)}"
  stdout.puts "location:     #{file}:#{line}"
end

.ran_reason(id, 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.



253
254
255
# File 'lib/rspec_tracer/cli/explain.rb', line 253

def self.ran_reason(id, snapshot)
  SnapshotHelpers.fetch_meta(snapshot.filtered_examples || {}, id)
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 = explanation printed, 1 = example not found / cache missing).

Parameters:

  • args (Array<String>)

    sub-command args. First positional arg is the example_id (or substring) to explain. --not-run flips the output to the skip-side view: why the example was NOT run last time, and what would make it run next time.

  • stdout (IO) (defaults to: $stdout)
  • stderr (IO) (defaults to: $stderr)

Returns:

  • (Integer)

    exit status (0 = explanation printed, 1 = example not found / cache missing).



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/rspec_tracer/cli/explain.rb', line 24

def self.run(args, stdout: $stdout, stderr: $stderr)
  args = args.dup
  not_run = !args.delete('--not-run').nil?
  return print_help(stdout) if SnapshotHelpers.help_requested?(args)

  loaded = SnapshotHelpers.load_snapshot(RSpecTracer.cache_path, command: 'explain', stderr: stderr)
  return 1 if loaded.nil?

  snapshot, backend = loaded
  match = find_example(snapshot.all_examples, args.first)
  return no_match(args.first, snapshot.all_examples, stderr) if match.nil?

  if not_run
    print_not_run_explanation(stdout, match, snapshot, backend)
  else
    print_explanation(stdout, match, snapshot)
  end
  0
rescue Errno::EPIPE
  # Downstream pipe (`... | head`) closed early -- routine in
  # shell pipelines, not a failure. Exit 0 silently.
  0
rescue StandardError => e
  stderr.puts "explain: #{e.class}: #{e.message}"
  1
end

.run_reason_field(meta, skipped) ⇒ 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.

The run-side run reason: value. For an example the filter SKIPPED on the last run, the persisted run_reason is the carry-forward reason seeded from an earlier snapshot -- it says why the example ran back then, not why it is in its current state. Printing it bare would misreport a skipped example as having a current run trigger, so flag it and point at the --not-run skip-side view instead.



124
125
126
127
128
129
130
# File 'lib/rspec_tracer/cli/explain.rb', line 124

def self.run_reason_field(meta, skipped)
  reason = SnapshotHelpers.fetch_meta(meta, 'run_reason') || '<not recorded>'
  return reason unless skipped

  "#{reason} (carried forward from an earlier run; " \
    'this example was SKIPPED last run -- use --not-run for the skip-side view)'
end

.skip_reason_lines(id, snapshot) ⇒ Array<String>

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.

Itemized derivation of WHY the filter skipped the example. Sound because a skip logically implies every trigger in the engine's precedence chain declined: no whole-suite invalidator fired, no boot file changed (the engine ORs the boot-set check into whole-suite invalidation), the example carried no always-re-run status, none of its tracked dependency files changed, and its tracked environment snapshot was unchanged.

Returns:

  • (Array<String>)


210
211
212
213
214
215
216
217
218
219
220
# File 'lib/rspec_tracer/cli/explain.rb', line 210

def self.skip_reason_lines(id, snapshot)
  deps = Array((snapshot.dependency || {})[id])
  [
    'skip reason:  no run trigger fired last run:',
    '  - whole-suite invalidators (Gemfile.lock, .ruby-version, .rspec-tracer, gem version): unchanged',
    '  - boot set: no boot file changed',
    '  - prior status: not failed / flaky / pending / interrupted',
    "  - dependency files: #{deps.size} tracked, none changed",
    '  - environment snapshot: unchanged for this example'
  ]
end

.skipped?(id, snapshot) ⇒ Boolean

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.

Returns:

  • (Boolean)


247
248
249
# File 'lib/rspec_tracer/cli/explain.rb', line 247

def self.skipped?(id, snapshot)
  in_id_set?(snapshot.skipped_examples, id)
end