Class: RSpecTracer::Engine

Inherits:
Object
  • Object
show all
Defined in:
lib/rspec_tracer/engine.rb

Overview

Top-level coordinator for the v2 core engine. Wires CoverageAdapter + IOHooks + DeclaredGlobs + NewFileDetector + WholeSuiteInvalidators + LoadedFilesTracker + ExampleRegistry + DependencyGraph + Storage into a single pipeline.

Named ‘Engine` rather than `Tracker` because the `Tracker` namespace is already taken by the sub-module that houses the leaf observers (`Tracker::CoverageAdapter`, `Tracker::IOHooks`, etc.). `RSpecTracer.engine` is the public accessor the RSpec hooks dispatch through during a run.

Lifecycle (driven by RSpec hooks in ‘lib/rspec_tracer.rb`):

engine = Engine.new(configuration: RSpecTracer)
engine.setup                       # install hooks, load cache,
                                   # compute filter decisions
engine.run_example?(id)            # per-example filter (from cache)
engine.register_example(example)   # record metadata + duplicates
engine.example_started             # peek baseline + open bucket
  # ... example body runs, IOHooks record into bucket ...
engine.example_finished(id)        # diff coverage, attribute, close
engine.on_example_{passed,failed,pending,skipped}(id, result)
engine.finalize                    # persist snapshot + coverage

Per-example coverage delta map: peek baseline at example_started, peek again at example_finished, store the per-line strength delta

under ‘@examples_coverage[file_path]`. Reporters

CoverageJsonReporter consumes the cumulative coverage at finalize via Tracker::CoverageAdapter#peek_unfiltered + the engine’s ‘merge_skipped_coverage` algorithm.

Cache parity: ‘finalize` builds a Snapshot with file-name-keyed dependency / reverse_dependency / all_files maps (matching the 1.x on-disk convention - root-stripped file names with a leading “/”) and hands it to Storage::JsonBackend. The 2.0 schema bump adds `boot_set`; everything else mirrors the 1.x cache layout byte-for-byte. rubocop:disable Metrics/ClassLength

Constant Summary collapse

EXAMPLE_RUN_REASON =

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.

{
  explicit_run: 'Explicit run',
  no_cache: 'No cache',
  interrupted: 'Interrupted previously',
  flaky_example: 'Flaky example',
  failed_example: 'Failed previously',
  pending_example: 'Pending previously',
  files_changed: 'Files changed',
  whole_suite_invalidator: 'Whole-suite invalidator changed',
  env_changed: 'Environment changed'
}.freeze
FILTER_REASON_STRINGS =

Map from Filter#select reasons to the legacy-shaped strings users see in test output (“foo (Files changed)”). Keeps the user surface unchanged under v2.

{
  whole_suite_invalidator: EXAMPLE_RUN_REASON[:whole_suite_invalidator],
  interrupted: EXAMPLE_RUN_REASON[:interrupted],
  flaky_example: EXAMPLE_RUN_REASON[:flaky_example],
  failed_example: EXAMPLE_RUN_REASON[:failed_example],
  pending_example: EXAMPLE_RUN_REASON[:pending_example],
  no_cache: EXAMPLE_RUN_REASON[:no_cache],
  files_changed: EXAMPLE_RUN_REASON[:files_changed],
  env_changed: EXAMPLE_RUN_REASON[:env_changed]
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(configuration: RSpecTracer) ⇒ Engine

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 method on the tracer pipeline.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/rspec_tracer/engine.rb', line 103

def initialize(configuration: RSpecTracer)
  @configuration = configuration
  @filtered_examples = {}
  @all_examples = {}
  @duplicate_examples = {}
  @examples_coverage = {}
  @all_files = {}
  @tracks_files = Hash.new { |h, id| h[id] = Set.new } # id => Set<abs_path>
  @tracks_env = Hash.new { |h, id| h[id] = Set.new }   # id => Set<env_name>
  @tracked_env_names = Set.new
  @config_tracked_env_names = Set.new # config-level subset (post-expansion)
  @previous_snapshot = nil
  @run_id = nil
  @before_peek = nil
end

Instance Attribute Details

#all_examplesObject (readonly)

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 attribute.



96
97
98
# File 'lib/rspec_tracer/engine.rb', line 96

def all_examples
  @all_examples
end

#all_filesObject (readonly)

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 attribute.



96
97
98
# File 'lib/rspec_tracer/engine.rb', line 96

def all_files
  @all_files
end

#coverage_adapterObject (readonly)

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 attribute.



96
97
98
# File 'lib/rspec_tracer/engine.rb', line 96

def coverage_adapter
  @coverage_adapter
end

#declared_globsObject (readonly)

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 attribute.



96
97
98
# File 'lib/rspec_tracer/engine.rb', line 96

def declared_globs
  @declared_globs
end

#duplicate_examplesObject (readonly)

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 attribute.



96
97
98
# File 'lib/rspec_tracer/engine.rb', line 96

def duplicate_examples
  @duplicate_examples
end

#env_snapshotObject (readonly)

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 attribute.



96
97
98
# File 'lib/rspec_tracer/engine.rb', line 96

def env_snapshot
  @env_snapshot
end

#examples_coverageObject (readonly)

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 attribute.



96
97
98
# File 'lib/rspec_tracer/engine.rb', line 96

def examples_coverage
  @examples_coverage
end

#graphObject (readonly)

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 attribute.



96
97
98
# File 'lib/rspec_tracer/engine.rb', line 96

def graph
  @graph
end

#loaded_files_trackerObject (readonly)

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 attribute.



96
97
98
# File 'lib/rspec_tracer/engine.rb', line 96

def loaded_files_tracker
  @loaded_files_tracker
end

#new_file_detectorObject (readonly)

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 attribute.



96
97
98
# File 'lib/rspec_tracer/engine.rb', line 96

def new_file_detector
  @new_file_detector
end

#registryObject (readonly)

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 attribute.



96
97
98
# File 'lib/rspec_tracer/engine.rb', line 96

def registry
  @registry
end

#storage_backendObject (readonly)

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 attribute.



96
97
98
# File 'lib/rspec_tracer/engine.rb', line 96

def storage_backend
  @storage_backend
end

#whole_suite_invalidatorsObject (readonly)

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 attribute.



96
97
98
# File 'lib/rspec_tracer/engine.rb', line 96

def whole_suite_invalidators
  @whole_suite_invalidators
end

Instance Method Details

#apply_env_filter_decisionsObject

Called from RunnerHook AFTER the filter-decision pre-walk has populated ‘@tracks_env` / `@tracked_env_names` for every example. Compares each declared env key against the previous snapshot’s ‘env_snapshot` via Tracker::EnvSnapshot; marks any example whose tracked-env set intersects the invalidated set as re-runnable. Strictly additive vs other filter reasons - if the example was already in `@filtered_examples` for a stronger reason (files_changed / whole_suite_invalidator / failed_example / …), env_changed does NOT overwrite.

Config-level path: when an invalidated key intersects ‘@config_tracked_env_names` (the post-expansion config-level set), every previously-seen example re-runs - mirrors the `track_files` “declared globs attach to every example” semantics. New examples (not in @previous_snapshot.all_examples) already run via the no_cache path; no special-casing needed.



212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/rspec_tracer/engine.rb', line 212

def apply_env_filter_decisions
  return self if @previous_snapshot.nil?
  return self if @tracked_env_names.empty?

  invalidated = @env_snapshot.invalidated_keys(
    @previous_snapshot.env_snapshot, @tracked_env_names
  )
  return self if invalidated.empty?

  reason = FILTER_REASON_STRINGS.fetch(:env_changed)
  mark_all_prev_examples(reason) if invalidated.intersect?(@config_tracked_env_names)
  mark_per_example_intersections(invalidated, reason)
  self
end

#deregister_duplicate_examplesObject

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 method on the tracer pipeline.



229
230
231
232
233
234
235
# File 'lib/rspec_tracer/engine.rb', line 229

def deregister_duplicate_examples
  @duplicate_examples.select! { |_, entries| entries.count > 1 }
  return if @duplicate_examples.empty?

  @all_examples.reject! { |id, _| @duplicate_examples.key?(id) }
  self
end

#example_finished(example_id) ⇒ 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 method on the tracer pipeline.



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/rspec_tracer/engine.rb', line 252

def example_finished(example_id)
  after_peek = @coverage_adapter.peek
  record_coverage_delta(example_id, @before_peek, after_peek)
  io_inputs = @current_bucket.values
  rails_inputs = @current_rails_bucket ? @current_rails_bucket.values : []
  RSpecTracer::Tracker::IOHooks.clear_bucket
  clear_rails_bucket

  transitive_inputs = @loaded_files_tracker.loaded_set_inputs |
    @loaded_files_tracker.stop_example(example_id)
  coverage_inputs = @coverage_adapter.compute_diff(@before_peek, after_peek)
  declared_inputs = @declared_globs.walk
  tracks_inputs = per_example_tracks_inputs(example_id)
  attribute_to_example(
    example_id,
    coverage_inputs | transitive_inputs | io_inputs.to_set |
      rails_inputs.to_set | declared_inputs | tracks_inputs
  )

  @before_peek = nil
  @current_bucket = nil
  @current_rails_bucket = nil
  self
end

#example_startedObject

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 method on the tracer pipeline.



241
242
243
244
245
246
247
248
# File 'lib/rspec_tracer/engine.rb', line 241

def example_started
  @before_peek = @coverage_adapter.peek
  @current_bucket = {}
  @current_rails_bucket = {}
  RSpecTracer::Tracker::IOHooks.set_bucket(@current_bucket)
  set_rails_bucket(@current_rails_bucket)
  self
end

#filtered_example_idsObject

— accessors used by specs ———————————



356
357
358
# File 'lib/rspec_tracer/engine.rb', line 356

def filtered_example_ids
  @filtered_examples.keys
end

#finalizeObject

— finalize ————————————————



317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/rspec_tracer/engine.rb', line 317

def finalize
  @registry.all_example_ids.each do |id|
    next if @registry.status_of(id)

    @registry.update_status(id, :interrupted)
  end

  snapshot = build_snapshot
  @storage_backend.save_graph(snapshot, schema_version: RSpecTracer::Storage::Schema::CURRENT)
  uninstall_rails_observers
  snapshot
end

#merge_skipped_coverage(skipped_ids, previous_examples_coverage = nil) ⇒ Object

For every previously-skipped example id, accumulate per-line coverage strengths from the previous run’s per-example coverage map into the missed_coverage return value. Deleted files / missing entries are skipped silently. Consumed by Reporters::CoverageJsonReporter at finalize time so coverage.json carries forward the contribution of skipped examples.

Returns Hash[file_path => Hash[line_number => cumulative_strength]].



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/rspec_tracer/engine.rb', line 338

def merge_skipped_coverage(skipped_ids, previous_examples_coverage = nil)
  source = previous_examples_coverage || @previous_snapshot&.examples_coverage || {}
  missed = Hash.new { |h, f| h[f] = Hash.new(0) }

  skipped_ids.each do |example_id|
    example_coverage = source[example_id]
    next if example_coverage.nil?

    example_coverage.each do |file_path, line_coverage|
      accumulate_line_coverage(missed[file_path], line_coverage)
    end
  end

  missed
end

#on_example_failed(example_id, result) ⇒ 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 method on the tracer pipeline.



297
298
299
300
301
302
303
# File 'lib/rspec_tracer/engine.rb', line 297

def on_example_failed(example_id, result)
  return if @duplicate_examples[example_id]&.count.to_i > 1

  @registry.update_status(example_id, :failed)
  record_execution_result(example_id, result)
  self
end

#on_example_passed(example_id, result) ⇒ 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 method on the tracer pipeline.



287
288
289
290
291
292
293
# File 'lib/rspec_tracer/engine.rb', line 287

def on_example_passed(example_id, result)
  return if @duplicate_examples[example_id]&.count.to_i > 1

  @registry.update_status(example_id, :passed)
  record_execution_result(example_id, result)
  self
end

#on_example_pending(example_id, result) ⇒ 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 method on the tracer pipeline.



307
308
309
310
311
312
313
# File 'lib/rspec_tracer/engine.rb', line 307

def on_example_pending(example_id, result)
  return if @duplicate_examples[example_id]&.count.to_i > 1

  @registry.update_status(example_id, :pending)
  record_execution_result(example_id, result)
  self
end

#on_example_skipped(example_id) ⇒ 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 method on the tracer pipeline.



279
280
281
282
283
# File 'lib/rspec_tracer/engine.rb', line 279

def on_example_skipped(example_id)
  @registry.register(example_id) unless @registry.registered?(example_id)
  @registry.update_status(example_id, :skipped)
  self
end

#previous_snapshot_loaded?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 method on the tracer pipeline.

Returns:

  • (Boolean)


362
363
364
# File 'lib/rspec_tracer/engine.rb', line 362

def previous_snapshot_loaded?
  !@previous_snapshot.nil?
end

#register_example(example) ⇒ 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 method on the tracer pipeline.



158
159
160
161
162
163
164
165
# File 'lib/rspec_tracer/engine.rb', line 158

def register_example(example)
  example_id = example[:example_id]
  @registry.register(example_id, metadata: example, identity_hash: example_id)
  @all_examples[example_id] ||= example
  @duplicate_examples[example_id] ||= []
  @duplicate_examples[example_id] << example
  self
end

#register_tracks(example_id, tracks) ⇒ Object

Per-example tracking DSL hook. Called from RunnerHook with the normalized ‘Set<String>, env: Set<String>` that `RSpec::Metadata.tracks_for(example)` produced. Resolves the file globs against the project root once per distinct glob string (memoized) and unions the matching Inputs into this example’s dependency set. Env names are accumulated into ‘@tracked_env_names` so the finalize snapshot covers every key the run cared about.

Per-example env entries may carry wildcard patterns (‘tracks: { env: ’RAILS_*‘ }`). `EnvMatcher.expand` is the single funnel

  • literals pass through, wildcards expand against the live ENV,

and unsupported syntax raises ArgumentError at this point (RunnerHook Pass 1, before any example body runs). rubocop:disable Metrics/PerceivedComplexity



182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/rspec_tracer/engine.rb', line 182

def register_tracks(example_id, tracks)
  files = tracks[:files] || tracks['files'] || Set.new
  envs = tracks[:env] || tracks['env'] || Set.new

  files.each { |glob| @tracks_files[example_id].merge(resolved_glob_inputs(glob)) } unless files.empty?
  return self if envs.empty?

  expanded = RSpecTracer::Tracker::EnvMatcher.expand(envs.map(&:to_s))
  @tracks_env[example_id].merge(expanded)
  @tracked_env_names.merge(expanded)
  self
end

#run_example?(example_id) ⇒ Boolean

— filter-phase surface (mirrors legacy Runner) ————–

Returns:

  • (Boolean)


141
142
143
144
145
146
# File 'lib/rspec_tracer/engine.rb', line 141

def run_example?(example_id)
  return true if @configuration.run_all_examples

  previously_seen = @previous_snapshot&.all_examples&.key?(example_id)
  !previously_seen || @filtered_examples.key?(example_id)
end

#run_example_reason(example_id) ⇒ 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 method on the tracer pipeline.



150
151
152
153
154
# File 'lib/rspec_tracer/engine.rb', line 150

def run_example_reason(example_id)
  return EXAMPLE_RUN_REASON[:explicit_run] if @configuration.run_all_examples

  @filtered_examples[example_id] || EXAMPLE_RUN_REASON[:no_cache]
end

#setupObject

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 method on the tracer pipeline.



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/rspec_tracer/engine.rb', line 121

def setup
  @configuration.freeze_declared_globs!

  build_observers
  install_io_hooks
  install_rails_observers
  ensure_coverage_started

  @loaded_files_tracker.capture_boot_set!
  @declared_globs.walk

  @previous_snapshot = load_previous_snapshot
  seed_state_from_previous(@previous_snapshot) if @previous_snapshot
  register_config_tracked_env_names
  compute_filter_decisions
  self
end