simplecov 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +56 -1451
- data/lib/simplecov/atomic_file.rb +70 -0
- data/lib/simplecov/cli/clean.rb +43 -3
- data/lib/simplecov/cli/command_helpers.rb +55 -0
- data/lib/simplecov/cli/coverage.rb +23 -28
- data/lib/simplecov/cli/coverage_file.rb +65 -0
- data/lib/simplecov/cli/diff.rb +43 -47
- data/lib/simplecov/cli/dotfile.rb +21 -10
- data/lib/simplecov/cli/merge.rb +17 -13
- data/lib/simplecov/cli/open.rb +3 -5
- data/lib/simplecov/cli/report.rb +38 -25
- data/lib/simplecov/cli/run.rb +1 -1
- data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
- data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
- data/lib/simplecov/cli/serve.rb +45 -99
- data/lib/simplecov/cli/uncovered.rb +20 -24
- data/lib/simplecov/cli.rb +15 -2
- data/lib/simplecov/color.rb +10 -1
- data/lib/simplecov/combine/branches_combiner.rb +43 -18
- data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
- data/lib/simplecov/combine/identity_interner.rb +30 -0
- data/lib/simplecov/combine/interned_counts.rb +30 -0
- data/lib/simplecov/combine/lines_combiner.rb +48 -20
- data/lib/simplecov/combine/methods_combiner.rb +53 -5
- data/lib/simplecov/combine/results_combiner.rb +12 -36
- data/lib/simplecov/combine.rb +5 -23
- data/lib/simplecov/command_guesser.rb +66 -9
- data/lib/simplecov/configuration/coverage.rb +17 -16
- data/lib/simplecov/configuration/coverage_criteria.rb +37 -39
- data/lib/simplecov/configuration/eval_coverage.rb +41 -0
- data/lib/simplecov/configuration/filters.rb +11 -44
- data/lib/simplecov/configuration/formatting.rb +33 -14
- data/lib/simplecov/configuration/groups.rb +42 -0
- data/lib/simplecov/configuration/merging.rb +8 -7
- data/lib/simplecov/configuration/thresholds.rb +27 -22
- data/lib/simplecov/configuration.rb +21 -42
- data/lib/simplecov/coverage_json.rb +24 -0
- data/lib/simplecov/coverage_statistics.rb +5 -2
- data/lib/simplecov/coverage_violations.rb +17 -6
- data/lib/simplecov/defaults.rb +10 -6
- data/lib/simplecov/directive.rb +9 -4
- data/lib/simplecov/exit_codes/check.rb +33 -0
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +11 -20
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +4 -17
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +14 -23
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +14 -25
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +7 -20
- data/lib/simplecov/exit_codes.rb +17 -0
- data/lib/simplecov/exit_handling.rb +13 -43
- data/lib/simplecov/file_list.rb +14 -12
- data/lib/simplecov/filter.rb +43 -9
- data/lib/simplecov/formatter/base.rb +18 -1
- data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +36 -5
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
- data/lib/simplecov/formatter/html_formatter.rb +71 -45
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -50
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +78 -88
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +70 -76
- data/lib/simplecov/formatter/json_formatter.rb +5 -46
- data/lib/simplecov/formatter/multi_formatter.rb +8 -3
- data/lib/simplecov/formatter/simple_formatter.rb +8 -3
- data/lib/simplecov/formatter.rb +12 -0
- data/lib/simplecov/group_names.rb +32 -0
- data/lib/simplecov/last_run.rb +16 -9
- data/lib/simplecov/lines_classifier.rb +29 -8
- data/lib/simplecov/load_global_config.rb +5 -2
- data/lib/simplecov/parallel_adapters/base.rb +17 -0
- data/lib/simplecov/parallel_adapters/generic.rb +9 -8
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
- data/lib/simplecov/parallel_adapters.rb +4 -2
- data/lib/simplecov/parallel_coordination.rb +6 -1
- data/lib/simplecov/parallel_result_merger.rb +230 -0
- data/lib/simplecov/process.rb +22 -0
- data/lib/simplecov/report_deferral.rb +49 -0
- data/lib/simplecov/report_stamp.rb +28 -0
- data/lib/simplecov/result.rb +40 -10
- data/lib/simplecov/result_adapter.rb +79 -10
- data/lib/simplecov/result_merger/resultset_file.rb +43 -7
- data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
- data/lib/simplecov/result_merger/resultset_store.rb +15 -12
- data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
- data/lib/simplecov/result_merger.rb +69 -42
- data/lib/simplecov/result_processing.rb +85 -46
- data/lib/simplecov/run_identity.rb +77 -0
- data/lib/simplecov/simulate_coverage.rb +37 -12
- data/lib/simplecov/source_file/line.rb +2 -2
- data/lib/simplecov/source_file/method.rb +8 -2
- data/lib/simplecov/source_file/method_builder.rb +3 -1
- data/lib/simplecov/source_file/ruby_data_parser.rb +25 -3
- data/lib/simplecov/source_file/skip_chunks.rb +7 -10
- data/lib/simplecov/source_file/source_loader.rb +25 -9
- data/lib/simplecov/source_file/statistics.rb +24 -16
- data/lib/simplecov/source_file.rb +2 -2
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +239 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +236 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +7 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +74 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +74 -62
- data/lib/simplecov/unloaded_file_injector.rb +75 -0
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov.rb +37 -17
- data/sig/simplecov.rbs +1777 -0
- metadata +32 -16
- data/doc/alternate-formatters.md +0 -66
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -30
- data/lib/simplecov/formatter/html_formatter/public/application.css +0 -1
- data/lib/simplecov/formatter/html_formatter/public/application.js +0 -18
- 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/schemas/coverage-v1.0.schema.json +0 -306
- data/schemas/coverage.schema.json +0 -306
data/lib/simplecov/exit_codes.rb
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
|
+
# Exit statuses SimpleCov sets when coverage checks fail, and the output
|
|
5
|
+
# helper the enforcement machinery reports through.
|
|
4
6
|
module ExitCodes
|
|
5
7
|
SUCCESS = 0
|
|
6
8
|
EXCEPTION = 1
|
|
7
9
|
MINIMUM_COVERAGE = 2
|
|
8
10
|
MAXIMUM_COVERAGE_DROP = 3
|
|
9
11
|
MAXIMUM_COVERAGE = 4
|
|
12
|
+
|
|
13
|
+
# Threshold-violation reports and exit-status notices are the output of
|
|
14
|
+
# the enforcement feature, not Ruby warnings: routing them through
|
|
15
|
+
# `Kernel#warn` made `-W0` swallow the explanation for a failing exit
|
|
16
|
+
# code, let `Warning.warn` hooks (warning trackers, raise-on-warning
|
|
17
|
+
# test setups) intercept them mid-`at_exit`, and fed colorized text to
|
|
18
|
+
# warning logs. `print_errors false` remains the intended opt-out.
|
|
19
|
+
def self.print_error(message)
|
|
20
|
+
$stderr.puts message # rubocop:disable Style/StderrPuts
|
|
21
|
+
rescue IOError
|
|
22
|
+
# A parallel runner can close a worker's stderr before its at_exit
|
|
23
|
+
# hooks run (rspec-conductor does). The violation still has to set
|
|
24
|
+
# the exit status even when its explanation cannot be printed.
|
|
25
|
+
end
|
|
10
26
|
end
|
|
11
27
|
end
|
|
12
28
|
|
|
29
|
+
require_relative "exit_codes/check"
|
|
13
30
|
require_relative "exit_codes/exit_code_handling"
|
|
14
31
|
require_relative "exit_codes/maximum_coverage_drop_check"
|
|
15
32
|
require_relative "exit_codes/maximum_overall_coverage_check"
|
|
@@ -25,50 +25,20 @@ module SimpleCov
|
|
|
25
25
|
# or a test consumed the result) then don't run exit tasks.
|
|
26
26
|
return unless Coverage.running?
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
SimpleCov.run_exit_tasks!
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Returns true when our process has no coverage data to contribute
|
|
36
|
-
# (after the resultset merge) and a newer report already exists on
|
|
37
|
-
# disk. Typically fires when `SimpleCov.start` ran in a parent
|
|
38
|
-
# process — e.g. a Rakefile or Rails' `Bundler.require` — that
|
|
39
|
-
# shelled out to the test runner. See issue #581.
|
|
40
|
-
def defer_to_existing_report?
|
|
41
|
-
return false unless existing_report_newer_than_us?
|
|
42
|
-
|
|
43
|
-
res = result
|
|
44
|
-
empty = res.nil? || res.files.empty?
|
|
45
|
-
warn_about_deferred_report if empty
|
|
46
|
-
empty
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def existing_report_newer_than_us?
|
|
50
|
-
return false unless process_start_time
|
|
51
|
-
|
|
52
|
-
last_run_path = File.join(coverage_path, ".last_run.json")
|
|
53
|
-
File.exist?(last_run_path) && File.mtime(last_run_path) > process_start_time
|
|
54
|
-
end
|
|
28
|
+
# Capture the suite's exit status BEFORE the deferral probe: its
|
|
29
|
+
# freshness check rescues filesystem errors, and completing any
|
|
30
|
+
# rescue inside an at_exit handler resets $ERROR_INFO to nil.
|
|
31
|
+
error_exit_status = exit_status_from_exception
|
|
55
32
|
|
|
56
|
-
|
|
57
|
-
return
|
|
33
|
+
# Stand down when we'd only clobber a fresher report (#581).
|
|
34
|
+
return if defer_to_existing_report?
|
|
58
35
|
|
|
59
|
-
|
|
60
|
-
"Skipping SimpleCov report — this process tracked no application code and a newer " \
|
|
61
|
-
"report already exists at #{coverage_path}. This usually means SimpleCov.start ran in a " \
|
|
62
|
-
"parent process (e.g. a Rakefile or Rails' Bundler.require) that shelled out to the test " \
|
|
63
|
-
"runner. See https://github.com/simplecov-ruby/simplecov/issues/581.",
|
|
64
|
-
:yellow
|
|
65
|
-
)
|
|
36
|
+
SimpleCov.run_exit_tasks!(error_exit_status)
|
|
66
37
|
end
|
|
67
38
|
|
|
68
|
-
# @api private — called from the at_exit block
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
39
|
+
# @api private — called from the at_exit block (which pre-captures
|
|
40
|
+
# the status, see there) and by `collate` (the default argument).
|
|
41
|
+
def run_exit_tasks!(error_exit_status = exit_status_from_exception)
|
|
72
42
|
at_exit.call
|
|
73
43
|
|
|
74
44
|
exit_and_report_previous_error(error_exit_status) if previous_error?(error_exit_status)
|
|
@@ -97,7 +67,7 @@ module SimpleCov
|
|
|
97
67
|
# @api private
|
|
98
68
|
def exit_and_report_previous_error(exit_status)
|
|
99
69
|
if print_errors
|
|
100
|
-
|
|
70
|
+
ExitCodes.print_error SimpleCov::Color.colorize(
|
|
101
71
|
"Stopped processing SimpleCov as a previous error not related to SimpleCov has been detected",
|
|
102
72
|
:yellow
|
|
103
73
|
)
|
|
@@ -113,7 +83,7 @@ module SimpleCov
|
|
|
113
83
|
# would surface a spurious "below minimum" violation about the
|
|
114
84
|
# missing slice rather than a real shortfall.
|
|
115
85
|
def ready_to_process_results?
|
|
116
|
-
merge_finalization_owner? &&
|
|
86
|
+
merge_finalization_owner? && result? &&
|
|
117
87
|
(collating_result? || parallel_results_complete?)
|
|
118
88
|
end
|
|
119
89
|
|
|
@@ -124,7 +94,7 @@ module SimpleCov
|
|
|
124
94
|
return unless exit_status.positive?
|
|
125
95
|
|
|
126
96
|
if print_errors
|
|
127
|
-
|
|
97
|
+
ExitCodes.print_error SimpleCov::Color.colorize(
|
|
128
98
|
"SimpleCov failed with exit #{exit_status} due to a coverage related error", :red
|
|
129
99
|
)
|
|
130
100
|
end
|
data/lib/simplecov/file_list.rb
CHANGED
|
@@ -27,8 +27,8 @@ module SimpleCov
|
|
|
27
27
|
# returns the `{line:, branch:, method:}` Hash; pass a criterion symbol
|
|
28
28
|
# (`:line` / `:branch` / `:method`) to get that one CoverageStatistics.
|
|
29
29
|
def coverage_statistics(criterion = nil)
|
|
30
|
-
@coverage_statistics ||= compute_coverage_statistics
|
|
31
|
-
criterion ?
|
|
30
|
+
stats = (@coverage_statistics ||= compute_coverage_statistics)
|
|
31
|
+
criterion ? stats[criterion] : stats
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def coverage_statistics_by_file
|
|
@@ -47,15 +47,11 @@ module SimpleCov
|
|
|
47
47
|
|
|
48
48
|
# Returns the count of lines that are not relevant for coverage
|
|
49
49
|
def never_lines
|
|
50
|
-
return 0.0 if empty?
|
|
51
|
-
|
|
52
50
|
sum { |f| f.never_lines.size }
|
|
53
51
|
end
|
|
54
52
|
|
|
55
53
|
# Returns the count of skipped lines
|
|
56
54
|
def skipped_lines
|
|
57
|
-
return 0.0 if empty?
|
|
58
|
-
|
|
59
55
|
sum { |f| f.skipped_lines.size }
|
|
60
56
|
end
|
|
61
57
|
|
|
@@ -65,9 +61,13 @@ module SimpleCov
|
|
|
65
61
|
map(&:covered_percent)
|
|
66
62
|
end
|
|
67
63
|
|
|
68
|
-
# Finds the least covered file and returns that file's name
|
|
64
|
+
# Finds the least covered file and returns that file's name, or nil
|
|
65
|
+
# for an empty list (e.g. a fully filtered result).
|
|
69
66
|
def least_covered_file
|
|
70
|
-
|
|
67
|
+
# `covered_percent` is nil only for an unmeasured criterion, and :line
|
|
68
|
+
# is always measured, so the `|| 0.0` arm never fires at runtime; it
|
|
69
|
+
# exists to satisfy min_by's Comparable requirement.
|
|
70
|
+
min_by { |file| file.covered_percent || 0.0 }&.filename
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Returns the overall amount of relevant lines of code across all files in this list
|
|
@@ -137,7 +137,10 @@ module SimpleCov
|
|
|
137
137
|
# enabled set so disabled criteria don't surface in totals, JSON,
|
|
138
138
|
# or the HTML report.
|
|
139
139
|
def compute_coverage_statistics_by_file
|
|
140
|
-
seed = enabled_criteria_for_reporting.to_h
|
|
140
|
+
seed = enabled_criteria_for_reporting.to_h do |criterion|
|
|
141
|
+
bucket = [] #: Array[CoverageStatistics]
|
|
142
|
+
[criterion, bucket]
|
|
143
|
+
end
|
|
141
144
|
@files.each_with_object(seed) do |file, together|
|
|
142
145
|
file.coverage_statistics.each do |criterion, stats|
|
|
143
146
|
together[criterion] << stats if together.key?(criterion)
|
|
@@ -153,9 +156,8 @@ module SimpleCov
|
|
|
153
156
|
# criterion is enabled; the JRuby-gated branch/method criteria are
|
|
154
157
|
# reported when they pass their own engine-support check.
|
|
155
158
|
def enabled_criteria_for_reporting
|
|
156
|
-
criteria = []
|
|
157
|
-
criteria << :line if SimpleCov.
|
|
158
|
-
SimpleCov.coverage_criterion_enabled?(:oneshot_line)
|
|
159
|
+
criteria = [] #: Array[SimpleCov::criterion]
|
|
160
|
+
criteria << :line if SimpleCov.line_coverage?
|
|
159
161
|
criteria << :branch if SimpleCov.branch_coverage?
|
|
160
162
|
criteria << :method if SimpleCov.method_coverage?
|
|
161
163
|
criteria
|
data/lib/simplecov/filter.rb
CHANGED
|
@@ -23,10 +23,26 @@ module SimpleCov
|
|
|
23
23
|
raise NotImplementedError, "The base filter class is not intended for direct use"
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
# Whether this filter's verdict depends only on the file's path, so it can
|
|
27
|
+
# be decided before that file has any coverage. Used when recording which
|
|
28
|
+
# tracked files a process did not load, where no coverage exists yet.
|
|
29
|
+
# Defaults to false so a custom filter is never guessed at. See #1250.
|
|
30
|
+
def path_only?
|
|
31
|
+
false
|
|
32
|
+
end
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
# `string_filter` selects the semantics of bare String arguments —
|
|
35
|
+
# StringFilter's segment-substring match for `add_filter`/`skip`,
|
|
36
|
+
# GlobFilter for `cover` — and threads through Array elements so a
|
|
37
|
+
# list gets the same treatment as its members.
|
|
38
|
+
def self.build_filter(filter_argument, string_filter: SimpleCov::StringFilter)
|
|
39
|
+
case filter_argument
|
|
40
|
+
when SimpleCov::Filter then filter_argument
|
|
41
|
+
when String then string_filter.new(filter_argument)
|
|
42
|
+
when Array
|
|
43
|
+
SimpleCov::ArrayFilter.new(filter_argument.map { |arg| build_filter(arg, string_filter: string_filter) })
|
|
44
|
+
else class_for_argument(filter_argument).new(filter_argument)
|
|
45
|
+
end
|
|
30
46
|
end
|
|
31
47
|
|
|
32
48
|
def self.class_for_argument(filter_argument)
|
|
@@ -57,6 +73,10 @@ module SimpleCov
|
|
|
57
73
|
source_file.project_filename.match?(segment_pattern)
|
|
58
74
|
end
|
|
59
75
|
|
|
76
|
+
def path_only?
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
|
|
60
80
|
private
|
|
61
81
|
|
|
62
82
|
def segment_pattern
|
|
@@ -68,14 +88,15 @@ module SimpleCov
|
|
|
68
88
|
escaped = Regexp.escape(normalized)
|
|
69
89
|
boundary = '(?:\A|/)'
|
|
70
90
|
|
|
71
|
-
if normalized.
|
|
72
|
-
# Filename pattern (e.g. "test.rb" matches "faked_test.rb"): allow
|
|
73
|
-
# substring match within the last path segment, anchored to a
|
|
74
|
-
# segment boundary.
|
|
75
|
-
%r{#{boundary}[^/]*#{escaped}}
|
|
76
|
-
elsif normalized.end_with?("/")
|
|
91
|
+
if normalized.end_with?("/")
|
|
77
92
|
# Trailing slash signals directory-only matching.
|
|
78
93
|
/#{boundary}#{escaped}/
|
|
94
|
+
elsif normalized.include?(".") && !normalized.include?("/")
|
|
95
|
+
# Bare filename pattern (e.g. "test.rb" matches "faked_test.rb"):
|
|
96
|
+
# allow a substring match within a single path segment. Multi-segment
|
|
97
|
+
# arguments must not get this relaxation, or "app/models/user.rb"
|
|
98
|
+
# would also match "webapp/models/user.rb".
|
|
99
|
+
%r{#{boundary}[^/]*#{escaped}(?=[/.]|\z)}
|
|
79
100
|
else
|
|
80
101
|
# Directory or path: require a segment-boundary match so "lib"
|
|
81
102
|
# matches "lib/" but not "library/".
|
|
@@ -95,6 +116,10 @@ module SimpleCov
|
|
|
95
116
|
def matches?(source_file)
|
|
96
117
|
filter_argument.match?(source_file.project_filename)
|
|
97
118
|
end
|
|
119
|
+
|
|
120
|
+
def path_only?
|
|
121
|
+
true
|
|
122
|
+
end
|
|
98
123
|
end
|
|
99
124
|
|
|
100
125
|
# Filter that matches when the configured block returns truthy for the
|
|
@@ -115,6 +140,10 @@ module SimpleCov
|
|
|
115
140
|
def matches?(source_file)
|
|
116
141
|
File.fnmatch?(filter_argument, source_file.project_filename, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
117
142
|
end
|
|
143
|
+
|
|
144
|
+
def path_only?
|
|
145
|
+
true
|
|
146
|
+
end
|
|
118
147
|
end
|
|
119
148
|
|
|
120
149
|
# Filter that matches when any of its component filters (built from the
|
|
@@ -128,6 +157,11 @@ module SimpleCov
|
|
|
128
157
|
super(filter_objects)
|
|
129
158
|
end
|
|
130
159
|
|
|
160
|
+
# Path-decidable only when every component filter is.
|
|
161
|
+
def path_only?
|
|
162
|
+
filter_argument.all?(&:path_only?)
|
|
163
|
+
end
|
|
164
|
+
|
|
131
165
|
# Returns true if any of the filters in the array match the given source file.
|
|
132
166
|
# Configure this Filter like StringFilter.new(['some/path', /^some_regex/, Proc.new {|src_file| ... }])
|
|
133
167
|
def matches?(source_files_list)
|
|
@@ -23,6 +23,22 @@ module SimpleCov
|
|
|
23
23
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
|
+
# The one home of the "status lines go to stderr, not through warn"
|
|
27
|
+
# decision (see #1225). stderr rather than stdout because this is a
|
|
28
|
+
# status message, not the program's output, so it stays out of
|
|
29
|
+
# pipelines like `rspec -f json`. And `$stderr.puts` rather than
|
|
30
|
+
# `warn` so the line neither reaches `Warning.warn` hooks (warning
|
|
31
|
+
# trackers, raise-on-warning test setups) nor vanishes under `-W0`.
|
|
32
|
+
# Subclasses call this at the end of their `format`.
|
|
33
|
+
def emit_status(result)
|
|
34
|
+
$stderr.puts output_message(result) unless @silent # rubocop:disable Style/StderrPuts
|
|
35
|
+
rescue IOError
|
|
36
|
+
# A parallel runner can close a worker's stderr before its at_exit
|
|
37
|
+
# hooks run (rspec-conductor does). Losing the status line must not
|
|
38
|
+
# abort the exit tasks that follow report generation, i.e. the
|
|
39
|
+
# threshold checks and the .last_run.json write.
|
|
40
|
+
end
|
|
41
|
+
|
|
26
42
|
# Subclasses override to prepend a marker (e.g. "JSON ") to the
|
|
27
43
|
# summary line. Default empty for the HTML formatter, which has
|
|
28
44
|
# historically been the unmarked default.
|
|
@@ -45,7 +61,8 @@ module SimpleCov
|
|
|
45
61
|
# confusing than the absolute form. See issue #197.
|
|
46
62
|
def displayable_output_path
|
|
47
63
|
directory = relative_or_absolute_output_path
|
|
48
|
-
|
|
64
|
+
entry_point = entry_point_filename
|
|
65
|
+
entry_point ? File.join(directory, entry_point) : directory
|
|
49
66
|
end
|
|
50
67
|
|
|
51
68
|
def relative_or_absolute_output_path
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "../atomic_file"
|
|
6
|
+
|
|
7
|
+
module SimpleCov
|
|
8
|
+
module Formatter
|
|
9
|
+
# Shared writer for the coverage.json artifact, used by JSONFormatter
|
|
10
|
+
# (as its report) and HTMLFormatter (as the side file feeding
|
|
11
|
+
# `simplecov serve` and external tools). Centralizing the write keeps
|
|
12
|
+
# the two copies byte-identical (same pretty printing, same binary
|
|
13
|
+
# mode, so no CRLF translation on Windows depending on which
|
|
14
|
+
# formatter wrote last) and gives both formatters the
|
|
15
|
+
# concurrent-overwrite warning of issue #1171.
|
|
16
|
+
module CoverageJSONWriter
|
|
17
|
+
FILENAME = "coverage.json"
|
|
18
|
+
|
|
19
|
+
# The previous report can embed the project's entire source text,
|
|
20
|
+
# so the overwrite check bounds how much of it is read looking for
|
|
21
|
+
# the meta object before falling back to a full parse.
|
|
22
|
+
META_SCAN_BYTES = 64 * 1024
|
|
23
|
+
private_constant :META_SCAN_BYTES
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def write(output_path, hash, result)
|
|
28
|
+
path = File.join(output_path, FILENAME)
|
|
29
|
+
warn_if_concurrent_overwrite(path, result)
|
|
30
|
+
AtomicFile.write(path, JSON.pretty_generate(hash), binary: true)
|
|
31
|
+
path
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Warns when the existing coverage.json has a timestamp newer than this
|
|
35
|
+
# process's start time — a strong signal that a sibling test process
|
|
36
|
+
# (e.g., parallel_tests) wrote it while we were running, and that our
|
|
37
|
+
# write is about to clobber their data.
|
|
38
|
+
def warn_if_concurrent_overwrite(path, result)
|
|
39
|
+
start_time = SimpleCov.process_start_time or return
|
|
40
|
+
existing = existing_meta(path) or return
|
|
41
|
+
return unless existing[:timestamp] > start_time
|
|
42
|
+
|
|
43
|
+
# Both formatters write coverage.json through this method, so when
|
|
44
|
+
# they are configured together the file found here was just written
|
|
45
|
+
# by our own run, not a concurrent one. A matching command_name
|
|
46
|
+
# means the same merged result, so there's nothing to lose by
|
|
47
|
+
# overwriting. See issue #1171.
|
|
48
|
+
return if existing[:command_name] == result.command_name
|
|
49
|
+
|
|
50
|
+
warn "simplecov: #{path} was written at #{existing[:timestamp].iso8601} — after " \
|
|
51
|
+
"this process started at #{start_time.iso8601}. Overwriting " \
|
|
52
|
+
"likely loses coverage data from a concurrent test run. For " \
|
|
53
|
+
"parallel test setups, use SimpleCov::ResultMerger or run a single " \
|
|
54
|
+
"collation step after all workers finish."
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def existing_meta(path)
|
|
58
|
+
return nil unless File.exist?(path)
|
|
59
|
+
|
|
60
|
+
meta = parse_meta(path) or return nil
|
|
61
|
+
timestamp = meta[:timestamp] or return nil
|
|
62
|
+
|
|
63
|
+
{timestamp: Time.iso8601(timestamp), command_name: meta[:command_name]}
|
|
64
|
+
rescue ArgumentError
|
|
65
|
+
# An unparseable timestamp disables the check for this file.
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_meta(path)
|
|
70
|
+
parse_meta_head(path) || parse_meta_full(path)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The meta object is flat and sits at the head of every file this
|
|
74
|
+
# module writes, so the common case parses just that slice instead
|
|
75
|
+
# of a multi-megabyte report. A miss (foreign key order, a brace
|
|
76
|
+
# inside a meta string) falls back to the full parse.
|
|
77
|
+
def parse_meta_head(path)
|
|
78
|
+
head = File.read(path, META_SCAN_BYTES).to_s
|
|
79
|
+
slice = head[/"meta"\s*:\s*(\{.*?\})/m, 1]
|
|
80
|
+
return nil unless slice
|
|
81
|
+
|
|
82
|
+
# The captured slice is always a JSON object, so a successful parse
|
|
83
|
+
# is always a Hash.
|
|
84
|
+
JSON.parse(slice, symbolize_names: true)
|
|
85
|
+
rescue JSON::ParserError
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_meta_full(path)
|
|
90
|
+
parsed = JSON.parse(File.read(path), symbolize_names: true)
|
|
91
|
+
parsed.is_a?(Hash) ? parsed[:meta] : nil
|
|
92
|
+
rescue JSON::ParserError
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|