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
|
@@ -1,23 +1,109 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
3
5
|
require_relative 'filter'
|
|
4
6
|
require_relative 'logger'
|
|
5
7
|
|
|
6
8
|
module RSpecTracer
|
|
9
|
+
# The user-facing configuration DSL. Mixed into `RSpecTracer` itself,
|
|
10
|
+
# so calls inside a `.rspec-tracer` (or `~/.rspec-tracer`) file appear
|
|
11
|
+
# against the top-level module:
|
|
12
|
+
#
|
|
13
|
+
# @example A typical `.rspec-tracer`
|
|
14
|
+
# RSpecTracer.configure do
|
|
15
|
+
# project_name 'My App'
|
|
16
|
+
# track_files 'config/locales/**/*.yml', 'db/schema.rb', 'Gemfile.lock'
|
|
17
|
+
# track_env 'AUTH_TOKEN', 'DATABASE_URL', 'RAILS_*'
|
|
18
|
+
# track_rails_defaults
|
|
19
|
+
#
|
|
20
|
+
# storage_backend :sqlite
|
|
21
|
+
# remote_cache_backend :s3, bucket: 'my-bucket', prefix: 'rspec-tracer'
|
|
22
|
+
#
|
|
23
|
+
# add_filter '/vendor/'
|
|
24
|
+
# add_coverage_filter %w[/spec/ /test/]
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# The DSL methods are technically `private` in Ruby; the `configure`
|
|
28
|
+
# block uses Docile to expose them as if public. Calling them
|
|
29
|
+
# outside a `.rspec-tracer` file raises `InvalidUsageError`. The
|
|
30
|
+
# configuration loader allowlist enforces this gate (see
|
|
31
|
+
# {ALLOWED_CONFIGURER}).
|
|
32
|
+
#
|
|
33
|
+
# See also:
|
|
34
|
+
# - {file:README.md} — user guide.
|
|
35
|
+
# - {file:UPGRADING.md} — 1.x → 2.0 migration.
|
|
36
|
+
# - {file:ARCHITECTURE.md} — input taxonomy + layer structure.
|
|
37
|
+
# - {file:COOKBOOK.md} — recipes for common scenarios.
|
|
38
|
+
#
|
|
39
|
+
# rubocop:disable Metrics/ModuleLength
|
|
7
40
|
module Configuration
|
|
41
|
+
# Raised when the configuration DSL is invoked outside a
|
|
42
|
+
# `.rspec-tracer` / `~/.rspec-tracer` loader, when a typo'd DSL
|
|
43
|
+
# method is called (with a `did_you_mean?` suggestion when one
|
|
44
|
+
# exists), or when an option value fails validation.
|
|
8
45
|
class InvalidUsageError < StandardError; end
|
|
9
46
|
|
|
47
|
+
# Internal constant.
|
|
48
|
+
# @api private
|
|
10
49
|
ALLOWED_CONFIGURER = %w[
|
|
11
50
|
lib/rspec_tracer/load_default_config.rb
|
|
12
51
|
lib/rspec_tracer/load_global_config.rb
|
|
13
52
|
lib/rspec_tracer/load_local_config.rb
|
|
14
53
|
].freeze
|
|
15
54
|
|
|
55
|
+
# Internal constant.
|
|
56
|
+
# @api private
|
|
16
57
|
DEFAULT_CACHE_DIR = 'rspec_tracer_cache'
|
|
58
|
+
# Internal constant.
|
|
59
|
+
# @api private
|
|
17
60
|
DEFAULT_COVERAGE_DIR = 'rspec_tracer_coverage'
|
|
61
|
+
# Internal constant.
|
|
62
|
+
# @api private
|
|
18
63
|
DEFAULT_REPORT_DIR = 'rspec_tracer_report'
|
|
64
|
+
# Internal constant.
|
|
65
|
+
# @api private
|
|
19
66
|
DEFAULT_LOCK_FILE = 'rspec_tracer.lock'
|
|
67
|
+
# Internal constant.
|
|
68
|
+
# @api private
|
|
69
|
+
DEFAULT_STORAGE_BACKEND = :json
|
|
70
|
+
# :sqlite opt-in: MRI >= 3.2 only (sqlite3 2.x gem requirement).
|
|
71
|
+
# Kept closed so typos raise early.
|
|
72
|
+
STORAGE_BACKEND_NAMES = %i[json sqlite].freeze
|
|
73
|
+
# Keys allowed in `storage_backend`'s opts hash. `:serializer` is
|
|
74
|
+
# accepted only for the :json backend (see validate_storage_opts!).
|
|
75
|
+
STORAGE_BACKEND_OPT_KEYS = %i[serializer].freeze
|
|
76
|
+
# Internal constant.
|
|
77
|
+
# @api private
|
|
78
|
+
STORAGE_BACKEND_SERIALIZERS = %i[json msgpack].freeze
|
|
79
|
+
# Default Ruby `Coverage` modes when the user has not called the
|
|
80
|
+
# `coverage_modes` DSL. Matches bare `Coverage.start` semantics so
|
|
81
|
+
# legacy configs continue to emit lines-only `coverage.json` shape.
|
|
82
|
+
DEFAULT_COVERAGE_MODES = %i[lines].freeze
|
|
83
|
+
# Allowed Ruby `Coverage` modes accepted by the `coverage_modes`
|
|
84
|
+
# DSL. The set tracks Ruby 3.1+ `Coverage.start` keyword args.
|
|
85
|
+
# `coverage.json` ships the lines-only `Array<Integer|nil>` shape
|
|
86
|
+
# per file regardless — branches / methods / oneshot_lines / eval
|
|
87
|
+
# data is collected by Ruby but the user-facing artifact stays on
|
|
88
|
+
# the documented 1.x shape (see UPGRADING `#SimpleCov branch
|
|
89
|
+
# coverage now works` for SimpleCov interop).
|
|
90
|
+
COVERAGE_MODES = %i[lines branches methods oneshot_lines eval].freeze
|
|
91
|
+
# Per-save retention on the local cache's run-id directories.
|
|
92
|
+
# 5 keeps enough history for rollback debugging without letting
|
|
93
|
+
# the cache grow unbounded (issue #20). 0 opts out entirely.
|
|
94
|
+
DEFAULT_CACHE_RETENTION_LOCAL_COUNT = 5
|
|
95
|
+
# Size budgets (MiB). Warn at save time when any single cache
|
|
96
|
+
# file exceeds the per-file threshold or when the cache total
|
|
97
|
+
# exceeds the aggregate threshold. Surfaces dependency.json
|
|
98
|
+
# ballooning past the few-MB range while the user can still
|
|
99
|
+
# act on them. Set to 0 to disable either individually.
|
|
100
|
+
DEFAULT_CACHE_SIZE_WARN_PER_FILE_MB = 50
|
|
101
|
+
# Internal constant.
|
|
102
|
+
# @api private
|
|
103
|
+
DEFAULT_CACHE_SIZE_WARN_TOTAL_MB = 500
|
|
20
104
|
|
|
105
|
+
# Internal constant.
|
|
106
|
+
# @api private
|
|
21
107
|
LOG_LEVEL = {
|
|
22
108
|
off: 0,
|
|
23
109
|
debug: 1,
|
|
@@ -26,8 +112,18 @@ module RSpecTracer
|
|
|
26
112
|
error: 4
|
|
27
113
|
}.freeze
|
|
28
114
|
|
|
115
|
+
# Internal method on the tracer pipeline.
|
|
116
|
+
# @api private
|
|
29
117
|
def configure(&)
|
|
30
|
-
|
|
118
|
+
# Scan the full caller chain (not just the immediate two frames):
|
|
119
|
+
# MRI / JRuby place the load_*_config.rb loader at depth 2, but
|
|
120
|
+
# TruffleRuby's `load` interposes an extra runtime frame, pushing
|
|
121
|
+
# the loader past the 2-frame window and tripping the InvalidUsage
|
|
122
|
+
# raise. Path-suffix match against the well-known loader filenames
|
|
123
|
+
# keeps the gate safe regardless of stack depth - user code would
|
|
124
|
+
# have to copy one of those filenames verbatim into a path it
|
|
125
|
+
# `load`s to get a false positive, which we treat as wilful.
|
|
126
|
+
configurers = caller_locations(1).map(&:path)
|
|
31
127
|
invalid = configurers.none? do |configurer|
|
|
32
128
|
ALLOWED_CONFIGURER.any? do |allowed_configurer|
|
|
33
129
|
configurer.end_with?(allowed_configurer)
|
|
@@ -40,8 +136,13 @@ module RSpecTracer
|
|
|
40
136
|
RSpecTracer::Configuration.private_instance_methods(false).each do |method_name|
|
|
41
137
|
alias_method :"_#{method_name}", method_name
|
|
42
138
|
|
|
43
|
-
|
|
44
|
-
|
|
139
|
+
# Forward `**kwargs` too so DSL methods can accept Ruby 3+
|
|
140
|
+
# keyword args (e.g. `track_rails_defaults except: [:views]`,
|
|
141
|
+
# `storage_backend :json, serializer: :msgpack`). Earlier
|
|
142
|
+
# forms of this wrapper forwarded `*args, &block` only and
|
|
143
|
+
# silently stripped kwargs.
|
|
144
|
+
define_method method_name do |*args, **kwargs, &block|
|
|
145
|
+
send(:"_#{method_name}", *args, **kwargs, &block)
|
|
45
146
|
end
|
|
46
147
|
end
|
|
47
148
|
end
|
|
@@ -51,6 +152,16 @@ module RSpecTracer
|
|
|
51
152
|
|
|
52
153
|
private
|
|
53
154
|
|
|
155
|
+
# Set the project root directory. Defaults to `Dir.getwd`.
|
|
156
|
+
# Affects how all other path-based DSL methods (`cache_dir`,
|
|
157
|
+
# `report_dir`, `coverage_dir`, `track_files` globs) are
|
|
158
|
+
# resolved.
|
|
159
|
+
#
|
|
160
|
+
# @param root [String, nil] absolute or relative path; nil
|
|
161
|
+
# returns the current value (or default).
|
|
162
|
+
# @return [String] absolute project root path.
|
|
163
|
+
# @example
|
|
164
|
+
# RSpecTracer.configure { root '/path/to/project' }
|
|
54
165
|
def root(root = nil)
|
|
55
166
|
return @root if defined?(@root) && root.nil?
|
|
56
167
|
|
|
@@ -61,6 +172,11 @@ module RSpecTracer
|
|
|
61
172
|
@root = File.expand_path(root || Dir.getwd)
|
|
62
173
|
end
|
|
63
174
|
|
|
175
|
+
# Set the project's display name; appears in HTML reports.
|
|
176
|
+
# Defaults to a humanized form of the project root's basename.
|
|
177
|
+
#
|
|
178
|
+
# @param new_name [String, nil] human-readable project name.
|
|
179
|
+
# @return [String] the configured (or derived) project name.
|
|
64
180
|
def project_name(new_name = nil)
|
|
65
181
|
return @project_name if defined?(@project_name) && @project_name && new_name.nil?
|
|
66
182
|
|
|
@@ -68,6 +184,14 @@ module RSpecTracer
|
|
|
68
184
|
@project_name ||= File.basename(root.split('/').last).capitalize.tr('_', ' ')
|
|
69
185
|
end
|
|
70
186
|
|
|
187
|
+
# Override the on-disk cache directory (default
|
|
188
|
+
# `rspec_tracer_cache`). Also reads `RSPEC_TRACER_CACHE_DIR`
|
|
189
|
+
# from env when set. The directory ships in the canonical
|
|
190
|
+
# `.gitignore` recipe; renaming silently leaks cache into
|
|
191
|
+
# source control.
|
|
192
|
+
#
|
|
193
|
+
# @param dir [String, nil] relative-to-root or absolute path.
|
|
194
|
+
# @return [String] configured cache directory.
|
|
71
195
|
def cache_dir(dir = nil)
|
|
72
196
|
return @cache_dir if defined?(@cache_dir) && dir.nil?
|
|
73
197
|
|
|
@@ -79,6 +203,11 @@ module RSpecTracer
|
|
|
79
203
|
end
|
|
80
204
|
end
|
|
81
205
|
|
|
206
|
+
# Resolve the absolute cache path; expanded against {#root} and
|
|
207
|
+
# scoped per `TEST_SUITE_ID` / `parallel_tests` worker. Creates
|
|
208
|
+
# the directory on first access.
|
|
209
|
+
#
|
|
210
|
+
# @return [String] absolute cache path.
|
|
82
211
|
def cache_path
|
|
83
212
|
@cache_path ||= begin
|
|
84
213
|
cache_path = File.expand_path(cache_dir, root)
|
|
@@ -91,6 +220,11 @@ module RSpecTracer
|
|
|
91
220
|
end
|
|
92
221
|
end
|
|
93
222
|
|
|
223
|
+
# Override the on-disk HTML/JSON report directory (default
|
|
224
|
+
# `rspec_tracer_report`). Also reads `RSPEC_TRACER_REPORT_DIR`.
|
|
225
|
+
#
|
|
226
|
+
# @param dir [String, nil] relative-to-root or absolute path.
|
|
227
|
+
# @return [String] configured report directory.
|
|
94
228
|
def report_dir(dir = nil)
|
|
95
229
|
return @report_dir if defined?(@report_dir) && dir.nil?
|
|
96
230
|
|
|
@@ -102,6 +236,11 @@ module RSpecTracer
|
|
|
102
236
|
end
|
|
103
237
|
end
|
|
104
238
|
|
|
239
|
+
# Resolve the absolute report path; expanded against {#root} and
|
|
240
|
+
# scoped per `TEST_SUITE_ID` / `parallel_tests` worker. Creates
|
|
241
|
+
# the directory on first access.
|
|
242
|
+
#
|
|
243
|
+
# @return [String] absolute report path.
|
|
105
244
|
def report_path
|
|
106
245
|
@report_path ||= begin
|
|
107
246
|
report_path = File.expand_path(report_dir, root)
|
|
@@ -114,6 +253,11 @@ module RSpecTracer
|
|
|
114
253
|
end
|
|
115
254
|
end
|
|
116
255
|
|
|
256
|
+
# Override the coverage output directory (default
|
|
257
|
+
# `rspec_tracer_coverage`). Also reads `RSPEC_TRACER_COVERAGE_DIR`.
|
|
258
|
+
#
|
|
259
|
+
# @param dir [String, nil] relative-to-root or absolute path.
|
|
260
|
+
# @return [String] configured coverage directory.
|
|
117
261
|
def coverage_dir(dir = nil)
|
|
118
262
|
return @coverage_dir if defined?(@coverage_dir) && dir.nil?
|
|
119
263
|
|
|
@@ -125,6 +269,11 @@ module RSpecTracer
|
|
|
125
269
|
end
|
|
126
270
|
end
|
|
127
271
|
|
|
272
|
+
# Resolve the absolute coverage path; expanded against {#root}
|
|
273
|
+
# and scoped per `TEST_SUITE_ID` / `parallel_tests` worker.
|
|
274
|
+
# Creates the directory on first access.
|
|
275
|
+
#
|
|
276
|
+
# @return [String] absolute coverage path.
|
|
128
277
|
def coverage_path
|
|
129
278
|
@coverage_path ||= begin
|
|
130
279
|
coverage_path = File.expand_path(coverage_dir, root)
|
|
@@ -137,9 +286,278 @@ module RSpecTracer
|
|
|
137
286
|
end
|
|
138
287
|
end
|
|
139
288
|
|
|
289
|
+
# Configure the remote-cache backend used by the
|
|
290
|
+
# `rake rspec_tracer:remote_cache:download` / `:upload` tasks.
|
|
291
|
+
# Single-entry accumulator: a second call raises
|
|
292
|
+
# {InvalidUsageError} so a misconfigured `.rspec-tracer` fails
|
|
293
|
+
# fast.
|
|
294
|
+
#
|
|
295
|
+
# @param name_or_class [Symbol, Class] one of `:s3`, `:local_fs`,
|
|
296
|
+
# `:redis`, OR a custom class implementing
|
|
297
|
+
# {RSpecTracer::RemoteCache::Backend}.
|
|
298
|
+
# @param opts [Hash] backend-specific keyword args (e.g. `bucket:`
|
|
299
|
+
# `prefix:` for `:s3`; `root:` for `:local_fs`; `url:` `ttl:`
|
|
300
|
+
# for `:redis`).
|
|
301
|
+
# @return [Array(Symbol, Hash)] the persisted entry pair.
|
|
302
|
+
# @example S3 (preserves the 1.x layout)
|
|
303
|
+
# remote_cache_backend :s3, bucket: 'my-bucket', prefix: 'rspec-tracer'
|
|
304
|
+
# @example LocalStack / awslocal (development)
|
|
305
|
+
# remote_cache_backend :s3, bucket: 'my-bucket', prefix: 'x', local: true
|
|
306
|
+
# @example Filesystem-backed (no S3 needed)
|
|
307
|
+
# remote_cache_backend :local_fs, root: '/tmp/rspec-tracer-cache'
|
|
308
|
+
# @example Redis (with TTL + PR-branch tracking sidecar)
|
|
309
|
+
# remote_cache_backend :redis, url: ENV['REDIS_URL'], ttl: 7 * 86_400
|
|
310
|
+
# @example Custom backend class
|
|
311
|
+
# remote_cache_backend MyCustomBackend, custom_opt: 'value'
|
|
312
|
+
def remote_cache_backend(name_or_class, **opts)
|
|
313
|
+
if defined?(@remote_cache_backend_entry) && @remote_cache_backend_entry
|
|
314
|
+
raise InvalidUsageError,
|
|
315
|
+
'remote_cache already configured. `remote_cache_backend` and ' \
|
|
316
|
+
'`remote_cache_uri` are alternative DSLs — call one exactly once.'
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
validate_remote_cache_backend(name_or_class)
|
|
320
|
+
@remote_cache_backend_entry = [name_or_class, opts.dup.freeze].freeze
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Reader: returns the [name_or_class, opts] pair or nil. Called
|
|
324
|
+
# from `RemoteCache::UserTasks` at task dispatch time.
|
|
325
|
+
def remote_cache_backend_entry
|
|
326
|
+
return nil unless defined?(@remote_cache_backend_entry)
|
|
327
|
+
|
|
328
|
+
@remote_cache_backend_entry
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Internal method on the tracer pipeline.
|
|
332
|
+
# @api private
|
|
333
|
+
def validate_remote_cache_backend(name_or_class)
|
|
334
|
+
case name_or_class
|
|
335
|
+
when ::Symbol, ::Class
|
|
336
|
+
nil
|
|
337
|
+
else
|
|
338
|
+
raise InvalidUsageError,
|
|
339
|
+
"remote_cache_backend: expected Symbol or Class, got #{name_or_class.class}"
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Convenience DSL accepting a single URI string and dispatching
|
|
344
|
+
# to {#remote_cache_backend} with the parsed bucket / prefix /
|
|
345
|
+
# host / path. Also reads `RSPEC_TRACER_REMOTE_CACHE_URI` from env.
|
|
346
|
+
#
|
|
347
|
+
# @param uri [String, nil] one of `s3://bucket/prefix`,
|
|
348
|
+
# `file:///abs/path`, `redis://host:port/db`. Pass nil to read
|
|
349
|
+
# the env var (or return the cached value if set).
|
|
350
|
+
# @return [String, nil] the configured URI, or nil when neither
|
|
351
|
+
# arg nor env is set.
|
|
352
|
+
# @example
|
|
353
|
+
# remote_cache_uri 's3://my-bucket/rspec-tracer'
|
|
354
|
+
# @example
|
|
355
|
+
# remote_cache_uri 'file:///tmp/rspec-tracer-cache'
|
|
356
|
+
# @example
|
|
357
|
+
# remote_cache_uri 'redis://localhost:6379/0'
|
|
358
|
+
#
|
|
359
|
+
# Convenience DSL: `remote_cache_uri '<scheme>://...'` parses the
|
|
360
|
+
# URI and calls `remote_cache_backend` with the right backend +
|
|
361
|
+
# connection params. Also accepts ENV `RSPEC_TRACER_REMOTE_CACHE_URI`.
|
|
362
|
+
# Supported schemes:
|
|
363
|
+
# s3://<bucket>/<prefix> -> S3Backend
|
|
364
|
+
# file:///absolute/path -> LocalFsBackend (root:)
|
|
365
|
+
# redis://[user:pass@]host:port/<db> -> RedisBackend (prefix: 'rspec-tracer')
|
|
366
|
+
# Users who need finer control (custom Redis prefix, LocalFs on a
|
|
367
|
+
# relative path) use `remote_cache_backend` directly.
|
|
368
|
+
def remote_cache_uri(uri = nil)
|
|
369
|
+
return @remote_cache_uri if defined?(@remote_cache_uri) && uri.nil?
|
|
370
|
+
|
|
371
|
+
value = ENV.fetch('RSPEC_TRACER_REMOTE_CACHE_URI', uri)
|
|
372
|
+
return nil if value.nil?
|
|
373
|
+
|
|
374
|
+
parsed = parse_remote_cache_uri(value)
|
|
375
|
+
dispatch_remote_cache_uri(parsed)
|
|
376
|
+
|
|
377
|
+
@remote_cache_uri = value
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Internal method on the tracer pipeline.
|
|
381
|
+
# @api private
|
|
382
|
+
def dispatch_remote_cache_uri(parsed)
|
|
383
|
+
case parsed.scheme
|
|
384
|
+
when 's3'
|
|
385
|
+
prefix = parsed.path.to_s.sub(%r{^/}, '')
|
|
386
|
+
remote_cache_backend(:s3, bucket: parsed.host, prefix: prefix)
|
|
387
|
+
when 'file'
|
|
388
|
+
remote_cache_backend(:local_fs, root: parsed.path.to_s)
|
|
389
|
+
when 'redis', 'rediss'
|
|
390
|
+
remote_cache_backend(:redis, url: parsed.to_s, prefix: 'rspec-tracer')
|
|
391
|
+
else
|
|
392
|
+
raise InvalidUsageError,
|
|
393
|
+
"unsupported remote_cache_uri scheme: #{parsed.scheme.inspect} " \
|
|
394
|
+
"(supported: 's3', 'file', 'redis', 'rediss')"
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Validate a parsed URI per the active scheme. Host presence is the
|
|
399
|
+
# common structural signal for S3 / Redis; `file://` uses path
|
|
400
|
+
# instead (host is optional and usually empty).
|
|
401
|
+
def parse_remote_cache_uri(value)
|
|
402
|
+
parsed = URI.parse(value)
|
|
403
|
+
raise InvalidUsageError, "invalid remote_cache_uri: #{value.inspect}" unless valid_remote_cache_uri?(parsed)
|
|
404
|
+
|
|
405
|
+
parsed
|
|
406
|
+
rescue URI::InvalidURIError => e
|
|
407
|
+
raise InvalidUsageError, "invalid remote_cache_uri: #{value.inspect} (#{e.message})"
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Internal method on the tracer pipeline.
|
|
411
|
+
# @api private
|
|
412
|
+
def valid_remote_cache_uri?(parsed)
|
|
413
|
+
return false if parsed.scheme.nil?
|
|
414
|
+
|
|
415
|
+
case parsed.scheme
|
|
416
|
+
when 'file'
|
|
417
|
+
!parsed.path.to_s.empty?
|
|
418
|
+
else
|
|
419
|
+
!parsed.host.nil? && !parsed.host.empty?
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Retention knobs (closes issue #20). Mutually exclusive: count
|
|
424
|
+
# and duration bound the main tier from different axes. PR tier
|
|
425
|
+
# uses `cache_retention_pr_branch_ttl` independently.
|
|
426
|
+
# Cap remote-cache main-tier refs to a count. Mutually exclusive
|
|
427
|
+
# with {#cache_retention_duration}; setting both raises.
|
|
428
|
+
#
|
|
429
|
+
# @param count [Integer, nil] positive integer; nil reads current
|
|
430
|
+
# value.
|
|
431
|
+
# @return [Integer, nil] configured count.
|
|
432
|
+
# @raise [InvalidUsageError] when count is non-positive or when
|
|
433
|
+
# {#cache_retention_duration} is already set.
|
|
434
|
+
def cache_retention_count(count = nil)
|
|
435
|
+
return @cache_retention_count if defined?(@cache_retention_count) && count.nil?
|
|
436
|
+
return nil if count.nil?
|
|
437
|
+
|
|
438
|
+
raise_if_retention_conflict(:cache_retention_duration)
|
|
439
|
+
unless count.is_a?(::Integer) && count.positive?
|
|
440
|
+
raise InvalidUsageError, "cache_retention_count must be a positive integer, got #{count.inspect}"
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
@cache_retention_count = count
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Cap remote-cache main-tier refs by age. Mutually exclusive with
|
|
447
|
+
# {#cache_retention_count}; setting both raises.
|
|
448
|
+
#
|
|
449
|
+
# @param spec [String, Integer, nil] duration string (e.g.
|
|
450
|
+
# `'7d'`, `'48h'`, `'30m'`) OR seconds as integer. nil reads
|
|
451
|
+
# current.
|
|
452
|
+
# @return [String, Integer, nil] configured raw spec.
|
|
453
|
+
# @raise [InvalidUsageError] when spec is unparseable or when
|
|
454
|
+
# {#cache_retention_count} is already set.
|
|
455
|
+
def cache_retention_duration(spec = nil)
|
|
456
|
+
return @cache_retention_duration_raw if defined?(@cache_retention_duration_raw) && spec.nil?
|
|
457
|
+
return nil if spec.nil?
|
|
458
|
+
|
|
459
|
+
raise_if_retention_conflict(:cache_retention_count)
|
|
460
|
+
seconds = parse_retention_duration_seconds(spec)
|
|
461
|
+
@cache_retention_duration_raw = spec
|
|
462
|
+
@cache_retention_duration_seconds = seconds
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# @return [Integer, nil] {#cache_retention_duration} expressed
|
|
466
|
+
# in seconds, or nil if not set.
|
|
467
|
+
def cache_retention_duration_seconds
|
|
468
|
+
return nil unless defined?(@cache_retention_duration_seconds)
|
|
469
|
+
|
|
470
|
+
@cache_retention_duration_seconds
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Cap remote-cache PR-tier refs by age. Independent of main-tier
|
|
474
|
+
# retention (PR branches typically need shorter TTLs than the
|
|
475
|
+
# historical main).
|
|
476
|
+
#
|
|
477
|
+
# @param spec [String, Integer, nil] duration string or seconds.
|
|
478
|
+
# @return [String, Integer, nil] configured raw spec.
|
|
479
|
+
def cache_retention_pr_branch_ttl(spec = nil)
|
|
480
|
+
return @cache_retention_pr_branch_ttl_raw if defined?(@cache_retention_pr_branch_ttl_raw) && spec.nil?
|
|
481
|
+
return nil if spec.nil?
|
|
482
|
+
|
|
483
|
+
seconds = parse_retention_duration_seconds(spec)
|
|
484
|
+
@cache_retention_pr_branch_ttl_raw = spec
|
|
485
|
+
@cache_retention_pr_branch_ttl_seconds = seconds
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# @return [Integer, nil] {#cache_retention_pr_branch_ttl}
|
|
489
|
+
# expressed in seconds.
|
|
490
|
+
def cache_retention_pr_branch_ttl_seconds
|
|
491
|
+
return nil unless defined?(@cache_retention_pr_branch_ttl_seconds)
|
|
492
|
+
|
|
493
|
+
@cache_retention_pr_branch_ttl_seconds
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Internal method on the tracer pipeline.
|
|
497
|
+
# @api private
|
|
498
|
+
def raise_if_retention_conflict(other_method)
|
|
499
|
+
other_ivar =
|
|
500
|
+
case other_method
|
|
501
|
+
when :cache_retention_count then :@cache_retention_count
|
|
502
|
+
when :cache_retention_duration then :@cache_retention_duration_seconds
|
|
503
|
+
end
|
|
504
|
+
return unless instance_variable_defined?(other_ivar) && instance_variable_get(other_ivar)
|
|
505
|
+
|
|
506
|
+
raise InvalidUsageError,
|
|
507
|
+
'cache_retention_count and cache_retention_duration are mutually exclusive'
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Internal method on the tracer pipeline.
|
|
511
|
+
# @api private
|
|
512
|
+
def parse_retention_duration_seconds(spec)
|
|
513
|
+
case spec
|
|
514
|
+
when ::Integer
|
|
515
|
+
raise InvalidUsageError, 'retention duration must be positive' unless spec.positive?
|
|
516
|
+
|
|
517
|
+
spec
|
|
518
|
+
when ::String
|
|
519
|
+
parse_retention_duration_from_string(spec)
|
|
520
|
+
else
|
|
521
|
+
raise InvalidUsageError,
|
|
522
|
+
"invalid retention duration: #{spec.inspect} (expected Integer seconds or String like '30 days')"
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# Internal method on the tracer pipeline.
|
|
527
|
+
# @api private
|
|
528
|
+
def parse_retention_duration_from_string(spec)
|
|
529
|
+
match = spec.strip.match(/\A(\d+)\s+(second|minute|hour|day|week)s?\z/i)
|
|
530
|
+
unless match
|
|
531
|
+
raise InvalidUsageError,
|
|
532
|
+
"invalid retention duration: #{spec.inspect} (expected e.g. '30 days', '2 weeks', '1 hour')"
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
units = { 'second' => 1, 'minute' => 60, 'hour' => 3600, 'day' => 86_400, 'week' => 604_800 }
|
|
536
|
+
count = match[1].to_i
|
|
537
|
+
raise InvalidUsageError, 'retention duration must be positive' unless count.positive?
|
|
538
|
+
|
|
539
|
+
count * units[match[2].downcase]
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# Deprecated in 2.0. Kept per USER_FACING_SURFACE.md section 3 "deprecated
|
|
543
|
+
# options keep working with one-time warnings." Migration target:
|
|
544
|
+
# `remote_cache_uri` (for the URI form) or
|
|
545
|
+
# `remote_cache_backend :s3, bucket:, prefix:` (for structured form).
|
|
546
|
+
# @deprecated Use {#remote_cache_uri} or {#remote_cache_backend}.
|
|
547
|
+
# Pre-2.0 1.x compatibility shim. Fires a one-time `logger.warn`
|
|
548
|
+
# at first use; the value still resolves so 1.x configs keep
|
|
549
|
+
# working until the 3.0 removal.
|
|
550
|
+
# @param s3_path [String, nil] `s3://bucket/prefix`.
|
|
551
|
+
# @return [String, nil]
|
|
140
552
|
def reports_s3_path(s3_path = nil)
|
|
141
553
|
return @reports_s3_path if defined?(@reports_s3_path) && s3_path.nil?
|
|
142
554
|
|
|
555
|
+
warn_once_deprecation(
|
|
556
|
+
:reports_s3_path,
|
|
557
|
+
'`reports_s3_path` / `RSPEC_TRACER_REPORTS_S3_PATH` is deprecated in 2.0; ' \
|
|
558
|
+
"use `remote_cache_uri 's3://bucket/prefix'` or `remote_cache_backend :s3, bucket:, prefix:` instead."
|
|
559
|
+
)
|
|
560
|
+
|
|
143
561
|
path = if ENV.key?('RSPEC_TRACER_REPORTS_S3_PATH')
|
|
144
562
|
ENV['RSPEC_TRACER_REPORTS_S3_PATH']
|
|
145
563
|
else
|
|
@@ -149,9 +567,42 @@ module RSpecTracer
|
|
|
149
567
|
@reports_s3_path = path if valid_s3_path?(path)
|
|
150
568
|
end
|
|
151
569
|
|
|
570
|
+
# Probe-path predicate. True when the user explicitly set
|
|
571
|
+
# `reports_s3_path` via the DSL OR the `RSPEC_TRACER_REPORTS_S3_PATH`
|
|
572
|
+
# environment variable. Distinct from {#reports_s3_path} (the
|
|
573
|
+
# getter), which fires a one-time deprecation warning on every
|
|
574
|
+
# call from an unconfigured state — including the
|
|
575
|
+
# `RemoteCache::UserTasks#derive_from_legacy_dsl` probe that runs
|
|
576
|
+
# whenever any `rake rspec_tracer:remote_cache:*` task fires.
|
|
577
|
+
# Callers that need to detect whether the legacy DSL was actually
|
|
578
|
+
# used (vs probing) should read this predicate first.
|
|
579
|
+
#
|
|
580
|
+
# @return [Boolean]
|
|
581
|
+
def reports_s3_path_set?
|
|
582
|
+
return true if defined?(@reports_s3_path) && @reports_s3_path
|
|
583
|
+
return false unless ENV.key?('RSPEC_TRACER_REPORTS_S3_PATH')
|
|
584
|
+
|
|
585
|
+
env_value = ENV.fetch('RSPEC_TRACER_REPORTS_S3_PATH', nil)
|
|
586
|
+
!env_value.nil? && !env_value.empty?
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Deprecated in 2.0. Migration: `remote_cache_backend :s3, ...,
|
|
590
|
+
# local: true`. Kept for backward compat; UserTasks derives the
|
|
591
|
+
# `local:` opt from this when `remote_cache_backend` is absent.
|
|
592
|
+
# @deprecated Use `remote_cache_backend :s3, ..., local: true`.
|
|
593
|
+
# Pre-2.0 1.x compatibility shim for `awslocal` / LocalStack.
|
|
594
|
+
# Fires a one-time `logger.warn` at first use.
|
|
595
|
+
# @param new_flag [Boolean, nil]
|
|
596
|
+
# @return [Boolean]
|
|
152
597
|
def use_local_aws(new_flag = nil)
|
|
153
598
|
return @use_local_aws if defined?(@use_local_aws) && new_flag.nil?
|
|
154
599
|
|
|
600
|
+
warn_once_deprecation(
|
|
601
|
+
:use_local_aws,
|
|
602
|
+
'`use_local_aws` / `RSPEC_TRACER_USE_LOCAL_AWS` is deprecated in 2.0; ' \
|
|
603
|
+
'use `remote_cache_backend :s3, ..., local: true` instead.'
|
|
604
|
+
)
|
|
605
|
+
|
|
155
606
|
@use_local_aws = if ENV.key?('RSPEC_TRACER_USE_LOCAL_AWS')
|
|
156
607
|
ENV['RSPEC_TRACER_USE_LOCAL_AWS'] == 'true'
|
|
157
608
|
else
|
|
@@ -159,6 +610,22 @@ module RSpecTracer
|
|
|
159
610
|
end
|
|
160
611
|
end
|
|
161
612
|
|
|
613
|
+
# Internal method on the tracer pipeline.
|
|
614
|
+
# @api private
|
|
615
|
+
def warn_once_deprecation(key, message)
|
|
616
|
+
@_deprecation_warnings ||= {}
|
|
617
|
+
return if @_deprecation_warnings.key?(key)
|
|
618
|
+
|
|
619
|
+
@_deprecation_warnings[key] = true
|
|
620
|
+
logger.warn("rspec-tracer deprecation: #{message}")
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# Allow remote-cache uploads from non-CI environments (default
|
|
624
|
+
# `false`). Reads `RSPEC_TRACER_UPLOAD_NON_CI_REPORTS=true` as
|
|
625
|
+
# an alternate enable.
|
|
626
|
+
#
|
|
627
|
+
# @param new_flag [Boolean, nil]
|
|
628
|
+
# @return [Boolean]
|
|
162
629
|
def upload_non_ci_reports(new_flag = nil)
|
|
163
630
|
return @upload_non_ci_reports if defined?(@upload_non_ci_reports) && new_flag.nil?
|
|
164
631
|
|
|
@@ -169,6 +636,13 @@ module RSpecTracer
|
|
|
169
636
|
end
|
|
170
637
|
end
|
|
171
638
|
|
|
639
|
+
# Force every example to run (default `false` — the tracer's
|
|
640
|
+
# whole point is to skip unaffected examples). Reads
|
|
641
|
+
# `RSPEC_TRACER_RUN_ALL_EXAMPLES=true` as an alternate enable.
|
|
642
|
+
# Useful for one-off "rebaseline the cache" runs.
|
|
643
|
+
#
|
|
644
|
+
# @param new_flag [Boolean, nil]
|
|
645
|
+
# @return [Boolean]
|
|
172
646
|
def run_all_examples(new_flag = nil)
|
|
173
647
|
return @run_all_examples if defined?(@run_all_examples) && new_flag.nil?
|
|
174
648
|
|
|
@@ -179,6 +653,13 @@ module RSpecTracer
|
|
|
179
653
|
end
|
|
180
654
|
end
|
|
181
655
|
|
|
656
|
+
# Exit with non-zero when duplicate-identity examples are
|
|
657
|
+
# detected (default `true` — duplicates break per-example
|
|
658
|
+
# attribution). Reads `RSPEC_TRACER_FAIL_ON_DUPLICATES=false` as
|
|
659
|
+
# an alternate disable.
|
|
660
|
+
#
|
|
661
|
+
# @param new_flag [Boolean, nil]
|
|
662
|
+
# @return [Boolean]
|
|
182
663
|
def fail_on_duplicates(new_flag = nil)
|
|
183
664
|
return @fail_on_duplicates if defined?(@fail_on_duplicates) && new_flag.nil?
|
|
184
665
|
|
|
@@ -189,6 +670,90 @@ module RSpecTracer
|
|
|
189
670
|
end
|
|
190
671
|
end
|
|
191
672
|
|
|
673
|
+
# Transitive-load attribution (closes the constants blind
|
|
674
|
+
# spot). Default `true` - the tracker observes files loaded during
|
|
675
|
+
# the process and attributes them as transitive deps of every
|
|
676
|
+
# subsequent example, so a change to a constants-defining file
|
|
677
|
+
# correctly invalidates tests that only reference its constants.
|
|
678
|
+
#
|
|
679
|
+
# Setting to `false` restores 1.x behavior (pure coverage-diff).
|
|
680
|
+
# Teams who explicitly accept the blind spot as a speed tradeoff
|
|
681
|
+
# can opt out; the cost is silent staleness on constants edits.
|
|
682
|
+
#
|
|
683
|
+
# Default-true breaks the `defined? && new_flag.nil?` memo shape
|
|
684
|
+
# (first read returns nil instead of true). Explicit ternary
|
|
685
|
+
# handles first-call / ENV / explicit-arg cases uniformly.
|
|
686
|
+
def transitive_load_tracking(new_flag = nil)
|
|
687
|
+
return @transitive_load_tracking if defined?(@transitive_load_tracking) && new_flag.nil?
|
|
688
|
+
|
|
689
|
+
@transitive_load_tracking = if ENV.key?('RSPEC_TRACER_TRANSITIVE_LOAD_TRACKING')
|
|
690
|
+
ENV['RSPEC_TRACER_TRANSITIVE_LOAD_TRACKING'] != 'false'
|
|
691
|
+
elsif new_flag.nil?
|
|
692
|
+
true
|
|
693
|
+
else
|
|
694
|
+
new_flag == true
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# Opt-in for narrow schema attribution. Default `false` - the
|
|
699
|
+
# Preset `:schema` category already declared-glob-tracks db/schema.rb
|
|
700
|
+
# + db/structure.sql as a safe over-approximation (any schema change
|
|
701
|
+
# re-runs every example). Calling this method opts in to an
|
|
702
|
+
# `sql.active_record` subscriber that emits schema inputs only for
|
|
703
|
+
# examples that actually touched AR during the run, narrowing the
|
|
704
|
+
# re-run set to DB-touching examples.
|
|
705
|
+
#
|
|
706
|
+
# Shape: setter-only DSL (like `track_rails_defaults`, `track_files`).
|
|
707
|
+
# Bare `track_ar_schema_notifications` in `.rspec-tracer` enables;
|
|
708
|
+
# explicit `track_ar_schema_notifications(false)` disables for tests
|
|
709
|
+
# or config overrides. Read the resulting state via the `?` variant,
|
|
710
|
+
# which layers the `RSPEC_TRACER_AR_SCHEMA_NOTIFICATIONS` ENV
|
|
711
|
+
# override on top of the DSL value.
|
|
712
|
+
#
|
|
713
|
+
# To use the narrower behavior, pair with `track_rails_defaults
|
|
714
|
+
# except: [:schema]` so the declared-glob path doesn't dominate the
|
|
715
|
+
# notification signal at graph registration. Leaving the Preset's
|
|
716
|
+
# :schema category on alongside this flag is a no-op in terms of
|
|
717
|
+
# the final re-run set - declared-glob attaches schema to every
|
|
718
|
+
# example regardless of what this subscriber emits.
|
|
719
|
+
# Setter DSL — bare call enables, explicit `(false)` disables.
|
|
720
|
+
# Any other positional value coerces to disabled (defensive for
|
|
721
|
+
# typos). Read the resulting state via {#track_ar_schema_notifications?}.
|
|
722
|
+
#
|
|
723
|
+
# @param args [Array] positional args (bare = enable; `false` =
|
|
724
|
+
# disable; anything else = disable defensively).
|
|
725
|
+
# @return [Boolean]
|
|
726
|
+
# @note **Common Rails setups widen this to whole-suite-on-schema-
|
|
727
|
+
# change.** Per-example AR cleanup mechanisms
|
|
728
|
+
# (`use_transactional_fixtures = true`, DatabaseCleaner
|
|
729
|
+
# `:truncation` / `:deletion` / `:transaction`) fire
|
|
730
|
+
# `sql.active_record` inside the per-example bucket, attributing
|
|
731
|
+
# `db/schema.rb` to every AR-touching example. A boot-time warn
|
|
732
|
+
# fires when the precondition isn't met. See README "Narrow
|
|
733
|
+
# AR-schema attribution".
|
|
734
|
+
def track_ar_schema_notifications(*args)
|
|
735
|
+
# Setter DSL: bare call enables, explicit `(false)` disables,
|
|
736
|
+
# any other positional coerces to false (defensive for typos).
|
|
737
|
+
@track_ar_schema_notifications = args.empty? || args.first == true
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# Reads the resolved AR-schema-notifications state. The
|
|
741
|
+
# `RSPEC_TRACER_AR_SCHEMA_NOTIFICATIONS` env var overrides the
|
|
742
|
+
# DSL value when set.
|
|
743
|
+
#
|
|
744
|
+
# @return [Boolean]
|
|
745
|
+
def track_ar_schema_notifications?
|
|
746
|
+
return ENV['RSPEC_TRACER_AR_SCHEMA_NOTIFICATIONS'] == 'true' if ENV.key?('RSPEC_TRACER_AR_SCHEMA_NOTIFICATIONS')
|
|
747
|
+
return false unless defined?(@track_ar_schema_notifications)
|
|
748
|
+
|
|
749
|
+
@track_ar_schema_notifications == true
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
# Override the parallel_tests lock-file path (default
|
|
753
|
+
# `rspec_tracer.lock`). Reads `RSPEC_TRACER_LOCK_FILE`.
|
|
754
|
+
#
|
|
755
|
+
# @param new_file [String, nil]
|
|
756
|
+
# @return [String]
|
|
192
757
|
def lock_file(new_file = nil)
|
|
193
758
|
return @lock_file if defined?(@lock_file) && @lock_file && new_file.nil?
|
|
194
759
|
|
|
@@ -199,6 +764,11 @@ module RSpecTracer
|
|
|
199
764
|
end
|
|
200
765
|
end
|
|
201
766
|
|
|
767
|
+
# Set the tracer's log level. Reads `RSPEC_TRACER_LOG_LEVEL`.
|
|
768
|
+
#
|
|
769
|
+
# @param new_level [Symbol, String, nil] one of `:off`, `:debug`,
|
|
770
|
+
# `:info`, `:warn`, `:error`. Default `:info`.
|
|
771
|
+
# @return [Integer] numeric level (LOG_LEVEL constant value).
|
|
202
772
|
def log_level(new_level = nil)
|
|
203
773
|
return @log_level if defined?(@log_level) && @log_level && new_level.nil?
|
|
204
774
|
|
|
@@ -212,42 +782,596 @@ module RSpecTracer
|
|
|
212
782
|
@log_level = LOG_LEVEL[level.to_s.to_sym].to_i
|
|
213
783
|
end
|
|
214
784
|
|
|
785
|
+
# Lazy-initialized {RSpecTracer::Logger} instance honoring
|
|
786
|
+
# {#log_level}.
|
|
787
|
+
#
|
|
788
|
+
# @return [RSpecTracer::Logger]
|
|
215
789
|
def logger
|
|
216
790
|
@logger ||= RSpecTracer::Logger.new(log_level)
|
|
217
791
|
end
|
|
218
792
|
|
|
793
|
+
# Track files for SimpleCov coverage emission only (NOT for the
|
|
794
|
+
# tracer's per-example dependency graph). Use {#track_files} for
|
|
795
|
+
# the dependency graph.
|
|
796
|
+
#
|
|
797
|
+
# @param glob [String] file glob pattern.
|
|
798
|
+
# @return [String]
|
|
219
799
|
def coverage_track_files(glob)
|
|
220
800
|
@coverage_track_files = glob
|
|
221
801
|
end
|
|
222
802
|
|
|
803
|
+
# @return [String, nil] the configured {#coverage_track_files} value.
|
|
223
804
|
def coverage_tracked_files
|
|
224
805
|
@coverage_track_files if defined?(@coverage_track_files)
|
|
225
806
|
end
|
|
226
807
|
|
|
808
|
+
# Declare globs of files every example depends on (e.g.
|
|
809
|
+
# `Gemfile.lock`, `db/schema.rb`, `config/locales/**/*.yml`).
|
|
810
|
+
# Globs accumulate across calls; the tracker resolves matched
|
|
811
|
+
# files at boot and attaches them as `:declared`-kind inputs
|
|
812
|
+
# to every example. For files only specific examples depend on,
|
|
813
|
+
# use the per-example `tracks: { files: '...' }` metadata DSL.
|
|
814
|
+
#
|
|
815
|
+
# @param globs [Array<String>] file glob patterns (each matched
|
|
816
|
+
# via `Dir.glob` with `FNM_PATHNAME | FNM_EXTGLOB`).
|
|
817
|
+
# @return [Array<String>] the accumulated glob list.
|
|
818
|
+
# @raise [InvalidUsageError] if called after the tracker started.
|
|
819
|
+
# @example
|
|
820
|
+
# track_files 'config/locales/**/*.yml', 'db/schema.rb', 'Gemfile.lock'
|
|
821
|
+
def track_files(*globs)
|
|
822
|
+
if defined?(@declared_globs_frozen) && @declared_globs_frozen
|
|
823
|
+
raise InvalidUsageError,
|
|
824
|
+
'track_files cannot be called after the tracker has started'
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
@track_files_globs ||= []
|
|
828
|
+
@track_files_globs.concat(globs.flatten.compact.map(&:to_s))
|
|
829
|
+
@track_files_globs
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
# Consolidated read-only view over `track_files` + legacy
|
|
833
|
+
# `coverage_track_files`. Order: user-declared first (preserves
|
|
834
|
+
# DSL call order), legacy value last. De-duplicated; frozen so
|
|
835
|
+
# downstream consumers treat it as immutable.
|
|
836
|
+
def declared_globs
|
|
837
|
+
all = []
|
|
838
|
+
all.concat(@track_files_globs) if defined?(@track_files_globs) && @track_files_globs
|
|
839
|
+
all << @coverage_track_files if defined?(@coverage_track_files) && @coverage_track_files
|
|
840
|
+
all.uniq.freeze
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
# Rails preset — attaches the common Rails-side declared globs in
|
|
844
|
+
# one DSL call. Expands to the glob set defined in
|
|
845
|
+
# `RSpecTracer::Rails::Preset` (views, helpers, locales, config,
|
|
846
|
+
# schema, factories, fixtures).
|
|
847
|
+
#
|
|
848
|
+
# @param except [Array<Symbol>] categories to opt out of (so a
|
|
849
|
+
# per-example subscriber attributes them instead). Common
|
|
850
|
+
# recipe: `track_rails_defaults except: [:views, :schema]`
|
|
851
|
+
# pairs with {#track_ar_schema_notifications} for narrow
|
|
852
|
+
# per-example schema attribution.
|
|
853
|
+
# @return [Array<String>] the resulting accumulated glob list.
|
|
854
|
+
# @example Default — all categories on
|
|
855
|
+
# track_rails_defaults
|
|
856
|
+
# @example Opt out of views + schema for per-example subscribers
|
|
857
|
+
# track_rails_defaults except: [:views, :schema]
|
|
858
|
+
# track_ar_schema_notifications
|
|
859
|
+
def track_rails_defaults(except: [])
|
|
860
|
+
require_relative 'rails/preset'
|
|
861
|
+
|
|
862
|
+
globs = RSpecTracer::Rails::Preset.globs(except: except)
|
|
863
|
+
track_files(*globs)
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
# One-way latch. Tracker.setup flips it so a stray
|
|
867
|
+
# `track_files` later in the boot sequence raises instead of
|
|
868
|
+
# silently accumulating into state that has already been read.
|
|
869
|
+
def freeze_declared_globs!
|
|
870
|
+
@declared_globs_frozen = true
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
# Config-level env-tracking DSL. Accumulates names across
|
|
874
|
+
# calls; bare entries flow into `Engine#setup`, are wildcard-
|
|
875
|
+
# expanded via `Tracker::EnvMatcher`, and attach to every
|
|
876
|
+
# previously-seen example (parallel to `track_files`'s "declared
|
|
877
|
+
# globs attach to every example" semantics).
|
|
878
|
+
#
|
|
879
|
+
# Wildcards: single trailing (`PREFIX_*`) or single leading
|
|
880
|
+
# (`*_SUFFIX`) only; `*` alone matches every env key. Multi-`*`,
|
|
881
|
+
# character classes, `?`, `!`, `\\` raise ArgumentError at run
|
|
882
|
+
# start (Engine#setup) so users see config errors immediately.
|
|
883
|
+
#
|
|
884
|
+
# Returns a frozen Array view (mirrors `ignore_spec_files` /
|
|
885
|
+
# `add_reporter` shape — defensive against callers mutating an
|
|
886
|
+
# internal accumulator). Setter raises if called after
|
|
887
|
+
# `freeze_declared_globs!` flipped the latch (same "tracker has
|
|
888
|
+
# started, no more declarations" contract as `track_files`).
|
|
889
|
+
# Declare env vars every example depends on. Wildcards expand
|
|
890
|
+
# against the live ENV at config load. For env vars only specific
|
|
891
|
+
# examples branch on, use the per-example
|
|
892
|
+
# `tracks: { env: '...' }` metadata DSL.
|
|
893
|
+
#
|
|
894
|
+
# @param names [Array<String, Symbol>] literal env names OR
|
|
895
|
+
# single-wildcard patterns (`'PREFIX_*'`, `'*_SUFFIX'`, `'*'`).
|
|
896
|
+
# @return [Array<String>] frozen accumulated names list.
|
|
897
|
+
# @raise [InvalidUsageError] if called after the tracker started,
|
|
898
|
+
# or if a pattern is malformed (multi-segment wildcards like
|
|
899
|
+
# `'A_*_B'`, character classes, etc. are rejected).
|
|
900
|
+
# @example
|
|
901
|
+
# track_env 'AUTH_TOKEN', 'DATABASE_URL', 'RAILS_*', '*_API_KEY'
|
|
902
|
+
def track_env(*names)
|
|
903
|
+
if defined?(@declared_globs_frozen) && @declared_globs_frozen
|
|
904
|
+
raise InvalidUsageError,
|
|
905
|
+
'track_env cannot be called after the tracker has started'
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
@track_env_names ||= []
|
|
909
|
+
@track_env_names.concat(names.flatten.compact.map(&:to_s))
|
|
910
|
+
@track_env_names.dup.freeze
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
# Reader for the accumulated config-level env names. Returns a
|
|
914
|
+
# frozen Array (`[].freeze` when never set), parallel to
|
|
915
|
+
# `declared_globs`'s shape.
|
|
916
|
+
def tracked_env_names
|
|
917
|
+
return EMPTY_TRACKED_ENV_NAMES unless defined?(@track_env_names) && @track_env_names
|
|
918
|
+
|
|
919
|
+
@track_env_names.dup.freeze
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
# Internal constant.
|
|
923
|
+
# @api private
|
|
924
|
+
EMPTY_TRACKED_ENV_NAMES = [].freeze
|
|
925
|
+
private_constant :EMPTY_TRACKED_ENV_NAMES
|
|
926
|
+
|
|
927
|
+
# Per-spec-file exclusion (closes upstream #41). Accumulates
|
|
928
|
+
# glob patterns that match test files rspec-tracer should leave
|
|
929
|
+
# alone: matching examples pass through RSpec unchanged, but the
|
|
930
|
+
# tracer does not compute an identity hash, does not run
|
|
931
|
+
# duplicate detection, and does not include them in any filter
|
|
932
|
+
# decision. Distinct from `add_filter` - that excludes *source*
|
|
933
|
+
# files from dependency attribution; `ignore_spec_files` excludes
|
|
934
|
+
# *spec* files from tracer visibility entirely.
|
|
935
|
+
#
|
|
936
|
+
# Typical use: a smoke-test spec with intentionally duplicated
|
|
937
|
+
# descriptions that rspec-tracer's `fail_on_duplicates=true`
|
|
938
|
+
# gate would otherwise reject.
|
|
939
|
+
#
|
|
940
|
+
# Shape mirrors `track_files`: bare call returns the current
|
|
941
|
+
# frozen array, any arg call accumulates.
|
|
942
|
+
def ignore_spec_files(*globs)
|
|
943
|
+
@ignore_spec_files_globs ||= []
|
|
944
|
+
@ignore_spec_files_globs.concat(globs.flatten.compact.map(&:to_s)) unless globs.empty?
|
|
945
|
+
@ignore_spec_files_globs.dup.freeze
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
# Runtime matcher consumed by RunnerHook. Normalizes `file_path`
|
|
949
|
+
# to three candidate forms - the raw RSpec-emitted path (often
|
|
950
|
+
# `./spec/foo_spec.rb`), the same with `./` stripped, and the
|
|
951
|
+
# root-relative form - then compares each against each configured
|
|
952
|
+
# glob via `File.fnmatch?`. Public because the RSpec hook layer
|
|
953
|
+
# calls it from outside the configure block.
|
|
954
|
+
def ignore_spec_file?(file_path)
|
|
955
|
+
return false if file_path.nil? || file_path.empty?
|
|
956
|
+
|
|
957
|
+
globs = defined?(@ignore_spec_files_globs) ? @ignore_spec_files_globs : nil
|
|
958
|
+
return false if globs.nil? || globs.empty?
|
|
959
|
+
|
|
960
|
+
candidates = _ignore_spec_file_candidates(file_path)
|
|
961
|
+
globs.any? do |glob|
|
|
962
|
+
candidates.any? { |candidate| File.fnmatch?(glob, candidate, File::FNM_PATHNAME) }
|
|
963
|
+
end
|
|
964
|
+
end
|
|
965
|
+
|
|
966
|
+
# Internal method on the tracer pipeline.
|
|
967
|
+
# @api private
|
|
968
|
+
def _ignore_spec_file_candidates(file_path)
|
|
969
|
+
candidates = [file_path]
|
|
970
|
+
stripped = file_path.delete_prefix('./')
|
|
971
|
+
candidates << stripped if stripped != file_path
|
|
972
|
+
relative = _relative_file_path(file_path)
|
|
973
|
+
candidates << relative if relative != file_path
|
|
974
|
+
candidates
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
# Internal method on the tracer pipeline.
|
|
978
|
+
# @api private
|
|
979
|
+
def _relative_file_path(file_path)
|
|
980
|
+
return file_path unless defined?(@root) && @root && file_path.start_with?("#{@root}/")
|
|
981
|
+
|
|
982
|
+
file_path[(@root.length + 1)..]
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
# Local-cache run-id retention. Default 5 keeps enough
|
|
986
|
+
# history for rollback debugging without letting the cache grow
|
|
987
|
+
# unbounded on long-lived machines (issue #20). 0 opts out (1.x
|
|
988
|
+
# behavior - dirs accumulate forever). ENV
|
|
989
|
+
# RSPEC_TRACER_CACHE_RETENTION_LOCAL_COUNT wins over the DSL
|
|
990
|
+
# argument, matching the other cache_* precedence conventions.
|
|
991
|
+
#
|
|
992
|
+
# Prune-on-save is handled by JsonBackend; this DSL just carries
|
|
993
|
+
# the configured value so the engine can pass it through on
|
|
994
|
+
# backend construction. One-off cleanup lives in the
|
|
995
|
+
# `rspec_tracer:cache:gc` Rake task.
|
|
996
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
997
|
+
def cache_retention_local_count(count = nil)
|
|
998
|
+
return @cache_retention_local_count if defined?(@cache_retention_local_count) && count.nil?
|
|
999
|
+
|
|
1000
|
+
if ENV.key?('RSPEC_TRACER_CACHE_RETENTION_LOCAL_COUNT')
|
|
1001
|
+
env_value = ENV['RSPEC_TRACER_CACHE_RETENTION_LOCAL_COUNT']
|
|
1002
|
+
unless env_value.match?(/\A\d+\z/)
|
|
1003
|
+
raise InvalidUsageError,
|
|
1004
|
+
"RSPEC_TRACER_CACHE_RETENTION_LOCAL_COUNT must be a non-negative integer, got #{env_value.inspect}"
|
|
1005
|
+
end
|
|
1006
|
+
@cache_retention_local_count = env_value.to_i
|
|
1007
|
+
elsif count.nil?
|
|
1008
|
+
@cache_retention_local_count = DEFAULT_CACHE_RETENTION_LOCAL_COUNT
|
|
1009
|
+
elsif count.is_a?(::Integer) && count >= 0
|
|
1010
|
+
@cache_retention_local_count = count
|
|
1011
|
+
else
|
|
1012
|
+
raise InvalidUsageError,
|
|
1013
|
+
"cache_retention_local_count must be a non-negative integer, got #{count.inspect}"
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
@cache_retention_local_count
|
|
1017
|
+
end
|
|
1018
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
1019
|
+
|
|
1020
|
+
# Size budgets. Both return non-negative Integer MiB.
|
|
1021
|
+
# Shape identical to cache_retention_local_count:
|
|
1022
|
+
# defined-and-nil-arg returns memo, ENV wins, then DSL arg, then
|
|
1023
|
+
# module default. 0 disables the respective check.
|
|
1024
|
+
#
|
|
1025
|
+
# Duplicated body rather than factored to a private helper
|
|
1026
|
+
# because configure's alias loop would wrap the helper as a
|
|
1027
|
+
# public `_name` DSL surface (see
|
|
1028
|
+
# feedback_configure_dsl_private_leak).
|
|
1029
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
1030
|
+
def cache_size_warn_per_file_mb(size_mb = nil)
|
|
1031
|
+
return @cache_size_warn_per_file_mb if defined?(@cache_size_warn_per_file_mb) && size_mb.nil?
|
|
1032
|
+
|
|
1033
|
+
if ENV.key?('RSPEC_TRACER_CACHE_SIZE_WARN_PER_FILE_MB')
|
|
1034
|
+
env_value = ENV['RSPEC_TRACER_CACHE_SIZE_WARN_PER_FILE_MB']
|
|
1035
|
+
unless env_value.match?(/\A\d+\z/)
|
|
1036
|
+
raise InvalidUsageError,
|
|
1037
|
+
"RSPEC_TRACER_CACHE_SIZE_WARN_PER_FILE_MB must be a non-negative integer, got #{env_value.inspect}"
|
|
1038
|
+
end
|
|
1039
|
+
@cache_size_warn_per_file_mb = env_value.to_i
|
|
1040
|
+
elsif size_mb.nil?
|
|
1041
|
+
@cache_size_warn_per_file_mb = DEFAULT_CACHE_SIZE_WARN_PER_FILE_MB
|
|
1042
|
+
elsif size_mb.is_a?(::Integer) && size_mb >= 0
|
|
1043
|
+
@cache_size_warn_per_file_mb = size_mb
|
|
1044
|
+
else
|
|
1045
|
+
raise InvalidUsageError,
|
|
1046
|
+
"cache_size_warn_per_file_mb must be a non-negative integer, got #{size_mb.inspect}"
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
@cache_size_warn_per_file_mb
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
# Internal method on the tracer pipeline.
|
|
1053
|
+
# @api private
|
|
1054
|
+
def cache_size_warn_total_mb(size_mb = nil)
|
|
1055
|
+
return @cache_size_warn_total_mb if defined?(@cache_size_warn_total_mb) && size_mb.nil?
|
|
1056
|
+
|
|
1057
|
+
if ENV.key?('RSPEC_TRACER_CACHE_SIZE_WARN_TOTAL_MB')
|
|
1058
|
+
env_value = ENV['RSPEC_TRACER_CACHE_SIZE_WARN_TOTAL_MB']
|
|
1059
|
+
unless env_value.match?(/\A\d+\z/)
|
|
1060
|
+
raise InvalidUsageError,
|
|
1061
|
+
"RSPEC_TRACER_CACHE_SIZE_WARN_TOTAL_MB must be a non-negative integer, got #{env_value.inspect}"
|
|
1062
|
+
end
|
|
1063
|
+
@cache_size_warn_total_mb = env_value.to_i
|
|
1064
|
+
elsif size_mb.nil?
|
|
1065
|
+
@cache_size_warn_total_mb = DEFAULT_CACHE_SIZE_WARN_TOTAL_MB
|
|
1066
|
+
elsif size_mb.is_a?(::Integer) && size_mb >= 0
|
|
1067
|
+
@cache_size_warn_total_mb = size_mb
|
|
1068
|
+
else
|
|
1069
|
+
raise InvalidUsageError,
|
|
1070
|
+
"cache_size_warn_total_mb must be a non-negative integer, got #{size_mb.inspect}"
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
@cache_size_warn_total_mb
|
|
1074
|
+
end
|
|
1075
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
1076
|
+
|
|
1077
|
+
# Storage backend selector with optional kwarg opts hash.
|
|
1078
|
+
# Symbol form: `storage_backend :json` or
|
|
1079
|
+
# `storage_backend :sqlite`. ENV `RSPEC_TRACER_STORAGE` wins over
|
|
1080
|
+
# the DSL argument, matching the `cache_dir` / `coverage_dir`
|
|
1081
|
+
# precedence convention so CI can swap backends without editing
|
|
1082
|
+
# `.rspec-tracer`.
|
|
1083
|
+
#
|
|
1084
|
+
# Options (:json only):
|
|
1085
|
+
# serializer: :json | :msgpack on-disk payload format;
|
|
1086
|
+
# ENV RSPEC_TRACER_STORAGE_SERIALIZER
|
|
1087
|
+
# wins over the opt.
|
|
1088
|
+
#
|
|
1089
|
+
# :sqlite does not accept any opts - its storage layout is a
|
|
1090
|
+
# single sqlite3 file with a normalized schema; serializer
|
|
1091
|
+
# substitution does not apply.
|
|
1092
|
+
# Configure the on-disk storage backend. Reads
|
|
1093
|
+
# `RSPEC_TRACER_STORAGE` for env-based override.
|
|
1094
|
+
#
|
|
1095
|
+
# @param name [Symbol, nil] one of `:json` (default; preserves
|
|
1096
|
+
# 1.x layout) or `:sqlite` (single-file DB; MRI 3.2+ only;
|
|
1097
|
+
# JRuby auto-falls-back to `:json` with a one-time warn).
|
|
1098
|
+
# @param opts [Hash] backend-specific opts. `:json` accepts
|
|
1099
|
+
# `serializer:` (`:json` default or `:msgpack`); `:sqlite`
|
|
1100
|
+
# accepts no opts.
|
|
1101
|
+
# @return [Symbol] the resolved backend name.
|
|
1102
|
+
# @raise [InvalidUsageError] for unknown backend names or
|
|
1103
|
+
# invalid opts.
|
|
1104
|
+
# @example JSON (default)
|
|
1105
|
+
# storage_backend :json
|
|
1106
|
+
# @example SQLite (faster cold reads above ~5,000 examples)
|
|
1107
|
+
# storage_backend :sqlite
|
|
1108
|
+
# @example JSON with msgpack serializer
|
|
1109
|
+
# storage_backend :json, serializer: :msgpack
|
|
1110
|
+
def storage_backend(name = nil, **opts)
|
|
1111
|
+
if defined?(@storage_backend_name) && @storage_backend_name && name.nil? && opts.empty?
|
|
1112
|
+
return @storage_backend_name
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
env = ENV.fetch('RSPEC_TRACER_STORAGE', nil)
|
|
1116
|
+
resolved = (env || name || DEFAULT_STORAGE_BACKEND).to_sym
|
|
1117
|
+
|
|
1118
|
+
unless STORAGE_BACKEND_NAMES.include?(resolved)
|
|
1119
|
+
raise InvalidUsageError,
|
|
1120
|
+
"unknown storage backend: #{resolved.inspect}; allowed: #{STORAGE_BACKEND_NAMES.inspect}"
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
@storage_backend_name = resolved
|
|
1124
|
+
@storage_backend_opts = validate_and_resolve_storage_opts(resolved, opts).freeze
|
|
1125
|
+
@storage_backend_name
|
|
1126
|
+
end
|
|
1127
|
+
|
|
1128
|
+
# Accessor for the opts hash passed to `storage_backend`.
|
|
1129
|
+
# Returns a frozen empty Hash when the DSL was called without
|
|
1130
|
+
# opts (or not called at all) so the backend dispatch can splat
|
|
1131
|
+
# `**storage_backend_opts` unconditionally.
|
|
1132
|
+
def storage_backend_opts
|
|
1133
|
+
return EMPTY_STORAGE_BACKEND_OPTS unless defined?(@storage_backend_opts) && @storage_backend_opts
|
|
1134
|
+
|
|
1135
|
+
@storage_backend_opts
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1138
|
+
# Internal constant.
|
|
1139
|
+
# @api private
|
|
1140
|
+
EMPTY_STORAGE_BACKEND_OPTS = {}.freeze
|
|
1141
|
+
private_constant :EMPTY_STORAGE_BACKEND_OPTS
|
|
1142
|
+
|
|
1143
|
+
# Configure which Ruby `Coverage` modes rspec-tracer enables on
|
|
1144
|
+
# the standalone path (when SimpleCov is not running). Pass a
|
|
1145
|
+
# Symbol Array drawn from {COVERAGE_MODES}; `coverage_modes [:lines,
|
|
1146
|
+
# :branches]` translates to `Coverage.start(lines: true, branches:
|
|
1147
|
+
# true)`.
|
|
1148
|
+
#
|
|
1149
|
+
# When SimpleCov is loaded and running at `RSpecTracer.start`
|
|
1150
|
+
# time, the DSL is inert — SimpleCov owns `Coverage.start` and
|
|
1151
|
+
# controls modes via its own `enable_coverage :branch` etc. (the
|
|
1152
|
+
# documented load-order contract; see UPGRADING `#SimpleCov branch
|
|
1153
|
+
# coverage now works`).
|
|
1154
|
+
#
|
|
1155
|
+
# The default `[:lines]` matches the pre-#195 bare `Coverage.start`
|
|
1156
|
+
# semantics so existing configs continue to emit the lines-only
|
|
1157
|
+
# `coverage.json` shape. The `coverage.json` artifact itself stays
|
|
1158
|
+
# on the 1.x `Array<Integer|nil>` per-file format regardless of
|
|
1159
|
+
# modes (storage-format stability). Branches / methods / etc.
|
|
1160
|
+
# data is available to SimpleCov via the documented interop and
|
|
1161
|
+
# to downstream consumers via `Coverage.peek_result` directly.
|
|
1162
|
+
#
|
|
1163
|
+
# @param modes [Array<Symbol>, Symbol, nil] one or more entries
|
|
1164
|
+
# from {COVERAGE_MODES} (`:lines`, `:branches`, `:methods`,
|
|
1165
|
+
# `:oneshot_lines`, `:eval`); a single Symbol is wrapped to
|
|
1166
|
+
# an Array.
|
|
1167
|
+
# @return [Array<Symbol>] the resolved (frozen) modes array. The
|
|
1168
|
+
# no-arg call returns the current setting (or
|
|
1169
|
+
# {DEFAULT_COVERAGE_MODES} if unset).
|
|
1170
|
+
# @raise [InvalidUsageError] when `modes` is empty or contains an
|
|
1171
|
+
# unknown mode.
|
|
1172
|
+
# @example Branch coverage on top of the default lines
|
|
1173
|
+
# coverage_modes [:lines, :branches]
|
|
1174
|
+
# @example Methods coverage in addition (for downstream tooling)
|
|
1175
|
+
# coverage_modes [:lines, :methods]
|
|
1176
|
+
def coverage_modes(*modes)
|
|
1177
|
+
return read_coverage_modes if modes.empty?
|
|
1178
|
+
|
|
1179
|
+
modes_array = Array(modes.length == 1 ? modes.first : modes).flatten.map(&:to_sym)
|
|
1180
|
+
raise InvalidUsageError, 'coverage_modes requires at least one mode (e.g. [:lines])' if modes_array.empty?
|
|
1181
|
+
|
|
1182
|
+
unknown = modes_array - COVERAGE_MODES
|
|
1183
|
+
unless unknown.empty?
|
|
1184
|
+
raise InvalidUsageError,
|
|
1185
|
+
"unknown coverage modes: #{unknown.inspect}; allowed: #{COVERAGE_MODES.inspect}"
|
|
1186
|
+
end
|
|
1187
|
+
|
|
1188
|
+
@coverage_modes = modes_array.uniq.freeze
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
# Hash form of {#coverage_modes} for splatting into
|
|
1192
|
+
# `Coverage.start`. `[:lines, :branches]` becomes
|
|
1193
|
+
# `{lines: true, branches: true}`.
|
|
1194
|
+
def coverage_modes_for_start
|
|
1195
|
+
coverage_modes.to_h { |m| [m, true] }.freeze
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
# Internal method on the tracer pipeline.
|
|
1199
|
+
# @api private
|
|
1200
|
+
def read_coverage_modes
|
|
1201
|
+
return @coverage_modes if defined?(@coverage_modes) && @coverage_modes
|
|
1202
|
+
|
|
1203
|
+
DEFAULT_COVERAGE_MODES
|
|
1204
|
+
end
|
|
1205
|
+
private :read_coverage_modes
|
|
1206
|
+
|
|
1207
|
+
# Internal method on the tracer pipeline.
|
|
1208
|
+
# @api private
|
|
1209
|
+
def validate_and_resolve_storage_opts(backend, opts)
|
|
1210
|
+
unknown = opts.keys - STORAGE_BACKEND_OPT_KEYS
|
|
1211
|
+
unless unknown.empty?
|
|
1212
|
+
raise InvalidUsageError,
|
|
1213
|
+
"unknown storage_backend options: #{unknown.inspect}; allowed: #{STORAGE_BACKEND_OPT_KEYS.inspect}"
|
|
1214
|
+
end
|
|
1215
|
+
|
|
1216
|
+
if backend == :sqlite && !opts.empty?
|
|
1217
|
+
raise InvalidUsageError,
|
|
1218
|
+
"storage_backend :sqlite does not accept options (got #{opts.keys.inspect})"
|
|
1219
|
+
end
|
|
1220
|
+
|
|
1221
|
+
return {} unless backend == :json
|
|
1222
|
+
|
|
1223
|
+
env_ser = ENV.fetch('RSPEC_TRACER_STORAGE_SERIALIZER', nil)
|
|
1224
|
+
serializer = (env_ser || opts[:serializer] || :json).to_sym
|
|
1225
|
+
|
|
1226
|
+
unless STORAGE_BACKEND_SERIALIZERS.include?(serializer)
|
|
1227
|
+
raise InvalidUsageError,
|
|
1228
|
+
"unknown storage serializer: #{serializer.inspect}; allowed: #{STORAGE_BACKEND_SERIALIZERS.inspect}"
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1231
|
+
{ serializer: serializer }
|
|
1232
|
+
end
|
|
1233
|
+
|
|
1234
|
+
# Reporter DSL. Accumulates each call onto an internal list of
|
|
1235
|
+
# `[name_or_class, opts]` pairs that `Reporters::Registry` walks
|
|
1236
|
+
# at finalize-time. Matches the `track_files` accumulator shape.
|
|
1237
|
+
#
|
|
1238
|
+
# Shapes:
|
|
1239
|
+
# add_reporter :terminal
|
|
1240
|
+
# add_reporter :json
|
|
1241
|
+
# add_reporter MyCustomReporter, color: false
|
|
1242
|
+
#
|
|
1243
|
+
# Symbol names must match `Reporters::Registry::BUILT_INS.keys`
|
|
1244
|
+
# (`:terminal`, `:json`, `:html`). Class values are
|
|
1245
|
+
# trusted - they must subclass / duck-type `Reporters::Base` but
|
|
1246
|
+
# validation is deferred to initialize-time so custom reporters
|
|
1247
|
+
# defined in user code don't have to exist at DSL-validate time.
|
|
1248
|
+
#
|
|
1249
|
+
# If the user never calls `add_reporter`, `Registry.emit_all`
|
|
1250
|
+
# falls back to `Registry::DEFAULTS` (`[:terminal, :json]`).
|
|
1251
|
+
def add_reporter(name_or_class, **opts)
|
|
1252
|
+
validate_reporter_entry(name_or_class)
|
|
1253
|
+
@reporters ||= []
|
|
1254
|
+
@reporters << [name_or_class, opts]
|
|
1255
|
+
@reporters.dup.freeze
|
|
1256
|
+
end
|
|
1257
|
+
|
|
1258
|
+
# Readonly access to the accumulated reporter entries. Returns
|
|
1259
|
+
# `nil` when no `add_reporter` calls were made - the Registry
|
|
1260
|
+
# distinguishes nil (fall back to defaults) from `[]` (user
|
|
1261
|
+
# opted out of every reporter).
|
|
1262
|
+
def reporters
|
|
1263
|
+
return nil unless defined?(@reporters)
|
|
1264
|
+
|
|
1265
|
+
@reporters.dup.freeze
|
|
1266
|
+
end
|
|
1267
|
+
|
|
1268
|
+
# Internal method on the tracer pipeline.
|
|
1269
|
+
# @api private
|
|
1270
|
+
def validate_reporter_entry(name_or_class)
|
|
1271
|
+
case name_or_class
|
|
1272
|
+
when ::Symbol
|
|
1273
|
+
allowed = reporter_builtins_keys
|
|
1274
|
+
return if allowed.include?(name_or_class)
|
|
1275
|
+
|
|
1276
|
+
raise InvalidUsageError,
|
|
1277
|
+
"unknown reporter: #{name_or_class.inspect}; allowed: #{allowed.inspect}"
|
|
1278
|
+
when ::Class
|
|
1279
|
+
nil
|
|
1280
|
+
else
|
|
1281
|
+
raise InvalidUsageError,
|
|
1282
|
+
"add_reporter: expected Symbol or Class, got #{name_or_class.class}"
|
|
1283
|
+
end
|
|
1284
|
+
end
|
|
1285
|
+
|
|
1286
|
+
# Internal method on the tracer pipeline.
|
|
1287
|
+
# @api private
|
|
1288
|
+
def reporter_builtins_keys
|
|
1289
|
+
return [] unless defined?(RSpecTracer::Reporters::Registry)
|
|
1290
|
+
|
|
1291
|
+
RSpecTracer::Reporters::Registry::BUILT_INS.keys
|
|
1292
|
+
end
|
|
1293
|
+
|
|
1294
|
+
# Add a filter to exclude files from the tracer's per-example
|
|
1295
|
+
# dependency graph. Files matching the filter are not registered
|
|
1296
|
+
# as deps, so changes to them don't trigger re-runs.
|
|
1297
|
+
#
|
|
1298
|
+
# The filter applies uniformly to both fresh per-example
|
|
1299
|
+
# attributions AND prior-snapshot carry-forward (warm-run path).
|
|
1300
|
+
# Adding a filter between runs immediately drops matching paths
|
|
1301
|
+
# from the next warm run's `@all_files` and dependency graph;
|
|
1302
|
+
# no cold run is required.
|
|
1303
|
+
#
|
|
1304
|
+
# The default filter list (loaded by `lib/rspec_tracer/load_default_config.rb`
|
|
1305
|
+
# before the user's `.rspec-tracer` runs) excludes Ruby toolchain
|
|
1306
|
+
# paths (`/vendor/bundle/`, `/usr/local/bundle/`, rbenv / asdf / rvm)
|
|
1307
|
+
# AND rspec-tracer's own output dirs (`/rspec_tracer_cache/`,
|
|
1308
|
+
# `/rspec_tracer_coverage/`, `/rspec_tracer_report/`,
|
|
1309
|
+
# `rspec_tracer.lock`). Use `filters.clear` in `.rspec-tracer`
|
|
1310
|
+
# before adding your own if you need to start from a blank list,
|
|
1311
|
+
# but be aware you'll then need to re-add the toolchain + tracer-
|
|
1312
|
+
# output exclusions yourself.
|
|
1313
|
+
#
|
|
1314
|
+
# @param filter [String, Regexp, Array, RSpecTracer::Filter, nil]
|
|
1315
|
+
# filter spec. Nil + a block also accepted; the block receives
|
|
1316
|
+
# a `source_file` Hash (`:name` / `:file_path`) and returns
|
|
1317
|
+
# true to exclude.
|
|
1318
|
+
# @return [Array] the updated filters list.
|
|
1319
|
+
# @example String match (substring)
|
|
1320
|
+
# add_filter '/vendor/bundle/'
|
|
1321
|
+
# @example Regex match
|
|
1322
|
+
# add_filter %r{^/helpers/}
|
|
1323
|
+
# @example Block
|
|
1324
|
+
# add_filter { |source_file| source_file[:file_path].include?('/helpers/') }
|
|
1325
|
+
# @example Array of mixed types
|
|
1326
|
+
# add_filter ['/helpers/', %r{^/utils/}]
|
|
227
1327
|
def add_filter(filter = nil, &)
|
|
228
1328
|
filters << parse_filter(filter, &)
|
|
229
1329
|
end
|
|
230
1330
|
|
|
1331
|
+
# @return [Array] the currently-registered dep-graph filters.
|
|
1332
|
+
# Use `filters.clear` to reset.
|
|
231
1333
|
def filters
|
|
232
1334
|
@filters ||= []
|
|
233
1335
|
end
|
|
234
1336
|
|
|
1337
|
+
# Bulk filters assignment is intentionally rejected — use
|
|
1338
|
+
# {#filters} `.clear` + repeated {#add_filter} instead so each
|
|
1339
|
+
# entry routes through the same parser.
|
|
1340
|
+
#
|
|
1341
|
+
# @raise [NotImplementedError]
|
|
235
1342
|
def filters=(new_filteres)
|
|
236
1343
|
raise NotImplementedError
|
|
237
1344
|
end
|
|
238
1345
|
|
|
1346
|
+
# Add a filter to exclude files from coverage reporting (NOT
|
|
1347
|
+
# from the tracer's dep graph). Use {#add_filter} for the
|
|
1348
|
+
# dep graph; this controls SimpleCov-style coverage output only.
|
|
1349
|
+
#
|
|
1350
|
+
# @param filter [String, Regexp, Array, nil] same shape as
|
|
1351
|
+
# {#add_filter}'s filter arg.
|
|
1352
|
+
# @return [Array]
|
|
1353
|
+
# @example
|
|
1354
|
+
# add_coverage_filter %w[/spec/ /test/ /vendor/bundle/]
|
|
239
1355
|
def add_coverage_filter(filter = nil, &)
|
|
240
1356
|
coverage_filters << parse_filter(filter, &)
|
|
241
1357
|
end
|
|
242
1358
|
|
|
1359
|
+
# @return [Array] the currently-registered coverage filters.
|
|
1360
|
+
# Use `coverage_filters.clear` to reset.
|
|
243
1361
|
def coverage_filters
|
|
244
1362
|
@coverage_filters ||= []
|
|
245
1363
|
end
|
|
246
1364
|
|
|
1365
|
+
# Bulk coverage_filters assignment intentionally rejected — see
|
|
1366
|
+
# {#filters=}.
|
|
1367
|
+
#
|
|
1368
|
+
# @raise [NotImplementedError]
|
|
247
1369
|
def coverage_filters=(new_filteres)
|
|
248
1370
|
raise NotImplementedError
|
|
249
1371
|
end
|
|
250
1372
|
|
|
1373
|
+
# Internal method on the tracer pipeline.
|
|
1374
|
+
# @api private
|
|
251
1375
|
def valid_s3_path?(s3_path)
|
|
252
1376
|
uri = URI.parse(s3_path)
|
|
253
1377
|
|
|
@@ -256,6 +1380,8 @@ module RSpecTracer
|
|
|
256
1380
|
false
|
|
257
1381
|
end
|
|
258
1382
|
|
|
1383
|
+
# Internal method on the tracer pipeline.
|
|
1384
|
+
# @api private
|
|
259
1385
|
def parallel_tests_id
|
|
260
1386
|
if ParallelTests.first_process?
|
|
261
1387
|
'parallel_tests_1'
|
|
@@ -264,6 +1390,8 @@ module RSpecTracer
|
|
|
264
1390
|
end
|
|
265
1391
|
end
|
|
266
1392
|
|
|
1393
|
+
# Internal method on the tracer pipeline.
|
|
1394
|
+
# @api private
|
|
267
1395
|
def parse_filter(filter = nil, &block)
|
|
268
1396
|
arg = filter || block
|
|
269
1397
|
|
|
@@ -271,5 +1399,70 @@ module RSpecTracer
|
|
|
271
1399
|
|
|
272
1400
|
RSpecTracer::Filter.register(arg)
|
|
273
1401
|
end
|
|
1402
|
+
|
|
1403
|
+
public
|
|
1404
|
+
|
|
1405
|
+
# Catches typos in `.rspec-tracer` config files. When the user
|
|
1406
|
+
# mistypes a DSL name (e.g. `track_files_glob` for `track_files`),
|
|
1407
|
+
# bare `NoMethodError` puts a Ruby backtrace in their face. This
|
|
1408
|
+
# surface raises `InvalidUsageError` with a stdlib `DidYouMean`
|
|
1409
|
+
# suggestion when the typo is close to a known DSL method;
|
|
1410
|
+
# otherwise it falls through to NoMethodError so internal
|
|
1411
|
+
# respond_to? probes / non-DSL undefined-method usage retains
|
|
1412
|
+
# standard Ruby semantics.
|
|
1413
|
+
def method_missing(name, *args, **kwargs, &)
|
|
1414
|
+
return super if name.to_s.end_with?('=')
|
|
1415
|
+
|
|
1416
|
+
candidates = RSpecTracer::Configuration::DslTypoSuggester.candidates
|
|
1417
|
+
suggestion = candidates.empty? ? nil : RSpecTracer::Configuration::DslTypoSuggester.nearest(name.to_s, candidates)
|
|
1418
|
+
return super if suggestion.nil?
|
|
1419
|
+
|
|
1420
|
+
raise InvalidUsageError,
|
|
1421
|
+
"unknown .rspec-tracer DSL method #{name.inspect}; did you mean #{suggestion.inspect}?"
|
|
1422
|
+
end
|
|
1423
|
+
|
|
1424
|
+
# Internal method on the tracer pipeline.
|
|
1425
|
+
# @api private
|
|
1426
|
+
def respond_to_missing?(name, include_private = false)
|
|
1427
|
+
super
|
|
1428
|
+
end
|
|
1429
|
+
|
|
1430
|
+
# Helpers for the DSL-typo `did you mean` surface. Lives in a
|
|
1431
|
+
# dedicated module so its methods stay outside the configure-time
|
|
1432
|
+
# `alias_method :"_#{name}"` wrapper loop in `Configuration#configure`
|
|
1433
|
+
# (which iterates Configuration's `private_instance_methods(false)`
|
|
1434
|
+
# twice during `load_default_config` + `load_local_config` and would
|
|
1435
|
+
# double-alias any helper instance methods we kept here).
|
|
1436
|
+
module DslTypoSuggester
|
|
1437
|
+
# Internal helper for the tracer pipeline.
|
|
1438
|
+
# @api private
|
|
1439
|
+
def self.candidates
|
|
1440
|
+
# Strip one or more leading `_` to canonicalize through the
|
|
1441
|
+
# configure wrapper's aliasing levels (single underscore after
|
|
1442
|
+
# one configure, double after two, etc.).
|
|
1443
|
+
RSpecTracer.private_methods(true).each_with_object([]) do |m, acc|
|
|
1444
|
+
s = m.to_s
|
|
1445
|
+
next unless s.start_with?('_')
|
|
1446
|
+
next if s.end_with?('=')
|
|
1447
|
+
|
|
1448
|
+
acc << s.sub(/\A_+/, '')
|
|
1449
|
+
end.uniq
|
|
1450
|
+
end
|
|
1451
|
+
|
|
1452
|
+
# Internal helper for the tracer pipeline.
|
|
1453
|
+
# @api private
|
|
1454
|
+
def self.nearest(typed, candidates)
|
|
1455
|
+
prefix_match = candidates
|
|
1456
|
+
.select { |c| typed.start_with?(c) || c.start_with?(typed) }
|
|
1457
|
+
.max_by(&:length)
|
|
1458
|
+
return prefix_match if prefix_match
|
|
1459
|
+
|
|
1460
|
+
require 'did_you_mean'
|
|
1461
|
+
::DidYouMean::SpellChecker.new(dictionary: candidates).correct(typed).first
|
|
1462
|
+
rescue LoadError
|
|
1463
|
+
nil
|
|
1464
|
+
end
|
|
1465
|
+
end
|
|
274
1466
|
end
|
|
1467
|
+
# rubocop:enable Metrics/ModuleLength
|
|
275
1468
|
end
|