simplecov 0.22.0 → 1.0.0.rc2
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 +89 -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 +197 -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 +86 -407
- 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/deprecation.rb +47 -0
- 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 +26 -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 +54 -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 +148 -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 +89 -56
- data/lib/simplecov/default_formatter.rb +0 -20
|
@@ -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,31 @@
|
|
|
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
|
+
active = defined?(SimpleCov) && Coverage.running?
|
|
19
|
+
# Assign the next serial in the PARENT, before the fork, so the child
|
|
20
|
+
# inherits its own stable ordinal via copy-on-write. The default
|
|
21
|
+
# at_fork uses it (instead of the child pid) to name the subprocess's
|
|
22
|
+
# result, keeping that name identical across runs. See issue #1171.
|
|
23
|
+
SimpleCov.next_subprocess_serial! if active
|
|
24
|
+
pid = super
|
|
25
|
+
SimpleCov.at_fork.call(::Process.pid) if pid.zero? && active
|
|
26
|
+
pid
|
|
27
|
+
end
|
|
18
28
|
end
|
|
19
29
|
end
|
|
30
|
+
|
|
31
|
+
Process.singleton_class.prepend(SimpleCov::ProcessForkHook)
|
|
@@ -3,16 +3,30 @@
|
|
|
3
3
|
SimpleCov.profiles.define "rails" do
|
|
4
4
|
load_profile "test_frameworks"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
skip %r{\Aconfig/}
|
|
7
|
+
skip %r{\Adb/}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
group "Controllers", "app/controllers"
|
|
10
|
+
group "Channels", "app/channels"
|
|
11
|
+
group "Models", "app/models"
|
|
12
|
+
group "Mailers", "app/mailers"
|
|
13
|
+
group "Helpers", "app/helpers"
|
|
14
|
+
group "Jobs", %w[app/jobs app/workers]
|
|
15
|
+
group "Libraries", "lib/"
|
|
16
16
|
|
|
17
|
-
track_files
|
|
17
|
+
# Preserve the legacy `track_files` semantics (additive disk-discovery
|
|
18
|
+
# without restricting the report's universe): write the ivar directly
|
|
19
|
+
# so loading the profile doesn't emit the public-API deprecation. Users
|
|
20
|
+
# migrating their own configs should prefer `cover "{app,lib}/**/*.rb"`,
|
|
21
|
+
# which both injects unloaded files and scopes the report to the match
|
|
22
|
+
# set — usually the intended behavior for a Rails project.
|
|
23
|
+
@tracked_files = "{app,lib}/**/*.rb"
|
|
24
|
+
|
|
25
|
+
# `parallelize(workers: ...)` forks worker processes that each run a
|
|
26
|
+
# slice of the suite. Without subprocess support, the workers' coverage
|
|
27
|
+
# is dropped on the floor and the parent records 0% for everything they
|
|
28
|
+
# touched. Hooking `Process._fork` makes each worker re-call
|
|
29
|
+
# `SimpleCov.start` with a unique command_name so the resultsets merge
|
|
30
|
+
# correctly.
|
|
31
|
+
merge_subprocesses true
|
|
18
32
|
end
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
SimpleCov.profiles.define "root_filter" do
|
|
4
|
-
# Exclude all files outside of simplecov root
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
# Exclude all files outside of simplecov root. Shares the regex with
|
|
5
|
+
# SimpleCov::UselessResultsRemover so the root-prefix logic lives in one
|
|
6
|
+
# place; this profile is the user-facing entry point that tools like
|
|
7
|
+
# `SimpleCov.filtered` apply.
|
|
8
|
+
skip do |src|
|
|
9
|
+
src.filename !~ SimpleCov::UselessResultsRemover.root_regex
|
|
9
10
|
end
|
|
10
11
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A "no shortfalls allowed" profile: every coverage criterion is
|
|
4
|
+
# enabled and held to 100%. Per
|
|
5
|
+
# https://github.com/simplecov-ruby/simplecov/issues/1061, this lives as
|
|
6
|
+
# an opt-in profile rather than the default — most projects can't
|
|
7
|
+
# realistically start at 100% on day one — so a team that has paid down
|
|
8
|
+
# the debt can flip the strict switch without re-typing the threshold
|
|
9
|
+
# trio every time.
|
|
10
|
+
#
|
|
11
|
+
# JRuby gracefully degrades: `enable_coverage :branch` / `:method` are
|
|
12
|
+
# accepted (they just add to the configured criteria set), but the
|
|
13
|
+
# runtime can't actually measure them, so the corresponding stats never
|
|
14
|
+
# materialize. `CoverageViolations` skips threshold lookups for
|
|
15
|
+
# criteria not in the stats, so the `:branch` / `:method` clauses below
|
|
16
|
+
# silently no-op on JRuby and only `:line` is enforced.
|
|
17
|
+
#
|
|
18
|
+
# `:eval` widens the universe of code held to 100% to include strings
|
|
19
|
+
# passed through `Kernel#eval` (typically ERB templates, when the user
|
|
20
|
+
# sets `ERB#filename=`). Guarded on `coverage_for_eval_supported?` so
|
|
21
|
+
# the profile stays quiet on Ruby < 3.2, where `enable_coverage :eval`
|
|
22
|
+
# would otherwise warn about the missing runtime support every time
|
|
23
|
+
# the profile loads.
|
|
24
|
+
SimpleCov.profiles.define "strict" do
|
|
25
|
+
enable_coverage :branch
|
|
26
|
+
enable_coverage :method
|
|
27
|
+
# simplecov:disable branch — dogfood runs on Ruby >= 3.2 only, so
|
|
28
|
+
# the else arm (eval coverage not supported) is unreachable from CI.
|
|
29
|
+
enable_coverage :eval if coverage_for_eval_supported?
|
|
30
|
+
# simplecov:enable
|
|
31
|
+
minimum_coverage line: 100, branch: 100, method: 100
|
|
32
|
+
end
|
data/lib/simplecov/profiles.rb
CHANGED
|
@@ -17,7 +17,7 @@ module SimpleCov
|
|
|
17
17
|
#
|
|
18
18
|
def define(name, &blk)
|
|
19
19
|
name = name.to_sym
|
|
20
|
-
raise "SimpleCov Profile '#{name}' is already defined" unless self[name].nil?
|
|
20
|
+
raise SimpleCov::ConfigurationError, "SimpleCov Profile '#{name}' is already defined" unless self[name].nil?
|
|
21
21
|
|
|
22
22
|
self[name] = blk
|
|
23
23
|
end
|
|
@@ -26,10 +26,39 @@ module SimpleCov
|
|
|
26
26
|
# Applies the profile of given name on SimpleCov.configure
|
|
27
27
|
#
|
|
28
28
|
def load(name)
|
|
29
|
+
SimpleCov.configure(&fetch_proc(name))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#
|
|
33
|
+
# Returns the proc registered for the given profile name, autoloading
|
|
34
|
+
# bundled or plugin-gem profiles on first lookup. Raises if the profile
|
|
35
|
+
# cannot be located.
|
|
36
|
+
#
|
|
37
|
+
# Lookup order:
|
|
38
|
+
# 1. already registered via #define
|
|
39
|
+
# 2. require "simplecov/profiles/<name>" (bundled profiles)
|
|
40
|
+
# 3. require "simplecov-profile-<name>" (third-party plugin gems)
|
|
41
|
+
#
|
|
42
|
+
def fetch_proc(name)
|
|
29
43
|
name = name.to_sym
|
|
30
|
-
|
|
44
|
+
autoload_profile(name) unless key?(name)
|
|
45
|
+
return self[name] if key?(name)
|
|
46
|
+
|
|
47
|
+
raise SimpleCov::ConfigurationError, "Could not find SimpleCov Profile called '#{name}'"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
31
51
|
|
|
32
|
-
|
|
52
|
+
def autoload_profile(name)
|
|
53
|
+
require "simplecov/profiles/#{name}"
|
|
54
|
+
rescue LoadError
|
|
55
|
+
begin
|
|
56
|
+
# simplecov:disable — third-party gem fallback (no such gem in test env)
|
|
57
|
+
require "simplecov-profile-#{name}"
|
|
58
|
+
# simplecov:enable
|
|
59
|
+
rescue LoadError
|
|
60
|
+
# fall through; #fetch_proc raises the user-facing error
|
|
61
|
+
end
|
|
33
62
|
end
|
|
34
63
|
end
|
|
35
64
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class Result
|
|
5
|
+
# When a resultset references source files that don't exist on the local
|
|
6
|
+
# filesystem they're silently dropped — which produces an empty `0 / 0
|
|
7
|
+
# (100.00%)` report that looks like success but isn't. Emit a single
|
|
8
|
+
# warning summarizing the drop and, when every entry was lost, point at
|
|
9
|
+
# the typical cause (`SimpleCov.collate` invoked from a machine or path
|
|
10
|
+
# different from where the resultsets were generated). See #980.
|
|
11
|
+
class MissingSourceFilesReporter
|
|
12
|
+
def initialize(missing_paths, input_size:, every_entry_dropped:)
|
|
13
|
+
@missing_paths = missing_paths
|
|
14
|
+
@input_size = input_size
|
|
15
|
+
@every_entry_dropped = every_entry_dropped
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def warn!
|
|
19
|
+
warn SimpleCov::Color.colorize(message, :yellow)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def message
|
|
23
|
+
@every_entry_dropped ? all_missing_warning : partial_missing_warning
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def all_missing_warning
|
|
29
|
+
"SimpleCov dropped all #{@missing_paths.size} source file(s) from the result — " \
|
|
30
|
+
"none of the paths in the resultset exist on this filesystem: " \
|
|
31
|
+
"#{summary}. If you're running `SimpleCov.collate`, the source " \
|
|
32
|
+
"files must be available at the same absolute paths as when the individual resultsets " \
|
|
33
|
+
"were generated."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def partial_missing_warning
|
|
37
|
+
"SimpleCov dropped #{@missing_paths.size} source file(s) from the result because " \
|
|
38
|
+
"they don't exist on this filesystem: #{summary}. They were " \
|
|
39
|
+
"tracked in the resultset but have since moved or been removed."
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def summary
|
|
43
|
+
sample = @missing_paths.first(5).join(", ")
|
|
44
|
+
remaining = @missing_paths.size - 5
|
|
45
|
+
remaining.positive? ? "#{sample} (+#{remaining} more)" : sample
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class Result
|
|
5
|
+
# Constructs `SimpleCov::SourceFile` instances from a raw coverage
|
|
6
|
+
# hash, sorts them by filename, and surfaces filenames whose source
|
|
7
|
+
# is no longer present on disk so the caller can warn about the
|
|
8
|
+
# silent drop (see #980).
|
|
9
|
+
class SourceFileBuilder
|
|
10
|
+
attr_reader :missing_source_files
|
|
11
|
+
|
|
12
|
+
def initialize(original_result, not_loaded_files:)
|
|
13
|
+
@original_result = original_result
|
|
14
|
+
@not_loaded_files = not_loaded_files
|
|
15
|
+
@missing_source_files = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
SimpleCov::FileList.new(
|
|
20
|
+
@original_result
|
|
21
|
+
.filter_map { |filename, coverage| build_source_file(filename, coverage) }
|
|
22
|
+
.sort_by(&:filename)
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build_source_file(filename, coverage)
|
|
29
|
+
unless File.file?(filename)
|
|
30
|
+
@missing_source_files << filename
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
SimpleCov::SourceFile.new(
|
|
35
|
+
filename,
|
|
36
|
+
stringify_outer_keys(coverage),
|
|
37
|
+
loaded: !@not_loaded_files.include?(filename)
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# `Coverage.result` returns symbol keys (`:lines`, `:branches`,
|
|
42
|
+
# `:methods`); resultsets loaded from disk are already string-keyed.
|
|
43
|
+
# SourceFile reads with strings, and handles both Array and
|
|
44
|
+
# stringified-Array branch/method keys via `restore_ruby_data_structure`,
|
|
45
|
+
# so only the outer hash needs normalizing.
|
|
46
|
+
def stringify_outer_keys(coverage)
|
|
47
|
+
coverage.transform_keys(&:to_s)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/simplecov/result.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "digest/sha1"
|
|
4
4
|
require "forwardable"
|
|
5
|
+
require_relative "result/missing_source_files_reporter"
|
|
6
|
+
require_relative "result/source_file_builder"
|
|
5
7
|
|
|
6
8
|
module SimpleCov
|
|
7
9
|
#
|
|
@@ -10,9 +12,11 @@ module SimpleCov
|
|
|
10
12
|
#
|
|
11
13
|
class Result
|
|
12
14
|
extend Forwardable
|
|
15
|
+
|
|
13
16
|
# Returns the original Coverage.result used for this instance of SimpleCov::Result
|
|
14
17
|
attr_reader :original_result
|
|
15
|
-
# Returns all files that are applicable to this result (sans filters!) as instances of
|
|
18
|
+
# Returns all files that are applicable to this result (sans filters!) as instances of
|
|
19
|
+
# SimpleCov::SourceFile. Aliased as :source_files
|
|
16
20
|
attr_reader :files
|
|
17
21
|
alias source_files files
|
|
18
22
|
# Explicitly set the Time this result has been created
|
|
@@ -20,20 +24,47 @@ module SimpleCov
|
|
|
20
24
|
# Explicitly set the command name that was used for this coverage result. Defaults to SimpleCov.command_name
|
|
21
25
|
attr_writer :command_name
|
|
22
26
|
|
|
23
|
-
def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength,
|
|
27
|
+
def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength,
|
|
28
|
+
:covered_lines, :missed_lines,
|
|
29
|
+
:total_branches, :covered_branches, :missed_branches,
|
|
30
|
+
:total_methods, :covered_methods, :missed_methods,
|
|
31
|
+
:coverage_statistics, :coverage_statistics_by_file
|
|
24
32
|
def_delegator :files, :lines_of_code, :total_lines
|
|
25
33
|
|
|
34
|
+
# Bundles the filter and grouping configuration a Result applies to its
|
|
35
|
+
# source files after building them. Each field defaults to the SimpleCov
|
|
36
|
+
# singleton's configuration, so ordinary callers never construct one;
|
|
37
|
+
# tests pass a custom instance to opt out of (or extend) the project's
|
|
38
|
+
# filters or groups (e.g. `filters: []` to keep every file). Grouping the
|
|
39
|
+
# three together keeps Result#initialize's parameter list small.
|
|
40
|
+
class FilterConfig
|
|
41
|
+
attr_reader :filters, :cover_filters, :groups
|
|
42
|
+
|
|
43
|
+
def initialize(filters: SimpleCov.filters, cover_filters: SimpleCov.cover_filters, groups: SimpleCov.groups)
|
|
44
|
+
@filters = filters
|
|
45
|
+
@cover_filters = cover_filters
|
|
46
|
+
@groups = groups
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
26
50
|
# Initialize a new SimpleCov::Result from given Coverage.result (a Hash of filenames each containing an array of
|
|
27
|
-
# coverage data)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
51
|
+
# coverage data).
|
|
52
|
+
#
|
|
53
|
+
# `filter_config` defaults to the SimpleCov singleton's filter / group
|
|
54
|
+
# configuration so existing call sites are unchanged. Pass a custom
|
|
55
|
+
# FilterConfig to opt out — useful for tests that build synthetic Results
|
|
56
|
+
# and don't want the project's filters or groups applied.
|
|
57
|
+
def initialize(original_result, command_name: nil, created_at: nil, not_loaded_files: Set.new,
|
|
58
|
+
filter_config: FilterConfig.new)
|
|
59
|
+
@original_result = original_result.freeze
|
|
31
60
|
@command_name = command_name
|
|
32
61
|
@created_at = created_at
|
|
33
|
-
@
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
62
|
+
@groups_config = filter_config.groups
|
|
63
|
+
builder = SourceFileBuilder.new(original_result, not_loaded_files: not_loaded_files)
|
|
64
|
+
@files = builder.call
|
|
65
|
+
warn_about_missing_source_files(builder.missing_source_files, original_result.size)
|
|
66
|
+
apply_cover_filters!(filter_config.cover_filters)
|
|
67
|
+
apply_filters!(filter_config.filters)
|
|
37
68
|
end
|
|
38
69
|
|
|
39
70
|
# Returns all filenames for source files contained in this result
|
|
@@ -41,14 +72,37 @@ module SimpleCov
|
|
|
41
72
|
files.map(&:filename)
|
|
42
73
|
end
|
|
43
74
|
|
|
44
|
-
# Returns
|
|
75
|
+
# Returns the SimpleCov::SourceFile for the given path, or nil if no
|
|
76
|
+
# matching file is in this result. The path is resolved against
|
|
77
|
+
# SimpleCov.root, so callers can pass either an absolute path or a
|
|
78
|
+
# project-relative one.
|
|
79
|
+
def source_file_for(path)
|
|
80
|
+
target = File.expand_path(path, SimpleCov.root)
|
|
81
|
+
files.find { |file| file.filename == target }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns the {line:/branch:/method:} coverage_statistics hash for the
|
|
85
|
+
# given file path, or nil if no matching source file is in this
|
|
86
|
+
# result. See SimpleCov::Result#source_file_for for path resolution.
|
|
87
|
+
def coverage_for(path)
|
|
88
|
+
source_file_for(path)&.coverage_statistics
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns a Hash of groups for this result. Define groups using SimpleCov.group 'Models', 'app/models'
|
|
45
92
|
def groups
|
|
46
|
-
@groups ||= SimpleCov.grouped(files)
|
|
93
|
+
@groups ||= SimpleCov.grouped(files, groups: @groups_config)
|
|
47
94
|
end
|
|
48
95
|
|
|
49
|
-
# Applies the configured SimpleCov.formatter on this result
|
|
96
|
+
# Applies the configured SimpleCov.formatter on this result. Returns
|
|
97
|
+
# nil if formatting has been opted out of (`SimpleCov.formatter false`
|
|
98
|
+
# / `SimpleCov.formatters []`) — the cheap path for non-final
|
|
99
|
+
# processes in a parallel CI run, which only need their
|
|
100
|
+
# `.resultset.json` on disk. See #964.
|
|
50
101
|
def format!
|
|
51
|
-
SimpleCov.formatter
|
|
102
|
+
formatter = SimpleCov.formatter
|
|
103
|
+
return nil if formatter.nil?
|
|
104
|
+
|
|
105
|
+
formatter.new.format(self)
|
|
52
106
|
end
|
|
53
107
|
|
|
54
108
|
# Defines when this result has been created. Defaults to Time.now
|
|
@@ -81,14 +135,38 @@ module SimpleCov
|
|
|
81
135
|
|
|
82
136
|
private
|
|
83
137
|
|
|
138
|
+
def warn_about_missing_source_files(missing, input_size)
|
|
139
|
+
return if missing.empty?
|
|
140
|
+
|
|
141
|
+
MissingSourceFilesReporter.new(
|
|
142
|
+
missing,
|
|
143
|
+
input_size: input_size,
|
|
144
|
+
every_entry_dropped: @files.empty? && missing.size == input_size
|
|
145
|
+
).warn!
|
|
146
|
+
end
|
|
147
|
+
|
|
84
148
|
def coverage
|
|
85
|
-
|
|
86
|
-
Hash[keys.zip(original_result.values_at(*keys))]
|
|
149
|
+
original_result.slice(*filenames)
|
|
87
150
|
end
|
|
88
151
|
|
|
89
|
-
# Applies
|
|
90
|
-
|
|
91
|
-
|
|
152
|
+
# Applies the given filter chain to `@files`, dropping each source
|
|
153
|
+
# file that any filter matches.
|
|
154
|
+
def apply_filters!(filters)
|
|
155
|
+
filters.each do |filter|
|
|
156
|
+
@files = SimpleCov::FileList.new(@files.reject { |source_file| filter.matches?(source_file) })
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# When any `cover` matcher is configured, restrict `@files` to source
|
|
161
|
+
# files matching at least one of them. With no cover matchers configured
|
|
162
|
+
# this is a no-op, preserving the historical "everything required, then
|
|
163
|
+
# filtered" universe.
|
|
164
|
+
def apply_cover_filters!(cover_filters)
|
|
165
|
+
return if cover_filters.empty?
|
|
166
|
+
|
|
167
|
+
@files = SimpleCov::FileList.new(
|
|
168
|
+
@files.select { |source_file| cover_filters.any? { |filter| filter.matches?(source_file) } }
|
|
169
|
+
)
|
|
92
170
|
end
|
|
93
171
|
end
|
|
94
172
|
end
|
|
@@ -18,13 +18,75 @@ module SimpleCov
|
|
|
18
18
|
def adapt
|
|
19
19
|
return unless result
|
|
20
20
|
|
|
21
|
-
result.
|
|
22
|
-
|
|
23
|
-
adapted_result.merge!(file_name => {"lines" => cover_statistic})
|
|
24
|
-
else
|
|
25
|
-
adapted_result.merge!(file_name => cover_statistic)
|
|
26
|
-
end
|
|
21
|
+
result.to_h do |file_name, cover_statistic|
|
|
22
|
+
[file_name, adapt_one(file_name, cover_statistic)]
|
|
27
23
|
end
|
|
28
24
|
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Pre-0.18 resultsets pointed each filename straight at a line-coverage
|
|
29
|
+
# array; everything since uses the `{lines:, branches:, methods:}`
|
|
30
|
+
# shape. Newer entries also need their methods table massaged before
|
|
31
|
+
# downstream code merges across processes.
|
|
32
|
+
def adapt_one(file_name, cover_statistic)
|
|
33
|
+
return {"lines" => cover_statistic} if cover_statistic.is_a?(Array)
|
|
34
|
+
|
|
35
|
+
adapt_oneshot_lines_if_needed(file_name, cover_statistic)
|
|
36
|
+
normalize_method_keys(cover_statistic)
|
|
37
|
+
cover_statistic
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Normalize memory addresses in method coverage keys so that results
|
|
41
|
+
# from different processes can be merged. Anonymous class names like
|
|
42
|
+
# "#<Class:0x00007ff19ab24790>" get inconsistent addresses across runs.
|
|
43
|
+
# Address widths vary by runtime (32-bit hosts: 8 hex chars; 64-bit
|
|
44
|
+
# CRuby: 16; some JVM/TruffleRuby formats may differ), so match any
|
|
45
|
+
# length of hex digits and collapse to a single placeholder.
|
|
46
|
+
ADDRESS_PATTERN = /0x\h+/
|
|
47
|
+
private_constant :ADDRESS_PATTERN
|
|
48
|
+
|
|
49
|
+
ADDRESS_PLACEHOLDER = "0x0"
|
|
50
|
+
private_constant :ADDRESS_PLACEHOLDER
|
|
51
|
+
|
|
52
|
+
# Strip the `#<Class:Foo>` wrapper Ruby's Coverage adds to singleton-class
|
|
53
|
+
# method keys. `module_function` and class methods get recorded both as
|
|
54
|
+
# singleton (`[#<Class:Foo>, :m, …]`) and instance/module (`[Foo, :m, …]`)
|
|
55
|
+
# entries pointing at the same source location; only one of the two is
|
|
56
|
+
# ever reachable at runtime, so we merge them. Only applies to named
|
|
57
|
+
# constants — anonymous-class addresses like `#<Class:0x0>` are left
|
|
58
|
+
# alone (handled by ADDRESS_PATTERN above).
|
|
59
|
+
SINGLETON_WRAPPER_PATTERN = /\A#<Class:([A-Z_][\w:]*)>\z/
|
|
60
|
+
private_constant :SINGLETON_WRAPPER_PATTERN
|
|
61
|
+
|
|
62
|
+
def normalize_method_keys(cover_statistic)
|
|
63
|
+
methods = cover_statistic[:methods]
|
|
64
|
+
return unless methods
|
|
65
|
+
|
|
66
|
+
cover_statistic[:methods] = methods.each_with_object({}) do |(key, count), normalized|
|
|
67
|
+
normalized_key = key.dup
|
|
68
|
+
normalized_key[0] = key[0].to_s
|
|
69
|
+
.gsub(ADDRESS_PATTERN, ADDRESS_PLACEHOLDER)
|
|
70
|
+
.sub(SINGLETON_WRAPPER_PATTERN, '\1')
|
|
71
|
+
# Keys may collide after normalization (anonymous classes sharing a
|
|
72
|
+
# method name, or singleton + instance forms of a module_function method).
|
|
73
|
+
normalized[normalized_key] = normalized.fetch(normalized_key, 0) + count
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def adapt_oneshot_lines_if_needed(file_name, cover_statistic)
|
|
78
|
+
return unless cover_statistic.key?(:oneshot_lines)
|
|
79
|
+
|
|
80
|
+
oneshot_lines = cover_statistic.delete(:oneshot_lines)
|
|
81
|
+
line_stub = build_line_stub(file_name, oneshot_lines)
|
|
82
|
+
oneshot_lines.each { |covered_line| line_stub[covered_line - 1] = 1 }
|
|
83
|
+
cover_statistic[:lines] = line_stub
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_line_stub(file_name, oneshot_lines)
|
|
87
|
+
Coverage.line_stub(file_name)
|
|
88
|
+
rescue Errno::ENOENT, SyntaxError
|
|
89
|
+
Array.new(oneshot_lines.max || 0, nil)
|
|
90
|
+
end
|
|
29
91
|
end
|
|
30
92
|
end
|