simplecov 0.22.0 → 1.0.0.rc1
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 +81 -1
- data/LICENSE +1 -1
- data/README.md +1009 -511
- data/doc/alternate-formatters.md +0 -5
- data/doc/commercial-services.md +5 -5
- data/exe/simplecov +11 -0
- data/lib/minitest/simplecov_plugin.rb +13 -5
- data/lib/simplecov/autostart.rb +11 -0
- data/lib/simplecov/cli/clean.rb +47 -0
- data/lib/simplecov/cli/coverage.rb +91 -0
- data/lib/simplecov/cli/diff.rb +151 -0
- data/lib/simplecov/cli/dotfile.rb +100 -0
- data/lib/simplecov/cli/merge.rb +116 -0
- data/lib/simplecov/cli/open.rb +50 -0
- data/lib/simplecov/cli/report.rb +84 -0
- data/lib/simplecov/cli/run.rb +36 -0
- data/lib/simplecov/cli/serve.rb +139 -0
- data/lib/simplecov/cli/uncovered.rb +107 -0
- data/lib/simplecov/cli.rb +150 -0
- data/lib/simplecov/color.rb +74 -0
- data/lib/simplecov/combine/branches_combiner.rb +3 -2
- data/lib/simplecov/combine/files_combiner.rb +7 -1
- data/lib/simplecov/combine/lines_combiner.rb +19 -17
- data/lib/simplecov/combine/methods_combiner.rb +26 -0
- data/lib/simplecov/combine/results_combiner.rb +5 -4
- data/lib/simplecov/command_guesser.rb +46 -32
- data/lib/simplecov/configuration/coverage.rb +171 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +156 -0
- data/lib/simplecov/configuration/filters.rb +195 -0
- data/lib/simplecov/configuration/formatting.rb +119 -0
- data/lib/simplecov/configuration/ignored_entries.rb +63 -0
- data/lib/simplecov/configuration/merging.rb +74 -0
- data/lib/simplecov/configuration/thresholds.rb +174 -0
- data/lib/simplecov/configuration.rb +79 -405
- data/lib/simplecov/coverage_statistics.rb +12 -9
- data/lib/simplecov/coverage_violations.rb +148 -0
- data/lib/simplecov/defaults.rb +27 -20
- data/lib/simplecov/directive.rb +162 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +19 -57
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -27
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +38 -21
- data/lib/simplecov/exit_codes.rb +3 -0
- data/lib/simplecov/exit_handling.rb +158 -0
- data/lib/simplecov/file_list.rb +61 -17
- data/lib/simplecov/filter.rb +69 -24
- data/lib/simplecov/formatter/base.rb +101 -0
- data/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
- data/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
- data/lib/simplecov/formatter/html_formatter.rb +79 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +84 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +127 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
- data/lib/simplecov/formatter/json_formatter.rb +77 -0
- data/lib/simplecov/formatter/multi_formatter.rb +4 -5
- data/lib/simplecov/formatter/simple_formatter.rb +9 -11
- data/lib/simplecov/formatter.rb +4 -0
- data/lib/simplecov/last_run.rb +10 -3
- data/lib/simplecov/lines_classifier.rb +26 -13
- data/lib/simplecov/load_global_config.rb +9 -4
- data/lib/simplecov/parallel_adapters/base.rb +51 -0
- data/lib/simplecov/parallel_adapters/generic.rb +42 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +77 -0
- data/lib/simplecov/parallel_adapters.rb +83 -0
- data/lib/simplecov/parallel_coordination.rb +95 -0
- data/lib/simplecov/process.rb +20 -14
- data/lib/simplecov/profiles/bundler_filter.rb +1 -1
- data/lib/simplecov/profiles/hidden_filter.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +24 -10
- data/lib/simplecov/profiles/root_filter.rb +6 -5
- data/lib/simplecov/profiles/strict.rb +32 -0
- data/lib/simplecov/profiles/test_frameworks.rb +1 -4
- data/lib/simplecov/profiles.rb +32 -3
- data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
- data/lib/simplecov/result/source_file_builder.rb +51 -0
- data/lib/simplecov/result.rb +97 -19
- data/lib/simplecov/result_adapter.rb +68 -6
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
- data/lib/simplecov/result_merger/resultset_file.rb +38 -0
- data/lib/simplecov/result_merger/resultset_store.rb +50 -0
- data/lib/simplecov/result_merger.rb +46 -90
- data/lib/simplecov/result_processing.rb +162 -0
- data/lib/simplecov/simulate_coverage.rb +54 -8
- data/lib/simplecov/source_file/branch.rb +1 -3
- data/lib/simplecov/source_file/branch_builder.rb +114 -0
- data/lib/simplecov/source_file/builder_context.rb +28 -0
- data/lib/simplecov/source_file/line.rb +7 -2
- data/lib/simplecov/source_file/line_builder.rb +43 -0
- data/lib/simplecov/source_file/method.rb +52 -0
- data/lib/simplecov/source_file/method_builder.rb +58 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
- data/lib/simplecov/source_file/skip_chunks.rb +77 -0
- data/lib/simplecov/source_file/source_loader.rb +63 -0
- data/lib/simplecov/source_file/statistics.rb +57 -0
- data/lib/simplecov/source_file.rb +66 -232
- data/lib/simplecov/static_coverage_extractor/visitor.rb +193 -0
- data/lib/simplecov/static_coverage_extractor.rb +111 -0
- data/lib/simplecov/useless_results_remover.rb +16 -7
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov-html.rb +4 -0
- data/lib/simplecov.rb +131 -377
- data/lib/simplecov_json_formatter.rb +4 -0
- data/schemas/coverage-v1.0.schema.json +300 -0
- data/schemas/coverage.schema.json +300 -0
- metadata +88 -56
- data/lib/simplecov/default_formatter.rb +0 -20
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module SimpleCov
|
|
9
|
+
module Formatter
|
|
10
|
+
# Writes coverage results as JSON to coverage/coverage.json. Used
|
|
11
|
+
# standalone, alongside the HTML formatter, or by external tools that
|
|
12
|
+
# consume SimpleCov output.
|
|
13
|
+
class JSONFormatter < Base
|
|
14
|
+
FILENAME = "coverage.json"
|
|
15
|
+
|
|
16
|
+
# `include_source:` defaults to `SimpleCov.source_in_json` (true
|
|
17
|
+
# by default) so the historical payload shape is unchanged.
|
|
18
|
+
# Callers that need the source array regardless of the global
|
|
19
|
+
# setting (the HTML formatter, which feeds the client-side
|
|
20
|
+
# viewer) pass `include_source: true` explicitly.
|
|
21
|
+
def self.build_hash(result, include_source: SimpleCov.source_in_json)
|
|
22
|
+
ResultHashFormatter.new(result, include_source: include_source).format
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def format(result)
|
|
26
|
+
FileUtils.mkdir_p(output_path)
|
|
27
|
+
path = File.join(output_path, FILENAME)
|
|
28
|
+
warn_if_concurrent_overwrite(path)
|
|
29
|
+
File.write(path, JSON.pretty_generate(self.class.build_hash(result)))
|
|
30
|
+
# stderr, not stdout: this is a status message, not the program's
|
|
31
|
+
# output. Keeps the line out of pipelines like `rspec -f json`.
|
|
32
|
+
warn output_message(result) unless @silent
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def message_prefix
|
|
38
|
+
"JSON "
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def entry_point_filename
|
|
42
|
+
FILENAME
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Warns when the existing coverage.json has a timestamp newer than this
|
|
46
|
+
# process's start time — a strong signal that a sibling test process
|
|
47
|
+
# (e.g., parallel_tests) wrote it while we were running, and that our
|
|
48
|
+
# write is about to clobber their data.
|
|
49
|
+
def warn_if_concurrent_overwrite(path)
|
|
50
|
+
start_time = SimpleCov.process_start_time or return
|
|
51
|
+
existing_ts = existing_timestamp(path) or return
|
|
52
|
+
return unless existing_ts > start_time
|
|
53
|
+
|
|
54
|
+
warn "simplecov: #{path} was written at #{existing_ts.iso8601} — after " \
|
|
55
|
+
"this process started at #{start_time.iso8601}. Overwriting " \
|
|
56
|
+
"likely loses coverage data from a concurrent test run. For " \
|
|
57
|
+
"parallel test setups, use SimpleCov::ResultMerger or run a single " \
|
|
58
|
+
"collation step after all workers finish."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def existing_timestamp(path)
|
|
62
|
+
return nil unless File.exist?(path)
|
|
63
|
+
|
|
64
|
+
timestamp = JSON.parse(File.read(path), symbolize_names: true).dig(:meta, :timestamp)
|
|
65
|
+
timestamp && Time.iso8601(timestamp)
|
|
66
|
+
rescue JSON::ParserError, ArgumentError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Loaded after the JSONFormatter class is defined so the nested
|
|
74
|
+
# `class JSONFormatter` reopen inside result_hash_formatter.rb doesn't
|
|
75
|
+
# accidentally create a JSONFormatter < Object before this file gets a
|
|
76
|
+
# chance to declare `JSONFormatter < Base`.
|
|
77
|
+
require_relative "json_formatter/result_hash_formatter"
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module Formatter
|
|
5
|
+
# Wraps multiple formatters so SimpleCov.formatter can drive several
|
|
6
|
+
# output formats (HTML + JSON, etc.) in a single run.
|
|
5
7
|
class MultiFormatter
|
|
8
|
+
# Shared `#format` implementation; included into individual
|
|
9
|
+
# MultiFormatter subclasses built by `MultiFormatter.new`.
|
|
6
10
|
module InstanceMethods
|
|
7
11
|
def format(result)
|
|
8
12
|
formatters.map do |formatter|
|
|
@@ -22,11 +26,6 @@ module SimpleCov
|
|
|
22
26
|
include InstanceMethods
|
|
23
27
|
end
|
|
24
28
|
end
|
|
25
|
-
|
|
26
|
-
def self.[](*args)
|
|
27
|
-
warn "#{Kernel.caller.first}: [DEPRECATION] ::[] is deprecated. Use ::new instead."
|
|
28
|
-
new(Array(args))
|
|
29
|
-
end
|
|
30
29
|
end
|
|
31
30
|
end
|
|
32
31
|
end
|
|
@@ -8,17 +8,15 @@ module SimpleCov
|
|
|
8
8
|
class SimpleFormatter
|
|
9
9
|
# Takes a SimpleCov::Result and generates a string out of it
|
|
10
10
|
def format(result)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
end
|
|
21
|
-
output
|
|
11
|
+
result.groups.map { |name, files| format_group(name, files) }.join
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def format_group(name, files)
|
|
17
|
+
header = "Group: #{name}\n#{'=' * 40}\n"
|
|
18
|
+
body = files.map { |file| "#{file.filename} (coverage: #{file.covered_percent.floor(2)}%)\n" }.join
|
|
19
|
+
"#{header}#{body}\n"
|
|
22
20
|
end
|
|
23
21
|
end
|
|
24
22
|
end
|
data/lib/simplecov/formatter.rb
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
|
+
# Namespace for SimpleCov result formatters. Built-in formatters live
|
|
5
|
+
# below this module; custom formatters should respond to `#format(result)`
|
|
6
|
+
# and can be wired up via `SimpleCov.formatter=`.
|
|
4
7
|
# TODO: Documentation on how to build your own formatters
|
|
5
8
|
module Formatter
|
|
6
9
|
end
|
|
@@ -8,3 +11,4 @@ end
|
|
|
8
11
|
|
|
9
12
|
require_relative "formatter/simple_formatter"
|
|
10
13
|
require_relative "formatter/multi_formatter"
|
|
14
|
+
require_relative "formatter/json_formatter"
|
data/lib/simplecov/last_run.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
3
4
|
require "json"
|
|
4
5
|
|
|
5
6
|
module SimpleCov
|
|
7
|
+
# Reads and writes coverage/.last_run.json — the previous run's coverage
|
|
8
|
+
# percentages used by MaximumCoverageDropCheck.
|
|
6
9
|
module LastRun
|
|
7
10
|
class << self
|
|
8
11
|
def last_run_path
|
|
@@ -18,10 +21,14 @@ module SimpleCov
|
|
|
18
21
|
JSON.parse(json, symbolize_names: true)
|
|
19
22
|
end
|
|
20
23
|
|
|
24
|
+
# Write to a process-private temp file, then atomically rename, so a
|
|
25
|
+
# concurrent reader (e.g. another parallel-tests worker checking
|
|
26
|
+
# MaximumCoverageDrop) never sees a half-written file.
|
|
21
27
|
def write(json)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
FileUtils.mkdir_p(SimpleCov.coverage_path)
|
|
29
|
+
temp_path = "#{last_run_path}.#{Process.pid}.tmp"
|
|
30
|
+
File.open(temp_path, "w") { |f| f.puts JSON.pretty_generate(json) }
|
|
31
|
+
File.rename(temp_path, last_run_path)
|
|
25
32
|
end
|
|
26
33
|
end
|
|
27
34
|
end
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
require_relative "directive"
|
|
5
|
+
|
|
3
6
|
module SimpleCov
|
|
4
7
|
# Classifies whether lines are relevant for code coverage analysis.
|
|
5
8
|
# Comments & whitespace lines, and :nocov: token blocks, are considered not relevant.
|
|
6
|
-
|
|
7
9
|
class LinesClassifier
|
|
8
10
|
RELEVANT = 0
|
|
9
11
|
NOT_RELEVANT = nil
|
|
10
12
|
|
|
11
|
-
WHITESPACE_LINE = /^\s
|
|
12
|
-
COMMENT_LINE = /^\s
|
|
13
|
+
WHITESPACE_LINE = /^\s*$/
|
|
14
|
+
COMMENT_LINE = /^\s*#/
|
|
13
15
|
WHITESPACE_OR_COMMENT_LINE = Regexp.union(WHITESPACE_LINE, COMMENT_LINE)
|
|
14
16
|
|
|
15
17
|
def self.no_cov_line
|
|
16
|
-
/^(\s*)#(\s*)(:#{SimpleCov.
|
|
18
|
+
/^(\s*)#(\s*)(:#{SimpleCov.current_nocov_token}:)/o
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def self.no_cov_line?(line)
|
|
@@ -31,17 +33,28 @@ module SimpleCov
|
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def classify(lines)
|
|
36
|
+
lines = lines.to_a
|
|
37
|
+
directive_disabled = directive_disabled_line_set(lines)
|
|
34
38
|
skipping = false
|
|
35
39
|
|
|
36
|
-
lines.map do |line|
|
|
37
|
-
if self.class.no_cov_line?(line)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
lines.map.with_index(1) do |line, line_number|
|
|
41
|
+
skipping = !skipping if self.class.no_cov_line?(line)
|
|
42
|
+
not_relevant_line?(line, line_number, skipping, directive_disabled) ? NOT_RELEVANT : RELEVANT
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def not_relevant_line?(line, line_number, skipping, directive_disabled)
|
|
49
|
+
skipping ||
|
|
50
|
+
self.class.no_cov_line?(line) ||
|
|
51
|
+
directive_disabled.include?(line_number) ||
|
|
52
|
+
self.class.whitespace_line?(line)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def directive_disabled_line_set(lines)
|
|
56
|
+
Directive.disabled_ranges(lines).fetch(:line).each_with_object(Set.new) do |range, set|
|
|
57
|
+
range.each { |line_number| set.add(line_number) }
|
|
45
58
|
end
|
|
46
59
|
end
|
|
47
60
|
end
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
# `~/.simplecov` was historically resolved via a three-step fallback chain
|
|
4
|
+
# (HOME, then `Etc.getpwuid.dir`, then `~$USER`) for hostile container
|
|
5
|
+
# environments circa 2017. Modern CRuby/JRuby/TruffleRuby all set HOME
|
|
6
|
+
# reliably, so trust it and skip silently when it isn't there.
|
|
7
|
+
if ENV.fetch("HOME", nil)
|
|
8
|
+
# simplecov:disable — only fires when ~/.simplecov exists, which is
|
|
9
|
+
# developer-machine-dependent (we can't rely on it for the dogfood).
|
|
10
|
+
global_config_path = File.join(File.expand_path("~"), ".simplecov")
|
|
7
11
|
load global_config_path if File.exist?(global_config_path)
|
|
12
|
+
# simplecov:enable
|
|
8
13
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ParallelAdapters
|
|
5
|
+
# Default no-op implementations for a parallel-test-runner adapter.
|
|
6
|
+
# Real adapters subclass and override what they need; everything else
|
|
7
|
+
# falls back to "behave like a single-process run."
|
|
8
|
+
#
|
|
9
|
+
# Adapters are classes (used as singletons, never instantiated) — they
|
|
10
|
+
# answer a small fixed set of questions about whether THIS worker
|
|
11
|
+
# process is the one that should do final-result work, and provide an
|
|
12
|
+
# optional hook for waiting on sibling workers.
|
|
13
|
+
#
|
|
14
|
+
# @see SimpleCov::ParallelAdapters for the registry and selection.
|
|
15
|
+
class Base
|
|
16
|
+
class << self
|
|
17
|
+
# Should this adapter be selected for the current process? Adapters
|
|
18
|
+
# are tried in registration order; the first one whose `active?`
|
|
19
|
+
# returns true is chosen. Inactive adapters return `false`.
|
|
20
|
+
def active?
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Among the parallel workers in this run, should THIS worker do
|
|
25
|
+
# the final-result work (wait for siblings, merge resultsets,
|
|
26
|
+
# run threshold checks, format the report)? Default is `true`
|
|
27
|
+
# for the single-process case.
|
|
28
|
+
def first_worker?
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Optional: block until sibling workers have finished writing
|
|
33
|
+
# their resultsets. An adapter that wraps a parallel-test runner
|
|
34
|
+
# with a native synchronization primitive (e.g., `parallel_tests`'s
|
|
35
|
+
# `wait_for_other_processes_to_finish`) implements this for
|
|
36
|
+
# lower latency; otherwise SimpleCov polls the resultset cache
|
|
37
|
+
# as a fallback (see `SimpleCov.wait_for_parallel_results`).
|
|
38
|
+
def wait_for_siblings
|
|
39
|
+
# No-op default; polling fallback handles correctness.
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# How many parallel workers are participating in this run. Used
|
|
43
|
+
# by the polling fallback to know how many resultset entries to
|
|
44
|
+
# expect. Defaults to 1 (single-process).
|
|
45
|
+
def expected_worker_count
|
|
46
|
+
1
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module ParallelAdapters
|
|
7
|
+
# Catch-all adapter for parallel test runners that follow the
|
|
8
|
+
# `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` env-var convention but
|
|
9
|
+
# don't ship a Ruby API for SimpleCov to hook (parallel_rspec,
|
|
10
|
+
# knapsack-style splitters, custom CI sharding scripts). Activates
|
|
11
|
+
# when `TEST_ENV_NUMBER` is set; doesn't require any specific gem to
|
|
12
|
+
# be loaded.
|
|
13
|
+
#
|
|
14
|
+
# Heuristic for `first_worker?`: the worker whose `TEST_ENV_NUMBER`
|
|
15
|
+
# is `""` (parallel_tests/parallel_rspec convention) or `"1"`
|
|
16
|
+
# (zero-based runners that start at 1). Any other value is treated
|
|
17
|
+
# as a non-first worker.
|
|
18
|
+
#
|
|
19
|
+
# `wait_for_siblings` is inherited from Base as a no-op — without a
|
|
20
|
+
# runner-provided API the only synchronization available is polling
|
|
21
|
+
# the resultset cache, which `SimpleCov.wait_for_parallel_results`
|
|
22
|
+
# does after the no-op returns.
|
|
23
|
+
class GenericAdapter < Base
|
|
24
|
+
class << self
|
|
25
|
+
def active?
|
|
26
|
+
ENV.key?("TEST_ENV_NUMBER")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# parallel_tests sets the first worker's TEST_ENV_NUMBER to "";
|
|
30
|
+
# parallel_rspec inherits that. Runners that number from 1 use
|
|
31
|
+
# "1" for the first worker. Both shapes match.
|
|
32
|
+
def first_worker?
|
|
33
|
+
["", "1"].include?(ENV.fetch("TEST_ENV_NUMBER", nil))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def expected_worker_count
|
|
37
|
+
ENV["PARALLEL_TEST_GROUPS"]&.to_i || 1
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module ParallelAdapters
|
|
7
|
+
# Adapter for [grosser/parallel_tests](https://github.com/grosser/parallel_tests).
|
|
8
|
+
# This is the historical default — SimpleCov has special-cased
|
|
9
|
+
# parallel_tests since 0.18 — and remains the most precise option for
|
|
10
|
+
# projects on it. Detection is the standard pair: the `ParallelTests`
|
|
11
|
+
# constant has been loaded AND `TEST_ENV_NUMBER` is set. The gem itself
|
|
12
|
+
# is autoloaded lazily on first `active?` check so users who don't have
|
|
13
|
+
# it installed see no warnings (see #1018).
|
|
14
|
+
class ParallelTestsAdapter < Base
|
|
15
|
+
class << self
|
|
16
|
+
def active?
|
|
17
|
+
ensure_loaded
|
|
18
|
+
# !! to coerce `defined?` (returns nil or "constant") to a proper bool.
|
|
19
|
+
!!(defined?(::ParallelTests) && ENV.key?("TEST_ENV_NUMBER"))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Pick the *first* started process to do the final-result work,
|
|
23
|
+
# not the last. The parallel_tests README recommends
|
|
24
|
+
# `first_process?` for "do something once after every worker
|
|
25
|
+
# finishes" hooks, so user code that has its own
|
|
26
|
+
# `wait_for_other_processes_to_finish` in an `RSpec.after(:suite)`
|
|
27
|
+
# overwhelmingly waits in the first process — picking the same
|
|
28
|
+
# side avoids the cross-process deadlock #922 reported. Also
|
|
29
|
+
# handles `PARALLEL_TEST_GROUPS=1` naturally (the only worker's
|
|
30
|
+
# `TEST_ENV_NUMBER` is "" and `first_process?` tests for that
|
|
31
|
+
# empty string).
|
|
32
|
+
def first_worker?
|
|
33
|
+
::ParallelTests.first_process?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def wait_for_siblings
|
|
37
|
+
::ParallelTests.wait_for_other_processes_to_finish
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def expected_worker_count
|
|
41
|
+
ENV["PARALLEL_TEST_GROUPS"]&.to_i || 1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Auto-require `parallel_tests` when it's installed AND the env
|
|
45
|
+
# vars it sets are present, so callers can rely on
|
|
46
|
+
# `defined?(::ParallelTests)` downstream. parallel_tests is an
|
|
47
|
+
# optional dependency (see https://github.com/grosser/parallel_tests/issues/772),
|
|
48
|
+
# and `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` are commonly set
|
|
49
|
+
# for other reasons (custom subprocess coordination, CI sharding,
|
|
50
|
+
# the parallel_rspec gem which intentionally mirrors the env-var
|
|
51
|
+
# convention), so a missing gem is treated as "user isn't using
|
|
52
|
+
# parallel_tests" — silently skip and let GenericAdapter handle
|
|
53
|
+
# it. Users who want to override the auto-detect can set
|
|
54
|
+
# `SimpleCov.parallel_tests true` (force on) or `false` (force
|
|
55
|
+
# off). See #1018.
|
|
56
|
+
def ensure_loaded
|
|
57
|
+
return if defined?(::ParallelTests) # simplecov:disable — only true after a previous load
|
|
58
|
+
return if SimpleCov.parallel_tests == false # simplecov:disable — only fires when user opts out
|
|
59
|
+
# simplecov:disable — env-var-only path
|
|
60
|
+
return unless SimpleCov.parallel_tests || env_suggests_parallel_tests?
|
|
61
|
+
|
|
62
|
+
# simplecov:disable — only fires under a real parallel_tests setup
|
|
63
|
+
require "parallel_tests"
|
|
64
|
+
rescue LoadError
|
|
65
|
+
# Gem isn't installed; stay quiet — warning here regressed
|
|
66
|
+
# users who use those env vars for their own subprocess
|
|
67
|
+
# coordination.
|
|
68
|
+
# simplecov:enable
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def env_suggests_parallel_tests?
|
|
72
|
+
ENV.key?("TEST_ENV_NUMBER") && ENV.key?("PARALLEL_TEST_GROUPS")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parallel_adapters/base"
|
|
4
|
+
require_relative "parallel_adapters/parallel_tests"
|
|
5
|
+
require_relative "parallel_adapters/generic"
|
|
6
|
+
|
|
7
|
+
module SimpleCov
|
|
8
|
+
# Registry + selection for parallel-test-runner adapters. An adapter
|
|
9
|
+
# answers a small fixed set of questions on SimpleCov's behalf:
|
|
10
|
+
#
|
|
11
|
+
# - `active?` — are WE the runner in charge for this process?
|
|
12
|
+
# - `first_worker?` — should this process do the final-result work?
|
|
13
|
+
# - `wait_for_siblings` — block until siblings finish (optional)
|
|
14
|
+
# - `expected_worker_count` — how many workers total
|
|
15
|
+
#
|
|
16
|
+
# `SimpleCov::ParallelAdapters::Base` provides safe no-op defaults; two
|
|
17
|
+
# adapters ship out of the box:
|
|
18
|
+
#
|
|
19
|
+
# - `ParallelTestsAdapter` — wraps the grosser/parallel_tests gem
|
|
20
|
+
# (precise sync + first-process detection via the gem's own API).
|
|
21
|
+
# - `GenericAdapter` — env-var-only detection for runners that follow
|
|
22
|
+
# the parallel_tests `TEST_ENV_NUMBER` convention but don't ship a
|
|
23
|
+
# Ruby API (parallel_rspec, custom CI sharding, knapsack-style
|
|
24
|
+
# splitters). See https://github.com/simplecov-ruby/simplecov/issues/1065.
|
|
25
|
+
#
|
|
26
|
+
# Users can plug in additional adapters:
|
|
27
|
+
#
|
|
28
|
+
# SimpleCov::ParallelAdapters.register MyRunnerAdapter
|
|
29
|
+
#
|
|
30
|
+
# An adapter just needs to be a class responding to the four methods
|
|
31
|
+
# above. Subclass `SimpleCov::ParallelAdapters::Base` to inherit the
|
|
32
|
+
# no-op defaults and override only what you need (the contract methods
|
|
33
|
+
# are defined as class methods, so plain inheritance is what carries
|
|
34
|
+
# them through; `extend Base` won't pick them up).
|
|
35
|
+
module ParallelAdapters
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# Adapters in selection order. ParallelTestsAdapter first (most
|
|
39
|
+
# specific — uses the gem's own API when the gem is loaded); then
|
|
40
|
+
# GenericAdapter as the env-var fallback. User-registered adapters
|
|
41
|
+
# are prepended (#register puts new entries at the front) so
|
|
42
|
+
# downstream code can override the built-ins by registering a more
|
|
43
|
+
# specific match.
|
|
44
|
+
def adapters
|
|
45
|
+
@adapters ||= [ParallelTestsAdapter, GenericAdapter]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Register a custom adapter. Newly registered adapters are inserted
|
|
49
|
+
# at the front of the selection list so a custom adapter for a
|
|
50
|
+
# specific runner takes precedence over the built-in ParallelTests
|
|
51
|
+
# and Generic adapters.
|
|
52
|
+
#
|
|
53
|
+
# class MyRunnerAdapter < SimpleCov::ParallelAdapters::Base
|
|
54
|
+
# def self.active? = ENV["MY_RUNNER_PID"]
|
|
55
|
+
# def self.first_worker? = ENV["MY_RUNNER_PID"].to_i == 1
|
|
56
|
+
# def self.expected_worker_count = ENV["MY_RUNNER_WORKERS"].to_i
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# SimpleCov::ParallelAdapters.register MyRunnerAdapter
|
|
60
|
+
def register(adapter)
|
|
61
|
+
reset_current!
|
|
62
|
+
adapters.unshift(adapter) unless adapters.include?(adapter)
|
|
63
|
+
adapter
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The adapter SimpleCov should consult for this process — the first
|
|
67
|
+
# registered adapter whose `active?` returns true. Returns nil when
|
|
68
|
+
# no adapter is active (i.e., we're not running under any recognized
|
|
69
|
+
# parallel test runner), in which case the caller should treat the
|
|
70
|
+
# process as single-worker.
|
|
71
|
+
def current
|
|
72
|
+
return @current if defined?(@current)
|
|
73
|
+
|
|
74
|
+
@current = adapters.find(&:active?)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Clear the memoized `current` selection. Primarily for tests that
|
|
78
|
+
# mutate env vars between examples; production runs are single-shot.
|
|
79
|
+
def reset_current!
|
|
80
|
+
remove_instance_variable(:@current) if defined?(@current)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# simplecov:disable
|
|
4
|
+
# Methods below only fire under a parallel test runner; not reachable
|
|
5
|
+
# from a single-process rspec run. Cucumber's test_projects exercise
|
|
6
|
+
# the parallel_tests integration end-to-end in subprocesses, but
|
|
7
|
+
# those subprocesses don't merge their Coverage data back into the
|
|
8
|
+
# parent this dogfood report measures.
|
|
9
|
+
module SimpleCov
|
|
10
|
+
class << self
|
|
11
|
+
# @api private
|
|
12
|
+
# How long the first worker is willing to wait for all sibling
|
|
13
|
+
# workers' resultsets to appear in the cache before proceeding
|
|
14
|
+
# with whatever it has. Tuned generously enough that slow CI
|
|
15
|
+
# runners with one straggler don't trip the "incomplete results"
|
|
16
|
+
# path on a routine basis. See #1065 for the parallel_rspec /
|
|
17
|
+
# GenericAdapter case where there is no native wait primitive
|
|
18
|
+
# and this poll is the only synchronization available.
|
|
19
|
+
PARALLEL_RESULTS_WAIT_TIMEOUT = 60
|
|
20
|
+
private_constant :PARALLEL_RESULTS_WAIT_TIMEOUT
|
|
21
|
+
|
|
22
|
+
# @api private
|
|
23
|
+
def final_result_process?
|
|
24
|
+
adapter = SimpleCov::ParallelAdapters.current
|
|
25
|
+
return true unless adapter
|
|
26
|
+
|
|
27
|
+
adapter.first_worker?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @api private
|
|
31
|
+
def wait_for_other_processes
|
|
32
|
+
adapter = SimpleCov::ParallelAdapters.current
|
|
33
|
+
return unless adapter && final_result_process?
|
|
34
|
+
|
|
35
|
+
# Native synchronization first (adapters that wrap a runner with a
|
|
36
|
+
# real "wait" primitive — parallel_tests'
|
|
37
|
+
# `wait_for_other_processes_to_finish` — implement this; adapters
|
|
38
|
+
# without a native API no-op and rely on the polling fallback below).
|
|
39
|
+
adapter.wait_for_siblings
|
|
40
|
+
|
|
41
|
+
# The native wait can return before sibling at_exit handlers finish
|
|
42
|
+
# writing resultsets, and adapters without a native wait have
|
|
43
|
+
# nothing else. Either way, poll the resultset cache until all
|
|
44
|
+
# expected workers have reported or a timeout is reached. Capture
|
|
45
|
+
# the outcome so `ready_to_process_results?` can suppress min/max
|
|
46
|
+
# threshold checks against a partial total.
|
|
47
|
+
@parallel_results_complete = wait_for_parallel_results(adapter.expected_worker_count)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @api private — true when every sibling reported its resultset
|
|
51
|
+
# before the wait deadline. Defaults to true outside a parallel
|
|
52
|
+
# run (when `wait_for_other_processes` is a no-op).
|
|
53
|
+
def parallel_results_complete?
|
|
54
|
+
defined?(@parallel_results_complete) ? @parallel_results_complete : true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @api private — returns true when every expected worker reported
|
|
58
|
+
# before the deadline, false on timeout. Single-process runs
|
|
59
|
+
# (expected <= 1) short-circuit to true with no waiting.
|
|
60
|
+
def wait_for_parallel_results(expected)
|
|
61
|
+
return true unless expected > 1 # simplecov:disable branch — only false in real parallel runs
|
|
62
|
+
|
|
63
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + PARALLEL_RESULTS_WAIT_TIMEOUT
|
|
64
|
+
loop do
|
|
65
|
+
seen = SimpleCov::ResultMerger.read_resultset.size
|
|
66
|
+
return true if seen >= expected
|
|
67
|
+
return false if parallel_wait_timed_out?(deadline, expected, seen)
|
|
68
|
+
|
|
69
|
+
sleep 0.1
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @api private — true once the wait deadline has passed; warns on
|
|
74
|
+
# the first timeout so the user knows the merged total is partial.
|
|
75
|
+
def parallel_wait_timed_out?(deadline, expected, seen)
|
|
76
|
+
return false unless Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
77
|
+
|
|
78
|
+
warn_about_incomplete_parallel_results(expected, seen)
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @api private
|
|
83
|
+
def warn_about_incomplete_parallel_results(expected, seen)
|
|
84
|
+
return unless print_errors
|
|
85
|
+
|
|
86
|
+
warn SimpleCov::Color.colorize(
|
|
87
|
+
"Only #{seen} of #{expected} parallel-test workers reported within " \
|
|
88
|
+
"#{PARALLEL_RESULTS_WAIT_TIMEOUT}s. Coverage totals are partial; " \
|
|
89
|
+
"minimum / maximum coverage checks are skipped for this run.",
|
|
90
|
+
:yellow
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
# simplecov:enable
|
data/lib/simplecov/process.rb
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
else
|
|
12
|
-
fork_without_simplecov(&block)
|
|
13
|
-
end
|
|
14
|
-
end
|
|
3
|
+
# Hooks `Process._fork` (Ruby 3.1+) so child processes inherit SimpleCov's
|
|
4
|
+
# coverage tracking when `SimpleCov.enable_for_subprocesses?` is set.
|
|
5
|
+
#
|
|
6
|
+
# `Process._fork` is the official extension point: `Kernel#fork`,
|
|
7
|
+
# `Process.fork`, `IO.popen("-")`, and similar all funnel through it.
|
|
8
|
+
# Hooking `_fork` (instead of redefining `Process.fork`) composes
|
|
9
|
+
# correctly with other libraries doing the same — they each prepend
|
|
10
|
+
# their own module and chain via `super`.
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
module SimpleCov
|
|
13
|
+
# Prepended onto Process's singleton class so every fork — direct or
|
|
14
|
+
# via Kernel#fork / IO.popen — re-runs SimpleCov's at_fork callback in
|
|
15
|
+
# the child.
|
|
16
|
+
module ProcessForkHook
|
|
17
|
+
def _fork
|
|
18
|
+
pid = super
|
|
19
|
+
SimpleCov.at_fork.call(::Process.pid) if pid.zero? && defined?(SimpleCov) && Coverage.running?
|
|
20
|
+
pid
|
|
21
|
+
end
|
|
18
22
|
end
|
|
19
23
|
end
|
|
24
|
+
|
|
25
|
+
Process.singleton_class.prepend(SimpleCov::ProcessForkHook)
|