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,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'metadata'
|
|
4
|
+
|
|
5
|
+
module RSpecTracer
|
|
6
|
+
# Internal RSpec — see {RSpecTracer} for the user-facing surface.
|
|
7
|
+
# @api private
|
|
8
|
+
module RSpec
|
|
9
|
+
# Prepended onto `RSpec::Core::Runner` by
|
|
10
|
+
# `RSpecTracer::RSpec::Installation.install!`. Replaces the 1.x
|
|
11
|
+
# `RSpecTracer::RSpecRunner` singleton-class prepend that the
|
|
12
|
+
# ObjectSpace loop in `setup_rspec?` used to install.
|
|
13
|
+
#
|
|
14
|
+
# Responsibilities, in `run_specs` order:
|
|
15
|
+
# 1. Early-return to `super` if the engine hasn't been set up
|
|
16
|
+
# (defensive; keeps the suite green under `RSPEC_TRACER_DISABLE=1`
|
|
17
|
+
# or when the user forgot to call `RSpecTracer.start`).
|
|
18
|
+
# 2. Partition `RSpec.world.filtered_examples` via the engine:
|
|
19
|
+
# tracked examples go through identity-hashing + filter decision;
|
|
20
|
+
# ignored examples (matched by `Configuration#ignore_spec_files`)
|
|
21
|
+
# pass through untouched - RSpec still runs them, but the tracer
|
|
22
|
+
# never sees them. Closes #41.
|
|
23
|
+
# 3. Detect duplicate example identities. Colliding examples are
|
|
24
|
+
# dropped from the run (per-example tracking can't attribute
|
|
25
|
+
# coverage to two examples sharing one identity hash); the
|
|
26
|
+
# rest of the suite still runs. `fail_on_duplicates=true` then
|
|
27
|
+
# surfaces a `::Kernel.exit(1)` in `at_exit_behavior`.
|
|
28
|
+
# 4. Overwrite `RSpec.world.@filtered_examples` +
|
|
29
|
+
# `@example_groups` with the filtered set, then log the run
|
|
30
|
+
# banner and delegate to `super`.
|
|
31
|
+
#
|
|
32
|
+
# The user-visible log line - `RSpec tracer is running N examples
|
|
33
|
+
# (actual: N, skipped: N)` - is preserved byte-for-byte from 1.x so
|
|
34
|
+
# cucumber scenarios and CI log parsers keep working.
|
|
35
|
+
module RunnerHook
|
|
36
|
+
# Internal method on the tracer pipeline.
|
|
37
|
+
# @api private
|
|
38
|
+
def run_specs(example_groups)
|
|
39
|
+
return super unless RSpecTracer.engine
|
|
40
|
+
|
|
41
|
+
actual_count = ::RSpec.world.example_count
|
|
42
|
+
if _rspec_tracer_no_examples?(actual_count)
|
|
43
|
+
super
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
48
|
+
filtered_examples_map, filtered_example_groups = _rspec_tracer_build_filter_decision
|
|
49
|
+
|
|
50
|
+
if _rspec_tracer_duplicates_detected?
|
|
51
|
+
RSpecTracer.duplicate_examples = RSpecTracer.fail_on_duplicates
|
|
52
|
+
filtered_examples_map, filtered_example_groups =
|
|
53
|
+
_rspec_tracer_drop_duplicate_examples(filtered_examples_map, filtered_example_groups)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
::RSpec.world.instance_variable_set(:@filtered_examples, filtered_examples_map)
|
|
57
|
+
::RSpec.world.instance_variable_set(:@example_groups, filtered_example_groups)
|
|
58
|
+
|
|
59
|
+
current_count = ::RSpec.world.example_count
|
|
60
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
61
|
+
elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
|
62
|
+
|
|
63
|
+
RSpecTracer.logger.info <<-EXAMPLES.strip.gsub(/\s+/, ' ')
|
|
64
|
+
RSpec tracer is running #{current_count} examples (actual: #{actual_count},
|
|
65
|
+
skipped: #{actual_count - current_count}) (took #{elapsed})
|
|
66
|
+
EXAMPLES
|
|
67
|
+
|
|
68
|
+
RSpecTracer.running = true
|
|
69
|
+
|
|
70
|
+
super(filtered_example_groups)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Internal method on the tracer pipeline.
|
|
76
|
+
# @api private
|
|
77
|
+
def _rspec_tracer_no_examples?(actual_count)
|
|
78
|
+
return false unless actual_count.zero?
|
|
79
|
+
|
|
80
|
+
RSpecTracer.running = true
|
|
81
|
+
RSpecTracer.no_examples = true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Two-pass filter-decision walk over RSpec.world.filtered_examples.
|
|
85
|
+
#
|
|
86
|
+
# Pass 1 pre-walks every tracked example to compute its identity
|
|
87
|
+
# hash (Example.from) and register the `tracks:` metadata via
|
|
88
|
+
# `Engine#register_tracks`. This must complete for every
|
|
89
|
+
# example before ANY `run_example?` call because the env-
|
|
90
|
+
# invalidation pass (`Engine#apply_env_filter_decisions`) needs
|
|
91
|
+
# the full set of tracked env names to classify which examples
|
|
92
|
+
# re-run. Caching the tracer-example per example.object_id
|
|
93
|
+
# avoids a second MD5 in Pass 2.
|
|
94
|
+
#
|
|
95
|
+
# Between passes, `apply_env_filter_decisions` unions the
|
|
96
|
+
# env-changed decisions into @filtered_examples so Pass 2's
|
|
97
|
+
# `run_example?` sees them alongside the file-change decisions
|
|
98
|
+
# computed at Engine.setup time.
|
|
99
|
+
#
|
|
100
|
+
# Pass 2 makes the actual run/skip decisions and tags metadata.
|
|
101
|
+
# Ignored spec files (Configuration#ignore_spec_files) are
|
|
102
|
+
# handled in Pass 2 and skip both passes' engine surface -
|
|
103
|
+
# RSpec still runs them, the tracer never sees them.
|
|
104
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
105
|
+
def _rspec_tracer_build_filter_decision
|
|
106
|
+
to_run = Hash.new { |hash, group| hash[group] = [] }
|
|
107
|
+
groups = Set.new
|
|
108
|
+
engine = RSpecTracer.engine
|
|
109
|
+
tracer_cache = {}.compare_by_identity
|
|
110
|
+
|
|
111
|
+
_rspec_tracer_collect_tracks(engine, tracer_cache)
|
|
112
|
+
engine.apply_env_filter_decisions
|
|
113
|
+
|
|
114
|
+
::RSpec.world.filtered_examples.each_pair do |example_group, examples|
|
|
115
|
+
examples.each do |example|
|
|
116
|
+
if RSpecTracer.ignore_spec_file?(example.metadata[:file_path])
|
|
117
|
+
to_run[example_group] << example
|
|
118
|
+
groups << example.example_group.parent_groups.last
|
|
119
|
+
next
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
tracer_example = tracer_cache.fetch(example)
|
|
123
|
+
example_id = tracer_example[:example_id]
|
|
124
|
+
|
|
125
|
+
if engine.run_example?(example_id)
|
|
126
|
+
run_reason = engine.run_example_reason(example_id)
|
|
127
|
+
tracer_example[:run_reason] = run_reason
|
|
128
|
+
example.metadata[:description] = "#{example.description} (#{run_reason})"
|
|
129
|
+
|
|
130
|
+
to_run[example_group] << example
|
|
131
|
+
groups << example.example_group.parent_groups.last
|
|
132
|
+
|
|
133
|
+
engine.register_example(tracer_example)
|
|
134
|
+
else
|
|
135
|
+
engine.on_example_skipped(example_id)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
engine.deregister_duplicate_examples
|
|
141
|
+
|
|
142
|
+
[to_run, groups.to_a]
|
|
143
|
+
end
|
|
144
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
145
|
+
|
|
146
|
+
def _rspec_tracer_collect_tracks(engine, tracer_cache)
|
|
147
|
+
::RSpec.world.filtered_examples.each_pair do |_example_group, examples|
|
|
148
|
+
examples.each do |example|
|
|
149
|
+
next if RSpecTracer.ignore_spec_file?(example.metadata[:file_path])
|
|
150
|
+
|
|
151
|
+
tracer_example = RSpecTracer::Example.from(example)
|
|
152
|
+
example_id = tracer_example[:example_id]
|
|
153
|
+
example.metadata[:rspec_tracer_example_id] = example_id
|
|
154
|
+
tracer_cache[example] = tracer_example
|
|
155
|
+
|
|
156
|
+
tracks = RSpecTracer::RSpec::Metadata.tracks_for(example)
|
|
157
|
+
next if tracks[:files].empty? && tracks[:env].empty?
|
|
158
|
+
|
|
159
|
+
engine.register_tracks(example_id, tracks)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Logs the duplicate-identity diagnostic and returns whether any
|
|
165
|
+
# were found. The summary line keeps its 1.x wording (CI log
|
|
166
|
+
# parsers + specs match on it); the indented detail names each
|
|
167
|
+
# colliding example so the user can find and rename them.
|
|
168
|
+
# @api private
|
|
169
|
+
def _rspec_tracer_duplicates_detected?
|
|
170
|
+
duplicates = RSpecTracer.engine.duplicate_examples
|
|
171
|
+
return false if duplicates.empty?
|
|
172
|
+
|
|
173
|
+
total = duplicates.sum { |_, entries| entries.count }
|
|
174
|
+
RSpecTracer.logger.error(
|
|
175
|
+
"RSpec tracer detected #{total} duplicate example(s) across " \
|
|
176
|
+
"#{duplicates.size} identity hash(es). Examples that share one rspec-tracer " \
|
|
177
|
+
'identity cannot be tracked separately and are dropped from this run - give ' \
|
|
178
|
+
"them distinct descriptions to fix:\n#{_rspec_tracer_duplicate_report(duplicates)}"
|
|
179
|
+
)
|
|
180
|
+
true
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# The indented per-hash detail block for the duplicate
|
|
184
|
+
# diagnostic: the identity hash, then one labelled line per
|
|
185
|
+
# colliding example under it.
|
|
186
|
+
# @api private
|
|
187
|
+
def _rspec_tracer_duplicate_report(duplicates)
|
|
188
|
+
duplicates.map do |example_id, entries|
|
|
189
|
+
labelled = entries.map { |entry| " - #{_rspec_tracer_example_label(entry)}" }
|
|
190
|
+
" #{example_id}\n#{labelled.join("\n")}"
|
|
191
|
+
end.join("\n")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# `file:line description` for one colliding example, read off the
|
|
195
|
+
# `Example.from` payload (rerun location preferred, mirroring the
|
|
196
|
+
# reporter + `explain` columns).
|
|
197
|
+
# @api private
|
|
198
|
+
def _rspec_tracer_example_label(entry)
|
|
199
|
+
file = entry[:rerun_file_name] || entry[:file_name]
|
|
200
|
+
line = entry[:rerun_line_number] || entry[:line_number]
|
|
201
|
+
|
|
202
|
+
"#{file}:#{line} #{entry[:full_description] || entry[:description]}".rstrip
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Drops the colliding examples from the filtered run set. The
|
|
206
|
+
# rest of the suite still runs; `fail_on_duplicates` governs only
|
|
207
|
+
# the exit code (via `at_exit_behavior`), not whether anything
|
|
208
|
+
# runs. A group is kept only if it still has examples after the
|
|
209
|
+
# colliding ones are removed.
|
|
210
|
+
#
|
|
211
|
+
# The returned Hash uses the same `Hash.new { |h, k| h[k] = [] }`
|
|
212
|
+
# default-block shape as `_rspec_tracer_build_filter_decision`'s
|
|
213
|
+
# `to_run`. That contract matters because rspec-core's
|
|
214
|
+
# `RSpec::Core::World#example_count` walks `g.descendants` for
|
|
215
|
+
# every top-level group and reads `e.filtered_examples`
|
|
216
|
+
# (= `world.filtered_examples[e]`) on each descendant — including
|
|
217
|
+
# INTERMEDIATE groups (a `describe` that contains only nested
|
|
218
|
+
# `describe`s, no direct `it`s). A plain Hash returns `nil` for
|
|
219
|
+
# those keys, which then NPEs in
|
|
220
|
+
# `inject(0) { |a, e| a + e.filtered_examples.size }`. The
|
|
221
|
+
# default block returns `[]` (`.size == 0`), matching the
|
|
222
|
+
# pre-drop path's behavior.
|
|
223
|
+
# @api private
|
|
224
|
+
def _rspec_tracer_drop_duplicate_examples(examples_map, example_groups)
|
|
225
|
+
duplicate_ids = Set.new(RSpecTracer.engine.duplicate_examples.keys)
|
|
226
|
+
|
|
227
|
+
kept_map = Hash.new { |hash, group| hash[group] = [] }
|
|
228
|
+
examples_map.each do |group, examples|
|
|
229
|
+
survivors = examples.reject do |example|
|
|
230
|
+
duplicate_ids.include?(example.metadata[:rspec_tracer_example_id])
|
|
231
|
+
end
|
|
232
|
+
kept_map[group] = survivors unless survivors.empty?
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
[kept_map, example_groups.select { |group| kept_map.key?(group) }]
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RSpecTracer
|
|
4
|
+
# Path manipulation helpers used by Example.location_file_name.
|
|
5
|
+
# Post-coverage-stack retirement, the only caller is example.rb
|
|
6
|
+
# (the legacy CoverageReporter that previously used these is gone).
|
|
7
|
+
#
|
|
8
|
+
# All methods declared `def self.x` per
|
|
9
|
+
# feedback_mutation_friendly_modules so mutant observes mutations
|
|
10
|
+
# through the singleton call path.
|
|
4
11
|
module SourceFile
|
|
12
|
+
# Internal constant.
|
|
13
|
+
# @api private
|
|
5
14
|
PROJECT_ROOT_REGEX = Regexp.new("^#{Regexp.escape(RSpecTracer.root)}").freeze
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def from_path(file_path)
|
|
16
|
+
# Internal helper for the tracer pipeline.
|
|
17
|
+
# @api private
|
|
18
|
+
def self.from_path(file_path)
|
|
10
19
|
return unless File.file?(file_path)
|
|
11
20
|
|
|
12
21
|
{
|
|
@@ -16,21 +25,29 @@ module RSpecTracer
|
|
|
16
25
|
}
|
|
17
26
|
end
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
# Internal helper for the tracer pipeline.
|
|
29
|
+
# @api private
|
|
30
|
+
def self.from_name(file_name)
|
|
20
31
|
from_path(file_path(file_name))
|
|
21
32
|
end
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
# Internal helper for the tracer pipeline.
|
|
35
|
+
# @api private
|
|
36
|
+
def self.file_name(file_path)
|
|
24
37
|
file_path.sub(PROJECT_ROOT_REGEX, '')
|
|
25
38
|
end
|
|
26
39
|
|
|
27
|
-
|
|
40
|
+
# Internal helper for the tracer pipeline.
|
|
41
|
+
# @api private
|
|
42
|
+
def self.file_path(file_name)
|
|
28
43
|
return file_name if absolute_external_file?(file_name)
|
|
29
44
|
|
|
30
45
|
File.expand_path(file_name.sub(%r{^/}, ''), RSpecTracer.root)
|
|
31
46
|
end
|
|
32
47
|
|
|
33
|
-
|
|
48
|
+
# Internal helper for the tracer pipeline.
|
|
49
|
+
# @api private
|
|
50
|
+
def self.absolute_external_file?(file_name)
|
|
34
51
|
file_name.start_with?('/') &&
|
|
35
52
|
!file_name.start_with?(RSpecTracer.root) &&
|
|
36
53
|
File.file?(file_name)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Storage
|
|
2
|
+
|
|
3
|
+
Persistence layer for the dependency graph and per-example metadata.
|
|
4
|
+
Pluggable via a backend protocol.
|
|
5
|
+
|
|
6
|
+
## Responsibilities
|
|
7
|
+
|
|
8
|
+
- Load and save the dependency graph atomically.
|
|
9
|
+
- Enforce `schema_version` — refuse to load a cache outside the supported
|
|
10
|
+
range. On mismatch: log, discard, fall back to cold boot. No
|
|
11
|
+
migrators.
|
|
12
|
+
- Never propagate storage errors to the caller — corrupted or missing
|
|
13
|
+
cache triggers a cold run, not a test-suite failure.
|
|
14
|
+
|
|
15
|
+
## Public protocol
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
module RSpecTracer::Storage::Backend
|
|
19
|
+
def load_graph(schema_version:); end
|
|
20
|
+
def save_graph(graph, schema_version:); end
|
|
21
|
+
def transactional_save(&block); end
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Planned backends
|
|
26
|
+
|
|
27
|
+
- `JsonBackend` — default, human-readable, diff-friendly.
|
|
28
|
+
- `SqliteBackend` — optional, for large suites where JSON load cost
|
|
29
|
+
dominates.
|
|
30
|
+
|
|
31
|
+
## Status
|
|
32
|
+
|
|
33
|
+
Shipping in 2.0. The legacy 1.x persistence files
|
|
34
|
+
(`lib/rspec_tracer/cache.rb`, `report_writer.rb`, `coverage_writer.rb`)
|
|
35
|
+
were retired and replaced by this storage subsystem.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpecTracer
|
|
4
|
+
# Internal Storage — see {RSpecTracer} for the user-facing surface.
|
|
5
|
+
# @api private
|
|
6
|
+
module Storage
|
|
7
|
+
# Protocol every storage backend must satisfy. JsonBackend is the
|
|
8
|
+
# default; SqliteBackend is an opt-in alternative on MRI. The
|
|
9
|
+
# `spec/contracts/storage_backend.rb` shared-examples group asserts
|
|
10
|
+
# the full contract on every implementation.
|
|
11
|
+
#
|
|
12
|
+
# Rationale for the method set (from ARCHITECTURE.md, Contracts
|
|
13
|
+
# between layers):
|
|
14
|
+
# - `load_graph(schema_version:)` returns `Snapshot` or `nil`.
|
|
15
|
+
# `nil` means "no cache, or schema mismatch." Never raises on
|
|
16
|
+
# corruption - the backend's job is to normalize malformed
|
|
17
|
+
# inputs into nil + a log line.
|
|
18
|
+
# - `save_graph(snapshot, schema_version:)` persists the graph.
|
|
19
|
+
# Either every file lands or none do (atomic via
|
|
20
|
+
# transactional_save).
|
|
21
|
+
# - `last_run_id` returns the identifier of the most recent
|
|
22
|
+
# successful save, or `nil` if no cache exists.
|
|
23
|
+
# - `transactional_save(&block)` runs the block with
|
|
24
|
+
# single-writer semantics and commits on clean exit; on any
|
|
25
|
+
# raise, the pre-block state is preserved.
|
|
26
|
+
# - `clear!` removes everything the backend owns.
|
|
27
|
+
#
|
|
28
|
+
# This module is intentionally documentation-only - it does not
|
|
29
|
+
# define stubs that raise NotImplementedError, because mutant
|
|
30
|
+
# would flag every `raise` as an alive mutation with no way to
|
|
31
|
+
# kill it. The shared-examples contract is the real gate.
|
|
32
|
+
#
|
|
33
|
+
# @example Registering a custom storage backend
|
|
34
|
+
# class MyBackend
|
|
35
|
+
# def load_graph(schema_version:); end
|
|
36
|
+
# def save_graph(snapshot, schema_version:); end
|
|
37
|
+
# def last_run_id; end
|
|
38
|
+
# def transactional_save(&block); yield; end
|
|
39
|
+
# def clear!; end
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# RSpecTracer.configure do
|
|
43
|
+
# storage_backend MyBackend.new
|
|
44
|
+
# end
|
|
45
|
+
module Backend
|
|
46
|
+
# Internal constant.
|
|
47
|
+
# @api private
|
|
48
|
+
REQUIRED_METHODS = %i[
|
|
49
|
+
load_graph
|
|
50
|
+
save_graph
|
|
51
|
+
last_run_id
|
|
52
|
+
transactional_save
|
|
53
|
+
clear!
|
|
54
|
+
].freeze
|
|
55
|
+
|
|
56
|
+
# Verifies a candidate object satisfies the backend protocol.
|
|
57
|
+
# Used by {RSpecTracer::Configuration#storage_backend} to gate
|
|
58
|
+
# custom-backend registration at config-load time.
|
|
59
|
+
#
|
|
60
|
+
# @param backend [Object] candidate backend instance
|
|
61
|
+
# @return [Boolean] true if every {REQUIRED_METHODS} entry is
|
|
62
|
+
# responded to
|
|
63
|
+
def self.conforms?(backend)
|
|
64
|
+
REQUIRED_METHODS.all? { |m| backend.respond_to?(m) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Construct the configured storage backend instance. Single
|
|
68
|
+
# source of truth for the json/sqlite dispatch + sqlite-gem-
|
|
69
|
+
# missing graceful fallback to :json, used by both
|
|
70
|
+
# {RSpecTracer::Engine} (runtime) and the
|
|
71
|
+
# {RSpecTracer::CLI::CacheInfo} / {RSpecTracer::CLI::Explain}
|
|
72
|
+
# sub-commands (post-run inspection). Pre-refactor, the
|
|
73
|
+
# dispatch lived only on Engine and the CLI sub-commands
|
|
74
|
+
# hardcoded the JsonBackend on-disk layout — so
|
|
75
|
+
# `bin/rspec-tracer cache:info` / `explain` reported "no
|
|
76
|
+
# last_run.json" even when `storage_backend :sqlite` had
|
|
77
|
+
# persisted a working cache.
|
|
78
|
+
#
|
|
79
|
+
# @param cache_path [String] root cache directory.
|
|
80
|
+
# @param configuration [Object] anything responding to the
|
|
81
|
+
# `storage_backend` / `storage_backend_opts` /
|
|
82
|
+
# `cache_retention_local_count` / `cache_size_warn_*` /
|
|
83
|
+
# `logger` accessors (defaults to the {RSpecTracer}
|
|
84
|
+
# top-level module).
|
|
85
|
+
# @return [Object] a backend instance satisfying {REQUIRED_METHODS}.
|
|
86
|
+
def self.build(cache_path:, configuration: RSpecTracer)
|
|
87
|
+
case configuration.storage_backend
|
|
88
|
+
when :sqlite
|
|
89
|
+
build_sqlite(cache_path: cache_path, configuration: configuration)
|
|
90
|
+
else
|
|
91
|
+
build_json(cache_path: cache_path, configuration: configuration)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Internal helper for the tracer pipeline.
|
|
96
|
+
# JsonBackend / SqliteBackend constants are pre-required by
|
|
97
|
+
# every call site that loads {Backend} — Engine at gem-boot
|
|
98
|
+
# time, CLI sub-commands at their own require chain — so the
|
|
99
|
+
# method body stays focused on the construction shape mutant
|
|
100
|
+
# can actually verify. Returning a `require_relative` call to
|
|
101
|
+
# the method would be a structurally-unkillable mutation
|
|
102
|
+
# (Ruby caches requires; the test process always has both
|
|
103
|
+
# backends loaded).
|
|
104
|
+
# @api private
|
|
105
|
+
def self.build_json(cache_path:, configuration:)
|
|
106
|
+
JsonBackend.new(
|
|
107
|
+
cache_path: cache_path,
|
|
108
|
+
logger: configuration.logger,
|
|
109
|
+
retention_local_count: configuration.cache_retention_local_count,
|
|
110
|
+
warn_per_file_mb: configuration.cache_size_warn_per_file_mb,
|
|
111
|
+
warn_total_mb: configuration.cache_size_warn_total_mb,
|
|
112
|
+
serializer: configuration.storage_backend_opts[:serializer] || :json
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Internal helper for the tracer pipeline.
|
|
117
|
+
# See {build_json} for why the SqliteBackend require is at the
|
|
118
|
+
# call site rather than inside the method body.
|
|
119
|
+
# @api private
|
|
120
|
+
def self.build_sqlite(cache_path:, configuration:)
|
|
121
|
+
SqliteBackend.new(cache_path: cache_path, logger: configuration.logger)
|
|
122
|
+
rescue SqliteBackend::SqliteBackendError => e
|
|
123
|
+
configuration.logger.warn(
|
|
124
|
+
"rspec-tracer: sqlite backend unavailable (#{e.message}); falling back to :json"
|
|
125
|
+
)
|
|
126
|
+
build_json(cache_path: cache_path, configuration: configuration)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|