Class: RSpecTracer::Tracker::LoadedFilesTracker Private

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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.expand_path(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_setObject (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

#rootObject (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_snapshotObject

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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


107
108
109
# File 'lib/rspec_tracer/tracker/loaded_files_tracker.rb', line 107

def enabled?
  @enabled
end

#loaded_setObject

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_inputsObject

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_sizeObject

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