rspec-tracer 1.2.2 → 2.0.0.pre.1
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 +197 -45
- data/README.md +439 -429
- data/bin/rspec-tracer +15 -0
- data/lib/rspec_tracer/cache/Rakefile +43 -0
- data/lib/rspec_tracer/cli/cache_clear.rb +98 -0
- data/lib/rspec_tracer/cli/cache_info.rb +103 -0
- data/lib/rspec_tracer/cli/doctor.rb +275 -0
- data/lib/rspec_tracer/cli/explain.rb +148 -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 +1100 -3
- data/lib/rspec_tracer/engine.rb +1076 -0
- data/lib/rspec_tracer/example.rb +21 -6
- 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 +397 -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 +178 -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 +68 -0
- data/lib/rspec_tracer/storage/json_backend.rb +866 -0
- data/lib/rspec_tracer/storage/lazy_snapshot.rb +65 -0
- data/lib/rspec_tracer/storage/schema.rb +43 -0
- data/lib/rspec_tracer/storage/serializer/json.rb +41 -0
- data/lib/rspec_tracer/storage/serializer/msgpack.rb +90 -0
- data/lib/rspec_tracer/storage/snapshot.rb +127 -0
- data/lib/rspec_tracer/storage/sqlite_backend.rb +686 -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 +232 -381
- metadata +93 -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,178 @@
|
|
|
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. `fail_on_duplicates=true`
|
|
24
|
+
# surfaces via `::Kernel.exit(1)` in `at_exit_behavior`; the
|
|
25
|
+
# hook passes `[]` to super so RSpec doesn't execute anything.
|
|
26
|
+
# 4. Overwrite `RSpec.world.@filtered_examples` +
|
|
27
|
+
# `@example_groups` with the filtered set, then log the run
|
|
28
|
+
# banner and delegate to `super`.
|
|
29
|
+
#
|
|
30
|
+
# The user-visible log line - `RSpec tracer is running N examples
|
|
31
|
+
# (actual: N, skipped: N)` - is preserved byte-for-byte from 1.x so
|
|
32
|
+
# cucumber scenarios and CI log parsers keep working.
|
|
33
|
+
module RunnerHook
|
|
34
|
+
# Internal method on the tracer pipeline.
|
|
35
|
+
# @api private
|
|
36
|
+
def run_specs(example_groups)
|
|
37
|
+
return super unless RSpecTracer.engine
|
|
38
|
+
|
|
39
|
+
actual_count = ::RSpec.world.example_count
|
|
40
|
+
if _rspec_tracer_no_examples?(actual_count)
|
|
41
|
+
super
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
46
|
+
filtered_examples_map, filtered_example_groups = _rspec_tracer_build_filter_decision
|
|
47
|
+
|
|
48
|
+
if _rspec_tracer_duplicates_detected?
|
|
49
|
+
RSpecTracer.running = true
|
|
50
|
+
RSpecTracer.duplicate_examples = RSpecTracer.fail_on_duplicates
|
|
51
|
+
super([])
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
::RSpec.world.instance_variable_set(:@filtered_examples, filtered_examples_map)
|
|
56
|
+
::RSpec.world.instance_variable_set(:@example_groups, filtered_example_groups)
|
|
57
|
+
|
|
58
|
+
current_count = ::RSpec.world.example_count
|
|
59
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
60
|
+
elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
|
61
|
+
|
|
62
|
+
RSpecTracer.logger.info <<-EXAMPLES.strip.gsub(/\s+/, ' ')
|
|
63
|
+
RSpec tracer is running #{current_count} examples (actual: #{actual_count},
|
|
64
|
+
skipped: #{actual_count - current_count}) (took #{elapsed})
|
|
65
|
+
EXAMPLES
|
|
66
|
+
|
|
67
|
+
RSpecTracer.running = true
|
|
68
|
+
|
|
69
|
+
super(filtered_example_groups)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Internal method on the tracer pipeline.
|
|
75
|
+
# @api private
|
|
76
|
+
def _rspec_tracer_no_examples?(actual_count)
|
|
77
|
+
return false unless actual_count.zero?
|
|
78
|
+
|
|
79
|
+
RSpecTracer.running = true
|
|
80
|
+
RSpecTracer.no_examples = true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Two-pass filter-decision walk over RSpec.world.filtered_examples.
|
|
84
|
+
#
|
|
85
|
+
# Pass 1 pre-walks every tracked example to compute its identity
|
|
86
|
+
# hash (Example.from) and register the `tracks:` metadata via
|
|
87
|
+
# `Engine#register_tracks`. This must complete for every
|
|
88
|
+
# example before ANY `run_example?` call because the env-
|
|
89
|
+
# invalidation pass (`Engine#apply_env_filter_decisions`) needs
|
|
90
|
+
# the full set of tracked env names to classify which examples
|
|
91
|
+
# re-run. Caching the tracer-example per example.object_id
|
|
92
|
+
# avoids a second MD5 in Pass 2.
|
|
93
|
+
#
|
|
94
|
+
# Between passes, `apply_env_filter_decisions` unions the
|
|
95
|
+
# env-changed decisions into @filtered_examples so Pass 2's
|
|
96
|
+
# `run_example?` sees them alongside the file-change decisions
|
|
97
|
+
# computed at Engine.setup time.
|
|
98
|
+
#
|
|
99
|
+
# Pass 2 makes the actual run/skip decisions and tags metadata.
|
|
100
|
+
# Ignored spec files (Configuration#ignore_spec_files) are
|
|
101
|
+
# handled in Pass 2 and skip both passes' engine surface -
|
|
102
|
+
# RSpec still runs them, the tracer never sees them.
|
|
103
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
104
|
+
def _rspec_tracer_build_filter_decision
|
|
105
|
+
to_run = Hash.new { |hash, group| hash[group] = [] }
|
|
106
|
+
groups = Set.new
|
|
107
|
+
engine = RSpecTracer.engine
|
|
108
|
+
tracer_cache = {}.compare_by_identity
|
|
109
|
+
|
|
110
|
+
_rspec_tracer_collect_tracks(engine, tracer_cache)
|
|
111
|
+
engine.apply_env_filter_decisions
|
|
112
|
+
|
|
113
|
+
::RSpec.world.filtered_examples.each_pair do |example_group, examples|
|
|
114
|
+
examples.each do |example|
|
|
115
|
+
if RSpecTracer.ignore_spec_file?(example.metadata[:file_path])
|
|
116
|
+
to_run[example_group] << example
|
|
117
|
+
groups << example.example_group.parent_groups.last
|
|
118
|
+
next
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
tracer_example = tracer_cache.fetch(example)
|
|
122
|
+
example_id = tracer_example[:example_id]
|
|
123
|
+
|
|
124
|
+
if engine.run_example?(example_id)
|
|
125
|
+
run_reason = engine.run_example_reason(example_id)
|
|
126
|
+
tracer_example[:run_reason] = run_reason
|
|
127
|
+
example.metadata[:description] = "#{example.description} (#{run_reason})"
|
|
128
|
+
|
|
129
|
+
to_run[example_group] << example
|
|
130
|
+
groups << example.example_group.parent_groups.last
|
|
131
|
+
|
|
132
|
+
engine.register_example(tracer_example)
|
|
133
|
+
else
|
|
134
|
+
engine.on_example_skipped(example_id)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
engine.deregister_duplicate_examples
|
|
140
|
+
|
|
141
|
+
[to_run, groups.to_a]
|
|
142
|
+
end
|
|
143
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
144
|
+
|
|
145
|
+
def _rspec_tracer_collect_tracks(engine, tracer_cache)
|
|
146
|
+
::RSpec.world.filtered_examples.each_pair do |_example_group, examples|
|
|
147
|
+
examples.each do |example|
|
|
148
|
+
next if RSpecTracer.ignore_spec_file?(example.metadata[:file_path])
|
|
149
|
+
|
|
150
|
+
tracer_example = RSpecTracer::Example.from(example)
|
|
151
|
+
example_id = tracer_example[:example_id]
|
|
152
|
+
example.metadata[:rspec_tracer_example_id] = example_id
|
|
153
|
+
tracer_cache[example] = tracer_example
|
|
154
|
+
|
|
155
|
+
tracks = RSpecTracer::RSpec::Metadata.tracks_for(example)
|
|
156
|
+
next if tracks[:files].empty? && tracks[:env].empty?
|
|
157
|
+
|
|
158
|
+
engine.register_tracks(example_id, tracks)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Internal method on the tracer pipeline.
|
|
164
|
+
# @api private
|
|
165
|
+
def _rspec_tracer_duplicates_detected?
|
|
166
|
+
duplicates = RSpecTracer.engine.duplicate_examples
|
|
167
|
+
return false if duplicates.empty?
|
|
168
|
+
|
|
169
|
+
total = duplicates.sum { |_, entries| entries.count }
|
|
170
|
+
hashes = duplicates.size
|
|
171
|
+
RSpecTracer.logger.error(
|
|
172
|
+
"RSpec tracer detected #{total} duplicate example(s) across #{hashes} identity hash(es)"
|
|
173
|
+
)
|
|
174
|
+
true
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
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,68 @@
|
|
|
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
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|