simplecov 0.22.0 → 1.0.2
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 +1058 -508
- 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 +93 -0
- data/lib/simplecov/cli/diff.rb +152 -0
- data/lib/simplecov/cli/dotfile.rb +105 -0
- data/lib/simplecov/cli/merge.rb +118 -0
- data/lib/simplecov/cli/open.rb +50 -0
- data/lib/simplecov/cli/report.rb +89 -0
- data/lib/simplecov/cli/run.rb +36 -0
- data/lib/simplecov/cli/serve.rb +149 -0
- data/lib/simplecov/cli/uncovered.rb +108 -0
- data/lib/simplecov/cli.rb +150 -0
- data/lib/simplecov/color.rb +79 -0
- data/lib/simplecov/combine/branches_combiner.rb +28 -5
- data/lib/simplecov/combine/files_combiner.rb +47 -1
- data/lib/simplecov/combine/lines_combiner.rb +19 -17
- data/lib/simplecov/combine/methods_combiner.rb +49 -0
- data/lib/simplecov/combine/results_combiner.rb +7 -5
- data/lib/simplecov/command_guesser.rb +46 -32
- data/lib/simplecov/configuration/coverage.rb +175 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +158 -0
- data/lib/simplecov/configuration/filters.rb +197 -0
- data/lib/simplecov/configuration/formatting.rb +129 -0
- data/lib/simplecov/configuration/ignored_entries.rb +63 -0
- data/lib/simplecov/configuration/merging.rb +164 -0
- data/lib/simplecov/configuration/thresholds.rb +178 -0
- data/lib/simplecov/configuration.rb +105 -412
- data/lib/simplecov/coverage_statistics.rb +15 -9
- data/lib/simplecov/coverage_violations.rb +149 -0
- data/lib/simplecov/defaults.rb +29 -22
- data/lib/simplecov/deprecation.rb +45 -0
- data/lib/simplecov/directive.rb +167 -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 +15 -0
- data/lib/simplecov/exit_handling.rb +158 -0
- data/lib/simplecov/file_list.rb +70 -19
- data/lib/simplecov/filter.rb +69 -24
- data/lib/simplecov/formatter/base.rb +102 -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 +85 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +90 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +128 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
- data/lib/simplecov/formatter/json_formatter.rb +89 -0
- data/lib/simplecov/formatter/multi_formatter.rb +11 -7
- data/lib/simplecov/formatter/simple_formatter.rb +11 -11
- data/lib/simplecov/formatter.rb +4 -0
- data/lib/simplecov/last_run.rb +10 -3
- data/lib/simplecov/lines_classifier.rb +25 -13
- data/lib/simplecov/load_global_config.rb +9 -4
- data/lib/simplecov/parallel_adapters/base.rb +61 -0
- data/lib/simplecov/parallel_adapters/generic.rb +44 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
- data/lib/simplecov/parallel_adapters.rb +83 -0
- data/lib/simplecov/parallel_coordination.rb +129 -0
- data/lib/simplecov/process.rb +52 -12
- 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 +108 -19
- data/lib/simplecov/result_adapter.rb +105 -8
- 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 +59 -91
- data/lib/simplecov/result_processing.rb +179 -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 +9 -4
- 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 +60 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
- data/lib/simplecov/source_file/skip_chunks.rb +75 -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/condition_folding.rb +49 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +247 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +55 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +82 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +196 -0
- data/lib/simplecov/static_coverage_extractor.rb +110 -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 +178 -380
- data/lib/simplecov_json_formatter.rb +4 -0
- data/schemas/coverage-v1.0.schema.json +306 -0
- data/schemas/coverage.schema.json +306 -0
- data/sig/simplecov.rbs +1638 -0
- metadata +94 -57
- data/CHANGELOG.md +0 -191
- data/lib/simplecov/default_formatter.rb +0 -20
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "errors_formatter"
|
|
6
|
+
require_relative "source_file_formatter"
|
|
7
|
+
|
|
8
|
+
module SimpleCov
|
|
9
|
+
module Formatter
|
|
10
|
+
class JSONFormatter
|
|
11
|
+
# Builds the hash that JSONFormatter serializes to coverage.json:
|
|
12
|
+
# meta, per-file coverage data, group totals, and aggregate stats.
|
|
13
|
+
class ResultHashFormatter
|
|
14
|
+
# Bump SCHEMA_VERSION (and SCHEMA_URL) when the JSON shape
|
|
15
|
+
# changes. Additive changes bump the minor segment, removals or
|
|
16
|
+
# shape changes bump the major segment. The versioned file at
|
|
17
|
+
# schemas/coverage-vX.Y.schema.json is the canonical artifact
|
|
18
|
+
# consumers should pin to, schemas/coverage.schema.json is a
|
|
19
|
+
# convenience alias that always tracks the latest. See the
|
|
20
|
+
# `coverage.json` schema section of the README for the rationale.
|
|
21
|
+
SCHEMA_VERSION = "1.0"
|
|
22
|
+
SCHEMA_URL = "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v#{SCHEMA_VERSION}.schema.json".freeze
|
|
23
|
+
private_constant :SCHEMA_VERSION, :SCHEMA_URL
|
|
24
|
+
|
|
25
|
+
def initialize(result, include_source: true)
|
|
26
|
+
@result = result
|
|
27
|
+
@include_source = include_source
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def format
|
|
31
|
+
{
|
|
32
|
+
:$schema => SCHEMA_URL,
|
|
33
|
+
:meta => format_meta,
|
|
34
|
+
:total => format_coverage_statistics(@result.coverage_statistics),
|
|
35
|
+
:coverage => format_files,
|
|
36
|
+
:groups => format_groups,
|
|
37
|
+
:errors => ErrorsFormatter.new(@result).call
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def format_files
|
|
44
|
+
@result.files.to_h do |source_file|
|
|
45
|
+
[source_file.project_filename, SourceFileFormatter.new(source_file, include_source: @include_source).call]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def format_groups
|
|
50
|
+
@result.groups.to_h do |name, file_list|
|
|
51
|
+
stats = format_coverage_statistics(file_list.coverage_statistics)
|
|
52
|
+
[name, stats.merge(files: file_list.map(&:project_filename))]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def format_meta
|
|
57
|
+
{
|
|
58
|
+
schema_version: SCHEMA_VERSION,
|
|
59
|
+
simplecov_version: SimpleCov::VERSION,
|
|
60
|
+
command_name: @result.command_name,
|
|
61
|
+
project_name: SimpleCov.project_name,
|
|
62
|
+
timestamp: @result.created_at.iso8601(3),
|
|
63
|
+
root: SimpleCov.root,
|
|
64
|
+
commit: git_commit,
|
|
65
|
+
primary_coverage: SimpleCov.primary_coverage.to_s
|
|
66
|
+
}.merge!(coverage_flags)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Full git commit SHA of `SimpleCov.root`'s HEAD, or nil when the
|
|
70
|
+
# project isn't a git checkout or git isn't on PATH. Recorded so tools
|
|
71
|
+
# can recover the exact source a report was generated against, which
|
|
72
|
+
# matters most when `source_in_json false` drops the source text from
|
|
73
|
+
# coverage.json. stderr is captured (not forwarded) so a non-git project
|
|
74
|
+
# doesn't print git's diagnostics to the build.
|
|
75
|
+
def git_commit
|
|
76
|
+
output, status = Open3.capture2e("git", "-C", SimpleCov.root.to_s, "rev-parse", "HEAD")
|
|
77
|
+
status.success? ? output.strip : nil
|
|
78
|
+
rescue StandardError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def coverage_flags
|
|
83
|
+
{
|
|
84
|
+
line_coverage: line_coverage_enabled?,
|
|
85
|
+
branch_coverage: SimpleCov.branch_coverage?,
|
|
86
|
+
method_coverage: SimpleCov.method_coverage?
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Mirrors SourceFileFormatter's predicate so meta.line_coverage
|
|
91
|
+
# tracks exactly which configurations cause the formatter to
|
|
92
|
+
# emit line stats.
|
|
93
|
+
def line_coverage_enabled?
|
|
94
|
+
SimpleCov.coverage_criterion_enabled?(:line) || SimpleCov.coverage_criterion_enabled?(:oneshot_line)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def format_coverage_statistics(statistics)
|
|
98
|
+
result = {} #: Hash[Symbol, untyped]
|
|
99
|
+
result[:lines] = format_line_statistic(statistics[:line]) if statistics[:line]
|
|
100
|
+
result[:branches] = format_single_statistic(statistics[:branch]) if statistics[:branch]
|
|
101
|
+
result[:methods] = format_single_statistic(statistics[:method]) if statistics[:method]
|
|
102
|
+
result
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def format_line_statistic(stat)
|
|
106
|
+
{
|
|
107
|
+
covered: stat.covered,
|
|
108
|
+
missed: stat.missed,
|
|
109
|
+
omitted: stat.omitted,
|
|
110
|
+
total: stat.total,
|
|
111
|
+
percent: stat.percent,
|
|
112
|
+
strength: stat.strength
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def format_single_statistic(stat)
|
|
117
|
+
{
|
|
118
|
+
covered: stat.covered,
|
|
119
|
+
missed: stat.missed,
|
|
120
|
+
total: stat.total,
|
|
121
|
+
percent: stat.percent,
|
|
122
|
+
strength: stat.strength
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module Formatter
|
|
5
|
+
class JSONFormatter
|
|
6
|
+
# Renders a single `SimpleCov::SourceFile` as the per-file payload
|
|
7
|
+
# in coverage.json: source code plus per-enabled-criterion arrays
|
|
8
|
+
# and totals.
|
|
9
|
+
class SourceFileFormatter
|
|
10
|
+
def initialize(source_file, include_source: true)
|
|
11
|
+
@source_file = source_file
|
|
12
|
+
@include_source = include_source
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
result = @include_source ? format_source_code : {} #: Hash[Symbol, untyped]
|
|
17
|
+
result.merge!(line_coverage_section) if line_coverage_enabled?
|
|
18
|
+
result.merge!(branch_coverage_section) if SimpleCov.branch_coverage?
|
|
19
|
+
result.merge!(method_coverage_section) if SimpleCov.method_coverage?
|
|
20
|
+
result
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# `:oneshot_line` is a synonym for `:line` for stats purposes
|
|
26
|
+
# (see `SimpleCov.coverage_statistics_key`), so treat either as
|
|
27
|
+
# "line coverage is on" for the line-block emit decisions.
|
|
28
|
+
def line_coverage_enabled?
|
|
29
|
+
SimpleCov.coverage_criterion_enabled?(:line) || SimpleCov.coverage_criterion_enabled?(:oneshot_line)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def format_source_code
|
|
33
|
+
{source: @source_file.lines.map { |line| ensure_utf8(line.src.chomp) }}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ensure_utf8(str)
|
|
37
|
+
str.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def line_coverage_section
|
|
41
|
+
covered = @source_file.covered_lines.size
|
|
42
|
+
missed = @source_file.missed_lines.size
|
|
43
|
+
{
|
|
44
|
+
lines: @source_file.lines.map { |line| format_line(line) },
|
|
45
|
+
lines_covered_percent: @source_file.covered_percent,
|
|
46
|
+
covered_lines: covered,
|
|
47
|
+
missed_lines: missed,
|
|
48
|
+
omitted_lines: @source_file.never_lines.size,
|
|
49
|
+
total_lines: covered + missed
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def branch_coverage_section
|
|
54
|
+
{
|
|
55
|
+
branches: @source_file.branches.map { |branch| format_branch(branch) },
|
|
56
|
+
branches_covered_percent: @source_file.covered_percent(:branch),
|
|
57
|
+
covered_branches: @source_file.covered_branches.size,
|
|
58
|
+
missed_branches: @source_file.missed_branches.size,
|
|
59
|
+
total_branches: @source_file.total_branches.size
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def method_coverage_section
|
|
64
|
+
{
|
|
65
|
+
methods: @source_file.methods.map { |method| format_method(method) },
|
|
66
|
+
methods_covered_percent: @source_file.covered_percent(:method),
|
|
67
|
+
covered_methods: @source_file.covered_methods.size,
|
|
68
|
+
missed_methods: @source_file.missed_methods.size,
|
|
69
|
+
total_methods: @source_file.methods.size
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def format_line(line)
|
|
74
|
+
line.skipped? ? "ignored" : line.coverage
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def format_branch(branch)
|
|
78
|
+
{
|
|
79
|
+
type: branch.type,
|
|
80
|
+
start_line: branch.start_line,
|
|
81
|
+
end_line: branch.end_line,
|
|
82
|
+
coverage: format_line(branch),
|
|
83
|
+
inline: branch.inline?,
|
|
84
|
+
report_line: branch.report_line
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def format_method(method)
|
|
89
|
+
{
|
|
90
|
+
name: method.to_s,
|
|
91
|
+
start_line: method.start_line,
|
|
92
|
+
end_line: method.end_line,
|
|
93
|
+
coverage: format_line(method)
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module SimpleCov
|
|
9
|
+
module Formatter
|
|
10
|
+
# Writes coverage results as JSON to coverage/coverage.json. Used
|
|
11
|
+
# standalone, alongside the HTML formatter, or by external tools that
|
|
12
|
+
# consume SimpleCov output.
|
|
13
|
+
class JSONFormatter < Base
|
|
14
|
+
FILENAME = "coverage.json"
|
|
15
|
+
|
|
16
|
+
# `include_source:` defaults to `SimpleCov.source_in_json` (true
|
|
17
|
+
# by default) so the historical payload shape is unchanged.
|
|
18
|
+
# Callers that need the source array regardless of the global
|
|
19
|
+
# setting (the HTML formatter, which feeds the client-side
|
|
20
|
+
# viewer) pass `include_source: true` explicitly.
|
|
21
|
+
def self.build_hash(result, include_source: SimpleCov.source_in_json)
|
|
22
|
+
ResultHashFormatter.new(result, include_source: include_source).format
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def format(result)
|
|
26
|
+
FileUtils.mkdir_p(output_path)
|
|
27
|
+
path = File.join(output_path, FILENAME)
|
|
28
|
+
warn_if_concurrent_overwrite(path, result)
|
|
29
|
+
File.write(path, JSON.pretty_generate(self.class.build_hash(result)))
|
|
30
|
+
# stderr, not stdout: this is a status message, not the program's
|
|
31
|
+
# output. Keeps the line out of pipelines like `rspec -f json`. And
|
|
32
|
+
# $stderr.puts, not `warn`: a status line should not reach
|
|
33
|
+
# `Warning.warn` hooks or vanish under `-W0` (see #1225).
|
|
34
|
+
$stderr.puts output_message(result) unless @silent # rubocop:disable Style/StderrPuts
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def message_prefix
|
|
40
|
+
"JSON "
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def entry_point_filename
|
|
44
|
+
FILENAME
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Warns when the existing coverage.json has a timestamp newer than this
|
|
48
|
+
# process's start time — a strong signal that a sibling test process
|
|
49
|
+
# (e.g., parallel_tests) wrote it while we were running, and that our
|
|
50
|
+
# write is about to clobber their data.
|
|
51
|
+
def warn_if_concurrent_overwrite(path, result)
|
|
52
|
+
start_time = SimpleCov.process_start_time or return
|
|
53
|
+
existing = existing_meta(path) or return
|
|
54
|
+
return unless existing[:timestamp] > start_time
|
|
55
|
+
|
|
56
|
+
# The HTML formatter also writes coverage.json (it shares the file as
|
|
57
|
+
# a side artifact), so when both formatters are configured the file we
|
|
58
|
+
# find was just written by our own run, not a concurrent one. A
|
|
59
|
+
# matching command_name means the same merged result, so there's
|
|
60
|
+
# nothing to lose by overwriting. See issue #1171.
|
|
61
|
+
return if existing[:command_name] == result.command_name
|
|
62
|
+
|
|
63
|
+
warn "simplecov: #{path} was written at #{existing[:timestamp].iso8601} — after " \
|
|
64
|
+
"this process started at #{start_time.iso8601}. Overwriting " \
|
|
65
|
+
"likely loses coverage data from a concurrent test run. For " \
|
|
66
|
+
"parallel test setups, use SimpleCov::ResultMerger or run a single " \
|
|
67
|
+
"collation step after all workers finish."
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def existing_meta(path)
|
|
71
|
+
return nil unless File.exist?(path)
|
|
72
|
+
|
|
73
|
+
meta = JSON.parse(File.read(path), symbolize_names: true)
|
|
74
|
+
timestamp = meta.dig(:meta, :timestamp)
|
|
75
|
+
return nil unless timestamp
|
|
76
|
+
|
|
77
|
+
{timestamp: Time.iso8601(timestamp), command_name: meta.dig(:meta, :command_name)}
|
|
78
|
+
rescue JSON::ParserError, ArgumentError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Loaded after the JSONFormatter class is defined so the nested
|
|
86
|
+
# `class JSONFormatter` reopen inside result_hash_formatter.rb doesn't
|
|
87
|
+
# accidentally create a JSONFormatter < Object before this file gets a
|
|
88
|
+
# chance to declare `JSONFormatter < Base`.
|
|
89
|
+
require_relative "json_formatter/result_hash_formatter"
|
|
@@ -2,31 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module Formatter
|
|
5
|
+
# Wraps multiple formatters so SimpleCov.formatter can drive several
|
|
6
|
+
# output formats (HTML + JSON, etc.) in a single run.
|
|
5
7
|
class MultiFormatter
|
|
8
|
+
# Shared `#format` implementation; included into individual
|
|
9
|
+
# MultiFormatter subclasses built by `MultiFormatter.new`.
|
|
6
10
|
module InstanceMethods
|
|
7
11
|
def format(result)
|
|
8
12
|
formatters.map do |formatter|
|
|
9
13
|
formatter.new.format(result)
|
|
10
14
|
rescue StandardError => e
|
|
11
|
-
warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
|
|
15
|
+
warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{(_ = e.backtrace).first})")
|
|
12
16
|
nil
|
|
13
17
|
end
|
|
14
18
|
end
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
def self.new(formatters = nil)
|
|
22
|
+
# Normalize eagerly and capture the list in the closure. Array()
|
|
23
|
+
# is pure for every accepted input shape (nil, single formatter,
|
|
24
|
+
# array, or another MultiFormatter class), so this is equivalent
|
|
25
|
+
# to the historical lazy per-instance memoization.
|
|
26
|
+
formatter_list = Array(formatters)
|
|
18
27
|
Class.new do
|
|
19
28
|
define_method :formatters do
|
|
20
|
-
|
|
29
|
+
formatter_list
|
|
21
30
|
end
|
|
22
31
|
include InstanceMethods
|
|
23
32
|
end
|
|
24
33
|
end
|
|
25
|
-
|
|
26
|
-
def self.[](*args)
|
|
27
|
-
warn "#{Kernel.caller.first}: [DEPRECATION] ::[] is deprecated. Use ::new instead."
|
|
28
|
-
new(Array(args))
|
|
29
|
-
end
|
|
30
34
|
end
|
|
31
35
|
end
|
|
32
36
|
end
|
|
@@ -8,17 +8,17 @@ module SimpleCov
|
|
|
8
8
|
class SimpleFormatter
|
|
9
9
|
# Takes a SimpleCov::Result and generates a string out of it
|
|
10
10
|
def format(result)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
result.groups.map { |name, files| format_group(name, files) }.join
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def format_group(name, files)
|
|
17
|
+
header = "Group: #{name}\n#{'=' * 40}\n"
|
|
18
|
+
# `covered_percent` is nilable across criteria, but line stats are
|
|
19
|
+
# always measured, so the no-argument call can't return nil here.
|
|
20
|
+
body = files.map { |file| "#{file.filename} (coverage: #{(_ = file.covered_percent).floor(2)}%)\n" }.join
|
|
21
|
+
"#{header}#{body}\n"
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
end
|
data/lib/simplecov/formatter.rb
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
|
+
# Namespace for SimpleCov result formatters. Built-in formatters live
|
|
5
|
+
# below this module; custom formatters should respond to `#format(result)`
|
|
6
|
+
# and can be wired up via `SimpleCov.formatter=`.
|
|
4
7
|
# TODO: Documentation on how to build your own formatters
|
|
5
8
|
module Formatter
|
|
6
9
|
end
|
|
@@ -8,3 +11,4 @@ end
|
|
|
8
11
|
|
|
9
12
|
require_relative "formatter/simple_formatter"
|
|
10
13
|
require_relative "formatter/multi_formatter"
|
|
14
|
+
require_relative "formatter/json_formatter"
|
data/lib/simplecov/last_run.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
3
4
|
require "json"
|
|
4
5
|
|
|
5
6
|
module SimpleCov
|
|
7
|
+
# Reads and writes coverage/.last_run.json — the previous run's coverage
|
|
8
|
+
# percentages used by MaximumCoverageDropCheck.
|
|
6
9
|
module LastRun
|
|
7
10
|
class << self
|
|
8
11
|
def last_run_path
|
|
@@ -18,10 +21,14 @@ module SimpleCov
|
|
|
18
21
|
JSON.parse(json, symbolize_names: true)
|
|
19
22
|
end
|
|
20
23
|
|
|
24
|
+
# Write to a process-private temp file, then atomically rename, so a
|
|
25
|
+
# concurrent reader (e.g. another parallel-tests worker checking
|
|
26
|
+
# MaximumCoverageDrop) never sees a half-written file.
|
|
21
27
|
def write(json)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
FileUtils.mkdir_p(SimpleCov.coverage_path)
|
|
29
|
+
temp_path = "#{last_run_path}.#{Process.pid}.tmp"
|
|
30
|
+
File.open(temp_path, "w") { |f| f.puts JSON.pretty_generate(json) }
|
|
31
|
+
File.rename(temp_path, last_run_path)
|
|
25
32
|
end
|
|
26
33
|
end
|
|
27
34
|
end
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "directive"
|
|
4
|
+
|
|
3
5
|
module SimpleCov
|
|
4
6
|
# Classifies whether lines are relevant for code coverage analysis.
|
|
5
7
|
# Comments & whitespace lines, and :nocov: token blocks, are considered not relevant.
|
|
6
|
-
|
|
7
8
|
class LinesClassifier
|
|
8
9
|
RELEVANT = 0
|
|
9
10
|
NOT_RELEVANT = nil
|
|
10
11
|
|
|
11
|
-
WHITESPACE_LINE = /^\s
|
|
12
|
-
COMMENT_LINE = /^\s
|
|
12
|
+
WHITESPACE_LINE = /^\s*$/
|
|
13
|
+
COMMENT_LINE = /^\s*#/
|
|
13
14
|
WHITESPACE_OR_COMMENT_LINE = Regexp.union(WHITESPACE_LINE, COMMENT_LINE)
|
|
14
15
|
|
|
15
16
|
def self.no_cov_line
|
|
16
|
-
/^(\s*)#(\s*)(:#{SimpleCov.
|
|
17
|
+
/^(\s*)#(\s*)(:#{SimpleCov.current_nocov_token}:)/o
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def self.no_cov_line?(line)
|
|
@@ -31,17 +32,28 @@ module SimpleCov
|
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def classify(lines)
|
|
35
|
+
lines = lines.to_a
|
|
36
|
+
directive_disabled = directive_disabled_line_set(lines)
|
|
34
37
|
skipping = false
|
|
35
38
|
|
|
36
|
-
lines.map do |line|
|
|
37
|
-
if self.class.no_cov_line?(line)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
lines.map.with_index(1) do |line, line_number|
|
|
40
|
+
skipping = !skipping if self.class.no_cov_line?(line)
|
|
41
|
+
not_relevant_line?(line, line_number, skipping, directive_disabled) ? NOT_RELEVANT : RELEVANT
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def not_relevant_line?(line, line_number, skipping, directive_disabled)
|
|
48
|
+
skipping ||
|
|
49
|
+
self.class.no_cov_line?(line) ||
|
|
50
|
+
directive_disabled.include?(line_number) ||
|
|
51
|
+
self.class.whitespace_line?(line)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def directive_disabled_line_set(lines)
|
|
55
|
+
Directive.disabled_ranges(lines).fetch(:line).each_with_object(Set.new) do |range, set|
|
|
56
|
+
range.each { |line_number| set.add(line_number) }
|
|
45
57
|
end
|
|
46
58
|
end
|
|
47
59
|
end
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
# `~/.simplecov` was historically resolved via a three-step fallback chain
|
|
4
|
+
# (HOME, then `Etc.getpwuid.dir`, then `~$USER`) for hostile container
|
|
5
|
+
# environments circa 2017. Modern CRuby/JRuby/TruffleRuby all set HOME
|
|
6
|
+
# reliably, so trust it and skip silently when it isn't there.
|
|
7
|
+
if ENV.fetch("HOME", nil)
|
|
8
|
+
# simplecov:disable — only fires when ~/.simplecov exists, which is
|
|
9
|
+
# developer-machine-dependent (we can't rely on it for the dogfood).
|
|
10
|
+
global_config_path = File.join(File.expand_path("~"), ".simplecov")
|
|
7
11
|
load global_config_path if File.exist?(global_config_path)
|
|
12
|
+
# simplecov:enable
|
|
8
13
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ParallelAdapters
|
|
5
|
+
# Default no-op implementations for a parallel-test-runner adapter.
|
|
6
|
+
# Real adapters subclass and override what they need; everything else
|
|
7
|
+
# falls back to "behave like a single-process run."
|
|
8
|
+
#
|
|
9
|
+
# Adapters are classes (used as singletons, never instantiated) — they
|
|
10
|
+
# answer a small fixed set of questions about whether THIS worker
|
|
11
|
+
# process is the one that should do final-result work, and provide an
|
|
12
|
+
# optional hook for waiting on sibling workers.
|
|
13
|
+
#
|
|
14
|
+
# @see SimpleCov::ParallelAdapters for the registry and selection.
|
|
15
|
+
class Base
|
|
16
|
+
class << self
|
|
17
|
+
# Should this adapter be selected for the current process? Adapters
|
|
18
|
+
# are tried in registration order; the first one whose `active?`
|
|
19
|
+
# returns true is chosen. Inactive adapters return `false`.
|
|
20
|
+
def active?
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Among the parallel workers in this run, should THIS worker do
|
|
25
|
+
# the final-result work (wait for siblings, merge resultsets,
|
|
26
|
+
# run threshold checks, format the report)? Default is `true`
|
|
27
|
+
# for the single-process case.
|
|
28
|
+
def first_worker?
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Optional: block until sibling workers have finished writing
|
|
33
|
+
# their resultsets. An adapter that wraps a parallel-test runner
|
|
34
|
+
# with a native synchronization primitive (e.g., `parallel_tests`'s
|
|
35
|
+
# `wait_for_other_processes_to_finish`) implements this for
|
|
36
|
+
# lower latency; otherwise SimpleCov polls the resultset cache
|
|
37
|
+
# as a fallback (see `SimpleCov.wait_for_parallel_results`).
|
|
38
|
+
def wait_for_siblings
|
|
39
|
+
# No-op default; polling fallback handles correctness.
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Does `wait_for_siblings` block until every sibling PROCESS has
|
|
43
|
+
# exited (so no further resultset can appear)? When true, the
|
|
44
|
+
# reporting worker can accept a settled resultset count below
|
|
45
|
+
# `expected_worker_count` as final instead of waiting out the whole
|
|
46
|
+
# `parallel_wait_timeout` for workers that produced no coverage.
|
|
47
|
+
# Defaults to false (no native wait; the poll is the only signal).
|
|
48
|
+
def native_wait?
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# How many parallel workers are participating in this run. Used
|
|
53
|
+
# by the polling fallback to know how many resultset entries to
|
|
54
|
+
# expect. Defaults to 1 (single-process).
|
|
55
|
+
def expected_worker_count
|
|
56
|
+
1
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module ParallelAdapters
|
|
7
|
+
# Catch-all adapter for parallel test runners that follow the
|
|
8
|
+
# `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` env-var convention but
|
|
9
|
+
# don't ship a Ruby API for SimpleCov to hook (parallel_rspec,
|
|
10
|
+
# knapsack-style splitters, custom CI sharding scripts). Activates
|
|
11
|
+
# when `TEST_ENV_NUMBER` is set; doesn't require any specific gem to
|
|
12
|
+
# be loaded.
|
|
13
|
+
#
|
|
14
|
+
# Heuristic for `first_worker?`: the worker whose `TEST_ENV_NUMBER`
|
|
15
|
+
# is `""` (parallel_tests/parallel_rspec convention) or `"1"`
|
|
16
|
+
# (zero-based runners that start at 1). Any other value is treated
|
|
17
|
+
# as a non-first worker.
|
|
18
|
+
#
|
|
19
|
+
# `wait_for_siblings` is inherited from Base as a no-op — without a
|
|
20
|
+
# runner-provided API the only synchronization available is polling
|
|
21
|
+
# the resultset cache, which `SimpleCov.wait_for_parallel_results`
|
|
22
|
+
# does after the no-op returns.
|
|
23
|
+
class GenericAdapter < Base
|
|
24
|
+
class << self
|
|
25
|
+
def active?
|
|
26
|
+
return false if SimpleCov.parallel_tests == false
|
|
27
|
+
|
|
28
|
+
ENV.key?("TEST_ENV_NUMBER")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# parallel_tests sets the first worker's TEST_ENV_NUMBER to "";
|
|
32
|
+
# parallel_rspec inherits that. Runners that number from 1 use
|
|
33
|
+
# "1" for the first worker. Both shapes match.
|
|
34
|
+
def first_worker?
|
|
35
|
+
["", "1"].include?(ENV.fetch("TEST_ENV_NUMBER", nil))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def expected_worker_count
|
|
39
|
+
ENV["PARALLEL_TEST_GROUPS"]&.to_i || 1
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|