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
Internal method on the tracer pipeline.
-
#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.
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_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.
96 97 98 |
# File 'lib/rspec_tracer/engine.rb', line 96 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.
96 97 98 |
# File 'lib/rspec_tracer/engine.rb', line 96 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.
96 97 98 |
# File 'lib/rspec_tracer/engine.rb', line 96 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.
96 97 98 |
# File 'lib/rspec_tracer/engine.rb', line 96 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.
96 97 98 |
# File 'lib/rspec_tracer/engine.rb', line 96 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.
96 97 98 |
# File 'lib/rspec_tracer/engine.rb', line 96 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.
96 97 98 |
# File 'lib/rspec_tracer/engine.rb', line 96 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.
96 97 98 |
# File 'lib/rspec_tracer/engine.rb', line 96 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.
96 97 98 |
# File 'lib/rspec_tracer/engine.rb', line 96 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.
96 97 98 |
# File 'lib/rspec_tracer/engine.rb', line 96 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.
96 97 98 |
# File 'lib/rspec_tracer/engine.rb', line 96 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.
96 97 98 |
# File 'lib/rspec_tracer/engine.rb', line 96 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.
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_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.
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_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.
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_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.
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_ids ⇒ Object
— accessors used by specs ———————————
356 357 358 |
# File 'lib/rspec_tracer/engine.rb', line 356 def filtered_example_ids @filtered_examples.keys end |
#finalize ⇒ Object
— 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.
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? = 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) ————–
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 |
#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.
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 |