Class: RSpecTracer::Tracker::LoadedFilesTracker Private
- Inherits:
-
Object
- Object
- RSpecTracer::Tracker::LoadedFilesTracker
- Defined in:
- lib/rspec_tracer/tracker/loaded_files_tracker.rb
Overview
This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.
Observer #5 in the 2.0 tracker pipeline. Closes the constants- lookup blind spot documented in KNOWN_ISSUES.md B10.
The bug: when file A defines constants at load time and example E2 references them without triggering a re-require, E2’s coverage diff is empty for A. If A changes, the filter incorrectly skips E2 on the next run.
The fix: track every project file Ruby has ever loaded during the process, and attribute them as transitive dependencies of every example that runs afterward.
- `@boot_set`: frozen Set<String> captured at Tracker.setup.
Files loaded before any example runs (spec_helper requires,
gem boot code, constant autoloads). Changes to these are
whole-suite invalidators - any modification re-runs every
example.
- `@loaded_set`: append-only Set<String>. Grows as examples run
and Coverage observes new files. Before each example, the
filter treats the entire @loaded_set as input to that example;
after each example, any newly-loaded path is attributed
specifically to the just-completed example *and* added to
@loaded_set for the benefit of subsequent examples.
Rationale - why not smarter?
The cheaper alternatives all fail correctness or cost:
- TracePoint(:class, :c_return) fires on every C method call;
orders-of-magnitude overhead.
- Ruby-AST scans for constant references are unreliable under
metaprogramming (const_get, send, autoload blocks).
- Constant-table introspection doesn't tell us which *example*
used which constant.
- Stack-trace sampling is probabilistic - inappropriate for
cache correctness.
The “loaded set” approach is the cheapest correct solution. Cost: the test cache is slightly less selective (a lib/constants.rb change re-runs every example that ran after it loaded rather than just some subset). Correctness is the win.
Input kind reuse
Emits Input values with ‘kind: :ruby` - every file in the loaded-set is a Ruby source file (`::Coverage` tracks Ruby only), and the dependency graph keys on path (ignoring kind) so a separate `:transitive_load` kind would buy nothing observable. Overlap with CoverageAdapter’s ‘:ruby` emissions dedupes naturally at graph registration.
Digest cache
Each path is digested at most once per run (first time it appears in either the boot set or a stop_example diff). The cache backs both ‘loaded_set_inputs` and `boot_set_digest_snapshot`, so boot-set invalidation comparison is free after the initial capture.
Enablement flag
‘enabled:` (default true) threads through from Configuration#transitive_load_tracking. When false, every method degrades to a no-op that returns empty collections. This gives teams an opt-out for pathological suites where the transitive over-approximation is too aggressive.
Constant Summary collapse
- DEFAULT_PEEK =
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.
-> { ::Coverage.peek_result.keys }
Instance Attribute Summary collapse
-
#boot_set ⇒ Object
readonly
private
boot_set is exposed as an attr_reader rather than a hand- rolled method so RuboCop’s Style/TrivialAccessors stays quiet; nil is a valid “not yet captured” state callers rely on.
-
#root ⇒ Object
readonly
private
boot_set is exposed as an attr_reader rather than a hand- rolled method so RuboCop’s Style/TrivialAccessors stays quiet; nil is a valid “not yet captured” state callers rely on.
Instance Method Summary collapse
-
#boot_set_digest_snapshot ⇒ Object
private
Hash[relative_path => sha256_hex] for every file in the boot set.
-
#boot_set_invalidated?(previous_snapshot) ⇒ Boolean
private
Compare the current boot set’s digest snapshot against a previously-stored one.
-
#capture_boot_set! ⇒ Object
private
Capture the boot set once.
-
#enabled? ⇒ Boolean
private
Internal method on the tracer pipeline.
-
#initialize(root:, peek: DEFAULT_PEEK, enabled: true) ⇒ LoadedFilesTracker
constructor
private
Internal method on the tracer pipeline.
-
#loaded_set ⇒ Object
private
Defensive copy for external callers / property specs.
-
#loaded_set_inputs ⇒ Object
private
Set<Input> covering the full @loaded_set.
-
#loaded_set_size ⇒ Object
private
Internal method on the tracer pipeline.
-
#stop_example(_example_id) ⇒ Object
private
Diff-and-grow.
Constructor Details
#initialize(root:, peek: DEFAULT_PEEK, enabled: true) ⇒ LoadedFilesTracker
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.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/rspec_tracer/tracker/loaded_files_tracker.rb', line 88 def initialize(root:, peek: DEFAULT_PEEK, enabled: true) @root = File.(root) @root_prefix = "#{@root}/" @peek = peek @enabled = enabled @boot_set = nil @loaded_set = Set.new @input_cache = {} # Steady-state fast-path cache for stop_example: ::Coverage's # tracked-file set grows monotonically; if peek_result.length # is unchanged since the last stop_example call, no new project # files can have appeared. Skip the per-path filter loop # entirely. Initialized to nil so the first call always falls # through to full work + populates the cache. @last_peek_length = nil end |
Instance Attribute Details
#boot_set ⇒ 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.
boot_set is exposed as an attr_reader rather than a hand- rolled method so RuboCop’s Style/TrivialAccessors stays quiet; nil is a valid “not yet captured” state callers rely on.
84 85 86 |
# File 'lib/rspec_tracer/tracker/loaded_files_tracker.rb', line 84 def boot_set @boot_set end |
#root ⇒ 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.
boot_set is exposed as an attr_reader rather than a hand- rolled method so RuboCop’s Style/TrivialAccessors stays quiet; nil is a valid “not yet captured” state callers rely on.
84 85 86 |
# File 'lib/rspec_tracer/tracker/loaded_files_tracker.rb', line 84 def root @root end |
Instance Method Details
#boot_set_digest_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.
Hash[relative_path => sha256_hex] for every file in the boot set. The engine compares this against the previous run’s stored ‘Snapshot.boot_set` - any inequality is a whole-suite invalidator.
Invariant (enforced by capture_boot_set!): every path in ‘@boot_set` has a matching `@input_cache` entry, so the fetch never raises. Disabled trackers produce an empty `@boot_set`, so the enumeration naturally returns {} without a guard.
150 151 152 153 154 |
# File 'lib/rspec_tracer/tracker/loaded_files_tracker.rb', line 150 def boot_set_digest_snapshot return {} if @boot_set.nil? @boot_set.to_h { |path| [relative_path(path), @input_cache.fetch(path).digest] } end |
#boot_set_invalidated?(previous_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.
Compare the current boot set’s digest snapshot against a previously-stored one. ‘nil` previous_snapshot (first run, no cache) is treated as “not invalidated by this signal” - first run is already a cold run for unrelated reasons.
Disabled tracker never invalidates - the engine ORs this with WholeSuiteInvalidators.invalidated?, so returning false keeps the tracker silent when the feature is off.
164 165 166 167 168 169 |
# File 'lib/rspec_tracer/tracker/loaded_files_tracker.rb', line 164 def boot_set_invalidated?(previous_snapshot) return false unless @enabled return false if previous_snapshot.nil? boot_set_digest_snapshot != previous_snapshot end |
#capture_boot_set! ⇒ 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.
Capture the boot set once. Idempotent: subsequent calls return the frozen Set captured on the first call. When disabled, returns an empty frozen Set without touching ::Coverage.
Paths whose digest fails (unreadable files) are dropped on the floor - they stay absent from @boot_set, @loaded_set, and @input_cache, preserving the “every tracked path has an Input” invariant. Downstream filtering accepts the slight under-count (a truly-unreadable boot file was never going to be a useful invalidation signal anyway).
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/rspec_tracer/tracker/loaded_files_tracker.rb', line 121 def capture_boot_set! return @boot_set unless @boot_set.nil? if @enabled successful_paths = build_inputs(filtered_peek_paths).each_with_object(Set.new) do |input, acc| acc << input.path end # Construct @loaded_set and @boot_set from distinct Set # instances so freezing one can't poison the other - # stop_example must be able to mutate @loaded_set forever # while @boot_set stays frozen for invalidator comparison. @loaded_set = successful_paths @boot_set = Set.new(successful_paths).freeze else @loaded_set = Set.new @boot_set = Set.new.freeze end @boot_set end |
#enabled? ⇒ 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.
107 108 109 |
# File 'lib/rspec_tracer/tracker/loaded_files_tracker.rb', line 107 def enabled? @enabled end |
#loaded_set ⇒ 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.
Defensive copy for external callers / property specs. Callers that want read-only size should use ‘loaded_set_size` instead - avoids the dup allocation.
210 211 212 |
# File 'lib/rspec_tracer/tracker/loaded_files_tracker.rb', line 210 def loaded_set @loaded_set.dup end |
#loaded_set_inputs ⇒ 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.
Set<Input> covering the full @loaded_set. Callers merge this into an example’s Input bucket at start_example time - every file loaded up to this point is a transitive dependency. Returns a fresh Set per call so mutation stays local.
Disabled trackers never populate @input_cache (capture_boot_set! skips the build_inputs pass), so no explicit enabled guard is needed - the enumeration naturally yields Set.new.
179 180 181 |
# File 'lib/rspec_tracer/tracker/loaded_files_tracker.rb', line 179 def loaded_set_inputs @input_cache.values.to_set end |
#loaded_set_size ⇒ 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.
216 217 218 |
# File 'lib/rspec_tracer/tracker/loaded_files_tracker.rb', line 216 def loaded_set_size @loaded_set.size end |
#stop_example(_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.
Diff-and-grow. Called after an example finishes: peeks ::Coverage, finds paths the tracker hadn’t seen yet, digests them, adds them to @loaded_set + @input_cache, and returns the new-paths-only Input set so the caller can attribute them to the just-completed example.
Paths whose digest fails are dropped from both @loaded_set and the returned set - the next stop_example will retry them (useful if the failure was transient) and keeps the “@loaded_set => @input_cache has an entry” invariant.
Steady state (no new files loaded this example) is ~O(|peek|) with no digest work.
196 197 198 199 200 201 202 203 204 205 |
# File 'lib/rspec_tracer/tracker/loaded_files_tracker.rb', line 196 def stop_example(_example_id) return Set.new unless @enabled new_paths = new_filtered_paths return Set.new if new_paths.empty? new_inputs = build_inputs(new_paths) @loaded_set.merge(new_inputs.map(&:path)) new_inputs end |