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.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +197 -45
  3. data/README.md +439 -429
  4. data/bin/rspec-tracer +15 -0
  5. data/lib/rspec_tracer/cache/Rakefile +43 -0
  6. data/lib/rspec_tracer/cli/cache_clear.rb +98 -0
  7. data/lib/rspec_tracer/cli/cache_info.rb +103 -0
  8. data/lib/rspec_tracer/cli/doctor.rb +275 -0
  9. data/lib/rspec_tracer/cli/explain.rb +148 -0
  10. data/lib/rspec_tracer/cli/report_open.rb +82 -0
  11. data/lib/rspec_tracer/cli.rb +116 -0
  12. data/lib/rspec_tracer/configuration.rb +1100 -3
  13. data/lib/rspec_tracer/engine.rb +1076 -0
  14. data/lib/rspec_tracer/example.rb +21 -6
  15. data/lib/rspec_tracer/filter.rb +35 -0
  16. data/lib/rspec_tracer/line_stub.rb +61 -0
  17. data/lib/rspec_tracer/load_config.rb +2 -2
  18. data/lib/rspec_tracer/logger.rb +15 -0
  19. data/lib/rspec_tracer/rails/README.md +78 -0
  20. data/lib/rspec_tracer/rails/i18n_tracking.rb +137 -0
  21. data/lib/rspec_tracer/rails/notifications.rb +263 -0
  22. data/lib/rspec_tracer/rails/preset.rb +94 -0
  23. data/lib/rspec_tracer/rails/railtie.rb +22 -0
  24. data/lib/rspec_tracer/rails.rb +15 -0
  25. data/lib/rspec_tracer/remote_cache/README.md +140 -0
  26. data/lib/rspec_tracer/remote_cache/Rakefile +35 -11
  27. data/lib/rspec_tracer/remote_cache/archive.rb +137 -0
  28. data/lib/rspec_tracer/remote_cache/backend.rb +73 -0
  29. data/lib/rspec_tracer/remote_cache/git_ancestry.rb +241 -0
  30. data/lib/rspec_tracer/remote_cache/local_fs_backend.rb +439 -0
  31. data/lib/rspec_tracer/remote_cache/redis_backend.rb +554 -0
  32. data/lib/rspec_tracer/remote_cache/s3_backend.rb +712 -0
  33. data/lib/rspec_tracer/remote_cache/user_tasks.rb +397 -0
  34. data/lib/rspec_tracer/remote_cache/validator.rb +40 -62
  35. data/lib/rspec_tracer/remote_cache.rb +22 -0
  36. data/lib/rspec_tracer/reporters/README.md +103 -0
  37. data/lib/rspec_tracer/reporters/base.rb +87 -0
  38. data/lib/rspec_tracer/reporters/coverage_json_reporter.rb +338 -0
  39. data/lib/rspec_tracer/reporters/html/.gitignore +19 -0
  40. data/lib/rspec_tracer/reporters/html/.prettierignore +4 -0
  41. data/lib/rspec_tracer/reporters/html/.prettierrc.json +9 -0
  42. data/lib/rspec_tracer/reporters/html/README.md +80 -0
  43. data/lib/rspec_tracer/reporters/html/dist/assets/index.css +2 -0
  44. data/lib/rspec_tracer/reporters/html/dist/assets/index.js +1 -0
  45. data/lib/rspec_tracer/reporters/html/dist/index.html +24 -0
  46. data/lib/rspec_tracer/reporters/html/eslint.config.js +62 -0
  47. data/lib/rspec_tracer/reporters/html/package-lock.json +4941 -0
  48. data/lib/rspec_tracer/reporters/html/package.json +29 -0
  49. data/lib/rspec_tracer/reporters/html/src/app.jsx +130 -0
  50. data/lib/rspec_tracer/reporters/html/src/components/AllExamples.jsx +86 -0
  51. data/lib/rspec_tracer/reporters/html/src/components/DuplicateExamples.jsx +68 -0
  52. data/lib/rspec_tracer/reporters/html/src/components/ExamplesDependency.jsx +78 -0
  53. data/lib/rspec_tracer/reporters/html/src/components/FilesDependency.jsx +72 -0
  54. data/lib/rspec_tracer/reporters/html/src/components/FlakyExamples.jsx +42 -0
  55. data/lib/rspec_tracer/reporters/html/src/components/ReportTable.jsx +131 -0
  56. data/lib/rspec_tracer/reporters/html/src/components/SearchBar.jsx +19 -0
  57. data/lib/rspec_tracer/reporters/html/src/index.html +23 -0
  58. data/lib/rspec_tracer/reporters/html/src/main.jsx +37 -0
  59. data/lib/rspec_tracer/reporters/html/src/styles.css +434 -0
  60. data/lib/rspec_tracer/reporters/html/vite.config.js +42 -0
  61. data/lib/rspec_tracer/reporters/html_reporter.rb +266 -0
  62. data/lib/rspec_tracer/reporters/json_reporter.rb +88 -0
  63. data/lib/rspec_tracer/reporters/payload_builder.rb +235 -0
  64. data/lib/rspec_tracer/reporters/registry.rb +120 -0
  65. data/lib/rspec_tracer/reporters/terminal_reporter.rb +264 -0
  66. data/lib/rspec_tracer/rspec/README.md +73 -0
  67. data/lib/rspec_tracer/rspec/installation.rb +97 -0
  68. data/lib/rspec_tracer/rspec/metadata.rb +96 -0
  69. data/lib/rspec_tracer/rspec/parallel_tests.rb +459 -0
  70. data/lib/rspec_tracer/rspec/reporter_hook.rb +84 -0
  71. data/lib/rspec_tracer/rspec/runner_hook.rb +178 -0
  72. data/lib/rspec_tracer/source_file.rb +24 -7
  73. data/lib/rspec_tracer/storage/README.md +35 -0
  74. data/lib/rspec_tracer/storage/backend.rb +68 -0
  75. data/lib/rspec_tracer/storage/json_backend.rb +866 -0
  76. data/lib/rspec_tracer/storage/lazy_snapshot.rb +65 -0
  77. data/lib/rspec_tracer/storage/schema.rb +43 -0
  78. data/lib/rspec_tracer/storage/serializer/json.rb +41 -0
  79. data/lib/rspec_tracer/storage/serializer/msgpack.rb +90 -0
  80. data/lib/rspec_tracer/storage/snapshot.rb +127 -0
  81. data/lib/rspec_tracer/storage/sqlite_backend.rb +686 -0
  82. data/lib/rspec_tracer/time_formatter.rb +37 -18
  83. data/lib/rspec_tracer/tracker/README.md +36 -0
  84. data/lib/rspec_tracer/tracker/coverage_adapter.rb +174 -0
  85. data/lib/rspec_tracer/tracker/declared_globs.rb +100 -0
  86. data/lib/rspec_tracer/tracker/dependency_graph.rb +134 -0
  87. data/lib/rspec_tracer/tracker/env_matcher.rb +127 -0
  88. data/lib/rspec_tracer/tracker/env_snapshot.rb +77 -0
  89. data/lib/rspec_tracer/tracker/example_registry.rb +153 -0
  90. data/lib/rspec_tracer/tracker/file_digest.rb +61 -0
  91. data/lib/rspec_tracer/tracker/filter.rb +127 -0
  92. data/lib/rspec_tracer/tracker/input.rb +99 -0
  93. data/lib/rspec_tracer/tracker/io_hooks/file.rb +55 -0
  94. data/lib/rspec_tracer/tracker/io_hooks/io.rb +24 -0
  95. data/lib/rspec_tracer/tracker/io_hooks/json.rb +23 -0
  96. data/lib/rspec_tracer/tracker/io_hooks/kernel.rb +26 -0
  97. data/lib/rspec_tracer/tracker/io_hooks/yaml.rb +38 -0
  98. data/lib/rspec_tracer/tracker/io_hooks.rb +195 -0
  99. data/lib/rspec_tracer/tracker/loaded_files_tracker.rb +295 -0
  100. data/lib/rspec_tracer/tracker/new_file_detector.rb +62 -0
  101. data/lib/rspec_tracer/tracker/whole_suite_invalidators.rb +96 -0
  102. data/lib/rspec_tracer/version.rb +4 -1
  103. data/lib/rspec_tracer.rb +232 -381
  104. metadata +93 -43
  105. data/lib/rspec_tracer/cache.rb +0 -207
  106. data/lib/rspec_tracer/coverage_merger.rb +0 -42
  107. data/lib/rspec_tracer/coverage_reporter.rb +0 -187
  108. data/lib/rspec_tracer/coverage_writer.rb +0 -58
  109. data/lib/rspec_tracer/html_reporter/Rakefile +0 -18
  110. data/lib/rspec_tracer/html_reporter/assets/javascripts/application.js +0 -56
  111. data/lib/rspec_tracer/html_reporter/assets/javascripts/libraries/jquery.js +0 -10881
  112. data/lib/rspec_tracer/html_reporter/assets/javascripts/plugins/datatables.js +0 -15381
  113. data/lib/rspec_tracer/html_reporter/assets/stylesheets/application.css +0 -196
  114. data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/datatables.css +0 -459
  115. data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/jquery-ui.css +0 -436
  116. data/lib/rspec_tracer/html_reporter/assets/stylesheets/print.css +0 -92
  117. data/lib/rspec_tracer/html_reporter/assets/stylesheets/reset.css +0 -265
  118. data/lib/rspec_tracer/html_reporter/public/application.css +0 -5
  119. data/lib/rspec_tracer/html_reporter/public/application.js +0 -6
  120. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc.png +0 -0
  121. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc_disabled.png +0 -0
  122. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_both.png +0 -0
  123. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc.png +0 -0
  124. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc_disabled.png +0 -0
  125. data/lib/rspec_tracer/html_reporter/public/favicon.png +0 -0
  126. data/lib/rspec_tracer/html_reporter/public/loading.gif +0 -0
  127. data/lib/rspec_tracer/html_reporter/reporter.rb +0 -242
  128. data/lib/rspec_tracer/html_reporter/views/duplicate_examples.erb +0 -34
  129. data/lib/rspec_tracer/html_reporter/views/examples.erb +0 -58
  130. data/lib/rspec_tracer/html_reporter/views/examples_dependency.erb +0 -36
  131. data/lib/rspec_tracer/html_reporter/views/files_dependency.erb +0 -36
  132. data/lib/rspec_tracer/html_reporter/views/flaky_examples.erb +0 -38
  133. data/lib/rspec_tracer/html_reporter/views/layout.erb +0 -38
  134. data/lib/rspec_tracer/remote_cache/aws.rb +0 -176
  135. data/lib/rspec_tracer/remote_cache/cache.rb +0 -75
  136. data/lib/rspec_tracer/remote_cache/repo.rb +0 -210
  137. data/lib/rspec_tracer/report_generator.rb +0 -158
  138. data/lib/rspec_tracer/report_merger.rb +0 -68
  139. data/lib/rspec_tracer/report_writer.rb +0 -141
  140. data/lib/rspec_tracer/reporter.rb +0 -204
  141. data/lib/rspec_tracer/rspec_reporter.rb +0 -41
  142. data/lib/rspec_tracer/rspec_runner.rb +0 -56
  143. data/lib/rspec_tracer/ruby_coverage.rb +0 -9
  144. 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
- module_function
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
- def from_name(file_name)
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
- def file_name(file_path)
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
- def file_path(file_name)
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
- def absolute_external_file?(file_name)
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