rspec-tracer 1.2.2 → 2.0.0.pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +197 -45
- data/README.md +439 -429
- data/bin/rspec-tracer +15 -0
- data/lib/rspec_tracer/cache/Rakefile +43 -0
- data/lib/rspec_tracer/cli/cache_clear.rb +98 -0
- data/lib/rspec_tracer/cli/cache_info.rb +103 -0
- data/lib/rspec_tracer/cli/doctor.rb +275 -0
- data/lib/rspec_tracer/cli/explain.rb +148 -0
- data/lib/rspec_tracer/cli/report_open.rb +82 -0
- data/lib/rspec_tracer/cli.rb +116 -0
- data/lib/rspec_tracer/configuration.rb +1100 -3
- data/lib/rspec_tracer/engine.rb +1076 -0
- data/lib/rspec_tracer/example.rb +21 -6
- data/lib/rspec_tracer/filter.rb +35 -0
- data/lib/rspec_tracer/line_stub.rb +61 -0
- data/lib/rspec_tracer/load_config.rb +2 -2
- data/lib/rspec_tracer/logger.rb +15 -0
- data/lib/rspec_tracer/rails/README.md +78 -0
- data/lib/rspec_tracer/rails/i18n_tracking.rb +137 -0
- data/lib/rspec_tracer/rails/notifications.rb +263 -0
- data/lib/rspec_tracer/rails/preset.rb +94 -0
- data/lib/rspec_tracer/rails/railtie.rb +22 -0
- data/lib/rspec_tracer/rails.rb +15 -0
- data/lib/rspec_tracer/remote_cache/README.md +140 -0
- data/lib/rspec_tracer/remote_cache/Rakefile +35 -11
- data/lib/rspec_tracer/remote_cache/archive.rb +137 -0
- data/lib/rspec_tracer/remote_cache/backend.rb +73 -0
- data/lib/rspec_tracer/remote_cache/git_ancestry.rb +241 -0
- data/lib/rspec_tracer/remote_cache/local_fs_backend.rb +439 -0
- data/lib/rspec_tracer/remote_cache/redis_backend.rb +554 -0
- data/lib/rspec_tracer/remote_cache/s3_backend.rb +712 -0
- data/lib/rspec_tracer/remote_cache/user_tasks.rb +397 -0
- data/lib/rspec_tracer/remote_cache/validator.rb +40 -62
- data/lib/rspec_tracer/remote_cache.rb +22 -0
- data/lib/rspec_tracer/reporters/README.md +103 -0
- data/lib/rspec_tracer/reporters/base.rb +87 -0
- data/lib/rspec_tracer/reporters/coverage_json_reporter.rb +338 -0
- data/lib/rspec_tracer/reporters/html/.gitignore +19 -0
- data/lib/rspec_tracer/reporters/html/.prettierignore +4 -0
- data/lib/rspec_tracer/reporters/html/.prettierrc.json +9 -0
- data/lib/rspec_tracer/reporters/html/README.md +80 -0
- data/lib/rspec_tracer/reporters/html/dist/assets/index.css +2 -0
- data/lib/rspec_tracer/reporters/html/dist/assets/index.js +1 -0
- data/lib/rspec_tracer/reporters/html/dist/index.html +24 -0
- data/lib/rspec_tracer/reporters/html/eslint.config.js +62 -0
- data/lib/rspec_tracer/reporters/html/package-lock.json +4941 -0
- data/lib/rspec_tracer/reporters/html/package.json +29 -0
- data/lib/rspec_tracer/reporters/html/src/app.jsx +130 -0
- data/lib/rspec_tracer/reporters/html/src/components/AllExamples.jsx +86 -0
- data/lib/rspec_tracer/reporters/html/src/components/DuplicateExamples.jsx +68 -0
- data/lib/rspec_tracer/reporters/html/src/components/ExamplesDependency.jsx +78 -0
- data/lib/rspec_tracer/reporters/html/src/components/FilesDependency.jsx +72 -0
- data/lib/rspec_tracer/reporters/html/src/components/FlakyExamples.jsx +42 -0
- data/lib/rspec_tracer/reporters/html/src/components/ReportTable.jsx +131 -0
- data/lib/rspec_tracer/reporters/html/src/components/SearchBar.jsx +19 -0
- data/lib/rspec_tracer/reporters/html/src/index.html +23 -0
- data/lib/rspec_tracer/reporters/html/src/main.jsx +37 -0
- data/lib/rspec_tracer/reporters/html/src/styles.css +434 -0
- data/lib/rspec_tracer/reporters/html/vite.config.js +42 -0
- data/lib/rspec_tracer/reporters/html_reporter.rb +266 -0
- data/lib/rspec_tracer/reporters/json_reporter.rb +88 -0
- data/lib/rspec_tracer/reporters/payload_builder.rb +235 -0
- data/lib/rspec_tracer/reporters/registry.rb +120 -0
- data/lib/rspec_tracer/reporters/terminal_reporter.rb +264 -0
- data/lib/rspec_tracer/rspec/README.md +73 -0
- data/lib/rspec_tracer/rspec/installation.rb +97 -0
- data/lib/rspec_tracer/rspec/metadata.rb +96 -0
- data/lib/rspec_tracer/rspec/parallel_tests.rb +459 -0
- data/lib/rspec_tracer/rspec/reporter_hook.rb +84 -0
- data/lib/rspec_tracer/rspec/runner_hook.rb +178 -0
- data/lib/rspec_tracer/source_file.rb +24 -7
- data/lib/rspec_tracer/storage/README.md +35 -0
- data/lib/rspec_tracer/storage/backend.rb +68 -0
- data/lib/rspec_tracer/storage/json_backend.rb +866 -0
- data/lib/rspec_tracer/storage/lazy_snapshot.rb +65 -0
- data/lib/rspec_tracer/storage/schema.rb +43 -0
- data/lib/rspec_tracer/storage/serializer/json.rb +41 -0
- data/lib/rspec_tracer/storage/serializer/msgpack.rb +90 -0
- data/lib/rspec_tracer/storage/snapshot.rb +127 -0
- data/lib/rspec_tracer/storage/sqlite_backend.rb +686 -0
- data/lib/rspec_tracer/time_formatter.rb +37 -18
- data/lib/rspec_tracer/tracker/README.md +36 -0
- data/lib/rspec_tracer/tracker/coverage_adapter.rb +174 -0
- data/lib/rspec_tracer/tracker/declared_globs.rb +100 -0
- data/lib/rspec_tracer/tracker/dependency_graph.rb +134 -0
- data/lib/rspec_tracer/tracker/env_matcher.rb +127 -0
- data/lib/rspec_tracer/tracker/env_snapshot.rb +77 -0
- data/lib/rspec_tracer/tracker/example_registry.rb +153 -0
- data/lib/rspec_tracer/tracker/file_digest.rb +61 -0
- data/lib/rspec_tracer/tracker/filter.rb +127 -0
- data/lib/rspec_tracer/tracker/input.rb +99 -0
- data/lib/rspec_tracer/tracker/io_hooks/file.rb +55 -0
- data/lib/rspec_tracer/tracker/io_hooks/io.rb +24 -0
- data/lib/rspec_tracer/tracker/io_hooks/json.rb +23 -0
- data/lib/rspec_tracer/tracker/io_hooks/kernel.rb +26 -0
- data/lib/rspec_tracer/tracker/io_hooks/yaml.rb +38 -0
- data/lib/rspec_tracer/tracker/io_hooks.rb +195 -0
- data/lib/rspec_tracer/tracker/loaded_files_tracker.rb +295 -0
- data/lib/rspec_tracer/tracker/new_file_detector.rb +62 -0
- data/lib/rspec_tracer/tracker/whole_suite_invalidators.rb +96 -0
- data/lib/rspec_tracer/version.rb +4 -1
- data/lib/rspec_tracer.rb +232 -381
- metadata +93 -43
- data/lib/rspec_tracer/cache.rb +0 -207
- data/lib/rspec_tracer/coverage_merger.rb +0 -42
- data/lib/rspec_tracer/coverage_reporter.rb +0 -187
- data/lib/rspec_tracer/coverage_writer.rb +0 -58
- data/lib/rspec_tracer/html_reporter/Rakefile +0 -18
- data/lib/rspec_tracer/html_reporter/assets/javascripts/application.js +0 -56
- data/lib/rspec_tracer/html_reporter/assets/javascripts/libraries/jquery.js +0 -10881
- data/lib/rspec_tracer/html_reporter/assets/javascripts/plugins/datatables.js +0 -15381
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/application.css +0 -196
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/datatables.css +0 -459
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/jquery-ui.css +0 -436
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/print.css +0 -92
- data/lib/rspec_tracer/html_reporter/assets/stylesheets/reset.css +0 -265
- data/lib/rspec_tracer/html_reporter/public/application.css +0 -5
- data/lib/rspec_tracer/html_reporter/public/application.js +0 -6
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc_disabled.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_both.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc_disabled.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/favicon.png +0 -0
- data/lib/rspec_tracer/html_reporter/public/loading.gif +0 -0
- data/lib/rspec_tracer/html_reporter/reporter.rb +0 -242
- data/lib/rspec_tracer/html_reporter/views/duplicate_examples.erb +0 -34
- data/lib/rspec_tracer/html_reporter/views/examples.erb +0 -58
- data/lib/rspec_tracer/html_reporter/views/examples_dependency.erb +0 -36
- data/lib/rspec_tracer/html_reporter/views/files_dependency.erb +0 -36
- data/lib/rspec_tracer/html_reporter/views/flaky_examples.erb +0 -38
- data/lib/rspec_tracer/html_reporter/views/layout.erb +0 -38
- data/lib/rspec_tracer/remote_cache/aws.rb +0 -176
- data/lib/rspec_tracer/remote_cache/cache.rb +0 -75
- data/lib/rspec_tracer/remote_cache/repo.rb +0 -210
- data/lib/rspec_tracer/report_generator.rb +0 -158
- data/lib/rspec_tracer/report_merger.rb +0 -68
- data/lib/rspec_tracer/report_writer.rb +0 -141
- data/lib/rspec_tracer/reporter.rb +0 -204
- data/lib/rspec_tracer/rspec_reporter.rb +0 -41
- data/lib/rspec_tracer/rspec_runner.rb +0 -56
- data/lib/rspec_tracer/ruby_coverage.rb +0 -9
- data/lib/rspec_tracer/runner.rb +0 -278
data/lib/rspec_tracer/example.rb
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
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
|
+
# All methods declared `def self.x` + `private_class_method` for
|
|
8
|
+
# the private helpers per feedback_mutation_friendly_modules:
|
|
9
|
+
# mutant maps tests to the singleton form. Public API
|
|
10
|
+
# (`Example.from`) is the only call site `runner_hook.rb` + the
|
|
11
|
+
# runner-hook spec drive; the three private helpers stay reachable
|
|
12
|
+
# only from `from` itself.
|
|
4
13
|
module Example
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def from(example)
|
|
14
|
+
# Internal helper for the tracer pipeline.
|
|
15
|
+
# @api private
|
|
16
|
+
def self.from(example)
|
|
8
17
|
data = {
|
|
9
18
|
example_group: example.example_group.name,
|
|
10
19
|
description: example.description,
|
|
@@ -16,7 +25,9 @@ module RSpecTracer
|
|
|
16
25
|
data.merge(example_id: Digest::MD5.hexdigest(data.to_json))
|
|
17
26
|
end
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
# Internal helper for the tracer pipeline.
|
|
29
|
+
# @api private
|
|
30
|
+
def self.example_location(example)
|
|
20
31
|
metadata = example.metadata
|
|
21
32
|
|
|
22
33
|
location = {
|
|
@@ -34,7 +45,9 @@ module RSpecTracer
|
|
|
34
45
|
location.merge(example_rerun_location(example.example_group.parent_groups))
|
|
35
46
|
end
|
|
36
47
|
|
|
37
|
-
|
|
48
|
+
# Internal helper for the tracer pipeline.
|
|
49
|
+
# @api private
|
|
50
|
+
def self.example_rerun_location(example_groups)
|
|
38
51
|
example_groups.each do |example_group|
|
|
39
52
|
metadata = example_group.metadata
|
|
40
53
|
|
|
@@ -47,7 +60,9 @@ module RSpecTracer
|
|
|
47
60
|
end
|
|
48
61
|
end
|
|
49
62
|
|
|
50
|
-
|
|
63
|
+
# Internal helper for the tracer pipeline.
|
|
64
|
+
# @api private
|
|
65
|
+
def self.location_file_name(rspec_file_name)
|
|
51
66
|
file_path = RSpecTracer::SourceFile.file_path(rspec_file_name)
|
|
52
67
|
|
|
53
68
|
RSpecTracer::SourceFile.file_name(file_path)
|
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
|