rspec-tracer 1.2.3 → 2.0.0.pre.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +384 -67
- data/README.md +454 -429
- data/bin/rspec-tracer +15 -0
- data/lib/rspec_tracer/cache/Rakefile +43 -0
- data/lib/rspec_tracer/cli/cache_clear.rb +111 -0
- data/lib/rspec_tracer/cli/cache_info.rb +104 -0
- data/lib/rspec_tracer/cli/doctor.rb +284 -0
- data/lib/rspec_tracer/cli/explain.rb +158 -0
- data/lib/rspec_tracer/cli/report_open.rb +82 -0
- data/lib/rspec_tracer/cli.rb +116 -0
- data/lib/rspec_tracer/configuration.rb +1196 -3
- data/lib/rspec_tracer/engine.rb +1168 -0
- data/lib/rspec_tracer/example.rb +141 -11
- data/lib/rspec_tracer/filter.rb +35 -0
- data/lib/rspec_tracer/line_stub.rb +61 -0
- data/lib/rspec_tracer/load_config.rb +2 -2
- data/lib/rspec_tracer/logger.rb +15 -0
- data/lib/rspec_tracer/rails/README.md +78 -0
- data/lib/rspec_tracer/rails/i18n_tracking.rb +137 -0
- data/lib/rspec_tracer/rails/notifications.rb +263 -0
- data/lib/rspec_tracer/rails/preset.rb +94 -0
- data/lib/rspec_tracer/rails/railtie.rb +22 -0
- data/lib/rspec_tracer/rails.rb +15 -0
- data/lib/rspec_tracer/remote_cache/README.md +140 -0
- data/lib/rspec_tracer/remote_cache/Rakefile +35 -11
- data/lib/rspec_tracer/remote_cache/archive.rb +137 -0
- data/lib/rspec_tracer/remote_cache/backend.rb +73 -0
- data/lib/rspec_tracer/remote_cache/git_ancestry.rb +241 -0
- data/lib/rspec_tracer/remote_cache/local_fs_backend.rb +439 -0
- data/lib/rspec_tracer/remote_cache/redis_backend.rb +554 -0
- data/lib/rspec_tracer/remote_cache/s3_backend.rb +712 -0
- data/lib/rspec_tracer/remote_cache/user_tasks.rb +436 -0
- data/lib/rspec_tracer/remote_cache/validator.rb +40 -62
- data/lib/rspec_tracer/remote_cache.rb +22 -0
- data/lib/rspec_tracer/reporters/README.md +103 -0
- data/lib/rspec_tracer/reporters/base.rb +87 -0
- data/lib/rspec_tracer/reporters/coverage_json_reporter.rb +338 -0
- data/lib/rspec_tracer/reporters/html/.gitignore +19 -0
- data/lib/rspec_tracer/reporters/html/.prettierignore +4 -0
- data/lib/rspec_tracer/reporters/html/.prettierrc.json +9 -0
- data/lib/rspec_tracer/reporters/html/README.md +80 -0
- data/lib/rspec_tracer/reporters/html/dist/assets/index.css +2 -0
- data/lib/rspec_tracer/reporters/html/dist/assets/index.js +1 -0
- data/lib/rspec_tracer/reporters/html/dist/index.html +24 -0
- data/lib/rspec_tracer/reporters/html/eslint.config.js +62 -0
- data/lib/rspec_tracer/reporters/html/package-lock.json +4941 -0
- data/lib/rspec_tracer/reporters/html/package.json +29 -0
- data/lib/rspec_tracer/reporters/html/src/app.jsx +130 -0
- data/lib/rspec_tracer/reporters/html/src/components/AllExamples.jsx +86 -0
- data/lib/rspec_tracer/reporters/html/src/components/DuplicateExamples.jsx +68 -0
- data/lib/rspec_tracer/reporters/html/src/components/ExamplesDependency.jsx +78 -0
- data/lib/rspec_tracer/reporters/html/src/components/FilesDependency.jsx +72 -0
- data/lib/rspec_tracer/reporters/html/src/components/FlakyExamples.jsx +42 -0
- data/lib/rspec_tracer/reporters/html/src/components/ReportTable.jsx +131 -0
- data/lib/rspec_tracer/reporters/html/src/components/SearchBar.jsx +19 -0
- data/lib/rspec_tracer/reporters/html/src/index.html +23 -0
- data/lib/rspec_tracer/reporters/html/src/main.jsx +37 -0
- data/lib/rspec_tracer/reporters/html/src/styles.css +434 -0
- data/lib/rspec_tracer/reporters/html/vite.config.js +42 -0
- data/lib/rspec_tracer/reporters/html_reporter.rb +266 -0
- data/lib/rspec_tracer/reporters/json_reporter.rb +88 -0
- data/lib/rspec_tracer/reporters/payload_builder.rb +235 -0
- data/lib/rspec_tracer/reporters/registry.rb +120 -0
- data/lib/rspec_tracer/reporters/terminal_reporter.rb +264 -0
- data/lib/rspec_tracer/rspec/README.md +73 -0
- data/lib/rspec_tracer/rspec/installation.rb +97 -0
- data/lib/rspec_tracer/rspec/metadata.rb +96 -0
- data/lib/rspec_tracer/rspec/parallel_tests.rb +459 -0
- data/lib/rspec_tracer/rspec/reporter_hook.rb +84 -0
- data/lib/rspec_tracer/rspec/runner_hook.rb +239 -0
- data/lib/rspec_tracer/source_file.rb +24 -7
- data/lib/rspec_tracer/storage/README.md +35 -0
- data/lib/rspec_tracer/storage/backend.rb +130 -0
- data/lib/rspec_tracer/storage/json_backend.rb +884 -0
- data/lib/rspec_tracer/storage/lazy_snapshot.rb +65 -0
- data/lib/rspec_tracer/storage/schema.rb +50 -0
- data/lib/rspec_tracer/storage/serializer/json.rb +41 -0
- data/lib/rspec_tracer/storage/serializer/msgpack.rb +167 -0
- data/lib/rspec_tracer/storage/snapshot.rb +141 -0
- data/lib/rspec_tracer/storage/sqlite_backend.rb +693 -0
- data/lib/rspec_tracer/time_formatter.rb +37 -18
- data/lib/rspec_tracer/tracker/README.md +36 -0
- data/lib/rspec_tracer/tracker/coverage_adapter.rb +174 -0
- data/lib/rspec_tracer/tracker/declared_globs.rb +100 -0
- data/lib/rspec_tracer/tracker/dependency_graph.rb +134 -0
- data/lib/rspec_tracer/tracker/env_matcher.rb +127 -0
- data/lib/rspec_tracer/tracker/env_snapshot.rb +77 -0
- data/lib/rspec_tracer/tracker/example_registry.rb +153 -0
- data/lib/rspec_tracer/tracker/file_digest.rb +61 -0
- data/lib/rspec_tracer/tracker/filter.rb +127 -0
- data/lib/rspec_tracer/tracker/input.rb +99 -0
- data/lib/rspec_tracer/tracker/io_hooks/file.rb +55 -0
- data/lib/rspec_tracer/tracker/io_hooks/io.rb +24 -0
- data/lib/rspec_tracer/tracker/io_hooks/json.rb +23 -0
- data/lib/rspec_tracer/tracker/io_hooks/kernel.rb +26 -0
- data/lib/rspec_tracer/tracker/io_hooks/yaml.rb +38 -0
- data/lib/rspec_tracer/tracker/io_hooks.rb +195 -0
- data/lib/rspec_tracer/tracker/loaded_files_tracker.rb +295 -0
- data/lib/rspec_tracer/tracker/new_file_detector.rb +62 -0
- data/lib/rspec_tracer/tracker/whole_suite_invalidators.rb +96 -0
- data/lib/rspec_tracer/version.rb +4 -1
- data/lib/rspec_tracer.rb +231 -491
- metadata +94 -43
- data/lib/rspec_tracer/cache.rb +0 -207
- data/lib/rspec_tracer/coverage_merger.rb +0 -42
- data/lib/rspec_tracer/coverage_reporter.rb +0 -187
- data/lib/rspec_tracer/coverage_writer.rb +0 -58
- data/lib/rspec_tracer/html_reporter/Rakefile +0 -18
- data/lib/rspec_tracer/html_reporter/assets/javascripts/application.js +0 -56
- data/lib/rspec_tracer/html_reporter/assets/javascripts/libraries/jquery.js +0 -10881
- data/lib/rspec_tracer/html_reporter/assets/javascripts/plugins/datatables.js +0 -15381
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/application.css +0 -196
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/datatables.css +0 -459
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/jquery-ui.css +0 -436
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/print.css +0 -92
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/reset.css +0 -265
- data/lib/rspec_tracer/html_reporter/public/application.css +0 -5
- data/lib/rspec_tracer/html_reporter/public/application.js +0 -6
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc_disabled.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_both.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc_disabled.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/favicon.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/loading.gif +0 -0
- data/lib/rspec_tracer/html_reporter/reporter.rb +0 -242
- data/lib/rspec_tracer/html_reporter/views/duplicate_examples.erb +0 -34
- data/lib/rspec_tracer/html_reporter/views/examples.erb +0 -58
- data/lib/rspec_tracer/html_reporter/views/examples_dependency.erb +0 -36
- data/lib/rspec_tracer/html_reporter/views/files_dependency.erb +0 -36
- data/lib/rspec_tracer/html_reporter/views/flaky_examples.erb +0 -38
- data/lib/rspec_tracer/html_reporter/views/layout.erb +0 -38
- data/lib/rspec_tracer/remote_cache/aws.rb +0 -176
- data/lib/rspec_tracer/remote_cache/cache.rb +0 -75
- data/lib/rspec_tracer/remote_cache/repo.rb +0 -210
- data/lib/rspec_tracer/report_generator.rb +0 -158
- data/lib/rspec_tracer/report_merger.rb +0 -68
- data/lib/rspec_tracer/report_writer.rb +0 -141
- data/lib/rspec_tracer/reporter.rb +0 -204
- data/lib/rspec_tracer/rspec_reporter.rb +0 -41
- data/lib/rspec_tracer/rspec_runner.rb +0 -56
- data/lib/rspec_tracer/ruby_coverage.rb +0 -9
- data/lib/rspec_tracer/runner.rb +0 -278
|
@@ -0,0 +1,1168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'coverage'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'set'
|
|
7
|
+
|
|
8
|
+
require_relative 'tracker/coverage_adapter'
|
|
9
|
+
require_relative 'tracker/declared_globs'
|
|
10
|
+
require_relative 'tracker/dependency_graph'
|
|
11
|
+
require_relative 'tracker/env_matcher'
|
|
12
|
+
require_relative 'tracker/env_snapshot'
|
|
13
|
+
require_relative 'tracker/example_registry'
|
|
14
|
+
require_relative 'tracker/file_digest'
|
|
15
|
+
require_relative 'tracker/filter'
|
|
16
|
+
require_relative 'tracker/input'
|
|
17
|
+
require_relative 'tracker/io_hooks'
|
|
18
|
+
require_relative 'tracker/loaded_files_tracker'
|
|
19
|
+
require_relative 'tracker/new_file_detector'
|
|
20
|
+
require_relative 'tracker/whole_suite_invalidators'
|
|
21
|
+
require_relative 'storage/backend'
|
|
22
|
+
require_relative 'storage/json_backend'
|
|
23
|
+
require_relative 'storage/schema'
|
|
24
|
+
require_relative 'storage/snapshot'
|
|
25
|
+
require_relative 'storage/sqlite_backend'
|
|
26
|
+
|
|
27
|
+
module RSpecTracer
|
|
28
|
+
# Top-level coordinator for the v2 core engine. Wires
|
|
29
|
+
# CoverageAdapter + IOHooks + DeclaredGlobs + NewFileDetector +
|
|
30
|
+
# WholeSuiteInvalidators + LoadedFilesTracker + ExampleRegistry +
|
|
31
|
+
# DependencyGraph + Storage into a single pipeline.
|
|
32
|
+
#
|
|
33
|
+
# Named `Engine` rather than `Tracker` because the `Tracker`
|
|
34
|
+
# namespace is already taken by the sub-module that houses the
|
|
35
|
+
# leaf observers (`Tracker::CoverageAdapter`, `Tracker::IOHooks`,
|
|
36
|
+
# etc.). `RSpecTracer.engine` is the public accessor the RSpec
|
|
37
|
+
# hooks dispatch through during a run.
|
|
38
|
+
#
|
|
39
|
+
# Lifecycle (driven by RSpec hooks in `lib/rspec_tracer.rb`):
|
|
40
|
+
#
|
|
41
|
+
# engine = Engine.new(configuration: RSpecTracer)
|
|
42
|
+
# engine.setup # install hooks, load cache,
|
|
43
|
+
# # compute filter decisions
|
|
44
|
+
# engine.run_example?(id) # per-example filter (from cache)
|
|
45
|
+
# engine.register_example(example) # record metadata + duplicates
|
|
46
|
+
# engine.example_started # peek baseline + open bucket
|
|
47
|
+
# # ... example body runs, IOHooks record into bucket ...
|
|
48
|
+
# engine.example_finished(id) # diff coverage, attribute, close
|
|
49
|
+
# engine.on_example_{passed,failed,pending,skipped}(id, result)
|
|
50
|
+
# engine.finalize # persist snapshot + coverage
|
|
51
|
+
#
|
|
52
|
+
# Per-example coverage delta map: peek baseline at example_started,
|
|
53
|
+
# peek again at example_finished, store the per-line strength delta
|
|
54
|
+
# under `@examples_coverage[id][file_path][line]`. Reporters::
|
|
55
|
+
# CoverageJsonReporter consumes the cumulative coverage at finalize
|
|
56
|
+
# via Tracker::CoverageAdapter#peek_unfiltered + the engine's
|
|
57
|
+
# `merge_skipped_coverage` algorithm.
|
|
58
|
+
#
|
|
59
|
+
# Cache parity: `finalize` builds a Snapshot with file-name-keyed
|
|
60
|
+
# dependency / reverse_dependency / all_files maps (matching the
|
|
61
|
+
# 1.x on-disk convention - root-stripped file names with a leading
|
|
62
|
+
# "/") and hands it to Storage::JsonBackend. The 2.0 schema bump
|
|
63
|
+
# adds `boot_set`; everything else mirrors the 1.x cache layout
|
|
64
|
+
# byte-for-byte.
|
|
65
|
+
# rubocop:disable Metrics/ClassLength
|
|
66
|
+
class Engine
|
|
67
|
+
# Internal constant.
|
|
68
|
+
# @api private
|
|
69
|
+
EXAMPLE_RUN_REASON = {
|
|
70
|
+
explicit_run: 'Explicit run',
|
|
71
|
+
no_cache: 'No cache',
|
|
72
|
+
interrupted: 'Interrupted previously',
|
|
73
|
+
flaky_example: 'Flaky example',
|
|
74
|
+
failed_example: 'Failed previously',
|
|
75
|
+
pending_example: 'Pending previously',
|
|
76
|
+
files_changed: 'Files changed',
|
|
77
|
+
whole_suite_invalidator: 'Whole-suite invalidator changed',
|
|
78
|
+
env_changed: 'Environment changed'
|
|
79
|
+
}.freeze
|
|
80
|
+
|
|
81
|
+
# Map from Filter#select reasons to the legacy-shaped strings
|
|
82
|
+
# users see in test output ("foo (Files changed)"). Keeps the
|
|
83
|
+
# user surface unchanged under v2.
|
|
84
|
+
FILTER_REASON_STRINGS = {
|
|
85
|
+
whole_suite_invalidator: EXAMPLE_RUN_REASON[:whole_suite_invalidator],
|
|
86
|
+
interrupted: EXAMPLE_RUN_REASON[:interrupted],
|
|
87
|
+
flaky_example: EXAMPLE_RUN_REASON[:flaky_example],
|
|
88
|
+
failed_example: EXAMPLE_RUN_REASON[:failed_example],
|
|
89
|
+
pending_example: EXAMPLE_RUN_REASON[:pending_example],
|
|
90
|
+
no_cache: EXAMPLE_RUN_REASON[:no_cache],
|
|
91
|
+
files_changed: EXAMPLE_RUN_REASON[:files_changed],
|
|
92
|
+
env_changed: EXAMPLE_RUN_REASON[:env_changed]
|
|
93
|
+
}.freeze
|
|
94
|
+
|
|
95
|
+
# Internal attribute.
|
|
96
|
+
# @api private
|
|
97
|
+
attr_reader :registry, :graph, :loaded_files_tracker, :coverage_adapter,
|
|
98
|
+
:declared_globs, :whole_suite_invalidators, :new_file_detector,
|
|
99
|
+
:env_snapshot, :storage_backend, :all_examples, :duplicate_examples,
|
|
100
|
+
:examples_coverage, :all_files
|
|
101
|
+
|
|
102
|
+
# Internal method on the tracer pipeline.
|
|
103
|
+
# @api private
|
|
104
|
+
def initialize(configuration: RSpecTracer)
|
|
105
|
+
@configuration = configuration
|
|
106
|
+
@filtered_examples = {}
|
|
107
|
+
@all_examples = {}
|
|
108
|
+
@duplicate_examples = {}
|
|
109
|
+
@examples_coverage = {}
|
|
110
|
+
@all_files = {}
|
|
111
|
+
@tracks_files = Hash.new { |h, id| h[id] = Set.new } # id => Set<abs_path>
|
|
112
|
+
@tracks_env = Hash.new { |h, id| h[id] = Set.new } # id => Set<env_name>
|
|
113
|
+
@tracked_env_names = Set.new
|
|
114
|
+
@config_tracked_env_names = Set.new # config-level subset (post-expansion)
|
|
115
|
+
@previous_snapshot = nil
|
|
116
|
+
@run_id = nil
|
|
117
|
+
@before_peek = nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Internal method on the tracer pipeline.
|
|
121
|
+
# @api private
|
|
122
|
+
def setup
|
|
123
|
+
@configuration.freeze_declared_globs!
|
|
124
|
+
|
|
125
|
+
build_observers
|
|
126
|
+
install_io_hooks
|
|
127
|
+
install_rails_observers
|
|
128
|
+
ensure_coverage_started
|
|
129
|
+
|
|
130
|
+
@loaded_files_tracker.capture_boot_set!
|
|
131
|
+
@declared_globs.walk
|
|
132
|
+
|
|
133
|
+
@previous_snapshot = load_previous_snapshot
|
|
134
|
+
seed_state_from_previous(@previous_snapshot) if @previous_snapshot
|
|
135
|
+
register_config_tracked_env_names
|
|
136
|
+
compute_filter_decisions
|
|
137
|
+
self
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# --- filter-phase surface (mirrors legacy Runner) --------------
|
|
141
|
+
|
|
142
|
+
def run_example?(example_id)
|
|
143
|
+
return true if @configuration.run_all_examples
|
|
144
|
+
|
|
145
|
+
previously_seen = @previous_snapshot&.all_examples&.key?(example_id)
|
|
146
|
+
!previously_seen || @filtered_examples.key?(example_id)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Internal method on the tracer pipeline.
|
|
150
|
+
# @api private
|
|
151
|
+
def run_example_reason(example_id)
|
|
152
|
+
return EXAMPLE_RUN_REASON[:explicit_run] if @configuration.run_all_examples
|
|
153
|
+
|
|
154
|
+
@filtered_examples[example_id] || EXAMPLE_RUN_REASON[:no_cache]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Records one example registration into the engine's per-run
|
|
158
|
+
# state. Overwrites any prior `@all_examples[id]` entry on
|
|
159
|
+
# purpose: on warm runs, `seed_all_examples_from_previous` seeds
|
|
160
|
+
# `@all_examples` with the prior snapshot's metadata (including
|
|
161
|
+
# the prior `:run_reason`), and the RunnerHook's per-example
|
|
162
|
+
# call here carries this run's freshly-tagged value. Preserving
|
|
163
|
+
# the seeded entry via `||=` would persist the stale prior
|
|
164
|
+
# reason and surface "No cache" in `report.json#run_reason` for
|
|
165
|
+
# examples re-run because they failed / pended / were
|
|
166
|
+
# interrupted / had files change / had env change. Duplicate
|
|
167
|
+
# detection is unaffected: duplicates accumulate in
|
|
168
|
+
# `@duplicate_examples`, and `deregister_duplicate_examples`
|
|
169
|
+
# drops them from `@all_examples` outright.
|
|
170
|
+
# @api private
|
|
171
|
+
def register_example(example)
|
|
172
|
+
example_id = example[:example_id]
|
|
173
|
+
@registry.register(example_id, metadata: example, identity_hash: example_id)
|
|
174
|
+
@all_examples[example_id] = example
|
|
175
|
+
@duplicate_examples[example_id] ||= []
|
|
176
|
+
@duplicate_examples[example_id] << example
|
|
177
|
+
self
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Per-example tracking DSL hook. Called from RunnerHook with the
|
|
181
|
+
# normalized `{files: Set<String>, env: Set<String>}` that
|
|
182
|
+
# `RSpec::Metadata.tracks_for(example)` produced. Resolves the
|
|
183
|
+
# file globs against the project root once per distinct glob
|
|
184
|
+
# string (memoized) and unions the matching Inputs into this
|
|
185
|
+
# example's dependency set. Env names are accumulated into
|
|
186
|
+
# `@tracked_env_names` so the finalize snapshot covers every key
|
|
187
|
+
# the run cared about.
|
|
188
|
+
#
|
|
189
|
+
# Per-example env entries may carry wildcard patterns (`tracks:
|
|
190
|
+
# { env: 'RAILS_*' }`). `EnvMatcher.expand` is the single funnel
|
|
191
|
+
# - literals pass through, wildcards expand against the live ENV,
|
|
192
|
+
# and unsupported syntax raises ArgumentError at this point
|
|
193
|
+
# (RunnerHook Pass 1, before any example body runs).
|
|
194
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
195
|
+
def register_tracks(example_id, tracks)
|
|
196
|
+
files = tracks[:files] || tracks['files'] || Set.new
|
|
197
|
+
envs = tracks[:env] || tracks['env'] || Set.new
|
|
198
|
+
|
|
199
|
+
files.each { |glob| @tracks_files[example_id].merge(resolved_glob_inputs(glob)) } unless files.empty?
|
|
200
|
+
return self if envs.empty?
|
|
201
|
+
|
|
202
|
+
expanded = RSpecTracer::Tracker::EnvMatcher.expand(envs.map(&:to_s))
|
|
203
|
+
@tracks_env[example_id].merge(expanded)
|
|
204
|
+
@tracked_env_names.merge(expanded)
|
|
205
|
+
self
|
|
206
|
+
end
|
|
207
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
208
|
+
|
|
209
|
+
# Called from RunnerHook AFTER the filter-decision pre-walk has
|
|
210
|
+
# populated `@tracks_env` / `@tracked_env_names` for every
|
|
211
|
+
# example. Compares each declared env key against the previous
|
|
212
|
+
# snapshot's `env_snapshot` via Tracker::EnvSnapshot; marks any
|
|
213
|
+
# example whose tracked-env set intersects the invalidated set
|
|
214
|
+
# as re-runnable. Strictly additive vs other filter reasons - if
|
|
215
|
+
# the example was already in `@filtered_examples` for a stronger
|
|
216
|
+
# reason (files_changed / whole_suite_invalidator /
|
|
217
|
+
# failed_example / ...), env_changed does NOT overwrite.
|
|
218
|
+
#
|
|
219
|
+
# Config-level path: when an invalidated key intersects
|
|
220
|
+
# `@config_tracked_env_names` (the post-expansion config-level
|
|
221
|
+
# set), every previously-seen example re-runs - mirrors the
|
|
222
|
+
# `track_files` "declared globs attach to every example"
|
|
223
|
+
# semantics. New examples (not in @previous_snapshot.all_examples)
|
|
224
|
+
# already run via the no_cache path; no special-casing needed.
|
|
225
|
+
def apply_env_filter_decisions
|
|
226
|
+
return self if @previous_snapshot.nil?
|
|
227
|
+
return self if @tracked_env_names.empty?
|
|
228
|
+
|
|
229
|
+
invalidated = @env_snapshot.invalidated_keys(
|
|
230
|
+
@previous_snapshot.env_snapshot, @tracked_env_names
|
|
231
|
+
)
|
|
232
|
+
return self if invalidated.empty?
|
|
233
|
+
|
|
234
|
+
reason = FILTER_REASON_STRINGS.fetch(:env_changed)
|
|
235
|
+
mark_all_prev_examples(reason) if invalidated.intersect?(@config_tracked_env_names)
|
|
236
|
+
mark_per_example_intersections(invalidated, reason)
|
|
237
|
+
self
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Internal method on the tracer pipeline.
|
|
241
|
+
# @api private
|
|
242
|
+
def deregister_duplicate_examples
|
|
243
|
+
@duplicate_examples.select! { |_, entries| entries.count > 1 }
|
|
244
|
+
return if @duplicate_examples.empty?
|
|
245
|
+
|
|
246
|
+
@all_examples.reject! { |id, _| @duplicate_examples.key?(id) }
|
|
247
|
+
self
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# --- per-example surface --------------------------------------
|
|
251
|
+
|
|
252
|
+
# Internal method on the tracer pipeline.
|
|
253
|
+
# @api private
|
|
254
|
+
def example_started
|
|
255
|
+
@before_peek = @coverage_adapter.peek
|
|
256
|
+
@current_bucket = {}
|
|
257
|
+
@current_rails_bucket = {}
|
|
258
|
+
RSpecTracer::Tracker::IOHooks.set_bucket(@current_bucket)
|
|
259
|
+
set_rails_bucket(@current_rails_bucket)
|
|
260
|
+
self
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Internal method on the tracer pipeline.
|
|
264
|
+
# @api private
|
|
265
|
+
def example_finished(example_id)
|
|
266
|
+
after_peek = @coverage_adapter.peek
|
|
267
|
+
record_coverage_delta(example_id, @before_peek, after_peek)
|
|
268
|
+
io_inputs = @current_bucket.values
|
|
269
|
+
rails_inputs = @current_rails_bucket ? @current_rails_bucket.values : []
|
|
270
|
+
RSpecTracer::Tracker::IOHooks.clear_bucket
|
|
271
|
+
clear_rails_bucket
|
|
272
|
+
|
|
273
|
+
transitive_inputs = @loaded_files_tracker.loaded_set_inputs |
|
|
274
|
+
@loaded_files_tracker.stop_example(example_id)
|
|
275
|
+
coverage_inputs = @coverage_adapter.compute_diff(@before_peek, after_peek)
|
|
276
|
+
declared_inputs = @declared_globs.walk
|
|
277
|
+
tracks_inputs = per_example_tracks_inputs(example_id)
|
|
278
|
+
attribute_to_example(
|
|
279
|
+
example_id,
|
|
280
|
+
coverage_inputs | transitive_inputs | io_inputs.to_set |
|
|
281
|
+
rails_inputs.to_set | declared_inputs | tracks_inputs
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
@before_peek = nil
|
|
285
|
+
@current_bucket = nil
|
|
286
|
+
@current_rails_bucket = nil
|
|
287
|
+
self
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Internal method on the tracer pipeline.
|
|
291
|
+
# @api private
|
|
292
|
+
def on_example_skipped(example_id)
|
|
293
|
+
@registry.register(example_id) unless @registry.registered?(example_id)
|
|
294
|
+
@registry.update_status(example_id, :skipped)
|
|
295
|
+
self
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Internal method on the tracer pipeline.
|
|
299
|
+
# @api private
|
|
300
|
+
def on_example_passed(example_id, result)
|
|
301
|
+
return if @duplicate_examples[example_id]&.count.to_i > 1
|
|
302
|
+
|
|
303
|
+
status = flaky_history?(example_id) ? :flaky : :passed
|
|
304
|
+
@registry.update_status(example_id, status)
|
|
305
|
+
record_execution_result(example_id, result)
|
|
306
|
+
self
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Internal method on the tracer pipeline.
|
|
310
|
+
# @api private
|
|
311
|
+
def on_example_failed(example_id, result)
|
|
312
|
+
return if @duplicate_examples[example_id]&.count.to_i > 1
|
|
313
|
+
|
|
314
|
+
status = previously_flaky?(example_id) ? :flaky : :failed
|
|
315
|
+
@registry.update_status(example_id, status)
|
|
316
|
+
record_execution_result(example_id, result)
|
|
317
|
+
self
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Internal method on the tracer pipeline.
|
|
321
|
+
# @api private
|
|
322
|
+
def on_example_pending(example_id, result)
|
|
323
|
+
return if @duplicate_examples[example_id]&.count.to_i > 1
|
|
324
|
+
|
|
325
|
+
@registry.update_status(example_id, :pending)
|
|
326
|
+
record_execution_result(example_id, result)
|
|
327
|
+
self
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# --- finalize ------------------------------------------------
|
|
331
|
+
|
|
332
|
+
def finalize
|
|
333
|
+
@registry.all_example_ids.each do |id|
|
|
334
|
+
next if @registry.status_of(id)
|
|
335
|
+
|
|
336
|
+
@registry.update_status(id, :interrupted)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
snapshot = build_snapshot
|
|
340
|
+
@storage_backend.save_graph(snapshot, schema_version: RSpecTracer::Storage::Schema::CURRENT)
|
|
341
|
+
uninstall_rails_observers
|
|
342
|
+
snapshot
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# For every previously-skipped example id, accumulate per-line
|
|
346
|
+
# coverage strengths from the previous run's per-example coverage
|
|
347
|
+
# map into the missed_coverage return value. Deleted files /
|
|
348
|
+
# missing entries are skipped silently. Consumed by
|
|
349
|
+
# Reporters::CoverageJsonReporter at finalize time so coverage.json
|
|
350
|
+
# carries forward the contribution of skipped examples.
|
|
351
|
+
#
|
|
352
|
+
# Returns Hash[file_path => Hash[line_number => cumulative_strength]].
|
|
353
|
+
def merge_skipped_coverage(skipped_ids, previous_examples_coverage = nil)
|
|
354
|
+
source = previous_examples_coverage || @previous_snapshot&.examples_coverage || {}
|
|
355
|
+
missed = Hash.new { |h, f| h[f] = Hash.new(0) }
|
|
356
|
+
|
|
357
|
+
skipped_ids.each do |example_id|
|
|
358
|
+
example_coverage = source[example_id]
|
|
359
|
+
next if example_coverage.nil?
|
|
360
|
+
|
|
361
|
+
example_coverage.each do |file_path, line_coverage|
|
|
362
|
+
accumulate_line_coverage(missed[file_path], line_coverage)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
missed
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# --- accessors used by specs ---------------------------------
|
|
370
|
+
|
|
371
|
+
def filtered_example_ids
|
|
372
|
+
@filtered_examples.keys
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Internal method on the tracer pipeline.
|
|
376
|
+
# @api private
|
|
377
|
+
def previous_snapshot_loaded?
|
|
378
|
+
!@previous_snapshot.nil?
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
private
|
|
382
|
+
|
|
383
|
+
# Read the config-level `track_env(*names)` accumulator,
|
|
384
|
+
# expand any wildcard patterns against the live ENV via
|
|
385
|
+
# EnvMatcher.expand (which raises ArgumentError on unsupported
|
|
386
|
+
# syntax - intentionally surfaces config errors at run start),
|
|
387
|
+
# and seed both `@config_tracked_env_names` (for the global
|
|
388
|
+
# mark-every-example branch in apply_env_filter_decisions) and
|
|
389
|
+
# `@tracked_env_names` (so the finalize snapshot includes every
|
|
390
|
+
# config-level key alongside per-example keys).
|
|
391
|
+
def register_config_tracked_env_names
|
|
392
|
+
patterns = @configuration.tracked_env_names
|
|
393
|
+
return if patterns.nil? || patterns.empty?
|
|
394
|
+
|
|
395
|
+
expanded = RSpecTracer::Tracker::EnvMatcher.expand(patterns)
|
|
396
|
+
@config_tracked_env_names.merge(expanded)
|
|
397
|
+
@tracked_env_names.merge(expanded)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Mark every previously-seen example for re-run. Called
|
|
401
|
+
# when a config-level env key flips between runs.
|
|
402
|
+
def mark_all_prev_examples(reason)
|
|
403
|
+
@previous_snapshot.all_examples.each_key do |example_id|
|
|
404
|
+
next if @filtered_examples.key?(example_id)
|
|
405
|
+
|
|
406
|
+
@filtered_examples[example_id] = reason
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Per-example env-changed mark. Walks @tracks_env,
|
|
411
|
+
# marks examples whose declared env set intersects invalidated.
|
|
412
|
+
# Additive vs other filter reasons (won't overwrite).
|
|
413
|
+
def mark_per_example_intersections(invalidated, reason)
|
|
414
|
+
@tracks_env.each do |example_id, envs|
|
|
415
|
+
next unless envs.intersect?(invalidated)
|
|
416
|
+
next if @filtered_examples.key?(example_id)
|
|
417
|
+
|
|
418
|
+
@filtered_examples[example_id] = reason
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Internal method on the tracer pipeline.
|
|
423
|
+
# @api private
|
|
424
|
+
def build_observers
|
|
425
|
+
@registry = RSpecTracer::Tracker::ExampleRegistry.new
|
|
426
|
+
@graph = RSpecTracer::Tracker::DependencyGraph.new
|
|
427
|
+
@coverage_adapter = RSpecTracer::Tracker::CoverageAdapter.new(
|
|
428
|
+
root: @configuration.root, filters: @configuration.filters
|
|
429
|
+
)
|
|
430
|
+
@declared_globs = RSpecTracer::Tracker::DeclaredGlobs.new(
|
|
431
|
+
root: @configuration.root, globs: @configuration.declared_globs
|
|
432
|
+
)
|
|
433
|
+
@whole_suite_invalidators = RSpecTracer::Tracker::WholeSuiteInvalidators.new(
|
|
434
|
+
root: @configuration.root
|
|
435
|
+
)
|
|
436
|
+
@env_snapshot = RSpecTracer::Tracker::EnvSnapshot.new
|
|
437
|
+
@new_file_detector = RSpecTracer::Tracker::NewFileDetector.new(
|
|
438
|
+
root: @configuration.root, declared_globs: @configuration.declared_globs
|
|
439
|
+
)
|
|
440
|
+
@loaded_files_tracker = RSpecTracer::Tracker::LoadedFilesTracker.new(
|
|
441
|
+
root: @configuration.root, enabled: @configuration.transitive_load_tracking
|
|
442
|
+
)
|
|
443
|
+
@storage_backend = build_storage_backend(@configuration.cache_path)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Resolve the configured storage backend to a concrete instance.
|
|
447
|
+
# Delegates to {RSpecTracer::Storage::Backend.build}, the single
|
|
448
|
+
# factory shared with the CLI sub-commands so `cache:info` /
|
|
449
|
+
# `explain` compose correctly with `storage_backend :sqlite`.
|
|
450
|
+
def build_storage_backend(cache_path)
|
|
451
|
+
RSpecTracer::Storage::Backend.build(
|
|
452
|
+
cache_path: cache_path, configuration: @configuration
|
|
453
|
+
)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Internal method on the tracer pipeline.
|
|
457
|
+
# @api private
|
|
458
|
+
def install_io_hooks
|
|
459
|
+
declared = @declared_globs
|
|
460
|
+
RSpecTracer::Tracker::IOHooks.install(
|
|
461
|
+
root: @configuration.root,
|
|
462
|
+
filter: ->(path) { !declared.covers?(path) }
|
|
463
|
+
)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Install the Rails-observer family (ActionView notifications +
|
|
467
|
+
# I18n backend prepend) when Rails is detected in the process.
|
|
468
|
+
# Errors here never propagate - the tracer gracefully degrades to
|
|
469
|
+
# IOHooks-only behavior.
|
|
470
|
+
#
|
|
471
|
+
# Two-mode dispatch handles the canonical README setup order
|
|
472
|
+
# (RSpecTracer.start BEFORE `require_relative '../config/environment'`,
|
|
473
|
+
# so Rails is not yet loaded at engine.setup time):
|
|
474
|
+
#
|
|
475
|
+
# - **Eager** (Rails already loaded): install subscribers + arm the
|
|
476
|
+
# AR-schema warn directly.
|
|
477
|
+
# - **Late-bind** (Rails not yet loaded): register a `before(:suite)`
|
|
478
|
+
# hook that re-checks `defined?(::Rails::VERSION)` after Rails has
|
|
479
|
+
# loaded (typically via the user's `rails_helper.rb` requiring
|
|
480
|
+
# `config/environment` later in the boot chain). The hook installs
|
|
481
|
+
# subscribers + the inline AR-schema warn at suite time.
|
|
482
|
+
#
|
|
483
|
+
# Without the late-bind path, `track_ar_schema_notifications` was
|
|
484
|
+
# silently inert under the canonical README setup -- the documented
|
|
485
|
+
# `use_transactional_fixtures` warn never fired and the
|
|
486
|
+
# `sql.active_record` subscriber never attached.
|
|
487
|
+
def install_rails_observers
|
|
488
|
+
if @configuration.rails?
|
|
489
|
+
do_install_rails_observers
|
|
490
|
+
arm_ar_schema_setup_warn if ar_schema_notifications_enabled?
|
|
491
|
+
else
|
|
492
|
+
arm_rails_late_bind_install
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Subscriber-install body. Idempotent via `@rails_observers_installed`
|
|
497
|
+
# so a re-call (e.g. eager path then late-bind firing on a slow Rails
|
|
498
|
+
# load) is a no-op. Shared between the eager and late-bind paths.
|
|
499
|
+
def do_install_rails_observers
|
|
500
|
+
return if rails_observers_installed?
|
|
501
|
+
|
|
502
|
+
require_relative 'rails/notifications'
|
|
503
|
+
require_relative 'rails/i18n_tracking'
|
|
504
|
+
|
|
505
|
+
declared = @declared_globs
|
|
506
|
+
filter = ->(path) { !declared.covers?(path) }
|
|
507
|
+
ar_paths = ar_schema_notifications_enabled? ? ar_schema_path_probes : []
|
|
508
|
+
|
|
509
|
+
RSpecTracer::Rails::Notifications.install(
|
|
510
|
+
root: @configuration.root, filter: filter, ar_schema_paths: ar_paths
|
|
511
|
+
)
|
|
512
|
+
RSpecTracer::Rails::I18nTracking.install(
|
|
513
|
+
root: @configuration.root, filter: filter
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
@rails_observers_installed = true
|
|
517
|
+
rescue StandardError => e
|
|
518
|
+
@configuration.logger.warn(
|
|
519
|
+
"rspec-tracer: Rails observer install failed (#{e.class}: #{e.message})"
|
|
520
|
+
)
|
|
521
|
+
@rails_observers_installed = false
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Late-bind: at engine.setup `defined?(::Rails::VERSION)` was false,
|
|
525
|
+
# so register a `before(:suite)` hook that re-checks Rails-loaded
|
|
526
|
+
# state at suite time. By that point the user's `rails_helper.rb`
|
|
527
|
+
# has typically required `config/environment` and Rails IS loaded.
|
|
528
|
+
# Installs subscribers + inline-fires the AR-schema warn so the
|
|
529
|
+
# documented behavior holds end-to-end under the canonical README
|
|
530
|
+
# setup order. No-op when Rails still hasn't loaded by suite start.
|
|
531
|
+
def arm_rails_late_bind_install
|
|
532
|
+
return unless defined?(::RSpec) && ::RSpec.respond_to?(:configure)
|
|
533
|
+
|
|
534
|
+
engine = self
|
|
535
|
+
::RSpec.configure do |config|
|
|
536
|
+
config.before(:suite) { engine.send(:rails_late_bind_install_hook) }
|
|
537
|
+
end
|
|
538
|
+
rescue StandardError => e
|
|
539
|
+
@configuration.logger.warn(
|
|
540
|
+
"rspec-tracer: rails late-bind install failed (#{e.class}: #{e.message})"
|
|
541
|
+
)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Body of the late-bind `before(:suite)` hook. Extracted so the
|
|
545
|
+
# registration shape stays flat and the per-fire logic is testable
|
|
546
|
+
# without invoking RSpec's suite machinery.
|
|
547
|
+
def rails_late_bind_install_hook
|
|
548
|
+
return unless defined?(::Rails::VERSION) && !::Rails::VERSION.nil?
|
|
549
|
+
return if rails_observers_installed?
|
|
550
|
+
|
|
551
|
+
do_install_rails_observers
|
|
552
|
+
return unless rails_observers_installed?
|
|
553
|
+
return unless ar_schema_notifications_enabled?
|
|
554
|
+
|
|
555
|
+
emit_ar_schema_setup_warn_if_needed
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# `track_ar_schema_notifications` promises per-example attribution
|
|
559
|
+
# of `db/schema.rb` via the `sql.active_record` subscriber. That
|
|
560
|
+
# narrow promise only holds when no per-example AR cleanup
|
|
561
|
+
# mechanism fires queries inside the rspec-tracer per-example
|
|
562
|
+
# bucket window. Common Rails setups trip this:
|
|
563
|
+
#
|
|
564
|
+
# - `use_transactional_fixtures = true` (Rails default): per-
|
|
565
|
+
# example BEGIN/COMMIT fires sql.active_record -> every
|
|
566
|
+
# example gets schema attributed -> any schema mutation re-
|
|
567
|
+
# runs every example (safe but wide).
|
|
568
|
+
# - DatabaseCleaner :truncation / :deletion / :transaction in
|
|
569
|
+
# around hooks: same outcome.
|
|
570
|
+
#
|
|
571
|
+
# Eager path: defer the check to before(:suite) -- at engine.setup
|
|
572
|
+
# the user has not run their RSpec.configure block yet, so
|
|
573
|
+
# `use_transactional_fixtures` is unset. The late-bind path
|
|
574
|
+
# short-circuits this method and inline-fires
|
|
575
|
+
# `emit_ar_schema_setup_warn_if_needed` from inside its own
|
|
576
|
+
# before(:suite) hook (no nested registration).
|
|
577
|
+
def arm_ar_schema_setup_warn
|
|
578
|
+
return unless defined?(::RSpec) && ::RSpec.respond_to?(:configure)
|
|
579
|
+
|
|
580
|
+
engine = self
|
|
581
|
+
::RSpec.configure do |config|
|
|
582
|
+
config.before(:suite) do
|
|
583
|
+
engine.send(:emit_ar_schema_setup_warn_if_needed)
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
rescue StandardError => e
|
|
587
|
+
@configuration.logger.warn(
|
|
588
|
+
"rspec-tracer: ar-schema warn install failed (#{e.class}: #{e.message})"
|
|
589
|
+
)
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# Inline check for the AR-schema attribution-widening precondition.
|
|
593
|
+
# Reused by `arm_ar_schema_setup_warn` (registered from engine.setup
|
|
594
|
+
# under the eager path) and `arm_rails_late_bind_install` (fired
|
|
595
|
+
# from inside its own before(:suite) hook). The shared helper
|
|
596
|
+
# avoids registering a nested before(:suite) under the late-bind
|
|
597
|
+
# path, which is unsafe across RSpec versions.
|
|
598
|
+
def emit_ar_schema_setup_warn_if_needed
|
|
599
|
+
return unless ::RSpec.configuration.respond_to?(:use_transactional_fixtures)
|
|
600
|
+
return if ::RSpec.configuration.use_transactional_fixtures == false
|
|
601
|
+
|
|
602
|
+
@configuration.logger.warn(
|
|
603
|
+
'rspec-tracer: track_ar_schema_notifications is enabled but ' \
|
|
604
|
+
'use_transactional_fixtures defaults to true; per-example ' \
|
|
605
|
+
'BEGIN/COMMIT fires sql.active_record so db/schema.rb gets ' \
|
|
606
|
+
'attributed to every AR-touching example (safe, but widens ' \
|
|
607
|
+
'invalidation). For narrow attribution, set ' \
|
|
608
|
+
'use_transactional_fixtures = false and use sequence-based ' \
|
|
609
|
+
'factories (or another non-AR cleanup mechanism). See ' \
|
|
610
|
+
'README section "Narrow AR-schema attribution".'
|
|
611
|
+
)
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Internal method on the tracer pipeline.
|
|
615
|
+
# @api private
|
|
616
|
+
def uninstall_rails_observers
|
|
617
|
+
return unless rails_observers_installed?
|
|
618
|
+
|
|
619
|
+
RSpecTracer::Rails::Notifications.uninstall
|
|
620
|
+
RSpecTracer::Rails::I18nTracking.uninstall
|
|
621
|
+
@rails_observers_installed = false
|
|
622
|
+
rescue StandardError
|
|
623
|
+
@rails_observers_installed = false
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Internal method on the tracer pipeline.
|
|
627
|
+
# @api private
|
|
628
|
+
def rails_observers_installed?
|
|
629
|
+
defined?(@rails_observers_installed) && @rails_observers_installed == true
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# rubocop:disable Naming/AccessorMethodName
|
|
633
|
+
def set_rails_bucket(bucket)
|
|
634
|
+
return unless rails_observers_installed?
|
|
635
|
+
|
|
636
|
+
RSpecTracer::Rails::Notifications.set_bucket(bucket)
|
|
637
|
+
end
|
|
638
|
+
# rubocop:enable Naming/AccessorMethodName
|
|
639
|
+
|
|
640
|
+
def clear_rails_bucket
|
|
641
|
+
return unless rails_observers_installed?
|
|
642
|
+
|
|
643
|
+
RSpecTracer::Rails::Notifications.clear_bucket
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# Internal method on the tracer pipeline.
|
|
647
|
+
# @api private
|
|
648
|
+
def ar_schema_notifications_enabled?
|
|
649
|
+
return false unless @configuration.respond_to?(:track_ar_schema_notifications?)
|
|
650
|
+
|
|
651
|
+
@configuration.track_ar_schema_notifications?
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# Internal method on the tracer pipeline.
|
|
655
|
+
# @api private
|
|
656
|
+
def ar_schema_path_probes
|
|
657
|
+
%w[db/schema.rb db/structure.sql].each_with_object([]) do |rel, acc|
|
|
658
|
+
abs = File.expand_path(rel, @configuration.root)
|
|
659
|
+
acc << abs if File.file?(abs)
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Delegate to the legacy ::Coverage bootstrap if SimpleCov isn't
|
|
664
|
+
# already running. Matches RSpecTracer.setup_coverage behavior so
|
|
665
|
+
# the two entry points agree on when to call ::Coverage.start
|
|
666
|
+
# AND on which Ruby Coverage modes are enabled.
|
|
667
|
+
def ensure_coverage_started
|
|
668
|
+
return if defined?(SimpleCov) && SimpleCov.running
|
|
669
|
+
return if ::Coverage.respond_to?(:running?) && ::Coverage.running?
|
|
670
|
+
|
|
671
|
+
::Coverage.start(**configuration_coverage_modes)
|
|
672
|
+
rescue RuntimeError
|
|
673
|
+
# ::Coverage.start raises if already started on some Rubies
|
|
674
|
+
# without a running? predicate; safe to ignore.
|
|
675
|
+
nil
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Reads `coverage_modes` off the configuration when available
|
|
679
|
+
# (extended with `RSpecTracer::Configuration`). Defaults to an
|
|
680
|
+
# empty Hash for stubbed-configuration unit tests so the splat
|
|
681
|
+
# `Coverage.start(**{})` keeps lines-only legacy behavior.
|
|
682
|
+
def configuration_coverage_modes
|
|
683
|
+
return {} unless @configuration.respond_to?(:coverage_modes_for_start)
|
|
684
|
+
|
|
685
|
+
@configuration.coverage_modes_for_start
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# Under parallel_tests, each worker writes to its own per-worker
|
|
689
|
+
# cache dir (`rspec_tracer_cache/parallel_tests_N/`) but warm-run
|
|
690
|
+
# reads come from the MERGED top-level cache (`rspec_tracer_cache/`,
|
|
691
|
+
# one level up). Last-process finalize at exit purges the per-worker
|
|
692
|
+
# dirs, so only the merged snapshot survives between runs. Match
|
|
693
|
+
# 1.x Cache#load_cache_for_run's `File.dirname(cache_path) if
|
|
694
|
+
# parallel_tests?` behavior so warm runs under parallel_tests
|
|
695
|
+
# actually skip examples.
|
|
696
|
+
def load_previous_snapshot
|
|
697
|
+
read_backend = build_read_backend
|
|
698
|
+
read_backend.load_graph(schema_version: RSpecTracer::Storage::Schema::CURRENT)
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
# Internal method on the tracer pipeline.
|
|
702
|
+
# @api private
|
|
703
|
+
def build_read_backend
|
|
704
|
+
return @storage_backend unless RSpecTracer.parallel_tests?
|
|
705
|
+
|
|
706
|
+
parent_cache_path = File.dirname(@configuration.cache_path)
|
|
707
|
+
build_storage_backend(parent_cache_path)
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# On warm runs, skipped examples don't re-populate @all_examples,
|
|
711
|
+
# @all_files, or @graph - only newly-run examples do. Without
|
|
712
|
+
# seeding, the next save would drop the skipped examples'
|
|
713
|
+
# metadata + deps, and the following warm run would see them as
|
|
714
|
+
# "not previously seen" and force a cold re-run of the entire
|
|
715
|
+
# suite. Seed the three state buckets from the previous snapshot
|
|
716
|
+
# so newly-run examples overwrite and skipped examples carry
|
|
717
|
+
# forward.
|
|
718
|
+
#
|
|
719
|
+
# examples_coverage is NOT seeded here. A populated cache can
|
|
720
|
+
# carry a large examples_coverage map; eagerly materializing it
|
|
721
|
+
# at setup defeats LazySnapshot's whole point for the SQLite
|
|
722
|
+
# backend. The merge happens at build_snapshot time via
|
|
723
|
+
# merge_examples_coverage_with_previous instead.
|
|
724
|
+
def seed_state_from_previous(prev)
|
|
725
|
+
seed_all_examples_from_previous(prev)
|
|
726
|
+
seed_all_files_from_previous(prev)
|
|
727
|
+
seed_graph_from_previous(prev)
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
# Internal method on the tracer pipeline.
|
|
731
|
+
# @api private
|
|
732
|
+
def seed_all_examples_from_previous(prev)
|
|
733
|
+
return unless prev.all_examples.is_a?(Hash)
|
|
734
|
+
|
|
735
|
+
prev.all_examples.each do |id, meta|
|
|
736
|
+
@all_examples[id] = meta
|
|
737
|
+
end
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# prev.all_files is file-name-keyed; the engine's @all_files is
|
|
741
|
+
# abs-path-keyed (path_to_file_name round-trips via the root prefix).
|
|
742
|
+
#
|
|
743
|
+
# Re-applies the current filter list at seed time so users who
|
|
744
|
+
# add a filter mid-development see the carried-over file entries
|
|
745
|
+
# drop on the next run (instead of waiting for a cold run). The
|
|
746
|
+
# filter contract should hold for both fresh attributions
|
|
747
|
+
# (attribute_to_example) and prior-snapshot carry-forward —
|
|
748
|
+
# otherwise add_filter has split semantics and a freshly-added
|
|
749
|
+
# filter does not exclude already-cached paths.
|
|
750
|
+
def seed_all_files_from_previous(prev)
|
|
751
|
+
return unless prev.all_files.is_a?(Hash)
|
|
752
|
+
|
|
753
|
+
prev.all_files.each_value do |meta|
|
|
754
|
+
next unless meta.is_a?(Hash)
|
|
755
|
+
|
|
756
|
+
file_path = symbol_or_string(meta, :file_path)
|
|
757
|
+
next if file_path.nil?
|
|
758
|
+
|
|
759
|
+
file_name = symbol_or_string(meta, :file_name) || path_to_file_name(file_path)
|
|
760
|
+
next if filtered_by_current_filters?(file_name)
|
|
761
|
+
|
|
762
|
+
@all_files[file_path] = {
|
|
763
|
+
file_name: file_name,
|
|
764
|
+
file_path: file_path,
|
|
765
|
+
digest: symbol_or_string(meta, :digest)
|
|
766
|
+
}
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
# prev.dependency keys on example_id, values are Set<file_name>.
|
|
771
|
+
# Register in @graph via absolute paths so the on-disk shape
|
|
772
|
+
# (file_name) converts at save time via dependency_by_name.
|
|
773
|
+
#
|
|
774
|
+
# Drops paths matching the current filter list so previously-
|
|
775
|
+
# cached deps that the new filter excludes do not leak through
|
|
776
|
+
# the carry-forward seed path. Same rationale as
|
|
777
|
+
# seed_all_files_from_previous: the filter contract is one
|
|
778
|
+
# contract, applied uniformly across fresh + carry-forward
|
|
779
|
+
# attributions.
|
|
780
|
+
def seed_graph_from_previous(prev)
|
|
781
|
+
return unless prev.dependency.is_a?(Hash)
|
|
782
|
+
|
|
783
|
+
prev.dependency.each do |example_id, file_names|
|
|
784
|
+
paths = Set.new
|
|
785
|
+
file_names.each do |name|
|
|
786
|
+
next if filtered_by_current_filters?(name)
|
|
787
|
+
|
|
788
|
+
paths << absolute_path(name)
|
|
789
|
+
end
|
|
790
|
+
@graph.register_example(example_id, paths)
|
|
791
|
+
end
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Helper: returns true if the file name matches any currently-
|
|
795
|
+
# configured filter. Mirrors the check site at
|
|
796
|
+
# `attribute_to_example` (engine.rb:770) so both fresh + carry-
|
|
797
|
+
# forward attributions converge on the same filter behavior.
|
|
798
|
+
def filtered_by_current_filters?(file_name)
|
|
799
|
+
@configuration.filters.any? { |f| f.match?(file_name: file_name) }
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# Internal method on the tracer pipeline.
|
|
803
|
+
# @api private
|
|
804
|
+
def compute_filter_decisions
|
|
805
|
+
prev = @previous_snapshot
|
|
806
|
+
return if prev.nil?
|
|
807
|
+
|
|
808
|
+
seed_registry_from_previous(prev)
|
|
809
|
+
change_set = compute_change_set(prev)
|
|
810
|
+
whole_suite = whole_suite_changed?(prev)
|
|
811
|
+
|
|
812
|
+
result = RSpecTracer::Tracker::Filter.select(
|
|
813
|
+
graph: graph_from_previous(prev),
|
|
814
|
+
change_set: change_set,
|
|
815
|
+
registry: @registry,
|
|
816
|
+
whole_suite_invalidated: whole_suite,
|
|
817
|
+
all_example_ids: prev.all_examples.keys.to_set
|
|
818
|
+
)
|
|
819
|
+
@filtered_examples = result.transform_values { |reason| FILTER_REASON_STRINGS.fetch(reason) }
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
# Internal constant.
|
|
823
|
+
# @api private
|
|
824
|
+
SEED_STATUS_ORDER = %i[interrupted flaky failed pending].freeze
|
|
825
|
+
private_constant :SEED_STATUS_ORDER
|
|
826
|
+
|
|
827
|
+
# Internal method on the tracer pipeline.
|
|
828
|
+
# @api private
|
|
829
|
+
def seed_registry_from_previous(prev)
|
|
830
|
+
SEED_STATUS_ORDER.each do |status|
|
|
831
|
+
ids = prev.send(:"#{status}_examples")
|
|
832
|
+
ids.each { |id| seed_registry_entry(id, status, prev.all_examples[id] || {}) }
|
|
833
|
+
end
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
# Internal method on the tracer pipeline.
|
|
837
|
+
# @api private
|
|
838
|
+
def seed_registry_entry(id, status, metadata)
|
|
839
|
+
return if @registry.registered?(id)
|
|
840
|
+
|
|
841
|
+
@registry.register(id, metadata: metadata)
|
|
842
|
+
@registry.update_status(id, status)
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
# Rebuild a DependencyGraph from the previous Snapshot so Filter
|
|
846
|
+
# can intersect its cached dependency sets with the change_set.
|
|
847
|
+
def graph_from_previous(prev)
|
|
848
|
+
graph = RSpecTracer::Tracker::DependencyGraph.new
|
|
849
|
+
prev.dependency.each { |id, paths| graph.register_example(id, paths) }
|
|
850
|
+
graph
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
# Internal method on the tracer pipeline.
|
|
854
|
+
# @api private
|
|
855
|
+
def compute_change_set(prev)
|
|
856
|
+
changed = Set.new
|
|
857
|
+
prev.all_files.each_value do |file_meta|
|
|
858
|
+
file_name = symbol_or_string(file_meta, :file_name)
|
|
859
|
+
cached_digest = symbol_or_string(file_meta, :digest)
|
|
860
|
+
next if file_name.nil? || cached_digest.nil?
|
|
861
|
+
|
|
862
|
+
current_digest = current_file_digest(file_name)
|
|
863
|
+
changed << file_name if current_digest.nil? || current_digest != cached_digest
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
new_files = @new_file_detector.new_files(
|
|
867
|
+
known_paths: prev.all_files.keys.to_set.to_set { |k| absolute_path(k) }
|
|
868
|
+
)
|
|
869
|
+
new_files.each { |input| changed << path_to_file_name(input.path) }
|
|
870
|
+
changed
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
# Internal method on the tracer pipeline.
|
|
874
|
+
# @api private
|
|
875
|
+
def whole_suite_changed?(prev)
|
|
876
|
+
wsi_prev = prev.wsi_snapshot if prev.respond_to?(:wsi_snapshot)
|
|
877
|
+
@whole_suite_invalidators.invalidated?(wsi_prev) ||
|
|
878
|
+
@loaded_files_tracker.boot_set_invalidated?(prev.boot_set)
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
# Internal method on the tracer pipeline.
|
|
882
|
+
# @api private
|
|
883
|
+
def current_file_digest(file_name)
|
|
884
|
+
RSpecTracer::Tracker::FileDigest.compute(absolute_path(file_name))
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
# Internal method on the tracer pipeline.
|
|
888
|
+
# @api private
|
|
889
|
+
def record_coverage_delta(example_id, before, after)
|
|
890
|
+
entry = @examples_coverage[example_id] ||= {}
|
|
891
|
+
|
|
892
|
+
(before.keys | after.keys).each do |path|
|
|
893
|
+
b_lines = before[path]
|
|
894
|
+
a_lines = after[path]
|
|
895
|
+
next if b_lines == a_lines
|
|
896
|
+
|
|
897
|
+
file_entry = entry[path] ||= {}
|
|
898
|
+
accumulate_delta(file_entry, b_lines, a_lines)
|
|
899
|
+
end
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
# Internal method on the tracer pipeline.
|
|
903
|
+
# @api private
|
|
904
|
+
def accumulate_delta(file_entry, before_lines, after_lines)
|
|
905
|
+
length = (after_lines || before_lines || []).length
|
|
906
|
+
length.times do |i|
|
|
907
|
+
delta = line_delta(before_lines && before_lines[i], after_lines && after_lines[i])
|
|
908
|
+
file_entry[i] = delta if delta
|
|
909
|
+
end
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
# Returns the positive coverage delta for one line, or nil if the
|
|
913
|
+
# line isn't a delta worth recording (non-executable, identical,
|
|
914
|
+
# or a stale-going-backward entry).
|
|
915
|
+
def line_delta(before, after)
|
|
916
|
+
return nil if after.nil? || before == after
|
|
917
|
+
|
|
918
|
+
delta = after - (before || 0)
|
|
919
|
+
delta.positive? ? delta : nil
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
# Internal method on the tracer pipeline.
|
|
923
|
+
# @api private
|
|
924
|
+
def accumulate_line_coverage(accumulator, line_coverage)
|
|
925
|
+
line_coverage.each do |line_key, strength|
|
|
926
|
+
index = line_key.to_i
|
|
927
|
+
accumulator[index] += strength || 0
|
|
928
|
+
end
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
# Internal method on the tracer pipeline.
|
|
932
|
+
# @api private
|
|
933
|
+
def attribute_to_example(example_id, inputs)
|
|
934
|
+
paths = Set.new
|
|
935
|
+
inputs.each do |input|
|
|
936
|
+
next if @configuration.filters.any? { |f| f.match?(file_name: path_to_file_name(input.path)) }
|
|
937
|
+
|
|
938
|
+
paths << input.path
|
|
939
|
+
@all_files[input.path] = {
|
|
940
|
+
file_name: path_to_file_name(input.path),
|
|
941
|
+
file_path: input.path,
|
|
942
|
+
digest: input.digest
|
|
943
|
+
}
|
|
944
|
+
end
|
|
945
|
+
@graph.register_example(example_id, paths)
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
# Internal method on the tracer pipeline.
|
|
949
|
+
# @api private
|
|
950
|
+
def record_execution_result(example_id, result)
|
|
951
|
+
return unless @all_examples.key?(example_id)
|
|
952
|
+
|
|
953
|
+
@all_examples[example_id][:execution_result] = formatted_execution_result(result)
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
# Ports the 1.x flaky-detection contract: an example transitions
|
|
957
|
+
# into `:flaky` when it (a) failed on the previous run and passes
|
|
958
|
+
# on this run (the canonical "intermittent" signal) OR (b) was
|
|
959
|
+
# already flaky on the previous run, regardless of this run's
|
|
960
|
+
# outcome (sticky once detected). Mirrors 1.x's
|
|
961
|
+
# `ReportGenerator#generate_flaky_examples_report` which iterated
|
|
962
|
+
# `prev_failed | prev_flaky` and registered the example as flaky
|
|
963
|
+
# unless the prev_failed branch failed again this run.
|
|
964
|
+
def flaky_history?(id)
|
|
965
|
+
previously_failed?(id) || previously_flaky?(id)
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
# Internal method on the tracer pipeline.
|
|
969
|
+
# @api private
|
|
970
|
+
def previously_failed?(id)
|
|
971
|
+
@previous_snapshot&.failed_examples&.include?(id)
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
# Internal method on the tracer pipeline.
|
|
975
|
+
# @api private
|
|
976
|
+
def previously_flaky?(id)
|
|
977
|
+
@previous_snapshot&.flaky_examples&.include?(id)
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
# Internal method on the tracer pipeline.
|
|
981
|
+
# @api private
|
|
982
|
+
def formatted_execution_result(result)
|
|
983
|
+
{
|
|
984
|
+
started_at: result.started_at.utc,
|
|
985
|
+
finished_at: result.finished_at.utc,
|
|
986
|
+
run_time: result.run_time,
|
|
987
|
+
status: result.status.to_s
|
|
988
|
+
}
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
# Internal method on the tracer pipeline.
|
|
992
|
+
# @api private
|
|
993
|
+
def build_snapshot
|
|
994
|
+
run_id = Digest::MD5.hexdigest(@all_examples.keys.sort.to_json)
|
|
995
|
+
@run_id = run_id
|
|
996
|
+
|
|
997
|
+
RSpecTracer::Storage::Snapshot.new(
|
|
998
|
+
schema_version: RSpecTracer::Storage::Schema::CURRENT,
|
|
999
|
+
run_id: run_id,
|
|
1000
|
+
all_examples: @all_examples,
|
|
1001
|
+
duplicate_examples: duplicates_for_snapshot,
|
|
1002
|
+
interrupted_examples: @registry.ids_with_status(:interrupted),
|
|
1003
|
+
flaky_examples: @registry.ids_with_status(:flaky),
|
|
1004
|
+
failed_examples: @registry.ids_with_status(:failed),
|
|
1005
|
+
pending_examples: @registry.ids_with_status(:pending),
|
|
1006
|
+
skipped_examples: @registry.ids_with_status(:skipped),
|
|
1007
|
+
all_files: all_files_by_name,
|
|
1008
|
+
dependency: dependency_by_name,
|
|
1009
|
+
reverse_dependency: reverse_dependency_by_name,
|
|
1010
|
+
examples_coverage: merge_examples_coverage_with_previous,
|
|
1011
|
+
boot_set: @loaded_files_tracker.boot_set_digest_snapshot,
|
|
1012
|
+
wsi_snapshot: @whole_suite_invalidators.digest_snapshot,
|
|
1013
|
+
env_snapshot: env_snapshot_for_persistence,
|
|
1014
|
+
env_dependency: env_dependency_for_persistence,
|
|
1015
|
+
cache_hit_reason: @filtered_examples.values.tally,
|
|
1016
|
+
filtered_examples: @filtered_examples
|
|
1017
|
+
)
|
|
1018
|
+
end
|
|
1019
|
+
|
|
1020
|
+
# Merge the current run's per-example coverage with the previous
|
|
1021
|
+
# run's. Re-run examples contribute their freshly-computed map
|
|
1022
|
+
# (record_coverage_delta overwrote per-line strengths, so
|
|
1023
|
+
# @examples_coverage[id] is the authoritative new coverage).
|
|
1024
|
+
# Skipped examples don't appear in @examples_coverage, so the
|
|
1025
|
+
# prev map carries them forward unchanged. Previously in
|
|
1026
|
+
# seed_state_from_previous; moved here so large caches don't
|
|
1027
|
+
# pay the full examples_coverage materialization cost at setup
|
|
1028
|
+
# time. Preserves 1.x semantics: the saved map is the union of
|
|
1029
|
+
# (prev minus this-run's keys) + this-run's entries.
|
|
1030
|
+
def merge_examples_coverage_with_previous
|
|
1031
|
+
merged = {}
|
|
1032
|
+
if @previous_snapshot
|
|
1033
|
+
prev = @previous_snapshot.examples_coverage
|
|
1034
|
+
if prev.is_a?(Hash)
|
|
1035
|
+
prev.each { |id, cov| merged[id] = cov unless @examples_coverage.key?(id) }
|
|
1036
|
+
end
|
|
1037
|
+
end
|
|
1038
|
+
@examples_coverage.each { |id, cov| merged[id] = cov }
|
|
1039
|
+
merged
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
# Snapshot only the env keys THIS run tracked - persisting keys
|
|
1043
|
+
# that stopped being tracked (user removed `tracks: env: ...`
|
|
1044
|
+
# between runs) would pin stale digests in the cache. Missing
|
|
1045
|
+
# keys on the next load just trigger a one-time re-run for any
|
|
1046
|
+
# example that reintroduces them, which is the correct behavior.
|
|
1047
|
+
def env_snapshot_for_persistence
|
|
1048
|
+
return {} if @env_snapshot.nil? || @tracked_env_names.empty?
|
|
1049
|
+
|
|
1050
|
+
@env_snapshot.digest_snapshot(@tracked_env_names)
|
|
1051
|
+
end
|
|
1052
|
+
|
|
1053
|
+
# Project the per-example `@tracks_env` map (Set<env_name> per
|
|
1054
|
+
# example_id) into a JSON-friendly Hash[id => sorted Array] for
|
|
1055
|
+
# persistence. Reporters consume this to render the env-
|
|
1056
|
+
# dependency view on the Examples Dependency report. Empty sets
|
|
1057
|
+
# drop out so the on-disk map stays narrow. Sort keeps the output
|
|
1058
|
+
# deterministic for downstream diffs and golden tests.
|
|
1059
|
+
def env_dependency_for_persistence
|
|
1060
|
+
result = {}
|
|
1061
|
+
@tracks_env.each do |example_id, names|
|
|
1062
|
+
next if names.nil? || names.empty?
|
|
1063
|
+
|
|
1064
|
+
result[example_id] = names.to_a.sort
|
|
1065
|
+
end
|
|
1066
|
+
result
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
# Resolve one glob against the project root into a Set of
|
|
1070
|
+
# `:declared`-kind Inputs. Walks via Dir.glob
|
|
1071
|
+
# (FNM_PATHNAME+FNM_EXTGLOB like DeclaredGlobs so user muscle
|
|
1072
|
+
# memory works identically) and digests each hit with SHA256.
|
|
1073
|
+
# Unreadable files are skipped silently - graceful degradation.
|
|
1074
|
+
# Memoized per distinct glob string so N examples declaring the
|
|
1075
|
+
# same glob pay the filesystem walk cost exactly once.
|
|
1076
|
+
def resolved_glob_inputs(glob)
|
|
1077
|
+
@resolved_glob_cache ||= {}
|
|
1078
|
+
@resolved_glob_cache[glob] ||= walk_one_glob(glob)
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
# Internal method on the tracer pipeline.
|
|
1082
|
+
# @api private
|
|
1083
|
+
def walk_one_glob(glob)
|
|
1084
|
+
inputs = Set.new
|
|
1085
|
+
root = @configuration.root
|
|
1086
|
+
Dir.glob(glob, File::FNM_PATHNAME | File::FNM_EXTGLOB, base: root).each do |rel|
|
|
1087
|
+
abs = File.expand_path(rel, root)
|
|
1088
|
+
next unless abs.start_with?("#{root}/") && File.file?(abs)
|
|
1089
|
+
|
|
1090
|
+
digest = tracks_file_digest(abs)
|
|
1091
|
+
next if digest.nil?
|
|
1092
|
+
|
|
1093
|
+
inputs << RSpecTracer::Tracker::Input.for_file(
|
|
1094
|
+
path: abs, kind: :declared, digest: digest, root: root
|
|
1095
|
+
)
|
|
1096
|
+
end
|
|
1097
|
+
inputs
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
# Internal method on the tracer pipeline.
|
|
1101
|
+
# @api private
|
|
1102
|
+
def tracks_file_digest(path)
|
|
1103
|
+
RSpecTracer::Tracker::FileDigest.compute(path)
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
# Materialize the per-example tracks-file Input set. Returns an
|
|
1107
|
+
# empty Set when the example has no `tracks: files:` metadata,
|
|
1108
|
+
# keeping the downstream union cheap for the 99% case.
|
|
1109
|
+
def per_example_tracks_inputs(example_id)
|
|
1110
|
+
set = @tracks_files[example_id]
|
|
1111
|
+
set.nil? || set.empty? ? Set.new : set.dup
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
# Internal method on the tracer pipeline.
|
|
1115
|
+
# @api private
|
|
1116
|
+
def duplicates_for_snapshot
|
|
1117
|
+
@duplicate_examples.select { |_, entries| entries.count > 1 }
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
# Internal method on the tracer pipeline.
|
|
1121
|
+
# @api private
|
|
1122
|
+
def all_files_by_name
|
|
1123
|
+
@all_files.each_with_object({}) do |(_, meta), acc|
|
|
1124
|
+
acc[meta[:file_name]] = meta
|
|
1125
|
+
end
|
|
1126
|
+
end
|
|
1127
|
+
|
|
1128
|
+
# Internal method on the tracer pipeline.
|
|
1129
|
+
# @api private
|
|
1130
|
+
def dependency_by_name
|
|
1131
|
+
@graph.dependency_hash.transform_values do |paths|
|
|
1132
|
+
paths.to_set { |p| path_to_file_name(p) }
|
|
1133
|
+
end
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
# Internal method on the tracer pipeline.
|
|
1137
|
+
# @api private
|
|
1138
|
+
def reverse_dependency_by_name
|
|
1139
|
+
@graph.reverse_dependency_hash.each_with_object({}) do |(path, ids), acc|
|
|
1140
|
+
acc[path_to_file_name(path)] = ids.to_set
|
|
1141
|
+
end
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1144
|
+
# Internal method on the tracer pipeline.
|
|
1145
|
+
# @api private
|
|
1146
|
+
def absolute_path(file_name)
|
|
1147
|
+
File.expand_path(file_name.to_s.sub(%r{^/}, ''), @configuration.root)
|
|
1148
|
+
end
|
|
1149
|
+
|
|
1150
|
+
# Internal method on the tracer pipeline.
|
|
1151
|
+
# @api private
|
|
1152
|
+
def path_to_file_name(abs_path)
|
|
1153
|
+
root_prefix = "#{@configuration.root}/"
|
|
1154
|
+
return abs_path unless abs_path.start_with?(root_prefix)
|
|
1155
|
+
|
|
1156
|
+
"/#{abs_path[root_prefix.length..]}"
|
|
1157
|
+
end
|
|
1158
|
+
|
|
1159
|
+
# Internal method on the tracer pipeline.
|
|
1160
|
+
# @api private
|
|
1161
|
+
def symbol_or_string(hash, key)
|
|
1162
|
+
return hash[key] if hash.key?(key)
|
|
1163
|
+
|
|
1164
|
+
hash[key.to_s]
|
|
1165
|
+
end
|
|
1166
|
+
end
|
|
1167
|
+
# rubocop:enable Metrics/ClassLength
|
|
1168
|
+
end
|