Class: RSpecTracer::Engine
- Inherits:
-
Object
- Object
- RSpecTracer::Engine
- 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
-
#all_examples ⇒ Object
readonly
private
Internal attribute.
-
#all_files ⇒ Object
readonly
private
Internal attribute.
-
#coverage_adapter ⇒ Object
readonly
private
Internal attribute.
-
#declared_globs ⇒ Object
readonly
private
Internal attribute.
-
#duplicate_examples ⇒ Object
readonly
private
Internal attribute.
-
#env_snapshot ⇒ Object
readonly
private
Internal attribute.
-
#examples_coverage ⇒ Object
readonly
private
Internal attribute.
-
#graph ⇒ Object
readonly
private
Internal attribute.
-
#loaded_files_tracker ⇒ Object
readonly
private
Internal attribute.
-
#new_file_detector ⇒ Object
readonly
private
Internal attribute.
-
#registry ⇒ Object
readonly
private
Internal attribute.
-
#storage_backend ⇒ Object
readonly
private
Internal attribute.
-
#whole_suite_invalidators ⇒ Object
readonly
private
Internal attribute.
Instance Method Summary collapse
-
#apply_env_filter_decisions ⇒ Object
Called from RunnerHook AFTER the filter-decision pre-walk has populated ‘@tracks_env` / `@tracked_env_names` for every example.
-
#deregister_duplicate_examples ⇒ Object
private
Internal method on the tracer pipeline.
-
#example_finished(example_id) ⇒ Object
private
Internal method on the tracer pipeline.
-
#example_started ⇒ Object
private
Internal method on the tracer pipeline.
-
#filtered_example_ids ⇒ Object
— accessors used by specs ———————————.
-
#finalize ⇒ Object
— finalize ————————————————.
-
#initialize(configuration: RSpecTracer) ⇒ Engine
constructor
private
Internal method on the tracer pipeline.
-
#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.
-
#on_example_failed(example_id, result) ⇒ Object
private
Internal method on the tracer pipeline.
-
#on_example_passed(example_id, result) ⇒ Object
private
Internal method on the tracer pipeline.
-
#on_example_pending(example_id, result) ⇒ Object
private
Internal method on the tracer pipeline.
-
#on_example_skipped(example_id) ⇒ Object
private
Internal method on the tracer pipeline.
-
#previous_snapshot_loaded? ⇒ Boolean
private
Internal method on the tracer pipeline.
-
#register_example(example) ⇒ Object
private
Records one example registration into the engine’s per-run state.
-
#register_tracks(example_id, tracks) ⇒ Object
Per-example tracking DSL hook.
-
#run_example?(example_id) ⇒ Boolean
— filter-phase surface (mirrors legacy Runner) ————–.
-
#run_example_reason(example_id) ⇒ Object
private
Internal method on the tracer pipeline.
-
#setup ⇒ Object
private
Internal method on the tracer pipeline.
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.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/rspec_tracer/engine.rb', line 104 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_examples ⇒ Object (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.
97 98 99 |
# File 'lib/rspec_tracer/engine.rb', line 97 def all_examples @all_examples end |
#all_files ⇒ Object (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.
97 98 99 |
# File 'lib/rspec_tracer/engine.rb', line 97 def all_files @all_files end |
#coverage_adapter ⇒ Object (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.
97 98 99 |
# File 'lib/rspec_tracer/engine.rb', line 97 def coverage_adapter @coverage_adapter end |
#declared_globs ⇒ Object (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.
97 98 99 |
# File 'lib/rspec_tracer/engine.rb', line 97 def declared_globs @declared_globs end |
#duplicate_examples ⇒ Object (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.
97 98 99 |
# File 'lib/rspec_tracer/engine.rb', line 97 def duplicate_examples @duplicate_examples end |
#env_snapshot ⇒ Object (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.
97 98 99 |
# File 'lib/rspec_tracer/engine.rb', line 97 def env_snapshot @env_snapshot end |
#examples_coverage ⇒ Object (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.
97 98 99 |
# File 'lib/rspec_tracer/engine.rb', line 97 def examples_coverage @examples_coverage end |
#graph ⇒ Object (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.
97 98 99 |
# File 'lib/rspec_tracer/engine.rb', line 97 def graph @graph end |
#loaded_files_tracker ⇒ Object (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.
97 98 99 |
# File 'lib/rspec_tracer/engine.rb', line 97 def loaded_files_tracker @loaded_files_tracker end |
#new_file_detector ⇒ Object (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.
97 98 99 |
# File 'lib/rspec_tracer/engine.rb', line 97 def new_file_detector @new_file_detector end |
#registry ⇒ Object (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.
97 98 99 |
# File 'lib/rspec_tracer/engine.rb', line 97 def registry @registry end |
#storage_backend ⇒ Object (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.
97 98 99 |
# File 'lib/rspec_tracer/engine.rb', line 97 def storage_backend @storage_backend end |
#whole_suite_invalidators ⇒ Object (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.
97 98 99 |
# File 'lib/rspec_tracer/engine.rb', line 97 def whole_suite_invalidators @whole_suite_invalidators end |
Instance Method Details
#apply_env_filter_decisions ⇒ Object
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.
225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/rspec_tracer/engine.rb', line 225 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_examples ⇒ 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.
242 243 244 245 246 247 248 |
# File 'lib/rspec_tracer/engine.rb', line 242 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.
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/rspec_tracer/engine.rb', line 265 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_started ⇒ 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.
254 255 256 257 258 259 260 261 |
# File 'lib/rspec_tracer/engine.rb', line 254 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_ids ⇒ Object
— accessors used by specs ———————————
371 372 373 |
# File 'lib/rspec_tracer/engine.rb', line 371 def filtered_example_ids @filtered_examples.keys end |
#finalize ⇒ Object
— finalize ————————————————
332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'lib/rspec_tracer/engine.rb', line 332 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]].
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 |
# File 'lib/rspec_tracer/engine.rb', line 353 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.
311 312 313 314 315 316 317 318 |
# File 'lib/rspec_tracer/engine.rb', line 311 def on_example_failed(example_id, result) return if @duplicate_examples[example_id]&.count.to_i > 1 status = previously_flaky?(example_id) ? :flaky : :failed @registry.update_status(example_id, status) 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.
300 301 302 303 304 305 306 307 |
# File 'lib/rspec_tracer/engine.rb', line 300 def on_example_passed(example_id, result) return if @duplicate_examples[example_id]&.count.to_i > 1 status = flaky_history?(example_id) ? :flaky : :passed @registry.update_status(example_id, status) 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.
322 323 324 325 326 327 328 |
# File 'lib/rspec_tracer/engine.rb', line 322 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.
292 293 294 295 296 |
# File 'lib/rspec_tracer/engine.rb', line 292 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.
377 378 379 |
# File 'lib/rspec_tracer/engine.rb', line 377 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.
Records one example registration into the engine’s per-run state. Overwrites any prior ‘@all_examples` entry on purpose: on warm runs, `seed_all_examples_from_previous` seeds `@all_examples` with the prior snapshot’s metadata (including the prior ‘:run_reason`), and the RunnerHook’s per-example call here carries this run’s freshly-tagged value. Preserving the seeded entry via ‘||=` would persist the stale prior reason and surface “No cache” in `report.json#run_reason` for examples re-run because they failed / pended / were interrupted / had files change / had env change. Duplicate detection is unaffected: duplicates accumulate in `@duplicate_examples`, and `deregister_duplicate_examples` drops them from `@all_examples` outright.
171 172 173 174 175 176 177 178 |
# File 'lib/rspec_tracer/engine.rb', line 171 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
195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/rspec_tracer/engine.rb', line 195 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? = RSpecTracer::Tracker::EnvMatcher.(envs.map(&:to_s)) @tracks_env[example_id].merge() @tracked_env_names.merge() self end |
#run_example?(example_id) ⇒ Boolean
— filter-phase surface (mirrors legacy Runner) ————–
142 143 144 145 146 147 |
# File 'lib/rspec_tracer/engine.rb', line 142 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.
151 152 153 154 155 |
# File 'lib/rspec_tracer/engine.rb', line 151 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 |
#setup ⇒ 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.
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/rspec_tracer/engine.rb', line 122 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 |