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.rb
CHANGED
|
@@ -10,84 +10,109 @@ require 'json'
|
|
|
10
10
|
require 'pathname'
|
|
11
11
|
require 'set'
|
|
12
12
|
|
|
13
|
-
require_relative 'rspec_tracer/coverage_merger'
|
|
14
|
-
require_relative 'rspec_tracer/coverage_reporter'
|
|
15
|
-
require_relative 'rspec_tracer/coverage_writer'
|
|
16
13
|
require_relative 'rspec_tracer/defaults'
|
|
14
|
+
require_relative 'rspec_tracer/engine'
|
|
17
15
|
require_relative 'rspec_tracer/example'
|
|
18
|
-
|
|
16
|
+
# Reporters must load before load_config so the Configuration DSL's
|
|
17
|
+
# `add_reporter` can validate symbol names against
|
|
18
|
+
# `Reporters::Registry::BUILT_INS` when a user `.rspec-tracer` calls
|
|
19
|
+
# it at configure time.
|
|
20
|
+
require_relative 'rspec_tracer/reporters/base'
|
|
21
|
+
require_relative 'rspec_tracer/reporters/payload_builder'
|
|
22
|
+
require_relative 'rspec_tracer/reporters/coverage_json_reporter'
|
|
23
|
+
require_relative 'rspec_tracer/reporters/json_reporter'
|
|
24
|
+
require_relative 'rspec_tracer/reporters/terminal_reporter'
|
|
25
|
+
require_relative 'rspec_tracer/reporters/html_reporter'
|
|
26
|
+
require_relative 'rspec_tracer/reporters/registry'
|
|
19
27
|
require_relative 'rspec_tracer/load_config'
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
require_relative 'rspec_tracer/
|
|
26
|
-
require_relative 'rspec_tracer/
|
|
27
|
-
require_relative 'rspec_tracer/runner'
|
|
28
|
+
# RemoteCache is loaded lazily from its Rakefile shim (user-driven),
|
|
29
|
+
# not at gem-load time. The user-facing tasks `rspec_tracer:remote_cache:*`
|
|
30
|
+
# pull in `lib/rspec_tracer/remote_cache.rb` when the user's Rakefile
|
|
31
|
+
# loads the shim. Test-suite runs that never invoke a cache task pay
|
|
32
|
+
# zero load cost for aws/git subshell code.
|
|
33
|
+
require_relative 'rspec_tracer/rspec/installation'
|
|
34
|
+
require_relative 'rspec_tracer/rspec/parallel_tests'
|
|
28
35
|
require_relative 'rspec_tracer/source_file'
|
|
29
36
|
require_relative 'rspec_tracer/time_formatter'
|
|
30
37
|
require_relative 'rspec_tracer/version'
|
|
31
38
|
|
|
39
|
+
# Top-level entry point. Drives the lifecycle:
|
|
40
|
+
#
|
|
41
|
+
# RSpecTracer.start
|
|
42
|
+
# -> RSpec::Installation.install! (prepend RunnerHook + ReporterHook)
|
|
43
|
+
# -> setup_coverage (::Coverage.start unless SimpleCov owns it)
|
|
44
|
+
# -> setup_rails (detect ::Rails::VERSION)
|
|
45
|
+
# -> Engine.new.setup (observers + cache load + filter decisions)
|
|
46
|
+
#
|
|
47
|
+
# at_exit_behavior (installed via `at_exit` elsewhere in the boot
|
|
48
|
+
# flow) runs the finalize stack: Engine#finalize writes the 13-file
|
|
49
|
+
# snapshot via Storage::JsonBackend, Reporters::CoverageJsonReporter
|
|
50
|
+
# writes coverage.json (single owner, replacing the 1.x
|
|
51
|
+
# CoverageReporter + CoverageWriter pair retired in 2.0),
|
|
52
|
+
# ParallelTests#finalize! merges per-worker caches on the last worker.
|
|
32
53
|
module RSpecTracer
|
|
33
|
-
# Filesystem barrier markers, layered on top of parallel_tests's
|
|
34
|
-
# pid-file wait to defend against the GHA-observed race where the
|
|
35
|
-
# gem's `wait_for_other_processes_to_finish` returns while a sibling
|
|
36
|
-
# worker hasn't fully flushed its `parallel_tests_N/` dir yet. Each
|
|
37
|
-
# worker writes BOOT at setup-time and DONE as the first step of its
|
|
38
|
-
# at_exit tasks; the elected worker waits for every booted peer's
|
|
39
|
-
# DONE marker (deadline-bounded) before proceeding to merge + purge.
|
|
40
|
-
PARALLEL_TESTS_BOOT_MARKER_FILENAME = '.rspec_tracer_boot'
|
|
41
|
-
PARALLEL_TESTS_DONE_MARKER_FILENAME = '.rspec_tracer_done'
|
|
42
|
-
PARALLEL_TESTS_PEER_DONE_DEADLINE_SECONDS = 5
|
|
43
|
-
|
|
44
54
|
class << self
|
|
55
|
+
# Internal attribute.
|
|
56
|
+
# @api private
|
|
45
57
|
attr_accessor :running, :pid, :no_examples, :duplicate_examples
|
|
46
58
|
|
|
59
|
+
# Boot the tracer. Idempotent — safe to call multiple times in a
|
|
60
|
+
# single process (subsequent calls return without re-installing
|
|
61
|
+
# hooks). Drives the lifecycle:
|
|
62
|
+
#
|
|
63
|
+
# * Installs the RSpec runner / reporter prepend chain.
|
|
64
|
+
# * Starts `::Coverage` unless SimpleCov already owns it.
|
|
65
|
+
# * Detects Rails (memoized in `RSpecTracer.rails?`).
|
|
66
|
+
# * Builds the {RSpecTracer::Engine} and installs observers.
|
|
67
|
+
#
|
|
68
|
+
# Must be called BEFORE any application code loads so the boot
|
|
69
|
+
# set captured by `Coverage.peek_result` is empty. With SimpleCov,
|
|
70
|
+
# call `SimpleCov.start` first; rspec-tracer warns at boot when
|
|
71
|
+
# SimpleCov is loaded but not started.
|
|
72
|
+
#
|
|
73
|
+
# @return [void]
|
|
47
74
|
def start
|
|
75
|
+
return if defined?(@started) && @started
|
|
76
|
+
|
|
48
77
|
RSpecTracer.running = false
|
|
49
78
|
RSpecTracer.pid = Process.pid
|
|
50
|
-
|
|
51
|
-
|
|
79
|
+
@run_started_at = ::Time.now.utc
|
|
80
|
+
@run_monotonic_start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
81
|
+
@started = true
|
|
52
82
|
|
|
53
83
|
RSpecTracer.logger.debug "Started RSpec tracer (pid: #{RSpecTracer.pid})"
|
|
54
84
|
|
|
55
|
-
|
|
85
|
+
warn_on_simplecov_load_order_mistake
|
|
86
|
+
|
|
87
|
+
@parallel_tests = RSpecTracer::RSpec::ParallelTests.active?
|
|
88
|
+
RSpecTracer::RSpec::ParallelTests.setup! if parallel_tests?
|
|
56
89
|
initial_setup
|
|
57
90
|
end
|
|
58
91
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
tracer_example[:run_reason] = run_reason
|
|
73
|
-
example.metadata[:description] = "#{example.description} (#{run_reason})"
|
|
74
|
-
|
|
75
|
-
to_run[example_group] << example
|
|
76
|
-
groups << example.example_group.parent_groups.last
|
|
77
|
-
|
|
78
|
-
runner.register_example(tracer_example)
|
|
79
|
-
else
|
|
80
|
-
runner.on_example_skipped(example_id)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
runner.deregister_duplicate_examples
|
|
92
|
+
# SimpleCov load-order is part of the documented contract -
|
|
93
|
+
# SimpleCov.start MUST run before RSpecTracer.start when both are
|
|
94
|
+
# used together (see README §SimpleCov interop). When the user
|
|
95
|
+
# has SimpleCov loaded but not started, we'd silently call
|
|
96
|
+
# ::Coverage.start ourselves and SimpleCov's later setup would
|
|
97
|
+
# bolt onto a Coverage already in flight, with the user's
|
|
98
|
+
# add_filter calls applied after rspec-tracer started consuming
|
|
99
|
+
# data. Surface the load-order mistake at start time so the user
|
|
100
|
+
# gets a one-line warning instead of mysteriously-broken
|
|
101
|
+
# coverage output.
|
|
102
|
+
def warn_on_simplecov_load_order_mistake
|
|
103
|
+
return unless defined?(::SimpleCov)
|
|
104
|
+
return if ::SimpleCov.respond_to?(:running) && ::SimpleCov.running
|
|
86
105
|
|
|
87
|
-
|
|
106
|
+
RSpecTracer.logger.warn(
|
|
107
|
+
'SimpleCov is loaded but not started. ' \
|
|
108
|
+
'Call SimpleCov.start before RSpecTracer.start so the ' \
|
|
109
|
+
'tracer respects SimpleCov\'s filter chain. See README ' \
|
|
110
|
+
'section "Working with SimpleCov".'
|
|
111
|
+
)
|
|
88
112
|
end
|
|
89
|
-
# rubocop:enable Metrics/AbcSize
|
|
90
113
|
|
|
114
|
+
# Internal method on the tracer pipeline.
|
|
115
|
+
# @api private
|
|
91
116
|
def at_exit_behavior
|
|
92
117
|
return unless RSpecTracer.pid == Process.pid && RSpecTracer.running
|
|
93
118
|
|
|
@@ -95,164 +120,69 @@ module RSpecTracer
|
|
|
95
120
|
|
|
96
121
|
run_exit_tasks
|
|
97
122
|
ensure
|
|
98
|
-
|
|
123
|
+
if RSpecTracer::RSpec::ParallelTests.active? &&
|
|
124
|
+
RSpecTracer::RSpec::ParallelTests.last_process?
|
|
125
|
+
RSpecTracer::RSpec::ParallelTests.remove_lock_file!
|
|
126
|
+
end
|
|
99
127
|
|
|
100
128
|
RSpecTracer.running = false
|
|
101
129
|
end
|
|
102
130
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
@examples_traced_files[example_id] = @traced_files
|
|
111
|
-
@traced_files = Set.new
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def runner
|
|
115
|
-
@runner if defined?(@runner)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def coverage_reporter
|
|
119
|
-
@coverage_reporter if defined?(@coverage_reporter)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def report_writer
|
|
123
|
-
@report_writer if defined?(@report_writer)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def coverage_merger
|
|
127
|
-
@coverage_merger if defined?(@coverage_merger)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def report_merger
|
|
131
|
-
@report_merger if defined?(@report_merger)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def trace_point
|
|
135
|
-
@trace_point if defined?(@trace_point)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def traced_files
|
|
139
|
-
@traced_files if defined?(@traced_files)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def examples_traced_files
|
|
143
|
-
@examples_traced_files if defined?(@examples_traced_files)
|
|
131
|
+
# The current {RSpecTracer::Engine} instance, or nil if
|
|
132
|
+
# {.start} hasn't been called yet.
|
|
133
|
+
#
|
|
134
|
+
# @return [RSpecTracer::Engine, nil]
|
|
135
|
+
def engine
|
|
136
|
+
@engine if defined?(@engine)
|
|
144
137
|
end
|
|
145
138
|
|
|
139
|
+
# True if SimpleCov was loaded AND running at the time
|
|
140
|
+
# {.start} was invoked. Determines whether rspec-tracer
|
|
141
|
+
# owns `::Coverage.start` itself (false) or defers to
|
|
142
|
+
# SimpleCov's coverage lifecycle (true).
|
|
143
|
+
#
|
|
144
|
+
# @return [Boolean]
|
|
146
145
|
def simplecov?
|
|
147
146
|
defined?(@simplecov) && @simplecov == true
|
|
148
147
|
end
|
|
149
148
|
|
|
149
|
+
# True if `parallel_tests` is in use (detected via
|
|
150
|
+
# `ParallelTests.active?` at {.start} time). Affects
|
|
151
|
+
# cache + report directory scoping (per-worker dirs)
|
|
152
|
+
# and finalize-time merging.
|
|
153
|
+
#
|
|
154
|
+
# @return [Boolean]
|
|
150
155
|
def parallel_tests?
|
|
151
156
|
defined?(@parallel_tests) && @parallel_tests == true
|
|
152
157
|
end
|
|
153
158
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
RSpec Tracer is not running as it requires debug and object space enabled. Use
|
|
164
|
-
command line options "--debug" and "-X+O" or set the "debug.fullTrace=true" and
|
|
165
|
-
"objectspace.enabled=true" options in your .jrubyrc file. You can also use
|
|
166
|
-
JRUBY_OPTS="--debug -X+O".
|
|
167
|
-
WARN
|
|
168
|
-
|
|
169
|
-
false
|
|
159
|
+
# True if Rails is loaded in this process (detected via
|
|
160
|
+
# `defined?(::Rails::VERSION)` at {.start} time). Memoized;
|
|
161
|
+
# subsequent Rails activations within the same run are not
|
|
162
|
+
# re-detected. Drives the auto-installation of Rails-side
|
|
163
|
+
# observers (template + AR notification subscribers).
|
|
164
|
+
#
|
|
165
|
+
# @return [Boolean]
|
|
166
|
+
def rails?
|
|
167
|
+
defined?(@rails) && @rails == true
|
|
170
168
|
end
|
|
171
169
|
|
|
172
|
-
|
|
173
|
-
unless setup_rspec?
|
|
174
|
-
RSpecTracer.logger.error 'Could not find a running RSpec process'
|
|
170
|
+
private
|
|
175
171
|
|
|
176
|
-
|
|
177
|
-
|
|
172
|
+
# Internal method on the tracer pipeline.
|
|
173
|
+
# @api private
|
|
174
|
+
def initial_setup
|
|
175
|
+
RSpecTracer::RSpec::Installation.install!
|
|
178
176
|
|
|
179
177
|
setup_coverage
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
@runner = RSpecTracer::Runner.new
|
|
183
|
-
@coverage_reporter = RSpecTracer::CoverageReporter.new
|
|
184
|
-
@report_writer = RSpecTracer::ReportWriter.new(RSpecTracer.cache_path, @runner.reporter)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def parallel_tests_setup
|
|
188
|
-
@parallel_tests = !(ENV.fetch('TEST_ENV_NUMBER', nil) && ENV.fetch('PARALLEL_TEST_GROUPS', nil)).nil?
|
|
189
|
-
|
|
190
|
-
return unless parallel_tests?
|
|
191
|
-
|
|
192
|
-
require 'parallel_tests' unless defined?(ParallelTests)
|
|
193
|
-
|
|
194
|
-
@coverage_merger = RSpecTracer::CoverageMerger.new
|
|
195
|
-
@report_merger = RSpecTracer::ReportMerger.new
|
|
196
|
-
rescue LoadError => e
|
|
197
|
-
RSpecTracer.logger.error "Failed to load parallel tests (Error: #{e.message})"
|
|
198
|
-
ensure
|
|
199
|
-
track_parallel_tests_test_env_number
|
|
200
|
-
parallel_tests_touch_boot!
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# Per-worker boot marker. Source-of-truth for "this worker booted
|
|
204
|
-
# past `RSpecTracer.start`", consumed by the elected worker's
|
|
205
|
-
# finalize-time peer enumeration. Idempotent; failures are warned
|
|
206
|
-
# and absorbed (boot-marker write must never block test execution).
|
|
207
|
-
def parallel_tests_touch_boot!
|
|
208
|
-
return unless parallel_tests?
|
|
209
|
-
|
|
210
|
-
FileUtils.mkdir_p(RSpecTracer.cache_path)
|
|
211
|
-
File.write(
|
|
212
|
-
File.join(RSpecTracer.cache_path, PARALLEL_TESTS_BOOT_MARKER_FILENAME),
|
|
213
|
-
JSON.generate(
|
|
214
|
-
pid: Process.pid,
|
|
215
|
-
test_env_number: ENV.fetch('TEST_ENV_NUMBER', ''),
|
|
216
|
-
started_at: Time.now.utc.iso8601
|
|
217
|
-
)
|
|
218
|
-
)
|
|
219
|
-
rescue StandardError => e
|
|
220
|
-
RSpecTracer.logger.warn(
|
|
221
|
-
"RSpec tracer: failed to write boot marker (#{e.class}: #{e.message})"
|
|
222
|
-
)
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def track_parallel_tests_test_env_number
|
|
226
|
-
return unless parallel_tests?
|
|
227
|
-
|
|
228
|
-
File.open(RSpecTracer.lock_file, File::RDWR | File::CREAT, 0o644) do |f|
|
|
229
|
-
f.flock(File::LOCK_EX)
|
|
178
|
+
setup_rails
|
|
230
179
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
f.rewind
|
|
234
|
-
f.write("#{test_num}\n")
|
|
235
|
-
f.flush
|
|
236
|
-
f.truncate(f.pos)
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def setup_rspec?
|
|
241
|
-
runners = ObjectSpace.each_object(::RSpec::Core::Runner) do |runner|
|
|
242
|
-
runner_clazz = runner.singleton_class
|
|
243
|
-
clazz = RSpecTracer::RSpecRunner
|
|
244
|
-
|
|
245
|
-
runner_clazz.prepend(clazz) unless runner_clazz.ancestors.include?(clazz)
|
|
246
|
-
|
|
247
|
-
reporter_clazz = runner.configuration.reporter.singleton_class
|
|
248
|
-
clazz = RSpecTracer::RSpecReporter
|
|
249
|
-
|
|
250
|
-
reporter_clazz.prepend(clazz) unless reporter_clazz.ancestors.include?(clazz)
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
runners.positive?
|
|
180
|
+
@engine = RSpecTracer::Engine.new(configuration: RSpecTracer)
|
|
181
|
+
@engine.setup
|
|
254
182
|
end
|
|
255
183
|
|
|
184
|
+
# Internal method on the tracer pipeline.
|
|
185
|
+
# @api private
|
|
256
186
|
def setup_coverage
|
|
257
187
|
@simplecov = defined?(SimpleCov) && SimpleCov.running
|
|
258
188
|
|
|
@@ -260,331 +190,141 @@ module RSpecTracer
|
|
|
260
190
|
|
|
261
191
|
require 'coverage'
|
|
262
192
|
|
|
263
|
-
::Coverage.
|
|
264
|
-
end
|
|
193
|
+
return if ::Coverage.respond_to?(:running?) && ::Coverage.running?
|
|
265
194
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
195
|
+
::Coverage.start(**coverage_modes_for_start)
|
|
196
|
+
rescue RuntimeError
|
|
197
|
+
# ::Coverage.start raises if already started on some Rubies
|
|
198
|
+
# without a running? predicate; safe to ignore (matches
|
|
199
|
+
# Engine#ensure_coverage_started behavior).
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
269
202
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
203
|
+
# Detects Rails by the presence of `::Rails::VERSION`. Users who
|
|
204
|
+
# require `rspec_tracer/rails` transitively load the Railtie (when
|
|
205
|
+
# Rails is also present); this method only sets the flag consumed
|
|
206
|
+
# by `RSpecTracer.rails?`. Safe when Rails is absent - the
|
|
207
|
+
# `defined?` guard returns nil, flag stays false.
|
|
208
|
+
def setup_rails
|
|
209
|
+
@rails = defined?(::Rails::VERSION) && !::Rails::VERSION.nil?
|
|
273
210
|
end
|
|
274
211
|
|
|
212
|
+
# Internal method on the tracer pipeline.
|
|
213
|
+
# @api private
|
|
275
214
|
def run_exit_tasks
|
|
276
215
|
if RSpecTracer.no_examples
|
|
277
216
|
RSpecTracer.logger.info 'Skipped reports generation since all examples were filtered out'
|
|
278
217
|
else
|
|
279
|
-
|
|
218
|
+
snapshot = run_finalize
|
|
219
|
+
# Under parallel_tests, defer reporter emission until
|
|
220
|
+
# last-process finalize merges per-worker snapshots into the
|
|
221
|
+
# top-level cache. Each worker still persists its per-worker
|
|
222
|
+
# snapshot for the merge to consume. Earlier behavior had every
|
|
223
|
+
# worker emit reporters into rspec_tracer_report/parallel_tests_N/
|
|
224
|
+
# and purge_worker_dirs! removed those dirs - leaving the user
|
|
225
|
+
# with no usable terminal/JSON/HTML output. Now reporters fire
|
|
226
|
+
# ONCE at the merged top-level location.
|
|
227
|
+
emit_reporters(snapshot) if snapshot && !parallel_tests?
|
|
280
228
|
end
|
|
281
229
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
run_parallel_tests_exit_tasks
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
def generate_reports
|
|
288
|
-
RSpecTracer.logger.debug "RSpec tracer is generating reports (pid: #{RSpecTracer.pid})"
|
|
289
|
-
|
|
290
|
-
process_dependency
|
|
291
|
-
process_coverage
|
|
230
|
+
emit_coverage_json
|
|
292
231
|
|
|
293
|
-
RSpecTracer::
|
|
294
|
-
report_writer.write_report
|
|
295
|
-
RSpecTracer::HTMLReporter::Reporter.new(RSpecTracer.report_path, runner.reporter).generate_report
|
|
232
|
+
RSpecTracer::RSpec::ParallelTests.finalize! if parallel_tests?
|
|
296
233
|
end
|
|
297
234
|
|
|
298
|
-
|
|
235
|
+
# Engine-owned finalize path. Writes the 15-file JSON cache via
|
|
236
|
+
# Storage::JsonBackend. Per-example coverage deltas live on the
|
|
237
|
+
# Engine; 2.0 retired the CoverageReporter mid-flow piece (the
|
|
238
|
+
# legacy `coverage_reporter.generate_final_examples_coverage +
|
|
239
|
+
# merge_coverage(engine.merge_skipped_coverage(...))` is now folded
|
|
240
|
+
# into Reporters::CoverageJsonReporter#generate, which fires from
|
|
241
|
+
# `emit_coverage_json` after this method returns).
|
|
242
|
+
def run_finalize
|
|
299
243
|
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
300
244
|
|
|
301
|
-
|
|
302
|
-
runner.register_deleted_examples
|
|
303
|
-
runner.register_dependency(coverage_reporter.examples_coverage)
|
|
304
|
-
runner.register_traced_dependency(@examples_traced_files)
|
|
245
|
+
snapshot = engine.finalize
|
|
305
246
|
|
|
306
247
|
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
307
248
|
elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
|
308
249
|
|
|
309
|
-
RSpecTracer.logger.debug "RSpec tracer
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
coverage_reporter.generate_final_coverage
|
|
340
|
-
|
|
341
|
-
file_name = File.join(RSpecTracer.coverage_path, 'coverage.json')
|
|
342
|
-
coverage_writer = RSpecTracer::CoverageWriter.new(file_name, coverage_reporter)
|
|
343
|
-
|
|
344
|
-
coverage_writer.write_report
|
|
345
|
-
|
|
346
|
-
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
347
|
-
|
|
348
|
-
coverage_writer.print_stats(ending - starting)
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
def run_parallel_tests_exit_tasks
|
|
352
|
-
# Every worker — elected or not — drops its `.done` marker as the
|
|
353
|
-
# first thing in finalize so the elected worker's
|
|
354
|
-
# `parallel_tests_wait_for_peer_done_markers!` can observe it.
|
|
355
|
-
# Non-elected workers stop here; the elected worker proceeds to
|
|
356
|
-
# the merge + purge sequence (gated by `parallel_tests_executed?`,
|
|
357
|
-
# which now layers the peer-done barrier on top of the existing
|
|
358
|
-
# pid-file wait).
|
|
359
|
-
parallel_tests_touch_done!
|
|
360
|
-
|
|
361
|
-
return unless parallel_tests_executed?
|
|
362
|
-
|
|
363
|
-
merge_parallel_tests_reports
|
|
364
|
-
write_parallel_tests_merged_report
|
|
365
|
-
merge_parallel_tests_coverage_reports
|
|
366
|
-
write_parallel_tests_coverage_report
|
|
367
|
-
purge_parallel_tests_reports
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
# Per-worker done marker. Written by every worker (elected or not)
|
|
371
|
-
# as the first step of `run_parallel_tests_exit_tasks`. Pairs with
|
|
372
|
-
# the boot marker for the elected worker's peer-done barrier:
|
|
373
|
-
# presence of `.done` means "this worker has signalled completion
|
|
374
|
-
# of its own writes"; absence (with `.boot` present) means "still
|
|
375
|
-
# mid-flush or crashed". Idempotent; failures are warned + absorbed.
|
|
376
|
-
def parallel_tests_touch_done!
|
|
377
|
-
return unless parallel_tests?
|
|
378
|
-
|
|
379
|
-
FileUtils.mkdir_p(RSpecTracer.cache_path)
|
|
380
|
-
File.write(
|
|
381
|
-
File.join(RSpecTracer.cache_path, PARALLEL_TESTS_DONE_MARKER_FILENAME),
|
|
382
|
-
Time.now.utc.iso8601
|
|
250
|
+
RSpecTracer.logger.debug "RSpec tracer persisted cache (took #{elapsed})"
|
|
251
|
+
snapshot
|
|
252
|
+
rescue StandardError => e
|
|
253
|
+
# Graceful-degradation contract per ARCHITECTURE.md
|
|
254
|
+
# section Cache corruption recovery: never propagate storage errors
|
|
255
|
+
# into the user's test suite. Read-only cache_path, disk-full
|
|
256
|
+
# mid-write, permission flips between runs - log and skip
|
|
257
|
+
# report emission. The caller (run_exit_tasks) checks for nil
|
|
258
|
+
# before calling emit_reporters; coverage / parallel_tests
|
|
259
|
+
# finalize paths run independently downstream.
|
|
260
|
+
RSpecTracer.logger.warn(
|
|
261
|
+
"rspec-tracer: cache persistence failed (#{e.class}: #{e.message}); " \
|
|
262
|
+
'skipping report generation. Verify cache_path is writable.'
|
|
263
|
+
)
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Fire the configured reporters against the persisted
|
|
268
|
+
# Snapshot. Fires per-worker under parallel_tests (same cadence as
|
|
269
|
+
# coverage.json emission); each worker produces its own report.json
|
|
270
|
+
# under its per-worker report_dir. The Registry rescues every
|
|
271
|
+
# reporter individually, so a buggy reporter warns and continues -
|
|
272
|
+
# never propagates a non-zero exit into the user's test suite
|
|
273
|
+
# (graceful degradation contract, same as Storage backends).
|
|
274
|
+
def emit_reporters(snapshot)
|
|
275
|
+
RSpecTracer::Reporters::Registry.emit_all(
|
|
276
|
+
configuration: RSpecTracer,
|
|
277
|
+
snapshot: snapshot,
|
|
278
|
+
report_dir: RSpecTracer.report_path,
|
|
279
|
+
run_metadata: build_run_metadata
|
|
383
280
|
)
|
|
384
281
|
rescue StandardError => e
|
|
385
282
|
RSpecTracer.logger.warn(
|
|
386
|
-
"
|
|
283
|
+
"rspec-tracer: reporter pipeline failed (#{e.class}: #{e.message})"
|
|
387
284
|
)
|
|
388
285
|
end
|
|
389
286
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
432
|
-
elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
|
433
|
-
|
|
434
|
-
RSpecTracer.logger.debug "RSpec tracer merged parallel tests coverage reports (took #{elapsed})"
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
def write_parallel_tests_coverage_report
|
|
438
|
-
return unless parallel_tests_executed? && !simplecov?
|
|
439
|
-
|
|
440
|
-
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
441
|
-
|
|
442
|
-
coverage_path = File.dirname(RSpecTracer.coverage_path)
|
|
443
|
-
file_name = File.join(coverage_path, 'coverage.json')
|
|
444
|
-
coverage_writer = RSpecTracer::CoverageWriter.new(file_name, coverage_merger)
|
|
445
|
-
|
|
446
|
-
coverage_writer.write_report
|
|
447
|
-
|
|
448
|
-
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
449
|
-
|
|
450
|
-
coverage_writer.print_stats(ending - starting)
|
|
451
|
-
end
|
|
452
|
-
|
|
453
|
-
def purge_parallel_tests_reports
|
|
454
|
-
return unless parallel_tests_executed?
|
|
455
|
-
|
|
456
|
-
[RSpecTracer.cache_path, RSpecTracer.coverage_path, RSpecTracer.report_path].each do |path|
|
|
457
|
-
parallel_tests_peer_dirs(File.dirname(path)).each do |worker_dir|
|
|
458
|
-
FileUtils.rm_rf(worker_dir)
|
|
459
|
-
end
|
|
460
|
-
end
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
# Returns every `parallel_tests_*` subdirectory directly under
|
|
464
|
-
# `base_path`. Used by the parallel_tests merge + purge paths.
|
|
465
|
-
#
|
|
466
|
-
# Earlier patches iterated `1..ENV['PARALLEL_TEST_GROUPS'].to_i`
|
|
467
|
-
# to construct dir names, but parallel_tests's own runner sets
|
|
468
|
-
# PARALLEL_TEST_GROUPS to the user-requested process count
|
|
469
|
-
# (`Parallel.processor_count` by default), NOT the actual worker
|
|
470
|
-
# count. When num_processes < spawned_worker_count, the upper
|
|
471
|
-
# bound was too small: peer caches with TEST_ENV_NUMBER above the
|
|
472
|
-
# bound were silently dropped from the merge AND left behind by
|
|
473
|
-
# the purge. PR #101's commit message documented this gem
|
|
474
|
-
# behaviour for `last_process?` detection but did not extend the
|
|
475
|
-
# fix to the iteration call-sites; this method closes that gap.
|
|
476
|
-
# Globbing the actual filesystem state is robust to the env
|
|
477
|
-
# discrepancy regardless of how the gem partitions specs.
|
|
478
|
-
def parallel_tests_peer_dirs(base_path)
|
|
479
|
-
Dir.glob(File.join(base_path, 'parallel_tests_*')).select do |path|
|
|
480
|
-
File.directory?(path)
|
|
481
|
-
end
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
def parallel_tests_executed?
|
|
485
|
-
return false unless parallel_tests? && parallel_tests_last_process?
|
|
486
|
-
|
|
487
|
-
ParallelTests.wait_for_other_processes_to_finish
|
|
488
|
-
|
|
489
|
-
# Belt-and-suspenders barrier: pid-file said everyone's done, but
|
|
490
|
-
# the gem's `wait_for_other_processes_to_finish` has been observed
|
|
491
|
-
# on GHA Linux x86_64 to return while a sibling's `parallel_tests_N/`
|
|
492
|
-
# is still mid-flush. Cross-check via the `.boot`/`.done` filesystem
|
|
493
|
-
# markers before declaring the peer set stable. Idempotent: once
|
|
494
|
-
# all peers have flushed, subsequent calls just glob, find nothing
|
|
495
|
-
# missing, and return.
|
|
496
|
-
parallel_tests_wait_for_peer_done_markers!
|
|
497
|
-
|
|
498
|
-
true
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
# Block until every peer that wrote `.boot` has also written `.done`,
|
|
502
|
-
# or the deadline elapses. Polled at 50ms — fine enough to close the
|
|
503
|
-
# typical "barrier returned a tick early" case within a poll or two,
|
|
504
|
-
# coarse enough not to dominate CPU.
|
|
505
|
-
#
|
|
506
|
-
# On timeout we log a warn and proceed: a peer that never wrote
|
|
507
|
-
# `.done` either crashed (then its dir is orphan content; the
|
|
508
|
-
# subsequent purge cleans it) or is genuinely hung (the elected
|
|
509
|
-
# can't fix that — we choose merge correctness over indefinite wait).
|
|
510
|
-
def parallel_tests_wait_for_peer_done_markers!
|
|
511
|
-
base_dir = File.dirname(RSpecTracer.cache_path)
|
|
512
|
-
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + PARALLEL_TESTS_PEER_DONE_DEADLINE_SECONDS
|
|
513
|
-
|
|
514
|
-
loop do
|
|
515
|
-
missing = parallel_tests_peer_dirs_missing_done(base_dir)
|
|
516
|
-
return if missing.empty?
|
|
517
|
-
|
|
518
|
-
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
519
|
-
RSpecTracer.logger.warn(
|
|
520
|
-
'RSpec tracer: peers booted without finishing within ' \
|
|
521
|
-
"#{PARALLEL_TESTS_PEER_DONE_DEADLINE_SECONDS}s: #{missing.inspect}; " \
|
|
522
|
-
'proceeding (peer dirs will be purged regardless of completion state)'
|
|
523
|
-
)
|
|
524
|
-
return
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
sleep 0.05
|
|
528
|
-
end
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
# Set difference of `.boot`-bearing peer dirs and `.done`-bearing
|
|
532
|
-
# peer dirs under `base_dir`. A returned entry means "this peer
|
|
533
|
-
# registered but has not signalled completion yet" — either still
|
|
534
|
-
# mid-flush or crashed.
|
|
535
|
-
def parallel_tests_peer_dirs_missing_done(base_dir)
|
|
536
|
-
boot_dirs = parallel_tests_peer_dirs_with_marker(base_dir, PARALLEL_TESTS_BOOT_MARKER_FILENAME)
|
|
537
|
-
done_dirs = parallel_tests_peer_dirs_with_marker(base_dir, PARALLEL_TESTS_DONE_MARKER_FILENAME)
|
|
538
|
-
boot_dirs - done_dirs
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
def parallel_tests_peer_dirs_with_marker(base_dir, marker_filename)
|
|
542
|
-
Dir.glob(File.join(base_dir, 'parallel_tests_*', marker_filename)).map do |path|
|
|
543
|
-
File.dirname(path)
|
|
544
|
-
end
|
|
545
|
-
end
|
|
546
|
-
|
|
547
|
-
# Elects the worker that performs the per-run merge. Delegates to
|
|
548
|
-
# `::ParallelTests.first_process?`, which returns true iff
|
|
549
|
-
# `TEST_ENV_NUMBER.to_i <= 1` — i.e. for exactly one worker
|
|
550
|
-
# (TEST_ENV_NUMBER == '' or '1'), regardless of how many workers
|
|
551
|
-
# were actually spawned vs. how many CPUs the runner reports.
|
|
552
|
-
#
|
|
553
|
-
# Two previously attempted approaches do NOT work here:
|
|
554
|
-
#
|
|
555
|
-
# 1. The lock-file scheme below (each worker writing its
|
|
556
|
-
# TEST_ENV_NUMBER to `rspec_tracer.lock` via
|
|
557
|
-
# `track_parallel_tests_test_env_number`; last_process picked
|
|
558
|
-
# the max) deadlocked under slow CI: worker 1 could finish
|
|
559
|
-
# its examples before worker 2 even loaded spec_helper,
|
|
560
|
-
# observe itself as the max, and enter
|
|
561
|
-
# `::ParallelTests.wait_for_other_processes_to_finish`
|
|
562
|
-
# concurrently with worker 2's own self-election — both
|
|
563
|
-
# workers then spun on each other's pid.
|
|
564
|
-
#
|
|
565
|
-
# 2. `::ParallelTests.last_process?` compares TEST_ENV_NUMBER
|
|
566
|
-
# against PARALLEL_TEST_GROUPS, which parallel_rspec sets to
|
|
567
|
-
# the CPU-based *intended* process count — NOT the actual
|
|
568
|
-
# worker count. When spec files < CPU count (common), no
|
|
569
|
-
# TEST_ENV_NUMBER ever matches PARALLEL_TEST_GROUPS and the
|
|
570
|
-
# merge is silently skipped.
|
|
571
|
-
#
|
|
572
|
-
# `first_process?` avoids both: set by the parent at spawn,
|
|
573
|
-
# immutable thereafter, and identifies exactly one worker
|
|
574
|
-
# regardless of CPU count. The elected worker still calls
|
|
575
|
-
# `wait_for_other_processes_to_finish` before merging so peer
|
|
576
|
-
# caches are guaranteed on disk.
|
|
577
|
-
#
|
|
578
|
-
# `track_parallel_tests_test_env_number` and the lock-file
|
|
579
|
-
# cleanup in `at_exit_behavior` are retained for backward
|
|
580
|
-
# compatibility with users who observe `rspec_tracer.lock` /
|
|
581
|
-
# set `RSPEC_TRACER_LOCK_FILE`; the file is still written and
|
|
582
|
-
# removed but is no longer consulted.
|
|
583
|
-
def parallel_tests_last_process?
|
|
584
|
-
return false unless parallel_tests?
|
|
585
|
-
return false unless defined?(::ParallelTests)
|
|
586
|
-
|
|
587
|
-
::ParallelTests.first_process?
|
|
287
|
+
# Internal method on the tracer pipeline.
|
|
288
|
+
# @api private
|
|
289
|
+
def build_run_metadata
|
|
290
|
+
{
|
|
291
|
+
pid: RSpecTracer.pid,
|
|
292
|
+
run_time: run_elapsed_seconds,
|
|
293
|
+
started_at: defined?(@run_started_at) ? @run_started_at : nil,
|
|
294
|
+
cache_path: RSpecTracer.cache_path,
|
|
295
|
+
parallel_tests: RSpecTracer.parallel_tests?,
|
|
296
|
+
rails: RSpecTracer.rails?
|
|
297
|
+
}
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Internal method on the tracer pipeline.
|
|
301
|
+
# @api private
|
|
302
|
+
def run_elapsed_seconds
|
|
303
|
+
return nil unless defined?(@run_monotonic_start) && @run_monotonic_start
|
|
304
|
+
|
|
305
|
+
(::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @run_monotonic_start).round(4)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Dedicated coverage.json firing path, parallel to the
|
|
309
|
+
# report-reporters Registry pipeline. Fires unconditionally - even
|
|
310
|
+
# when no examples ran (matches 1.x where coverage.json gets
|
|
311
|
+
# written with whatever boot-time peek_result returned + filter +
|
|
312
|
+
# stub). The emitter handles SimpleCov interop internally
|
|
313
|
+
# (installs `::Coverage.singleton_class.prepend` shim instead of
|
|
314
|
+
# writing coverage.json when SimpleCov is loaded).
|
|
315
|
+
def emit_coverage_json
|
|
316
|
+
return unless engine
|
|
317
|
+
|
|
318
|
+
RSpecTracer::Reporters::CoverageJsonReporter.new(
|
|
319
|
+
snapshot: nil,
|
|
320
|
+
report_dir: RSpecTracer.report_path,
|
|
321
|
+
run_metadata: build_run_metadata,
|
|
322
|
+
logger: RSpecTracer.logger
|
|
323
|
+
).generate
|
|
324
|
+
rescue StandardError => e
|
|
325
|
+
RSpecTracer.logger.warn(
|
|
326
|
+
"rspec-tracer: coverage.json emit failed (#{e.class}: #{e.message})"
|
|
327
|
+
)
|
|
588
328
|
end
|
|
589
329
|
end
|
|
590
330
|
end
|