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
data/lib/rspec_tracer/example.rb
CHANGED
|
@@ -1,22 +1,147 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RSpecTracer
|
|
4
|
+
# Builds the identity-hash payload (`:example_id`-keyed Hash) that
|
|
5
|
+
# RSpec::RunnerHook attaches to every example pre-run.
|
|
6
|
+
#
|
|
7
|
+
# == Identity stability contract
|
|
8
|
+
#
|
|
9
|
+
# `example_id` is the MD5 of a stable subset of the payload:
|
|
10
|
+
# `example_group` (the describe block's *description* string),
|
|
11
|
+
# `description`, `full_description`, `shared_group` (inclusion
|
|
12
|
+
# locations with the trailing line number stripped), and
|
|
13
|
+
# `file_name`. The contract, in one line: *rename = new identity;
|
|
14
|
+
# restructure = same identity.*
|
|
15
|
+
#
|
|
16
|
+
# Identity is PRESERVED when:
|
|
17
|
+
# - blank lines or comments are added/removed around the example
|
|
18
|
+
# - examples are reordered within a describe block (named
|
|
19
|
+
# examples only - see "Unnamed examples" below)
|
|
20
|
+
# - a sibling describe / example in the same file is renamed
|
|
21
|
+
# - metadata changes (`skip:`, tags, `tracks: { ... }`) - the
|
|
22
|
+
# spec-file digest still triggers the re-run and status history
|
|
23
|
+
# is kept
|
|
24
|
+
# - the example body or its hooks (`before`, `let`) are edited -
|
|
25
|
+
# again, the file digest triggers the re-run
|
|
26
|
+
#
|
|
27
|
+
# Identity CHANGES (one cold "No cache" run) when:
|
|
28
|
+
# - the file is renamed or moved
|
|
29
|
+
# - the `describe` / `it` / shared-example name is changed
|
|
30
|
+
# - the example moves to a different describe block
|
|
31
|
+
#
|
|
32
|
+
# == Unnamed examples (`it { }`, `specify { }`, `example { }`)
|
|
33
|
+
#
|
|
34
|
+
# An example with no description string has no stable name to hash,
|
|
35
|
+
# and identity must be computed pre-run (for the filter decision),
|
|
36
|
+
# before RSpec generates a matcher-derived description. The only
|
|
37
|
+
# line-independent signal RSpec exposes pre-run is position, so an
|
|
38
|
+
# unnamed example's identity is derived from its ordinal among the
|
|
39
|
+
# *unnamed* examples of its group. (RSpec's `description` for an
|
|
40
|
+
# unnamed example is the line-bearing `"example at <path>:<line>"`
|
|
41
|
+
# fallback, which would otherwise leak the line number straight
|
|
42
|
+
# back into the digest - issue #210.)
|
|
43
|
+
#
|
|
44
|
+
# For unnamed examples the contract above is amended: identity is
|
|
45
|
+
# still PRESERVED across blank-line / comment edits, sibling
|
|
46
|
+
# renames, and adding or removing *named* siblings - but it CHANGES
|
|
47
|
+
# when the unnamed examples are reordered, or one is inserted or
|
|
48
|
+
# removed ahead of it. Give an example an explicit description
|
|
49
|
+
# (`it 'does X' do`) for a fully reorder-stable identity.
|
|
50
|
+
#
|
|
51
|
+
# `line_number` / `rerun_file_name` / `rerun_line_number` stay in
|
|
52
|
+
# the returned Hash for the reporter + `explain` location columns,
|
|
53
|
+
# but are DELIBERATELY EXCLUDED from the digest - a no-op edit that
|
|
54
|
+
# shifts line numbers must not invalidate the cache. `example_group`
|
|
55
|
+
# uses `example_group.description` (the user's string) rather than
|
|
56
|
+
# `example_group.name`: RSpec's generated class name carries a
|
|
57
|
+
# load-order-dependent `_2` / `_3` suffix when two files share a
|
|
58
|
+
# describe name, which would otherwise flip the id across runs.
|
|
59
|
+
#
|
|
60
|
+
# Helpers are `def self.x` + `private_class_method` so mutant
|
|
61
|
+
# attributes mutations through the singleton call path.
|
|
4
62
|
module Example
|
|
5
|
-
|
|
63
|
+
# Identity-keyed cache of `<example_group> => Array<unnamed sibling>`
|
|
64
|
+
# populated lazily by `unnamed_description`. A group with N unnamed
|
|
65
|
+
# examples computes the sibling list once per group rather than N
|
|
66
|
+
# times. Memory is bounded by the live group set (RSpec retains
|
|
67
|
+
# those for the run anyway).
|
|
68
|
+
@unnamed_siblings_cache = {}.compare_by_identity
|
|
6
69
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
70
|
+
# Builds the identity payload for one RSpec example. The MD5 is
|
|
71
|
+
# taken over the stability-contract subset (see `digest_identity`
|
|
72
|
+
# for the named-vs-unnamed split); `line_number` / `rerun_*` ride
|
|
73
|
+
# along in the returned Hash for the reporters but never enter the
|
|
74
|
+
# digest. See the module comment for the full stability contract.
|
|
75
|
+
# @api private
|
|
76
|
+
def self.from(example)
|
|
77
|
+
location = example_location(example)
|
|
78
|
+
identity = {
|
|
79
|
+
example_group: example.example_group.description,
|
|
10
80
|
description: example.description,
|
|
11
81
|
full_description: example.full_description,
|
|
12
82
|
shared_group: example.metadata[:shared_group_inclusion_backtrace]
|
|
13
|
-
.map(
|
|
14
|
-
|
|
83
|
+
.map { |frame| frame.formatted_inclusion_location.sub(/:\d+\z/, '') },
|
|
84
|
+
file_name: location[:file_name]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
identity
|
|
88
|
+
.merge(location)
|
|
89
|
+
.merge(example_id: Digest::MD5.hexdigest(digest_identity(example, identity).to_json))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The Hash actually fed to the MD5. For a named example this is
|
|
93
|
+
# `identity` unchanged - byte-identical to the pre-#210 digest.
|
|
94
|
+
# For an unnamed example (`it { }` / `specify { }` / `example { }`)
|
|
95
|
+
# RSpec's `description` is the line-bearing location fallback
|
|
96
|
+
# `"example at <path>:<line>"`, so `description` is swapped for a
|
|
97
|
+
# line-independent positional discriminator before hashing. The
|
|
98
|
+
# returned/stored payload still carries RSpec's `description` /
|
|
99
|
+
# `full_description` untouched - only the digest input differs.
|
|
100
|
+
# @api private
|
|
101
|
+
def self.digest_identity(example, identity)
|
|
102
|
+
return identity unless unnamed?(example)
|
|
103
|
+
|
|
104
|
+
identity.merge(description: unnamed_description(example))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# True when the example has no explicit description string. RSpec's
|
|
108
|
+
# raw `metadata[:description]` is `""` for `it { }` / `specify { }`
|
|
109
|
+
# / `example { }`; the `description` *method* would instead return
|
|
110
|
+
# the line-bearing `"example at <path>:<line>"` fallback, so the
|
|
111
|
+
# raw metadata value is what cleanly tells named from unnamed.
|
|
112
|
+
# @api private
|
|
113
|
+
def self.unnamed?(example)
|
|
114
|
+
example.metadata[:description].to_s.strip.empty?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Line-independent identity discriminator for an unnamed example:
|
|
118
|
+
# its 0-based ordinal among the *unnamed* examples of its group.
|
|
119
|
+
# Stable across blank-line / comment edits and across adding or
|
|
120
|
+
# renaming *named* siblings; changes only when the unnamed examples
|
|
121
|
+
# are reordered or one is inserted / removed ahead of it (the
|
|
122
|
+
# documented carve-out - see the module comment). Closes the
|
|
123
|
+
# remaining #196 gap reported in #210.
|
|
124
|
+
#
|
|
125
|
+
# The unnamed-siblings list is memoized per example_group object,
|
|
126
|
+
# so a group with N unnamed examples computes the list once, not N
|
|
127
|
+
# times. The discriminator string takes a Ruby-inspect-style
|
|
128
|
+
# `#<...>` form to make spoofing implausible - and even if a user
|
|
129
|
+
# description literally matched, full_description would still
|
|
130
|
+
# differ between the unnamed example (`"<group> "`) and the named
|
|
131
|
+
# one (`"<group> #<...>"`), so no real digest collision is
|
|
132
|
+
# possible.
|
|
133
|
+
# @api private
|
|
134
|
+
def self.unnamed_description(example)
|
|
135
|
+
group = example.example_group
|
|
136
|
+
unnamed_siblings = @unnamed_siblings_cache[group] ||=
|
|
137
|
+
group.examples.select { |sibling| unnamed?(sibling) }
|
|
15
138
|
|
|
16
|
-
|
|
139
|
+
"#<rspec-tracer unnamed example #{unnamed_siblings.index(example)}>"
|
|
17
140
|
end
|
|
18
141
|
|
|
19
|
-
|
|
142
|
+
# Internal helper for the tracer pipeline.
|
|
143
|
+
# @api private
|
|
144
|
+
def self.example_location(example)
|
|
20
145
|
metadata = example.metadata
|
|
21
146
|
|
|
22
147
|
location = {
|
|
@@ -34,7 +159,9 @@ module RSpecTracer
|
|
|
34
159
|
location.merge(example_rerun_location(example.example_group.parent_groups))
|
|
35
160
|
end
|
|
36
161
|
|
|
37
|
-
|
|
162
|
+
# Internal helper for the tracer pipeline.
|
|
163
|
+
# @api private
|
|
164
|
+
def self.example_rerun_location(example_groups)
|
|
38
165
|
example_groups.each do |example_group|
|
|
39
166
|
metadata = example_group.metadata
|
|
40
167
|
|
|
@@ -47,12 +174,15 @@ module RSpecTracer
|
|
|
47
174
|
end
|
|
48
175
|
end
|
|
49
176
|
|
|
50
|
-
|
|
177
|
+
# Internal helper for the tracer pipeline.
|
|
178
|
+
# @api private
|
|
179
|
+
def self.location_file_name(rspec_file_name)
|
|
51
180
|
file_path = RSpecTracer::SourceFile.file_path(rspec_file_name)
|
|
52
181
|
|
|
53
182
|
RSpecTracer::SourceFile.file_name(file_path)
|
|
54
183
|
end
|
|
55
184
|
|
|
56
|
-
private_class_method :
|
|
185
|
+
private_class_method :digest_identity, :unnamed?, :unnamed_description,
|
|
186
|
+
:example_location, :example_rerun_location, :location_file_name
|
|
57
187
|
end
|
|
58
188
|
end
|
data/lib/rspec_tracer/filter.rb
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RSpecTracer
|
|
4
|
+
# Internal Filter — see {RSpecTracer} for the user-facing surface.
|
|
5
|
+
# @api private
|
|
6
|
+
#
|
|
7
|
+
# Internal dispatch shim for the user-facing `add_filter` /
|
|
8
|
+
# `add_coverage_filter` DSL. The user passes a String / Regexp /
|
|
9
|
+
# Proc / Array; `Filter.register` wraps it in the matching subclass
|
|
10
|
+
# so the engine can call a uniform `#match?(source_file)`.
|
|
4
11
|
class Filter
|
|
12
|
+
# Internal attribute.
|
|
13
|
+
# @api private
|
|
5
14
|
attr_reader :filter
|
|
6
15
|
|
|
16
|
+
# Internal helper for the tracer pipeline.
|
|
17
|
+
# @api private
|
|
7
18
|
def self.register(filter)
|
|
8
19
|
return filter if filter.is_a?(Filter)
|
|
9
20
|
|
|
10
21
|
filter_class(filter).new(filter)
|
|
11
22
|
end
|
|
12
23
|
|
|
24
|
+
# Internal helper for the tracer pipeline.
|
|
25
|
+
# @api private
|
|
13
26
|
def self.filter_class(filter)
|
|
14
27
|
case filter
|
|
15
28
|
when String
|
|
@@ -25,16 +38,24 @@ module RSpecTracer
|
|
|
25
38
|
end
|
|
26
39
|
end
|
|
27
40
|
|
|
41
|
+
# Internal method on the tracer pipeline.
|
|
42
|
+
# @api private
|
|
28
43
|
def initialize(filter)
|
|
29
44
|
@filter = filter
|
|
30
45
|
end
|
|
31
46
|
|
|
47
|
+
# Internal method on the tracer pipeline.
|
|
48
|
+
# @api private
|
|
32
49
|
def match?(_source_file)
|
|
33
50
|
raise "#{self.class.name}#match? is not intended for direct use"
|
|
34
51
|
end
|
|
35
52
|
end
|
|
36
53
|
|
|
54
|
+
# Internal ArrayFilter — see {RSpecTracer} for the user-facing surface.
|
|
55
|
+
# @api private
|
|
37
56
|
class ArrayFilter < RSpecTracer::Filter
|
|
57
|
+
# Internal method on the tracer pipeline.
|
|
58
|
+
# @api private
|
|
38
59
|
def initialize(filters)
|
|
39
60
|
filter_list = filters.each_with_object([]) do |filter, list|
|
|
40
61
|
list << Filter.register(filter)
|
|
@@ -43,24 +64,38 @@ module RSpecTracer
|
|
|
43
64
|
super(filter_list)
|
|
44
65
|
end
|
|
45
66
|
|
|
67
|
+
# Internal method on the tracer pipeline.
|
|
68
|
+
# @api private
|
|
46
69
|
def match?(source_file)
|
|
47
70
|
@filter.any? { |filter| filter.match?(source_file) }
|
|
48
71
|
end
|
|
49
72
|
end
|
|
50
73
|
|
|
74
|
+
# Internal BlockFilter — see {RSpecTracer} for the user-facing surface.
|
|
75
|
+
# @api private
|
|
51
76
|
class BlockFilter < RSpecTracer::Filter
|
|
77
|
+
# Internal method on the tracer pipeline.
|
|
78
|
+
# @api private
|
|
52
79
|
def match?(source_file)
|
|
53
80
|
@filter.call(source_file)
|
|
54
81
|
end
|
|
55
82
|
end
|
|
56
83
|
|
|
84
|
+
# Internal RegexFilter — see {RSpecTracer} for the user-facing surface.
|
|
85
|
+
# @api private
|
|
57
86
|
class RegexFilter < RSpecTracer::Filter
|
|
87
|
+
# Internal method on the tracer pipeline.
|
|
88
|
+
# @api private
|
|
58
89
|
def match?(source_file)
|
|
59
90
|
source_file[:file_name] =~ @filter
|
|
60
91
|
end
|
|
61
92
|
end
|
|
62
93
|
|
|
94
|
+
# Internal StringFilter — see {RSpecTracer} for the user-facing surface.
|
|
95
|
+
# @api private
|
|
63
96
|
class StringFilter < RSpecTracer::Filter
|
|
97
|
+
# Internal method on the tracer pipeline.
|
|
98
|
+
# @api private
|
|
64
99
|
def match?(source_file)
|
|
65
100
|
source_file[:file_name].include?(@filter)
|
|
66
101
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpecTracer
|
|
4
|
+
# Per-engine line-stub builder for files that need a synthetic
|
|
5
|
+
# all-nil-but-executable-lines coverage array. Used by
|
|
6
|
+
# `Reporters::CoverageJsonReporter` when a tracked file is in
|
|
7
|
+
# `coverage_tracked_files` but has no recorded coverage (file never
|
|
8
|
+
# loaded during this run).
|
|
9
|
+
#
|
|
10
|
+
# Lives at the top level of lib/rspec_tracer so the per-engine
|
|
11
|
+
# branches sit outside the tracker_coverage_gate's
|
|
12
|
+
# 100%-line+branch contract (the JRuby branch cannot be exercised
|
|
13
|
+
# on MRI; cross-engine coverage rollup would require fork-per-engine
|
|
14
|
+
# CI work that isn't justified for stub-line generation).
|
|
15
|
+
#
|
|
16
|
+
# `def self.x` per feedback_mutation_friendly_modules so future
|
|
17
|
+
# mutation gating maps to the singleton form.
|
|
18
|
+
module LineStub
|
|
19
|
+
# Internal helper for the tracer pipeline.
|
|
20
|
+
# @api private
|
|
21
|
+
def self.for(file_path)
|
|
22
|
+
case RUBY_ENGINE
|
|
23
|
+
when 'ruby'
|
|
24
|
+
ruby(file_path)
|
|
25
|
+
when 'jruby'
|
|
26
|
+
jruby(file_path)
|
|
27
|
+
else
|
|
28
|
+
File.foreach(file_path).map { nil }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Internal helper for the tracer pipeline.
|
|
33
|
+
# @api private
|
|
34
|
+
def self.ruby(file_path)
|
|
35
|
+
lines = File.foreach(file_path).map { nil }
|
|
36
|
+
iseqs = [::RubyVM::InstructionSequence.compile_file(file_path)]
|
|
37
|
+
until iseqs.empty?
|
|
38
|
+
iseq = iseqs.pop
|
|
39
|
+
iseq.trace_points.each { |line_number, type| lines[line_number - 1] = 0 if type == :line }
|
|
40
|
+
iseq.each_child { |child| iseqs << child }
|
|
41
|
+
end
|
|
42
|
+
lines
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Internal helper for the tracer pipeline.
|
|
46
|
+
# @api private
|
|
47
|
+
def self.jruby(file_path)
|
|
48
|
+
lines = File.foreach(file_path).map { nil }
|
|
49
|
+
root_node = ::JRuby.parse(File.read(file_path, encoding: 'UTF-8'))
|
|
50
|
+
visitor = org.jruby.ast.visitor.NodeVisitor.impl do |_name, node|
|
|
51
|
+
if node.newline?
|
|
52
|
+
ln = node.respond_to?(:position) ? node.position.line : node.line
|
|
53
|
+
lines[ln] = 0
|
|
54
|
+
end
|
|
55
|
+
node.child_nodes.each { |child| child&.accept(visitor) }
|
|
56
|
+
end
|
|
57
|
+
root_node.accept(visitor)
|
|
58
|
+
lines
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -8,8 +8,8 @@ require_relative 'load_global_config'
|
|
|
8
8
|
require_relative 'load_local_config'
|
|
9
9
|
|
|
10
10
|
# NOTE: `Configuration#configure` installs the public DSL wrappers
|
|
11
|
-
# (alias `_name` + redefine `name` to forward `|*args, &block|`)
|
|
12
|
-
# first time any configurer runs. `load_default_config` is
|
|
11
|
+
# (alias `_name` + redefine `name` to forward `|*args, **kwargs, &block|`)
|
|
12
|
+
# the first time any configurer runs. `load_default_config` is
|
|
13
13
|
# unconditional, so the wrappers are guaranteed to exist before anyone
|
|
14
14
|
# can call `RSpecTracer.add_filter` etc. No second install step is
|
|
15
15
|
# needed here.
|
data/lib/rspec_tracer/logger.rb
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RSpecTracer
|
|
4
|
+
# Internal Logger — see {RSpecTracer} for the user-facing surface.
|
|
5
|
+
# @api private
|
|
6
|
+
#
|
|
7
|
+
# Internal logger; thin wrapper around `puts` gated on numeric log
|
|
8
|
+
# level. Exposed via `RSpecTracer.logger`.
|
|
4
9
|
class Logger
|
|
10
|
+
# Internal method on the tracer pipeline.
|
|
11
|
+
# @api private
|
|
5
12
|
def initialize(log_level)
|
|
6
13
|
@log_level = log_level
|
|
7
14
|
end
|
|
8
15
|
|
|
16
|
+
# Internal method on the tracer pipeline.
|
|
17
|
+
# @api private
|
|
9
18
|
def debug(message)
|
|
10
19
|
puts message if @log_level == 1
|
|
11
20
|
end
|
|
12
21
|
|
|
22
|
+
# Internal method on the tracer pipeline.
|
|
23
|
+
# @api private
|
|
13
24
|
def info(message)
|
|
14
25
|
puts message if @log_level.between?(1, 2)
|
|
15
26
|
end
|
|
16
27
|
|
|
28
|
+
# Internal method on the tracer pipeline.
|
|
29
|
+
# @api private
|
|
17
30
|
def warn(message)
|
|
18
31
|
puts message if @log_level.between?(1, 3)
|
|
19
32
|
end
|
|
20
33
|
|
|
34
|
+
# Internal method on the tracer pipeline.
|
|
35
|
+
# @api private
|
|
21
36
|
def error(message)
|
|
22
37
|
puts message if @log_level.between?(1, 4)
|
|
23
38
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Rails integration
|
|
2
|
+
|
|
3
|
+
Loaded by `require 'rspec_tracer/rails'`. Exposes the Rails preset, the
|
|
4
|
+
Rails-side observer family, and a Railtie that integrates with the Rails
|
|
5
|
+
lifecycle when Rails is present in the process.
|
|
6
|
+
|
|
7
|
+
## Surface
|
|
8
|
+
|
|
9
|
+
- **`RSpecTracer::Rails::Preset`** - closed-enum glob set covering the
|
|
10
|
+
Coverage-invisible Rails surface (views, helpers, locales, config
|
|
11
|
+
YAML, schema, factories, fixtures). Opt in via `track_rails_defaults`
|
|
12
|
+
in `.rspec-tracer`; opt out per category via
|
|
13
|
+
`track_rails_defaults except: [:views, :locales]`.
|
|
14
|
+
- **`RSpecTracer::Rails::Railtie`** - only loaded when
|
|
15
|
+
`defined?(::Rails::Railtie)`. Registers a single `rspec_tracer.setup`
|
|
16
|
+
initializer that logs a confirmation line when Rails boots.
|
|
17
|
+
- **`RSpecTracer::Rails::Notifications`** - ActiveSupport::Notifications
|
|
18
|
+
observer for the render_template / render_partial / render_collection
|
|
19
|
+
events (ActionView) plus an opt-in sql.active_record subscriber for
|
|
20
|
+
narrow schema attribution. Installed by `Engine.setup` when
|
|
21
|
+
`RSpecTracer.rails?` is true. Emits `:template` Inputs for observed
|
|
22
|
+
template renders and `:notification` Inputs for schema files on the
|
|
23
|
+
first AR query per example.
|
|
24
|
+
- **`RSpecTracer::Rails::I18nTracking`** - prepends onto
|
|
25
|
+
`::I18n::Backend::Base#load_translations` so every I18n backend
|
|
26
|
+
(including custom Redis/DB/Chain backends that bypass
|
|
27
|
+
`YAML.load_file`) emits `:notification` Inputs for the translation
|
|
28
|
+
files it loads.
|
|
29
|
+
|
|
30
|
+
## Detection
|
|
31
|
+
|
|
32
|
+
`RSpecTracer.rails?` returns true when `::Rails::VERSION` is defined at
|
|
33
|
+
the time `RSpecTracer.start` runs. The flag is computed once during
|
|
34
|
+
`initial_setup`; subsequent Rails loads do not flip it.
|
|
35
|
+
|
|
36
|
+
## Zero-cost when Rails is absent
|
|
37
|
+
|
|
38
|
+
`require 'rspec_tracer/rails'` loads Preset unconditionally and skips
|
|
39
|
+
the Railtie via `defined?(::Rails::Railtie)`. Notifications and
|
|
40
|
+
I18nTracking are required lazily by `Engine.setup` only when
|
|
41
|
+
`RSpecTracer.rails?` is truthy. A pure-Ruby suite that accidentally
|
|
42
|
+
requires the file never pays for Rails-specific code.
|
|
43
|
+
|
|
44
|
+
## Narrow schema attribution (opt-in)
|
|
45
|
+
|
|
46
|
+
By default, `track_rails_defaults` attaches `db/schema.rb` and
|
|
47
|
+
`db/structure.sql` to every example via the Preset's `:schema`
|
|
48
|
+
declared-glob - a conservative whole-suite signal. Teams that want
|
|
49
|
+
schema changes to re-run only examples that actually touched AR can
|
|
50
|
+
opt into an `sql.active_record` subscriber:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
RSpecTracer.configure do
|
|
54
|
+
track_rails_defaults except: [:schema]
|
|
55
|
+
track_ar_schema_notifications
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
On the first `sql.active_record` event inside an example, the
|
|
60
|
+
subscriber emits `:notification` Inputs for `db/schema.rb` and
|
|
61
|
+
`db/structure.sql` if they exist under the project root, then
|
|
62
|
+
short-circuits for the remainder of that example. A non-DB-touching
|
|
63
|
+
example never sees the schema inputs and is not re-run on schema edits.
|
|
64
|
+
|
|
65
|
+
Leaving `:schema` in `track_rails_defaults` while also enabling the
|
|
66
|
+
subscriber is a no-op in terms of the re-run set - declared-glob
|
|
67
|
+
dominates at graph registration.
|
|
68
|
+
|
|
69
|
+
## Components
|
|
70
|
+
|
|
71
|
+
- **Preset + detection + Railtie scaffold.**
|
|
72
|
+
- **Notifications + I18nTracking observers**, the
|
|
73
|
+
`track_ar_schema_notifications` opt-in DSL, and `Engine.setup`
|
|
74
|
+
wiring. Factory and fixture coverage ride the existing
|
|
75
|
+
`LoadedFilesTracker` / `YAML.load_file` hook surface.
|
|
76
|
+
- **Integration coverage** against the reference Rails app
|
|
77
|
+
(`spec/fixtures/rails_app/`) verifying the full "change X -> Y
|
|
78
|
+
re-runs" behavior matrix.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../tracker/file_digest'
|
|
4
|
+
require_relative '../tracker/input'
|
|
5
|
+
require_relative 'notifications'
|
|
6
|
+
|
|
7
|
+
module RSpecTracer
|
|
8
|
+
# Internal Rails — see {RSpecTracer} for the user-facing surface.
|
|
9
|
+
# @api private
|
|
10
|
+
module Rails
|
|
11
|
+
# I18n backend observer. Covers custom backends (Redis-backed,
|
|
12
|
+
# DB-backed, Chain) that bypass YAML.load_file and would otherwise
|
|
13
|
+
# miss the IOHooks YAML hook.
|
|
14
|
+
#
|
|
15
|
+
# Mechanism: Module#prepend onto ::I18n::Backend::Base - every
|
|
16
|
+
# backend subclass's load_translations resolves through the hook,
|
|
17
|
+
# even when the subclass overrides and super-calls Base. Backends
|
|
18
|
+
# that never super-call Base#load_translations fall through the
|
|
19
|
+
# hook, but the common backends (Simple, Chain, Cascade) all do.
|
|
20
|
+
#
|
|
21
|
+
# Shares Notifications' thread-local bucket so Engine.setup opens
|
|
22
|
+
# and clears one bucket per example that covers both observer
|
|
23
|
+
# families. Engine harvests `bucket.values` at example_finished.
|
|
24
|
+
#
|
|
25
|
+
# Graceful degradation:
|
|
26
|
+
# - install no-ops if ::I18n::Backend::Base is absent (tracer
|
|
27
|
+
# boot survives even in weird I18n-free app graphs).
|
|
28
|
+
# - Every record call swallows StandardError (CLAUDE.md) - a
|
|
29
|
+
# digest failure or bucket-shape surprise never propagates into
|
|
30
|
+
# the user's test run.
|
|
31
|
+
class I18nTracking
|
|
32
|
+
class << self
|
|
33
|
+
# Internal attribute.
|
|
34
|
+
# @api private
|
|
35
|
+
attr_reader :root
|
|
36
|
+
|
|
37
|
+
# Internal method on the tracer pipeline.
|
|
38
|
+
# @api private
|
|
39
|
+
def install(root:, filter: ->(_path) { true })
|
|
40
|
+
@root = File.expand_path(root)
|
|
41
|
+
@root_prefix = "#{@root}/"
|
|
42
|
+
@filter = filter
|
|
43
|
+
@prepended = prepend_backend_hook
|
|
44
|
+
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Prepended modules cannot be removed from the ancestry chain
|
|
49
|
+
# (Ruby has no public API), mirroring IOHooks.uninstall. Every
|
|
50
|
+
# hook entry point fast-rejects on @root_prefix nil once
|
|
51
|
+
# install state clears, so post-uninstall the hook is a no-op.
|
|
52
|
+
def uninstall
|
|
53
|
+
@root = nil
|
|
54
|
+
@root_prefix = nil
|
|
55
|
+
@filter = nil
|
|
56
|
+
@prepended = false
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Internal method on the tracer pipeline.
|
|
61
|
+
# @api private
|
|
62
|
+
def installed?
|
|
63
|
+
!@root_prefix.nil?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Called from LoadTranslationsHook for every filename passed
|
|
67
|
+
# to I18n::Backend::Base#load_translations. Array form keeps
|
|
68
|
+
# the hook a single call site.
|
|
69
|
+
def record_translations(filenames)
|
|
70
|
+
return nil if @root_prefix.nil?
|
|
71
|
+
|
|
72
|
+
Array(filenames).each { |path| record_translation(path) }
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Pure-logic entry point. Same fast-reject ladder as
|
|
77
|
+
# Notifications#record_template. Emits :notification kind
|
|
78
|
+
# so the I18n source is distinguishable from template events
|
|
79
|
+
# in downstream reporters. The ladder is longer than rubocop's
|
|
80
|
+
# perceived-complexity threshold by design - each guard is a
|
|
81
|
+
# cheap fast-reject.
|
|
82
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
83
|
+
def record_translation(path)
|
|
84
|
+
return nil if @root_prefix.nil?
|
|
85
|
+
|
|
86
|
+
bucket = Notifications.current_bucket
|
|
87
|
+
return nil if bucket.nil?
|
|
88
|
+
return nil unless path.is_a?(String) || path.respond_to?(:to_s)
|
|
89
|
+
|
|
90
|
+
path_str = path.to_s
|
|
91
|
+
return nil unless path_str.start_with?(@root_prefix)
|
|
92
|
+
return nil unless @filter.call(path_str)
|
|
93
|
+
|
|
94
|
+
identity = "notification:#{path_str[@root_prefix.length..]}"
|
|
95
|
+
return nil if bucket.key?(identity)
|
|
96
|
+
|
|
97
|
+
digest = Tracker::FileDigest.compute(path_str)
|
|
98
|
+
return nil if digest.nil?
|
|
99
|
+
|
|
100
|
+
bucket[identity] = Tracker::Input.for_file(
|
|
101
|
+
path: path_str, kind: :notification, digest: digest, root: @root
|
|
102
|
+
)
|
|
103
|
+
rescue StandardError
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# Internal method on the tracer pipeline.
|
|
111
|
+
# @api private
|
|
112
|
+
def prepend_backend_hook
|
|
113
|
+
return false unless defined?(::I18n::Backend::Base)
|
|
114
|
+
|
|
115
|
+
::I18n::Backend::Base.prepend(LoadTranslationsHook)
|
|
116
|
+
true
|
|
117
|
+
rescue StandardError
|
|
118
|
+
false
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Prepended onto I18n::Backend::Base. Every subclass's
|
|
123
|
+
# load_translations ultimately resolves through here via super.
|
|
124
|
+
# Intercepts the filename list, delegates recording to the
|
|
125
|
+
# singleton, and forwards to the real implementation.
|
|
126
|
+
# @api private
|
|
127
|
+
module LoadTranslationsHook
|
|
128
|
+
# Internal method on the tracer pipeline.
|
|
129
|
+
# @api private
|
|
130
|
+
def load_translations(*filenames)
|
|
131
|
+
RSpecTracer::Rails::I18nTracking.record_translations(filenames)
|
|
132
|
+
super
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|