simplecov 0.22.0 → 1.3.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/LICENSE +1 -1
- data/README.md +173 -875
- data/exe/simplecov +8 -0
- data/lib/minitest/simplecov_plugin.rb +18 -7
- data/lib/simplecov/atomic_file.rb +67 -0
- data/lib/simplecov/autostart.rb +9 -0
- 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/annotations.rb +166 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +86 -0
- data/lib/simplecov/cli/command_helpers.rb +91 -0
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +83 -0
- data/lib/simplecov/cli/coverage_file.rb +109 -0
- 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 +115 -0
- data/lib/simplecov/cli/dotfile.rb +156 -0
- 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 +115 -0
- data/lib/simplecov/cli/open.rb +45 -0
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +151 -0
- data/lib/simplecov/cli/patch.rb +175 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +91 -0
- data/lib/simplecov/cli/run.rb +35 -0
- data/lib/simplecov/cli/serve/report_preparer.rb +30 -0
- data/lib/simplecov/cli/serve/static_file_handler.rb +128 -0
- data/lib/simplecov/cli/serve.rb +88 -0
- 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 +33 -0
- data/lib/simplecov/cli/uncovered.rb +127 -0
- data/lib/simplecov/cli/usage.rb +153 -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 +114 -0
- data/lib/simplecov/color.rb +65 -0
- data/lib/simplecov/combine/branches_combiner.rb +44 -21
- data/lib/simplecov/combine/coverage_accumulator.rb +175 -0
- data/lib/simplecov/combine/identity_interner.rb +27 -0
- data/lib/simplecov/combine/interned_counts.rb +28 -0
- data/lib/simplecov/combine/lines_combiner.rb +35 -29
- data/lib/simplecov/combine/methods_combiner.rb +48 -0
- data/lib/simplecov/combine/results_combiner.rb +9 -49
- data/lib/simplecov/combine.rb +0 -24
- data/lib/simplecov/command_guesser.rb +99 -42
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +184 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +143 -0
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +37 -0
- data/lib/simplecov/configuration/filters.rb +131 -0
- data/lib/simplecov/configuration/formatting.rb +129 -0
- data/lib/simplecov/configuration/groups.rb +48 -0
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +72 -0
- data/lib/simplecov/configuration/merging.rb +197 -0
- 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 +150 -0
- data/lib/simplecov/configuration/view_coverage.rb +37 -0
- data/lib/simplecov/configuration.rb +100 -443
- 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 +21 -0
- data/lib/simplecov/coverage_statistics.rb +24 -28
- data/lib/simplecov/coverage_violations.rb +254 -0
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +20 -28
- data/lib/simplecov/deprecation.rb +63 -0
- data/lib/simplecov/directive/erb.rb +44 -0
- data/lib/simplecov/directive/haml.rb +33 -0
- data/lib/simplecov/directive/indented_template.rb +37 -0
- data/lib/simplecov/directive/slim.rb +35 -0
- data/lib/simplecov/directive/template.rb +29 -0
- data/lib/simplecov/directive.rb +152 -0
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +39 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +24 -5
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +17 -67
- 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 +27 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +20 -38
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +28 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -34
- data/lib/simplecov/exit_codes.rb +26 -0
- data/lib/simplecov/exit_handling.rb +163 -0
- data/lib/simplecov/file_list.rb +63 -48
- data/lib/simplecov/filter.rb +98 -42
- data/lib/simplecov/formatter/base.rb +100 -0
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +95 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +93 -0
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +153 -0
- data/lib/simplecov/formatter/html_formatter.rb +88 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +122 -0
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +132 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +97 -0
- data/lib/simplecov/formatter/json_formatter.rb +40 -0
- data/lib/simplecov/formatter/multi_formatter.rb +8 -10
- data/lib/simplecov/formatter/simple_formatter.rb +12 -15
- data/lib/simplecov/formatter.rb +21 -1
- data/lib/simplecov/group_names.rb +33 -0
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +16 -5
- data/lib/simplecov/lines_classifier.rb +67 -19
- data/lib/simplecov/load_global_config.rb +12 -4
- data/lib/simplecov/parallel_adapters/base.rb +50 -0
- data/lib/simplecov/parallel_adapters/generic.rb +33 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +72 -0
- data/lib/simplecov/parallel_adapters.rb +54 -0
- data/lib/simplecov/parallel_coordination.rb +124 -0
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +171 -0
- data/lib/simplecov/process.rb +34 -12
- 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/bundler_filter.rb +1 -1
- data/lib/simplecov/profiles/hidden_filter.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +13 -10
- data/lib/simplecov/profiles/root_filter.rb +2 -5
- data/lib/simplecov/profiles/strict.rb +22 -0
- data/lib/simplecov/profiles/test_frameworks.rb +1 -4
- data/lib/simplecov/profiles.rb +26 -19
- data/lib/simplecov/report_deferral.rb +45 -0
- data/lib/simplecov/report_stamp.rb +25 -0
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +48 -0
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +46 -0
- data/lib/simplecov/result.rb +123 -47
- data/lib/simplecov/result_adapter.rb +130 -11
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +24 -0
- data/lib/simplecov/result_merger/resultset_file.rb +67 -0
- data/lib/simplecov/result_merger/resultset_run_identity.rb +65 -0
- data/lib/simplecov/result_merger/resultset_store.rb +62 -0
- data/lib/simplecov/result_merger/unloaded_files.rb +95 -0
- data/lib/simplecov/result_merger.rb +120 -124
- data/lib/simplecov/result_processing.rb +188 -0
- data/lib/simplecov/run_identity.rb +75 -0
- data/lib/simplecov/simulate_coverage.rb +72 -17
- data/lib/simplecov/source_file/branch.rb +8 -35
- data/lib/simplecov/source_file/branch_builder.rb +95 -0
- data/lib/simplecov/source_file/builder_context.rb +28 -0
- data/lib/simplecov/source_file/line.rb +17 -32
- data/lib/simplecov/source_file/line_builder.rb +37 -0
- data/lib/simplecov/source_file/method.rb +54 -0
- data/lib/simplecov/source_file/method_builder.rb +51 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +103 -0
- data/lib/simplecov/source_file/skip_chunks.rb +72 -0
- data/lib/simplecov/source_file/source_loader.rb +89 -0
- data/lib/simplecov/source_file/statistics.rb +76 -0
- data/lib/simplecov/source_file.rb +64 -260
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +155 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +191 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +45 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +46 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +61 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +163 -0
- data/lib/simplecov/static_coverage_extractor.rb +78 -0
- 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 +62 -0
- data/lib/simplecov/useless_results_remover.rb +17 -7
- 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 +5 -0
- data/lib/simplecov.rb +143 -394
- data/lib/simplecov_json_formatter.rb +5 -0
- data/man/simplecov.1 +354 -0
- data/sig/simplecov.rbs +1710 -0
- metadata +183 -61
- data/CHANGELOG.md +0 -191
- data/doc/alternate-formatters.md +0 -71
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -24
- data/lib/simplecov/default_formatter.rb +0 -20
data/lib/simplecov/filter.rb
CHANGED
|
@@ -2,15 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
#
|
|
5
|
-
# Base filter class. Inherit from this to create custom filters,
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# # A sample class that rejects all source files.
|
|
9
|
-
# class StupidFilter < SimpleCov::Filter
|
|
10
|
-
# def passes?(source_file)
|
|
11
|
-
# false
|
|
12
|
-
# end
|
|
13
|
-
# end
|
|
5
|
+
# Base filter class. Inherit from this to create custom filters, and
|
|
6
|
+
# override `matches?(source_file)`.
|
|
14
7
|
#
|
|
15
8
|
class Filter
|
|
16
9
|
attr_reader :filter_argument
|
|
@@ -20,61 +13,122 @@ module SimpleCov
|
|
|
20
13
|
end
|
|
21
14
|
|
|
22
15
|
def matches?(_source_file)
|
|
23
|
-
raise "The base filter class is not intended for direct use"
|
|
16
|
+
raise NotImplementedError, "The base filter class is not intended for direct use"
|
|
24
17
|
end
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
# Whether this filter's verdict depends only on the file's path, so it can
|
|
20
|
+
# be decided before that file has any coverage. Used when recording which
|
|
21
|
+
# tracked files a process did not load. Defaults to false so a custom
|
|
22
|
+
# filter is never guessed at (#1250).
|
|
23
|
+
def path_only?
|
|
24
|
+
false
|
|
29
25
|
end
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
# `string_filter` selects the semantics of bare String arguments:
|
|
28
|
+
# StringFilter's segment-substring match for `skip`, GlobFilter for
|
|
29
|
+
# `cover`. It threads through Array elements so a list gets the same
|
|
30
|
+
# treatment as its members.
|
|
31
|
+
def self.build_filter(filter_argument, string_filter: StringFilter)
|
|
32
|
+
case filter_argument
|
|
33
|
+
when Filter then filter_argument
|
|
34
|
+
when String then string_filter.new(filter_argument)
|
|
35
|
+
when Array
|
|
36
|
+
ArrayFilter.new(filter_argument.map { |arg| build_filter(arg, string_filter: string_filter) })
|
|
37
|
+
else class_for_argument(filter_argument).new(filter_argument)
|
|
38
|
+
end
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
def self.class_for_argument(filter_argument)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
filter_classes_by_argument_type.find { |type, _| filter_argument.is_a?(type) }&.last ||
|
|
43
|
+
raise(ConfigurationError, "You have provided an unrecognized filter type")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.filter_classes_by_argument_type
|
|
47
|
+
@filter_classes_by_argument_type ||= {
|
|
48
|
+
String => StringFilter,
|
|
49
|
+
Regexp => RegexFilter,
|
|
50
|
+
Array => ArrayFilter,
|
|
51
|
+
Proc => BlockFilter
|
|
52
|
+
}.freeze
|
|
53
|
+
end
|
|
54
|
+
private_class_method :filter_classes_by_argument_type
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class StringFilter < Filter
|
|
58
|
+
# Matching is path-segment-aware: the argument must appear immediately
|
|
59
|
+
# after a "/" and be followed by "/" or end-of-string, so "lib" matches
|
|
60
|
+
# "/lib/foo.rb" but not "/app/models/library.rb".
|
|
61
|
+
def matches?(source_file)
|
|
62
|
+
source_file.project_filename.match?(segment_pattern)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def path_only?
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def segment_pattern
|
|
72
|
+
@segment_pattern ||= compute_segment_pattern
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def compute_segment_pattern
|
|
76
|
+
normalized = filter_argument.delete_prefix("/")
|
|
77
|
+
escaped = Regexp.escape(normalized)
|
|
78
|
+
boundary = '(?:\A|/)'
|
|
79
|
+
|
|
80
|
+
if normalized.end_with?("/")
|
|
81
|
+
# Trailing slash signals directory-only matching.
|
|
82
|
+
/#{boundary}#{escaped}/
|
|
83
|
+
elsif normalized.include?(".") && !normalized.include?("/")
|
|
84
|
+
# Bare filename pattern ("test.rb" matches "faked_test.rb"): allow a
|
|
85
|
+
# substring match within a single path segment. Multi-segment arguments
|
|
86
|
+
# must not get this relaxation, or "app/models/user.rb" would also
|
|
87
|
+
# match "webapp/models/user.rb". No boundary here: `[^/]*` already
|
|
88
|
+
# reaches back to the segment start, so prefixing one matches the same
|
|
89
|
+
# paths.
|
|
90
|
+
%r{[^/]*#{escaped}(?=[/.]|\z)}
|
|
47
91
|
else
|
|
48
|
-
|
|
92
|
+
# Require a segment-boundary match so "lib" matches "lib/" but not
|
|
93
|
+
# "library/".
|
|
94
|
+
%r{#{boundary}#{escaped}(?=[/.]|\z)}
|
|
49
95
|
end
|
|
50
96
|
end
|
|
51
97
|
end
|
|
52
98
|
|
|
53
|
-
class
|
|
54
|
-
#
|
|
55
|
-
#
|
|
99
|
+
class RegexFilter < Filter
|
|
100
|
+
# `Regexp#match?` rather than `=~` so the predicate returns a real boolean:
|
|
101
|
+
# `=~` would return the match position, which trips rspec-mocks 4's
|
|
102
|
+
# stricter predicate-matcher type check.
|
|
56
103
|
def matches?(source_file)
|
|
57
|
-
source_file.project_filename
|
|
104
|
+
filter_argument.match?(source_file.project_filename)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def path_only?
|
|
108
|
+
true
|
|
58
109
|
end
|
|
59
110
|
end
|
|
60
111
|
|
|
61
|
-
class
|
|
62
|
-
# Returns true when the given source file's filename matches the
|
|
63
|
-
# regex configured when initializing this Filter with RegexFilter.new(/someregex/)
|
|
112
|
+
class BlockFilter < Filter
|
|
64
113
|
def matches?(source_file)
|
|
65
|
-
(source_file
|
|
114
|
+
filter_argument.call(source_file)
|
|
66
115
|
end
|
|
67
116
|
end
|
|
68
117
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
118
|
+
# Matches when the source file's project path matches the configured shell
|
|
119
|
+
# glob. Used by `cover`, and by `skip` when callers want glob semantics
|
|
120
|
+
# instead of `StringFilter`'s substring match.
|
|
121
|
+
class GlobFilter < Filter
|
|
72
122
|
def matches?(source_file)
|
|
73
|
-
|
|
123
|
+
File.fnmatch?(filter_argument, source_file.project_filename, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def path_only?
|
|
127
|
+
true
|
|
74
128
|
end
|
|
75
129
|
end
|
|
76
130
|
|
|
77
|
-
class ArrayFilter <
|
|
131
|
+
class ArrayFilter < Filter
|
|
78
132
|
def initialize(filter_argument)
|
|
79
133
|
filter_objects = filter_argument.map do |arg|
|
|
80
134
|
Filter.build_filter(arg)
|
|
@@ -83,8 +137,10 @@ module SimpleCov
|
|
|
83
137
|
super(filter_objects)
|
|
84
138
|
end
|
|
85
139
|
|
|
86
|
-
|
|
87
|
-
|
|
140
|
+
def path_only?
|
|
141
|
+
filter_argument.all?(&:path_only?)
|
|
142
|
+
end
|
|
143
|
+
|
|
88
144
|
def matches?(source_files_list)
|
|
89
145
|
filter_argument.any? do |arg|
|
|
90
146
|
arg.matches?(source_files_list)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module Formatter
|
|
7
|
+
# @api private
|
|
8
|
+
#
|
|
9
|
+
# Shared scaffolding for formatters that write a coverage report to an
|
|
10
|
+
# output directory and emit a "Coverage report generated for X to Y"
|
|
11
|
+
# summary on stderr. Subclasses override `format`, and may override
|
|
12
|
+
# `message_prefix`.
|
|
13
|
+
class Base
|
|
14
|
+
# `output_dir` defaults to `SimpleCov.coverage_path`. Pass it explicitly
|
|
15
|
+
# to write somewhere else.
|
|
16
|
+
def initialize(silent: false, output_dir: nil)
|
|
17
|
+
@silent = silent
|
|
18
|
+
@output_dir = output_dir
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# The one home of the "status lines go to stderr, not through warn"
|
|
24
|
+
# decision (#1225). stderr rather than stdout because this is a status
|
|
25
|
+
# message, not the program's output, so it stays out of pipelines like
|
|
26
|
+
# `rspec -f json`. And `$stderr.puts` rather than `warn` so the line
|
|
27
|
+
# neither reaches `Warning.warn` hooks nor vanishes under `-W0`.
|
|
28
|
+
def emit_status(result)
|
|
29
|
+
$stderr.puts output_message(result) unless @silent # rubocop:disable Style/StderrPuts
|
|
30
|
+
rescue IOError
|
|
31
|
+
# A parallel runner can close a worker's stderr before its at_exit hooks
|
|
32
|
+
# run (rspec-conductor does). Losing the status line must not abort the
|
|
33
|
+
# exit tasks that follow report generation.
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Subclasses override to prepend a marker (e.g. "JSON ") to the summary
|
|
37
|
+
# line. Empty for the HTML formatter, historically the unmarked default.
|
|
38
|
+
def message_prefix
|
|
39
|
+
""
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def output_path
|
|
43
|
+
@output_dir || SimpleCov.coverage_path
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The path shown in the "Coverage report generated for X to Y" status line.
|
|
47
|
+
# Rendered relative to cwd when `output_path` lives inside cwd, with the
|
|
48
|
+
# formatter's `entry_point_filename` appended so the line points at a
|
|
49
|
+
# concrete file a terminal can hyperlink. Paths outside cwd stay
|
|
50
|
+
# absolute; a `../../../tmp/cov` display would be more confusing (#197).
|
|
51
|
+
def displayable_output_path
|
|
52
|
+
directory = relative_or_absolute_output_path
|
|
53
|
+
entry_point = entry_point_filename
|
|
54
|
+
entry_point ? File.join(directory, entry_point) : directory
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def relative_or_absolute_output_path
|
|
58
|
+
absolute = output_path
|
|
59
|
+
relative = Pathname.new(absolute).relative_path_from(Pathname.pwd).to_s
|
|
60
|
+
relative.start_with?("..") ? absolute : relative
|
|
61
|
+
rescue ArgumentError
|
|
62
|
+
# Pathname#relative_path_from raises across mixed absolute/relative
|
|
63
|
+
# inputs, and across Windows drives.
|
|
64
|
+
output_path
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Subclasses override to name the report's entry-point file, which gets
|
|
68
|
+
# appended to the directory in the status line. The empty body answers
|
|
69
|
+
# nil, which leaves the bare directory in place for any third-party
|
|
70
|
+
# formatter that has no single canonical entry point.
|
|
71
|
+
def entry_point_filename
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# One summary line per criterion the run actually measured, in the order
|
|
75
|
+
# of `result.coverage_statistics`, which reflects what the user enabled.
|
|
76
|
+
def output_message(result)
|
|
77
|
+
header = "#{message_prefix}Coverage report generated for #{result.command_name} to #{displayable_output_path}"
|
|
78
|
+
body = result.coverage_statistics.filter_map { |criterion, stat| stats_line(criterion, stat) }
|
|
79
|
+
[header, *body].join("\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Returns nil for branch/method criteria that have nothing to measure.
|
|
83
|
+
# Showing "Branch coverage: 0 / 0 (100.00%)" is noise.
|
|
84
|
+
def stats_line(criterion, stat)
|
|
85
|
+
return if !criterion.equal?(:line) && !stat.total.positive?
|
|
86
|
+
|
|
87
|
+
percent = SimpleCov.round_coverage(stat.percent)
|
|
88
|
+
# `Symbol#capitalize` answers a Symbol, which `%<label>s` renders as its
|
|
89
|
+
# capitalized name, so the label needs no `to_s` step of its own.
|
|
90
|
+
Kernel.format(
|
|
91
|
+
"%<label>s coverage: %<covered>d / %<total>d (%<percent>s)",
|
|
92
|
+
label: criterion.capitalize,
|
|
93
|
+
covered: stat.covered,
|
|
94
|
+
total: stat.total,
|
|
95
|
+
percent: Color.colorize_percent(percent)
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../atomic_file"
|
|
5
|
+
|
|
6
|
+
module SimpleCov
|
|
7
|
+
module Formatter
|
|
8
|
+
# Auto-ratchets the per-file coverage baseline as a formatter, for teams
|
|
9
|
+
# that want floors to tighten on every run instead of by deliberate
|
|
10
|
+
# `simplecov ratchet` invocations. The semantics are exactly the CLI's, and
|
|
11
|
+
# the file is rewritten only when a floor actually moved.
|
|
12
|
+
#
|
|
13
|
+
# The exit checks still judge the run against the floors as they were when
|
|
14
|
+
# it started (the baseline is read once per path), and ratcheting never
|
|
15
|
+
# loosens a floor, so tightening before the checks cannot flip a failing
|
|
16
|
+
# file to passing.
|
|
17
|
+
class BaselineFormatter < Base
|
|
18
|
+
def format(result)
|
|
19
|
+
emit_status(ratchet(result))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def ratchet(result)
|
|
25
|
+
current = current_floors(result)
|
|
26
|
+
existing = SimpleCov.baseline
|
|
27
|
+
return generate(current) unless existing
|
|
28
|
+
|
|
29
|
+
outcome = existing.ratchet(current)
|
|
30
|
+
changed = outcome.tightened.any? || outcome.pruned.any?
|
|
31
|
+
write(outcome.baseline) if changed
|
|
32
|
+
note = summary(outcome, changed)
|
|
33
|
+
regressed = outcome.regressed
|
|
34
|
+
regressed.empty? ? note : "#{note}#{below_floors(regressed)}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def generate(current)
|
|
38
|
+
baseline = Baseline.generate(current)
|
|
39
|
+
write(baseline)
|
|
40
|
+
files = baseline.entries.size
|
|
41
|
+
"Coverage baseline generated to #{displayable_output_path} (#{files} #{files.eql?(1) ? "file" : "files"})"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def summary(outcome, changed)
|
|
45
|
+
return "Coverage baseline unchanged at #{displayable_output_path}" unless changed
|
|
46
|
+
|
|
47
|
+
"Coverage baseline ratcheted to #{displayable_output_path} " \
|
|
48
|
+
"(#{outcome.tightened.size} tightened, #{outcome.pruned.size} pruned)"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def below_floors(regressed)
|
|
52
|
+
noun = regressed.size.eql?(1) ? "1 file below its floor" : "#{regressed.size} files below their floors"
|
|
53
|
+
", #{noun}: #{regressed.join(", ")}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# `SourceFile#coverage_statistics` answers for disabled criteria too, as
|
|
57
|
+
# empty statistics, so the criteria are gated on the configuration rather
|
|
58
|
+
# than on the stats' presence.
|
|
59
|
+
def current_floors(result)
|
|
60
|
+
criteria = measured_criteria
|
|
61
|
+
floors = {} #: Baseline::current_floors
|
|
62
|
+
result.files.each do |file|
|
|
63
|
+
floors[file.project_filename] = criteria.to_h { |criterion| [criterion, floor_of(file, criterion)] }
|
|
64
|
+
end
|
|
65
|
+
floors
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def measured_criteria
|
|
69
|
+
criteria = [] #: Array[Symbol]
|
|
70
|
+
criteria << :line if SimpleCov.line_coverage?
|
|
71
|
+
criteria << :branch if SimpleCov.branch_coverage?
|
|
72
|
+
criteria << :method if SimpleCov.method_coverage?
|
|
73
|
+
criteria
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def floor_of(file, criterion)
|
|
77
|
+
stats = file.coverage_statistics(criterion)
|
|
78
|
+
{percent: SimpleCov.round_coverage(stats.percent), missed: stats.missed}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def write(baseline)
|
|
82
|
+
AtomicFile.write(output_path, baseline.to_yaml)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The baseline lives where the exit check reads it, not under the coverage
|
|
86
|
+
# directory: it is checked-in policy, not a report artifact.
|
|
87
|
+
def output_path
|
|
88
|
+
File.expand_path(SimpleCov.baseline_file, SimpleCov.root)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# `format` hands the ratchet's summary to `emit_status`, which passes it
|
|
92
|
+
# through to here in the result's place.
|
|
93
|
+
def output_message(message)
|
|
94
|
+
message
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
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 as its
|
|
10
|
+
# report and by HTMLFormatter as the side file feeding `simplecov serve` and
|
|
11
|
+
# external tools. Centralizing the write keeps the two copies byte-identical
|
|
12
|
+
# and gives both formatters the concurrent-overwrite warning of #1171.
|
|
13
|
+
module CoverageJSONWriter
|
|
14
|
+
FILENAME = "coverage.json"
|
|
15
|
+
|
|
16
|
+
META_SCAN_BYTES = 64 * 1024
|
|
17
|
+
private_constant :META_SCAN_BYTES
|
|
18
|
+
|
|
19
|
+
extend self
|
|
20
|
+
|
|
21
|
+
def write(output_path, hash, result)
|
|
22
|
+
path = File.join(output_path, FILENAME)
|
|
23
|
+
warn_if_concurrent_overwrite(path, result)
|
|
24
|
+
AtomicFile.write(path, JSON.pretty_generate(hash), binary: true)
|
|
25
|
+
path
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Warns when the existing coverage.json has a timestamp newer than this
|
|
29
|
+
# process's start time, a strong signal that a sibling test process wrote it
|
|
30
|
+
# while we were running. A matching command_name means the same merged
|
|
31
|
+
# result, which is what both formatters configured together produce, so
|
|
32
|
+
# there is nothing to lose by overwriting (#1171).
|
|
33
|
+
def warn_if_concurrent_overwrite(path, result)
|
|
34
|
+
start_time = SimpleCov.process_start_time or return
|
|
35
|
+
existing = existing_meta(path) or return
|
|
36
|
+
return unless existing.fetch(:timestamp) > start_time
|
|
37
|
+
|
|
38
|
+
return if existing.fetch(:command_name).eql?(result.command_name)
|
|
39
|
+
|
|
40
|
+
warn "simplecov: #{path} was written at #{existing[:timestamp].iso8601} — after " \
|
|
41
|
+
"this process started at #{start_time.iso8601}. Overwriting " \
|
|
42
|
+
"likely loses coverage data from a concurrent test run. For " \
|
|
43
|
+
"parallel test setups, use SimpleCov::ResultMerger or run a single " \
|
|
44
|
+
"collation step after all workers finish."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def existing_meta(path)
|
|
48
|
+
return nil unless File.exist?(path)
|
|
49
|
+
|
|
50
|
+
meta = parse_meta(path) or return nil
|
|
51
|
+
timestamp = parse_time(meta[:timestamp]) or return nil
|
|
52
|
+
|
|
53
|
+
{timestamp: timestamp, command_name: meta[:command_name]}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Our own writer emits an ISO 8601 string, but coverage.json is whatever
|
|
57
|
+
# wrote it last: third-party formatters rewrite it in their own shape with
|
|
58
|
+
# an epoch integer, and `Time.iso8601` raised TypeError on that, taking the
|
|
59
|
+
# whole report down on the next run. Anything but the two spellings disables
|
|
60
|
+
# the concurrency check for this file instead of failing the write (#1285).
|
|
61
|
+
def parse_time(value)
|
|
62
|
+
case value
|
|
63
|
+
when String then Time.iso8601(value)
|
|
64
|
+
when Numeric then Time.at(value)
|
|
65
|
+
end
|
|
66
|
+
rescue ArgumentError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parse_meta(path)
|
|
71
|
+
parse_meta_head(path) || parse_meta_full(path)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The meta object is flat and sits at the head of every file this module
|
|
75
|
+
# writes, so the common case parses just that slice instead of a
|
|
76
|
+
# multi-megabyte report. A miss 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
|
+
JSON.parse(slice, symbolize_names: true)
|
|
83
|
+
rescue JSON::ParserError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def parse_meta_full(path)
|
|
88
|
+
parsed = JSON.parse(File.read(path), symbolize_names: true)
|
|
89
|
+
parsed[:meta] if parsed.is_a?(Hash)
|
|
90
|
+
rescue JSON::ParserError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|