simplecov 1.1.0 → 1.2.0
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/README.md +165 -57
- data/exe/simplecov +0 -3
- data/lib/minitest/simplecov_plugin.rb +12 -9
- data/lib/simplecov/atomic_file.rb +16 -19
- data/lib/simplecov/autostart.rb +5 -7
- data/lib/simplecov/baseline/parser.rb +58 -0
- data/lib/simplecov/baseline.rb +170 -0
- data/lib/simplecov/cli/affected/changed_files.rb +57 -0
- data/lib/simplecov/cli/affected/selection.rb +117 -0
- data/lib/simplecov/cli/affected.rb +148 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +23 -24
- data/lib/simplecov/cli/command_helpers.rb +55 -19
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +25 -28
- data/lib/simplecov/cli/coverage_file.rb +58 -14
- data/lib/simplecov/cli/dead_code/output.rb +100 -0
- data/lib/simplecov/cli/dead_code.rb +139 -0
- data/lib/simplecov/cli/diff/output.rb +45 -0
- data/lib/simplecov/cli/diff.rb +35 -67
- data/lib/simplecov/cli/dotfile.rb +83 -38
- data/lib/simplecov/cli/git.rb +41 -0
- data/lib/simplecov/cli/history/output.rb +135 -0
- data/lib/simplecov/cli/history.rb +67 -0
- data/lib/simplecov/cli/merge.rb +34 -39
- data/lib/simplecov/cli/open.rb +12 -15
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +135 -0
- data/lib/simplecov/cli/patch.rb +169 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +18 -24
- data/lib/simplecov/cli/run.rb +8 -9
- data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
- data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
- data/lib/simplecov/cli/serve.rb +27 -24
- data/lib/simplecov/cli/show/annotator.rb +87 -0
- data/lib/simplecov/cli/show/sweep.rb +36 -0
- data/lib/simplecov/cli/show.rb +133 -0
- data/lib/simplecov/cli/status/facts.rb +87 -0
- data/lib/simplecov/cli/status.rb +93 -0
- data/lib/simplecov/cli/tests/redundancy.rb +78 -0
- data/lib/simplecov/cli/tests.rb +148 -0
- data/lib/simplecov/cli/uncovered/misses.rb +37 -0
- data/lib/simplecov/cli/uncovered.rb +70 -32
- data/lib/simplecov/cli/usage.rb +152 -0
- data/lib/simplecov/cli/watch/live_report.rb +73 -0
- data/lib/simplecov/cli/watch/narrator.rb +55 -0
- data/lib/simplecov/cli/watch/poller.rb +45 -0
- data/lib/simplecov/cli/watch/session.rb +134 -0
- data/lib/simplecov/cli/watch/test_plan.rb +43 -0
- data/lib/simplecov/cli/watch.rb +85 -0
- data/lib/simplecov/cli.rb +43 -92
- data/lib/simplecov/color.rb +21 -35
- data/lib/simplecov/combine/branches_combiner.rb +7 -31
- data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
- data/lib/simplecov/combine/identity_interner.rb +10 -13
- data/lib/simplecov/combine/interned_counts.rb +7 -9
- data/lib/simplecov/combine/lines_combiner.rb +20 -44
- data/lib/simplecov/combine/methods_combiner.rb +9 -35
- data/lib/simplecov/combine/results_combiner.rb +4 -21
- data/lib/simplecov/combine.rb +0 -6
- data/lib/simplecov/command_guesser.rb +20 -34
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +91 -79
- data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +6 -10
- data/lib/simplecov/configuration/filters.rb +40 -73
- data/lib/simplecov/configuration/formatting.rb +74 -83
- data/lib/simplecov/configuration/groups.rb +19 -13
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +31 -22
- data/lib/simplecov/configuration/merging.rb +94 -62
- data/lib/simplecov/configuration/missed_caps.rb +51 -0
- data/lib/simplecov/configuration/production.rb +23 -0
- data/lib/simplecov/configuration/test_tracking.rb +48 -0
- data/lib/simplecov/configuration/thresholds.rb +33 -62
- data/lib/simplecov/configuration/view_coverage.rb +38 -0
- data/lib/simplecov/configuration.rb +70 -76
- data/lib/simplecov/configuration_error.rb +5 -0
- data/lib/simplecov/context_map/union.rb +81 -0
- data/lib/simplecov/context_map.rb +187 -0
- data/lib/simplecov/coverage_json.rb +2 -5
- data/lib/simplecov/coverage_statistics.rb +14 -24
- data/lib/simplecov/coverage_violations.rb +157 -64
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +4 -23
- data/lib/simplecov/deprecation.rb +45 -27
- data/lib/simplecov/directive.rb +46 -62
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +17 -11
- data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
- data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
- data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
- data/lib/simplecov/exit_codes.rb +17 -7
- data/lib/simplecov/exit_handling.rb +94 -59
- data/lib/simplecov/file_list.rb +20 -51
- data/lib/simplecov/filter.rb +43 -66
- data/lib/simplecov/formatter/base.rb +38 -51
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
- data/lib/simplecov/formatter/html_formatter/public/index.html +17 -10
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
- data/lib/simplecov/formatter/html_formatter.rb +24 -41
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
- data/lib/simplecov/formatter/json_formatter.rb +7 -13
- data/lib/simplecov/formatter/multi_formatter.rb +5 -11
- data/lib/simplecov/formatter/simple_formatter.rb +2 -8
- data/lib/simplecov/formatter.rb +14 -10
- data/lib/simplecov/group_names.rb +13 -12
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +6 -9
- data/lib/simplecov/lines_classifier.rb +31 -16
- data/lib/simplecov/load_global_config.rb +7 -7
- data/lib/simplecov/parallel_adapters/base.rb +13 -41
- data/lib/simplecov/parallel_adapters/generic.rb +7 -18
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
- data/lib/simplecov/parallel_adapters.rb +24 -53
- data/lib/simplecov/parallel_coordination.rb +39 -49
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +67 -126
- data/lib/simplecov/process.rb +14 -32
- data/lib/simplecov/production/error.rb +11 -0
- data/lib/simplecov/production/file_sink.rb +130 -0
- data/lib/simplecov/production.rb +249 -0
- data/lib/simplecov/profiles/rails.rb +1 -12
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +9 -19
- data/lib/simplecov/profiles.rb +11 -33
- data/lib/simplecov/report_deferral.rb +11 -15
- data/lib/simplecov/report_stamp.rb +6 -9
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +7 -12
- data/lib/simplecov/result.rb +68 -117
- data/lib/simplecov/result_adapter.rb +58 -68
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
- data/lib/simplecov/result_merger/resultset_file.rb +20 -27
- data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
- data/lib/simplecov/result_merger/resultset_store.rb +23 -14
- data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
- data/lib/simplecov/result_merger.rb +59 -58
- data/lib/simplecov/result_processing.rb +74 -119
- data/lib/simplecov/run_identity.rb +16 -18
- data/lib/simplecov/simulate_coverage.rb +35 -50
- data/lib/simplecov/source_file/branch.rb +8 -33
- data/lib/simplecov/source_file/branch_builder.rb +28 -47
- data/lib/simplecov/source_file/builder_context.rb +6 -13
- data/lib/simplecov/source_file/line.rb +13 -33
- data/lib/simplecov/source_file/line_builder.rb +9 -15
- data/lib/simplecov/source_file/method.rb +3 -7
- data/lib/simplecov/source_file/method_builder.rb +6 -15
- data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
- data/lib/simplecov/source_file/skip_chunks.rb +19 -28
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +10 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
- data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
- data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
- data/lib/simplecov/static_coverage_extractor.rb +46 -62
- data/lib/simplecov/test_tracker/accessors.rb +34 -0
- data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
- data/lib/simplecov/test_tracker/delta.rb +69 -0
- data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
- data/lib/simplecov/test_tracker.rb +162 -0
- data/lib/simplecov/unloaded_file_injector.rb +18 -31
- data/lib/simplecov/useless_results_remover.rb +7 -6
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
- data/lib/simplecov/view_coverage.rb +55 -0
- data/lib/simplecov-html.rb +1 -0
- data/lib/simplecov.rb +59 -114
- data/lib/simplecov_json_formatter.rb +1 -0
- data/man/simplecov.1 +349 -0
- data/sig/simplecov.rbs +523 -595
- metadata +72 -7
- data/schemas/coverage-v1.0.schema.json +0 -306
- data/schemas/coverage.schema.json +0 -306
|
@@ -1,48 +1,71 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
|
-
# Computes coverage threshold violations for a given result. Shared by
|
|
5
|
-
# the exit-code checks and the JSON formatter's `errors` section.
|
|
6
|
-
#
|
|
7
|
-
# Each method returns an array of violation hashes. All percents are
|
|
8
|
-
# rounded via `SimpleCov.round_coverage` so downstream consumers don't
|
|
9
|
-
# need to round again.
|
|
10
4
|
module CoverageViolations
|
|
11
5
|
class << self
|
|
12
|
-
# @return [Array<Hash>] {:criterion, :expected, :actual}
|
|
13
6
|
def minimum_overall(result, thresholds)
|
|
14
7
|
thresholds.filter_map do |criterion, expected|
|
|
15
|
-
actual =
|
|
8
|
+
actual = floored_percent_for(result, criterion) or next
|
|
16
9
|
{criterion: criterion, expected: expected, actual: actual} if actual < expected
|
|
17
10
|
end
|
|
18
11
|
end
|
|
19
12
|
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# places (matching the existing minimum-coverage behavior), so an
|
|
23
|
-
# actual of e.g. 95.4287 is treated as 95.42 — meaning a maximum of
|
|
24
|
-
# 95.42 still passes. See issue #187 for the rationale.
|
|
13
|
+
# An actual of 95.4287 floors to 95.42, so a maximum of 95.42 passes
|
|
14
|
+
# (#187).
|
|
25
15
|
def maximum_overall(result, thresholds)
|
|
26
16
|
thresholds.filter_map do |criterion, expected|
|
|
27
|
-
actual =
|
|
17
|
+
actual = floored_percent_for(result, criterion) or next
|
|
28
18
|
{criterion: criterion, expected: expected, actual: actual} if actual > expected
|
|
29
19
|
end
|
|
30
20
|
end
|
|
31
21
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
# overrides; for each file, defaults are merged with every matching override
|
|
37
|
-
# (later wins per criterion, overrides win over defaults).
|
|
38
|
-
def minimum_by_file(result, defaults, overrides = {})
|
|
22
|
+
# A file and criterion with a `baseline` floor is exempt here: the baseline
|
|
23
|
+
# check holds it to its own floor instead, which is the fall-through the
|
|
24
|
+
# ratchet workflow rests on (#1268).
|
|
25
|
+
def minimum_by_file(result, defaults, overrides = {}, baseline: nil)
|
|
39
26
|
result.files.flat_map do |file|
|
|
40
27
|
effective = effective_per_file_thresholds(file, defaults, overrides)
|
|
41
|
-
effective.filter_map
|
|
28
|
+
effective.filter_map do |criterion, expected|
|
|
29
|
+
next if baseline&.covers?(file.project_filename, criterion)
|
|
30
|
+
|
|
31
|
+
file_minimum_violation(file, criterion, expected)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A baseline violation needs the file below its floor on both axes: a
|
|
37
|
+
# percent under the floor's, and (when the floor records one) more misses
|
|
38
|
+
# than it allows. The missed count is what keeps percent movement from
|
|
39
|
+
# pure edits out of the answer.
|
|
40
|
+
def baseline(result, baseline)
|
|
41
|
+
return [] unless baseline
|
|
42
|
+
|
|
43
|
+
result.files.flat_map do |file|
|
|
44
|
+
entry = baseline.entry_for(file.project_filename)
|
|
45
|
+
next [] unless entry
|
|
46
|
+
|
|
47
|
+
entry.filter_map { |criterion, floor| baseline_violation(file, criterion, floor) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def maximum_missed(result, caps)
|
|
52
|
+
caps.filter_map do |criterion, maximum|
|
|
53
|
+
actual = missed_for(result, criterion) or next
|
|
54
|
+
{criterion: criterion, maximum: maximum, actual: actual} if actual > maximum
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def maximum_missed_by_file(result, defaults, overrides = {}, baseline: nil)
|
|
59
|
+
result.files.flat_map do |file|
|
|
60
|
+
effective = effective_per_file_thresholds(file, defaults, overrides)
|
|
61
|
+
effective.filter_map do |criterion, maximum|
|
|
62
|
+
next if baseline&.covers?(file.project_filename, criterion)
|
|
63
|
+
|
|
64
|
+
file_missed_cap_violation(file, criterion, maximum)
|
|
65
|
+
end
|
|
42
66
|
end
|
|
43
67
|
end
|
|
44
68
|
|
|
45
|
-
# @return [Array<Hash>] {:group_name, :criterion, :expected, :actual}
|
|
46
69
|
def minimum_by_group(result, thresholds)
|
|
47
70
|
thresholds.flat_map do |group_name, minimums|
|
|
48
71
|
group = lookup_group(result, group_name)
|
|
@@ -51,50 +74,38 @@ module SimpleCov
|
|
|
51
74
|
end
|
|
52
75
|
end
|
|
53
76
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return [] unless last_run
|
|
77
|
+
def maximum_drop(result, thresholds, last_run: nil, mode: SimpleCov.drop_baseline)
|
|
78
|
+
baseline = drop_baseline_percents(mode, last_run)
|
|
79
|
+
return [] unless baseline
|
|
58
80
|
|
|
59
81
|
thresholds.filter_map do |criterion, maximum|
|
|
60
|
-
actual = compute_drop(criterion, result,
|
|
82
|
+
actual = compute_drop(criterion, result, baseline)
|
|
61
83
|
{criterion: criterion, maximum: maximum, actual: actual} if actual && actual > maximum
|
|
62
84
|
end
|
|
63
85
|
end
|
|
64
86
|
|
|
65
|
-
|
|
87
|
+
private
|
|
66
88
|
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
# module doesn't emit branch data). The config-time
|
|
73
|
-
# `raise_if_criterion_disabled` check still catches the genuine
|
|
74
|
-
# "forgot to enable the criterion" mistake before we ever get here.
|
|
75
|
-
def percent_for(stats_source, criterion)
|
|
89
|
+
# Answers nil, and the caller silently skips, when the criterion was
|
|
90
|
+
# configured but not measured by the runtime (`branch: 100` under the
|
|
91
|
+
# "strict" profile on JRuby, say). The config-time
|
|
92
|
+
# `raise_if_criterion_disabled` check catches the genuine mistake earlier.
|
|
93
|
+
def floored_percent_for(stats_source, criterion)
|
|
76
94
|
stats = stats_source.coverage_statistics[SimpleCov.coverage_statistics_key(criterion)]
|
|
77
95
|
round(stats.percent) if stats
|
|
78
96
|
end
|
|
79
97
|
|
|
80
|
-
# Walk the overrides in declaration order, merging each one that matches
|
|
81
|
-
# the file's project path into the running effective threshold (so the
|
|
82
|
-
# most-specific or latest-declared override wins per criterion). Returns
|
|
83
|
-
# the defaults Hash unchanged when nothing matches.
|
|
84
98
|
def effective_per_file_thresholds(file, defaults, overrides)
|
|
85
|
-
return defaults if overrides.empty?
|
|
86
|
-
|
|
87
99
|
path = file.project_filename
|
|
88
100
|
overrides.reduce(defaults) do |acc, (pattern, criterion_thresholds)|
|
|
89
101
|
path_matches?(path, pattern) ? acc.merge(criterion_thresholds) : acc
|
|
90
102
|
end
|
|
91
103
|
end
|
|
92
104
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
# so no dead `else` branch is needed here.
|
|
105
|
+
# `==` and `eql?` are indistinguishable for the String operands, so that
|
|
106
|
+
# mutation is equivalent. The override examples pin every arm instead.
|
|
107
|
+
# mutant:disable
|
|
108
|
+
# mutant:disable
|
|
98
109
|
def path_matches?(project_filename, pattern)
|
|
99
110
|
return project_filename.match?(pattern) if pattern.is_a?(Regexp)
|
|
100
111
|
return project_filename.start_with?(pattern) if pattern.end_with?("/")
|
|
@@ -102,8 +113,33 @@ module SimpleCov
|
|
|
102
113
|
project_filename == pattern
|
|
103
114
|
end
|
|
104
115
|
|
|
116
|
+
def missed_for(stats_source, criterion)
|
|
117
|
+
stats = stats_source.coverage_statistics[SimpleCov.coverage_statistics_key(criterion)]
|
|
118
|
+
stats&.missed
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def file_missed_cap_violation(file, criterion, maximum)
|
|
122
|
+
actual = missed_for(file, criterion) or return
|
|
123
|
+
return unless actual > maximum
|
|
124
|
+
|
|
125
|
+
{criterion: criterion, maximum: maximum, actual: actual,
|
|
126
|
+
filename: file.filename, project_filename: file.project_filename}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def baseline_violation(file, criterion, floor)
|
|
130
|
+
stats = file.coverage_statistics[SimpleCov.coverage_statistics_key(criterion)]
|
|
131
|
+
return unless stats
|
|
132
|
+
|
|
133
|
+
actual = round(stats.percent)
|
|
134
|
+
return unless actual < floor.percent
|
|
135
|
+
return if floor.missed && stats.missed <= floor.missed
|
|
136
|
+
|
|
137
|
+
{criterion: criterion, expected: floor.percent, allowed_missed: floor.missed, actual: actual,
|
|
138
|
+
actual_missed: stats.missed, filename: file.filename, project_filename: file.project_filename}
|
|
139
|
+
end
|
|
140
|
+
|
|
105
141
|
def file_minimum_violation(file, criterion, expected)
|
|
106
|
-
actual =
|
|
142
|
+
actual = floored_percent_for(file, criterion) or return
|
|
107
143
|
return unless actual < expected
|
|
108
144
|
|
|
109
145
|
{
|
|
@@ -117,37 +153,94 @@ module SimpleCov
|
|
|
117
153
|
|
|
118
154
|
def group_minimum_violations(group_name, group, minimums)
|
|
119
155
|
minimums.filter_map do |criterion, expected|
|
|
120
|
-
actual =
|
|
156
|
+
actual = floored_percent_for(group, criterion) or next
|
|
121
157
|
{group_name: group_name, criterion: criterion, expected: expected, actual: actual} if actual < expected
|
|
122
158
|
end
|
|
123
159
|
end
|
|
124
160
|
|
|
125
|
-
# The misconfiguration notice is enforcement output, not a Ruby
|
|
126
|
-
#
|
|
127
|
-
# `-W0` and `Warning.warn` hooks (see ExitCodes.print_error) and
|
|
128
|
-
# honor the `print_errors` opt-out.
|
|
161
|
+
# The misconfiguration notice is enforcement output, not a Ruby warning: it
|
|
162
|
+
# must survive `-W0` and `Warning.warn` hooks, and honor `print_errors`.
|
|
129
163
|
def lookup_group(result, group_name)
|
|
130
164
|
group = result.groups[group_name]
|
|
131
165
|
if group.nil? && SimpleCov.print_errors
|
|
132
166
|
ExitCodes.print_error "minimum_coverage_by_group: no group named '#{group_name}' exists. " \
|
|
133
|
-
"Available groups: #{result.groups.keys.join(
|
|
167
|
+
"Available groups: #{result.groups.keys.join(", ")}"
|
|
134
168
|
end
|
|
135
169
|
group
|
|
136
170
|
end
|
|
137
171
|
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
172
|
+
def drop_baseline_percents(mode, last_run)
|
|
173
|
+
case mode
|
|
174
|
+
when :median then median_baseline
|
|
175
|
+
when :branch then branch_baseline
|
|
176
|
+
else last_run_baseline(last_run || LastRun.read)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# A hand-edited .last_run.json can carry any value type, and `LastRun.read`
|
|
181
|
+
# only vouches for the top level being a Hash. `:covered_percent` is the
|
|
182
|
+
# pre-criteria file format's spelling of the line percent.
|
|
183
|
+
def last_run_baseline(last_run)
|
|
184
|
+
return nil unless last_run.is_a?(Hash)
|
|
185
|
+
|
|
186
|
+
previous = (_ = last_run)[:result]
|
|
187
|
+
return nil unless previous.is_a?(Hash)
|
|
188
|
+
|
|
189
|
+
previous = _ = previous
|
|
190
|
+
{line: previous[:line] || previous[:covered_percent],
|
|
191
|
+
branch: previous[:branch], method: previous[:method]}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def median_baseline
|
|
195
|
+
totals = history_totals
|
|
196
|
+
baseline = {} #: Hash[Symbol, untyped]
|
|
197
|
+
%i[line branch method].each do |criterion|
|
|
198
|
+
values = totals.map { |entry| entry[criterion.to_s] }.grep(Numeric)
|
|
199
|
+
baseline[criterion] = median(values) unless values.empty?
|
|
200
|
+
end
|
|
201
|
+
baseline
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def history_totals
|
|
205
|
+
History.read.filter_map do |entry|
|
|
206
|
+
entry.fetch("totals") if entry.is_a?(Hash) && entry["totals"].is_a?(Hash)
|
|
143
207
|
end
|
|
144
|
-
|
|
145
|
-
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def median(values)
|
|
211
|
+
sorted = values.sort
|
|
212
|
+
mid = sorted.length / 2
|
|
213
|
+
sorted.length.odd? ? sorted.fetch(mid) : (sorted.fetch(mid - 1) + sorted.fetch(mid)).fdiv(2)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def branch_baseline
|
|
217
|
+
branch, = History.git_info
|
|
218
|
+
return nil unless branch
|
|
219
|
+
|
|
220
|
+
entry = History.read.reverse_each.find { |candidate| branch_entry?(candidate, branch) }
|
|
221
|
+
return nil unless entry
|
|
222
|
+
|
|
223
|
+
totals = {} #: Hash[Symbol, untyped]
|
|
224
|
+
entry.fetch("totals").each { |key, value| totals[key.to_sym] = value }
|
|
225
|
+
totals
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# `==` and `eql?` are indistinguishable for the String operands, so that
|
|
229
|
+
# mutation is equivalent. The branch-baseline examples pin every clause.
|
|
230
|
+
# mutant:disable
|
|
231
|
+
# mutant:disable
|
|
232
|
+
def branch_entry?(candidate, branch)
|
|
233
|
+
candidate.is_a?(Hash) && candidate["branch"] == branch && candidate["totals"].is_a?(Hash)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def compute_drop(criterion, result, baseline)
|
|
237
|
+
stats_key = SimpleCov.coverage_statistics_key(criterion)
|
|
238
|
+
last_coverage_percent = baseline[stats_key]
|
|
146
239
|
# Treat a non-numeric percent like a missing one instead of
|
|
147
240
|
# raising out of the at_exit hook.
|
|
148
241
|
return unless last_coverage_percent.is_a?(Numeric)
|
|
149
242
|
|
|
150
|
-
current =
|
|
243
|
+
current = floored_percent_for(result, criterion) or return
|
|
151
244
|
(last_coverage_percent - current).floor(10)
|
|
152
245
|
end
|
|
153
246
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
# The state one measured run accumulates in one process: the pid and start
|
|
5
|
+
# time that identify it, the result it computes, the flags the merge
|
|
6
|
+
# coordination reads, and the serial the parent hands each forked
|
|
7
|
+
# subprocess. A new run is a new object, which is what lets anything needing
|
|
8
|
+
# a clean slate begin one instead of unsetting state by hand.
|
|
9
|
+
class CurrentRun
|
|
10
|
+
attr_accessor :pid, :process_start_time, :result
|
|
11
|
+
attr_writer :collating_result
|
|
12
|
+
|
|
13
|
+
# The result, the coordination flags and the identity belong to the run that
|
|
14
|
+
# ends; the fork genealogy belongs to the process and carries over. A child
|
|
15
|
+
# that restarts tracking must not forget it was forked, or it would produce
|
|
16
|
+
# the final report itself, and a parent must not recount from zero, or its
|
|
17
|
+
# next children's names would collide with its first ones (#1171).
|
|
18
|
+
def successor
|
|
19
|
+
following = self.class.new
|
|
20
|
+
following.subprocess_serial = @subprocess_serial
|
|
21
|
+
following.mark_forked_subprocess! if forked_subprocess?
|
|
22
|
+
following
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Answers exactly false while no result was ever stored, which is how "never
|
|
26
|
+
# computed" stays distinct from "cleared to nil".
|
|
27
|
+
def result?
|
|
28
|
+
instance_variable_defined?(:@result) && (_ = @result)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def collating_result?
|
|
32
|
+
!!@collating_result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Reading a mark that was never set answers nil, which is the answer wanted.
|
|
36
|
+
def forked_subprocess?
|
|
37
|
+
!!@forked_subprocess
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def mark_forked_subprocess!
|
|
41
|
+
@forked_subprocess = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A monotonically increasing serial the parent assigns to each forked
|
|
45
|
+
# subprocess. The default `at_fork` builds the worker's command_name from
|
|
46
|
+
# this rather than the OS pid: the serial sequence is the same from one run
|
|
47
|
+
# to the next, so a re-run overwrites the previous run's resultset entries
|
|
48
|
+
# instead of writing uniquely-named ones that pile up (#1171).
|
|
49
|
+
def subprocess_serial
|
|
50
|
+
@subprocess_serial ||= 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def next_subprocess_serial!
|
|
54
|
+
@subprocess_serial = subprocess_serial + 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
protected
|
|
58
|
+
|
|
59
|
+
attr_writer :subprocess_serial
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/simplecov/defaults.rb
CHANGED
|
@@ -4,46 +4,27 @@ require "English"
|
|
|
4
4
|
require "pathname"
|
|
5
5
|
require_relative "formatter/html_formatter"
|
|
6
6
|
|
|
7
|
-
# Default configuration. Profiles autoload on first reference via
|
|
8
|
-
# `SimpleCov.profiles.fetch_proc`; the unused ones (e.g. "rails")
|
|
9
|
-
# never get required unless a user opts in.
|
|
10
7
|
SimpleCov.configure do
|
|
11
8
|
formatter SimpleCov::Formatter::HTMLFormatter
|
|
12
9
|
|
|
13
10
|
load_profile "bundler_filter"
|
|
14
11
|
load_profile "hidden_filter"
|
|
15
|
-
# Exclude files outside of SimpleCov.root. Mirrors the early prune done
|
|
16
|
-
# by SimpleCov::UselessResultsRemover so the user-facing filter chain
|
|
17
|
-
# honors the same boundary; both share the regex.
|
|
18
12
|
load_profile "root_filter"
|
|
19
|
-
# Exclude test framework directories (`test/`, `spec/`, `features/`,
|
|
20
|
-
# `autotest/`). The test suite runs 100% of the test files themselves,
|
|
21
|
-
# which inflates totals and obscures the application coverage that
|
|
22
|
-
# actually matters. Drop with `remove_filter %r{\A(test|features|spec|autotest)/}`
|
|
23
|
-
# if you want test files counted (e.g. to surface dead helpers).
|
|
24
13
|
load_profile "test_frameworks"
|
|
25
14
|
end
|
|
26
15
|
|
|
27
|
-
#
|
|
28
|
-
# The program name is kept separately as well: the join
|
|
29
|
-
# path containing a space, and the guesser reads the executable from it.
|
|
30
|
-
SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(
|
|
16
|
+
# Stashed as early as possible, before rake or test/unit tamper with ARGV
|
|
17
|
+
# (#110). The program name is kept separately as well: the join is lossy for
|
|
18
|
+
# a path containing a space, and the guesser reads the executable from it.
|
|
19
|
+
SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(" ")}"
|
|
31
20
|
SimpleCov::CommandGuesser.original_program_name = $PROGRAM_NAME
|
|
32
21
|
|
|
33
|
-
# Autoload config from ~/.simplecov if present
|
|
34
22
|
require_relative "load_global_config"
|
|
35
23
|
|
|
36
|
-
# Autoload config from .simplecov if present
|
|
37
|
-
# Recurse upwards until we find .simplecov or reach the root directory
|
|
38
|
-
|
|
39
24
|
config_path = Pathname.new(SimpleCov.root)
|
|
40
25
|
loop do
|
|
41
26
|
filename = config_path.join(".simplecov")
|
|
42
27
|
if filename.exist?
|
|
43
|
-
# `.simplecov` is a configuration file; SimpleCov.start calls inside
|
|
44
|
-
# it draw a deprecation warning, and tracking still begins for
|
|
45
|
-
# backward compatibility. See `SimpleCov.with_dot_simplecov_autoload`
|
|
46
|
-
# and issue #581.
|
|
47
28
|
SimpleCov.with_dot_simplecov_autoload do
|
|
48
29
|
load filename.to_s
|
|
49
30
|
rescue LoadError, StandardError => e
|
|
@@ -1,45 +1,63 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "configuration_error"
|
|
4
|
+
|
|
3
5
|
module SimpleCov
|
|
4
|
-
# Emits legacy-API deprecation warnings, deduplicated by the source
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# to fix. See issue #1204.
|
|
6
|
+
# Emits legacy-API deprecation warnings, deduplicated by the source location
|
|
7
|
+
# that triggered them. A deprecated method called in a loop, or a config
|
|
8
|
+
# block re-evaluated once per parallel worker, otherwise repeats the same
|
|
9
|
+
# notice until stderr is unreadable. Keying on the caller location collapses
|
|
10
|
+
# those repeats while still warning separately about each distinct call site
|
|
11
|
+
# the user needs to fix (#1204).
|
|
11
12
|
module Deprecation
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
# `
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
MODES = %i[warn raise].freeze
|
|
16
|
+
|
|
17
|
+
# The mode lives here rather than on the configuration singleton so that
|
|
18
|
+
# `warn` depends on nothing but itself. `SimpleCov.deprecations` is the
|
|
19
|
+
# front door that writes it, and the CLI, which loads none of the
|
|
20
|
+
# configuration surface, can still warn.
|
|
21
|
+
def mode
|
|
22
|
+
@mode ||= :warn
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def mode=(mode)
|
|
26
|
+
raise ConfigurationError, "deprecations takes :warn or :raise, got #{mode.inspect}" unless MODES.include?(mode)
|
|
27
|
+
|
|
28
|
+
@mode = mode
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# `message` is the notice without the `[DEPRECATION]` tag or location
|
|
32
|
+
# prefix, both of which are added here. `deprecations :raise` turns every
|
|
33
|
+
# deprecated API into an error before the dedup: an error is not a notice to
|
|
34
|
+
# collapse, and a CI guard must fail on the second offender as surely as on
|
|
35
|
+
# the first. A missing backtrace falls back to keying on the message, so it
|
|
36
|
+
# never silently swallows every notice.
|
|
37
|
+
def warn(message, location: caller_location)
|
|
38
|
+
raise ConfigurationError, message if mode.equal?(:raise)
|
|
39
|
+
|
|
29
40
|
return unless emitted.add?(location || message)
|
|
30
41
|
|
|
31
42
|
Kernel.warn "#{"#{location}: " if location}[DEPRECATION] #{message}"
|
|
32
43
|
end
|
|
33
44
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
45
|
+
# Every shipped call site is a one-level alias such as `track_files`, so the
|
|
46
|
+
# frame three up from here is the user's own code. `Array(...)` coerces a
|
|
47
|
+
# missing backtrace to `[]` so `.first` yields nil rather than raising.
|
|
48
|
+
def caller_location
|
|
49
|
+
Array(Kernel.caller(3..3)).first
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Parallel workers are separate processes with their own set, so each warns
|
|
53
|
+
# at most once.
|
|
36
54
|
def emitted
|
|
37
55
|
@emitted ||= Set.new
|
|
38
56
|
end
|
|
39
57
|
|
|
40
|
-
# @api private — reset emitted state between tests.
|
|
41
58
|
def reset!
|
|
42
59
|
@emitted = Set.new
|
|
60
|
+
@mode = :warn
|
|
43
61
|
end
|
|
44
62
|
end
|
|
45
63
|
end
|