simplecov 0.22.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/LICENSE +1 -1
- data/README.md +71 -916
- data/exe/simplecov +11 -0
- data/lib/minitest/simplecov_plugin.rb +13 -5
- data/lib/simplecov/atomic_file.rb +70 -0
- data/lib/simplecov/autostart.rb +11 -0
- data/lib/simplecov/cli/clean.rb +87 -0
- data/lib/simplecov/cli/command_helpers.rb +55 -0
- data/lib/simplecov/cli/coverage.rb +86 -0
- data/lib/simplecov/cli/coverage_file.rb +65 -0
- data/lib/simplecov/cli/diff.rb +147 -0
- data/lib/simplecov/cli/dotfile.rb +111 -0
- data/lib/simplecov/cli/merge.rb +120 -0
- data/lib/simplecov/cli/open.rb +48 -0
- data/lib/simplecov/cli/report.rb +97 -0
- data/lib/simplecov/cli/run.rb +36 -0
- 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 +85 -0
- data/lib/simplecov/cli/uncovered.rb +103 -0
- data/lib/simplecov/cli.rb +163 -0
- data/lib/simplecov/color.rb +83 -0
- data/lib/simplecov/combine/branches_combiner.rb +54 -7
- 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 +53 -23
- data/lib/simplecov/combine/methods_combiner.rb +74 -0
- data/lib/simplecov/combine/results_combiner.rb +13 -36
- data/lib/simplecov/combine.rb +5 -23
- data/lib/simplecov/command_guesser.rb +104 -33
- data/lib/simplecov/configuration/coverage.rb +172 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +154 -0
- data/lib/simplecov/configuration/eval_coverage.rb +41 -0
- data/lib/simplecov/configuration/filters.rb +164 -0
- data/lib/simplecov/configuration/formatting.rb +138 -0
- data/lib/simplecov/configuration/groups.rb +42 -0
- data/lib/simplecov/configuration/ignored_entries.rb +63 -0
- data/lib/simplecov/configuration/merging.rb +165 -0
- data/lib/simplecov/configuration/thresholds.rb +179 -0
- data/lib/simplecov/configuration.rb +83 -420
- data/lib/simplecov/coverage_json.rb +24 -0
- data/lib/simplecov/coverage_statistics.rb +16 -10
- data/lib/simplecov/coverage_violations.rb +159 -0
- data/lib/simplecov/defaults.rb +34 -23
- data/lib/simplecov/deprecation.rb +45 -0
- data/lib/simplecov/directive.rb +167 -0
- data/lib/simplecov/exit_codes/check.rb +33 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +18 -65
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +32 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -36
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +30 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -33
- data/lib/simplecov/exit_codes.rb +20 -0
- data/lib/simplecov/exit_handling.rb +128 -0
- data/lib/simplecov/file_list.rb +70 -24
- data/lib/simplecov/filter.rb +105 -26
- data/lib/simplecov/formatter/base.rb +118 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +87 -0
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
- data/lib/simplecov/formatter/html_formatter.rb +105 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +88 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +118 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +93 -0
- data/lib/simplecov/formatter/json_formatter.rb +46 -0
- data/lib/simplecov/formatter/multi_formatter.rb +12 -8
- data/lib/simplecov/formatter/simple_formatter.rb +14 -11
- data/lib/simplecov/formatter.rb +16 -0
- data/lib/simplecov/group_names.rb +32 -0
- data/lib/simplecov/last_run.rb +18 -4
- data/lib/simplecov/lines_classifier.rb +48 -15
- data/lib/simplecov/load_global_config.rb +12 -4
- data/lib/simplecov/parallel_adapters/base.rb +78 -0
- data/lib/simplecov/parallel_adapters/generic.rb +45 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
- data/lib/simplecov/parallel_adapters.rb +85 -0
- data/lib/simplecov/parallel_coordination.rb +134 -0
- data/lib/simplecov/parallel_result_merger.rb +230 -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/report_deferral.rb +49 -0
- data/lib/simplecov/report_stamp.rb +28 -0
- 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 +145 -26
- data/lib/simplecov/result_adapter.rb +139 -8
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
- data/lib/simplecov/result_merger/resultset_file.rb +74 -0
- data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
- data/lib/simplecov/result_merger/resultset_store.rb +53 -0
- data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
- data/lib/simplecov/result_merger.rb +109 -114
- data/lib/simplecov/result_processing.rb +218 -0
- data/lib/simplecov/run_identity.rb +77 -0
- data/lib/simplecov/simulate_coverage.rb +83 -12
- 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 +58 -0
- data/lib/simplecov/source_file/method_builder.rb +60 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +110 -0
- data/lib/simplecov/source_file/skip_chunks.rb +72 -0
- data/lib/simplecov/source_file/source_loader.rb +79 -0
- data/lib/simplecov/source_file/statistics.rb +65 -0
- data/lib/simplecov/source_file.rb +66 -232
- 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 +62 -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 +183 -0
- data/lib/simplecov/static_coverage_extractor.rb +110 -0
- data/lib/simplecov/unloaded_file_injector.rb +75 -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 +185 -380
- data/lib/simplecov_json_formatter.rb +4 -0
- data/sig/simplecov.rbs +1777 -0
- metadata +111 -62
- 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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "base"
|
|
6
|
+
require_relative "../atomic_file"
|
|
7
|
+
require_relative "../coverage_json"
|
|
8
|
+
require_relative "json_formatter"
|
|
9
|
+
|
|
10
|
+
module SimpleCov
|
|
11
|
+
module Formatter
|
|
12
|
+
# Generates a single self-contained HTML coverage report. The compiled
|
|
13
|
+
# template (public/index.html) already carries the viewer app's JS and
|
|
14
|
+
# CSS inline; format substitutes the coverage JSON at the data marker
|
|
15
|
+
# and writes one index.html, so the report can be mailed, uploaded as
|
|
16
|
+
# a single CI artifact, or opened from anywhere without sibling files.
|
|
17
|
+
# A coverage.json is written alongside from the same in-memory hash.
|
|
18
|
+
class HTMLFormatter < Base
|
|
19
|
+
# Placeholder in the compiled template where the report's data goes.
|
|
20
|
+
DATA_MARKER = "<!-- SIMPLECOV_COVERAGE_DATA -->"
|
|
21
|
+
|
|
22
|
+
# The sibling files a 1.0.0 through 1.0.3 report wrote next to
|
|
23
|
+
# index.html. They are stale the moment a single-file report lands
|
|
24
|
+
# on top of them — `simplecov serve` would happily keep serving the
|
|
25
|
+
# old coverage_data.js — so formatting removes the known names.
|
|
26
|
+
LEGACY_REPORT_FILES = %w[
|
|
27
|
+
coverage_data.js application.js application.css
|
|
28
|
+
favicon_green.png favicon_red.png favicon_yellow.png
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
def format(result)
|
|
32
|
+
# The inlined report data feeds the client-side viewer, which
|
|
33
|
+
# renders source from the embedded array — it always needs
|
|
34
|
+
# `source`, regardless of `SimpleCov.source_in_json`. The
|
|
35
|
+
# side-file `coverage.json` honors the setting so downstream
|
|
36
|
+
# tools that read source from disk can opt into a smaller
|
|
37
|
+
# payload. The embedded copy is serialized compactly (pretty
|
|
38
|
+
# printing puts every element of every per-line coverage array
|
|
39
|
+
# on its own indented line, roughly doubling the payload);
|
|
40
|
+
# coverage.json stays pretty-printed for human readers.
|
|
41
|
+
viewer_hash = JSONFormatter.build_hash(result, include_source: true)
|
|
42
|
+
json_hash = SimpleCov.source_in_json ? viewer_hash : source_less_hash(viewer_hash)
|
|
43
|
+
write_report_files(json_hash, viewer_hash, result)
|
|
44
|
+
emit_status(result)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Generate HTML from a pre-existing coverage.json file without
|
|
48
|
+
# needing a live SimpleCov::Result or even a running test suite.
|
|
49
|
+
# The round-trip through parse/generate compacts the (typically
|
|
50
|
+
# pretty-printed) input and rejects invalid JSON here rather than
|
|
51
|
+
# embedding it and failing at view time.
|
|
52
|
+
def format_from_json(json_path, output_dir)
|
|
53
|
+
data = ViewerDataValidator.call(SimpleCov::CoverageJSON.load(json_path))
|
|
54
|
+
json = JSON.generate(data)
|
|
55
|
+
AtomicFile.write(File.join(output_dir, "index.html"), render_report(json), binary: true)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def entry_point_filename
|
|
61
|
+
"index.html"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def source_less_hash(hash)
|
|
65
|
+
coverage = hash.fetch(:coverage).transform_values { |file| file.except(:source) }
|
|
66
|
+
hash.merge(coverage: coverage)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def write_report_files(json_hash, viewer_hash, result)
|
|
70
|
+
CoverageJSONWriter.write(output_path, json_hash, result)
|
|
71
|
+
AtomicFile.write(File.join(output_path, "index.html"), render_report(JSON.generate(viewer_hash)), binary: true)
|
|
72
|
+
FileUtils.rm_f(LEGACY_REPORT_FILES.map { |name| File.join(output_path, name) })
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Substitute the coverage JSON into the compiled template. `<` is
|
|
76
|
+
# escaped as \u003c — valid JSON, since `<` can only occur inside
|
|
77
|
+
# strings — so embedded source text containing "</script>" or
|
|
78
|
+
# "<!--" cannot terminate the surrounding <script> element. Block
|
|
79
|
+
# forms keep gsub/sub from interpreting backslashes in the JSON as
|
|
80
|
+
# replacement-string back-references.
|
|
81
|
+
def render_report(json)
|
|
82
|
+
# UTF-8 explicitly, not the locale's `Encoding.default_external`:
|
|
83
|
+
# the JSON payload is UTF-8, and substituting it into a template
|
|
84
|
+
# tagged US-ASCII (or a Windows code page) raises
|
|
85
|
+
# Encoding::CompatibilityError the moment either side carries a
|
|
86
|
+
# non-ASCII character.
|
|
87
|
+
template = File.read(File.join(public_dir, "index.html"), encoding: Encoding::UTF_8)
|
|
88
|
+
unless template.include?(DATA_MARKER)
|
|
89
|
+
raise "SimpleCov's HTML template is missing its #{DATA_MARKER.inspect} marker"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
data_script = "<script>window.SIMPLECOV_DATA = #{json.gsub('<') { '\u003c' }};</script>"
|
|
93
|
+
template.sub(DATA_MARKER) { data_script }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def public_dir
|
|
97
|
+
# `.to_s` collapses `__dir__`'s nil arm (only possible under eval,
|
|
98
|
+
# which can't happen for a file on disk) so the path is a String.
|
|
99
|
+
File.join(__dir__.to_s, "html_formatter/public/")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
require_relative "html_formatter/viewer_data_validator"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module Formatter
|
|
5
|
+
class JSONFormatter
|
|
6
|
+
# Translates the threshold violations reported by
|
|
7
|
+
# `SimpleCov::CoverageViolations` into the `:errors` section of
|
|
8
|
+
# coverage.json. Each violation is keyed by criterion
|
|
9
|
+
# (`:lines` / `:branches` / `:methods`) so consumers can render
|
|
10
|
+
# per-criterion messages without re-deriving them.
|
|
11
|
+
class ErrorsFormatter
|
|
12
|
+
CRITERION_KEYS = {line: :lines, branch: :branches, method: :methods}.freeze
|
|
13
|
+
private_constant :CRITERION_KEYS
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def call(result)
|
|
17
|
+
errors = {} #: Hash[Symbol, Hash[untyped, untyped]]
|
|
18
|
+
format_minimum_overall(result, errors)
|
|
19
|
+
format_minimum_by_file(result, errors)
|
|
20
|
+
format_minimum_by_group(result, errors)
|
|
21
|
+
format_maximum_overall(result, errors)
|
|
22
|
+
format_maximum_drop(result, errors)
|
|
23
|
+
errors
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def format_minimum_overall(result, errors)
|
|
29
|
+
SimpleCov::CoverageViolations.minimum_overall(result, SimpleCov.minimum_coverage).each do |violation|
|
|
30
|
+
bucket(errors, :minimum_coverage)[key_for(violation)] = expected_actual(violation)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def format_minimum_by_file(result, errors)
|
|
35
|
+
violations = SimpleCov::CoverageViolations.minimum_by_file(
|
|
36
|
+
result, SimpleCov.minimum_coverage_by_file, SimpleCov.minimum_coverage_by_file_overrides
|
|
37
|
+
)
|
|
38
|
+
violations.each { |violation| record_by_file(violation, errors) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def record_by_file(violation, errors)
|
|
42
|
+
by_file = bucket(errors, :minimum_coverage_by_file)
|
|
43
|
+
file_bucket = by_file[violation.fetch(:project_filename)] ||= {} #: Hash[untyped, untyped]
|
|
44
|
+
file_bucket[key_for(violation)] = expected_actual(violation)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def format_minimum_by_group(result, errors)
|
|
48
|
+
violations = SimpleCov::CoverageViolations.minimum_by_group(result, SimpleCov.minimum_coverage_by_group)
|
|
49
|
+
return if violations.empty?
|
|
50
|
+
|
|
51
|
+
# `bucket` lazily creates the errors key, so only touch it when
|
|
52
|
+
# there is a violation to record.
|
|
53
|
+
by_group = bucket(errors, :minimum_coverage_by_group)
|
|
54
|
+
violations.each do |violation|
|
|
55
|
+
group_bucket = by_group[violation.fetch(:group_name)] ||= {} #: Hash[untyped, untyped]
|
|
56
|
+
group_bucket[key_for(violation)] = expected_actual(violation)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def format_maximum_overall(result, errors)
|
|
61
|
+
SimpleCov::CoverageViolations.maximum_overall(result, SimpleCov.maximum_coverage).each do |violation|
|
|
62
|
+
bucket(errors, :maximum_coverage)[key_for(violation)] = expected_actual(violation)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def format_maximum_drop(result, errors)
|
|
67
|
+
SimpleCov::CoverageViolations.maximum_drop(result, SimpleCov.maximum_coverage_drop).each do |violation|
|
|
68
|
+
bucket(errors, :maximum_coverage_drop)[key_for(violation)] =
|
|
69
|
+
{maximum: violation.fetch(:maximum), actual: violation.fetch(:actual)}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def bucket(errors, name)
|
|
74
|
+
errors[name] ||= {} #: Hash[untyped, untyped]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def key_for(violation)
|
|
78
|
+
CRITERION_KEYS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion)))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def expected_actual(violation)
|
|
82
|
+
{expected: violation.fetch(:expected), actual: violation.fetch(:actual)}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
class << self
|
|
26
|
+
def format(result, include_source: true)
|
|
27
|
+
{
|
|
28
|
+
:$schema => SCHEMA_URL,
|
|
29
|
+
:meta => format_meta(result),
|
|
30
|
+
:total => format_coverage_statistics(result.coverage_statistics),
|
|
31
|
+
:coverage => format_files(result, include_source: include_source),
|
|
32
|
+
:groups => format_groups(result),
|
|
33
|
+
:errors => ErrorsFormatter.call(result)
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def format_files(result, include_source:)
|
|
40
|
+
result.files.to_h do |source_file|
|
|
41
|
+
[source_file.project_filename, SourceFileFormatter.call(source_file, include_source: include_source)]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def format_groups(result)
|
|
46
|
+
result.groups.to_h do |name, file_list|
|
|
47
|
+
stats = format_coverage_statistics(file_list.coverage_statistics)
|
|
48
|
+
[name, stats.merge(files: file_list.map(&:project_filename))]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def format_meta(result)
|
|
53
|
+
{
|
|
54
|
+
schema_version: SCHEMA_VERSION,
|
|
55
|
+
simplecov_version: SimpleCov::VERSION,
|
|
56
|
+
command_name: result.command_name,
|
|
57
|
+
project_name: SimpleCov.project_name,
|
|
58
|
+
timestamp: result.created_at.iso8601(3),
|
|
59
|
+
root: SimpleCov.root,
|
|
60
|
+
commit: git_commit,
|
|
61
|
+
primary_coverage: SimpleCov.primary_coverage.to_s
|
|
62
|
+
}.merge!(coverage_flags)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Full git commit SHA of `SimpleCov.root`'s HEAD, or nil when the
|
|
66
|
+
# project isn't a git checkout or git isn't on PATH. Recorded so tools
|
|
67
|
+
# can recover the exact source a report was generated against, which
|
|
68
|
+
# matters most when `source_in_json false` drops the source text from
|
|
69
|
+
# coverage.json. stderr is captured (not forwarded) so a non-git project
|
|
70
|
+
# doesn't print git's diagnostics to the build.
|
|
71
|
+
def git_commit
|
|
72
|
+
output, status = Open3.capture2e("git", "-C", SimpleCov.root.to_s, "rev-parse", "HEAD")
|
|
73
|
+
status.success? ? output.strip : nil
|
|
74
|
+
rescue StandardError
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def coverage_flags
|
|
79
|
+
{
|
|
80
|
+
line_coverage: SimpleCov.line_coverage?,
|
|
81
|
+
branch_coverage: SimpleCov.branch_coverage?,
|
|
82
|
+
method_coverage: SimpleCov.method_coverage?
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def format_coverage_statistics(statistics)
|
|
87
|
+
result = {} #: Hash[Symbol, untyped]
|
|
88
|
+
result[:lines] = format_line_statistic(statistics[:line]) if statistics[:line]
|
|
89
|
+
result[:branches] = format_single_statistic(statistics[:branch]) if statistics[:branch]
|
|
90
|
+
result[:methods] = format_single_statistic(statistics[:method]) if statistics[:method]
|
|
91
|
+
result
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def format_line_statistic(stat)
|
|
95
|
+
{
|
|
96
|
+
covered: stat.covered,
|
|
97
|
+
missed: stat.missed,
|
|
98
|
+
omitted: stat.omitted,
|
|
99
|
+
total: stat.total,
|
|
100
|
+
percent: stat.percent,
|
|
101
|
+
strength: stat.strength
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def format_single_statistic(stat)
|
|
106
|
+
{
|
|
107
|
+
covered: stat.covered,
|
|
108
|
+
missed: stat.missed,
|
|
109
|
+
total: stat.total,
|
|
110
|
+
percent: stat.percent,
|
|
111
|
+
strength: stat.strength
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
class << self
|
|
11
|
+
def call(source_file, include_source: true)
|
|
12
|
+
result = include_source ? format_source_code(source_file) : {} #: Hash[Symbol, untyped]
|
|
13
|
+
result.merge!(line_coverage_section(source_file)) if SimpleCov.line_coverage?
|
|
14
|
+
result.merge!(branch_coverage_section(source_file)) if SimpleCov.branch_coverage?
|
|
15
|
+
result.merge!(method_coverage_section(source_file)) if SimpleCov.method_coverage?
|
|
16
|
+
result
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# No per-line encoding conversion here: SourceLoader guarantees
|
|
22
|
+
# every line leaves it as valid UTF-8 (transcoding declared
|
|
23
|
+
# encodings, scrubbing invalid bytes), and the converter copy
|
|
24
|
+
# this used to make per line was the single largest allocation
|
|
25
|
+
# source in formatting a large report.
|
|
26
|
+
def format_source_code(source_file)
|
|
27
|
+
{source: source_file.lines.map { |line| line.src.chomp }}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def line_coverage_section(source_file)
|
|
31
|
+
covered = source_file.covered_lines.size
|
|
32
|
+
missed = source_file.missed_lines.size
|
|
33
|
+
{
|
|
34
|
+
lines: source_file.lines.map { |line| format_line(line) },
|
|
35
|
+
lines_covered_percent: source_file.covered_percent,
|
|
36
|
+
covered_lines: covered,
|
|
37
|
+
missed_lines: missed,
|
|
38
|
+
omitted_lines: source_file.never_lines.size,
|
|
39
|
+
total_lines: covered + missed
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def branch_coverage_section(source_file)
|
|
44
|
+
{
|
|
45
|
+
branches: source_file.branches.map { |branch| format_branch(branch) },
|
|
46
|
+
branches_covered_percent: source_file.covered_percent(:branch),
|
|
47
|
+
covered_branches: source_file.covered_branches.size,
|
|
48
|
+
missed_branches: source_file.missed_branches.size,
|
|
49
|
+
total_branches: source_file.total_branches.size
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def method_coverage_section(source_file)
|
|
54
|
+
covered = source_file.covered_methods.size
|
|
55
|
+
missed = source_file.missed_methods.size
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
methods: source_file.methods.map { |method| format_method(method) },
|
|
59
|
+
methods_covered_percent: source_file.covered_percent(:method),
|
|
60
|
+
covered_methods: covered,
|
|
61
|
+
missed_methods: missed,
|
|
62
|
+
total_methods: covered + missed
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def format_line(line)
|
|
67
|
+
line.skipped? ? "ignored" : line.coverage
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def format_branch(branch)
|
|
71
|
+
{
|
|
72
|
+
type: branch.type,
|
|
73
|
+
start_line: branch.start_line,
|
|
74
|
+
end_line: branch.end_line,
|
|
75
|
+
coverage: format_line(branch),
|
|
76
|
+
inline: branch.inline?,
|
|
77
|
+
report_line: branch.report_line
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def format_method(method)
|
|
82
|
+
{
|
|
83
|
+
name: method.to_s,
|
|
84
|
+
start_line: method.start_line,
|
|
85
|
+
end_line: method.end_line,
|
|
86
|
+
coverage: format_line(method)
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "coverage_json_writer"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module SimpleCov
|
|
8
|
+
module Formatter
|
|
9
|
+
# Writes coverage results as JSON to coverage/coverage.json. Used
|
|
10
|
+
# standalone, alongside the HTML formatter, or by external tools that
|
|
11
|
+
# consume SimpleCov output.
|
|
12
|
+
class JSONFormatter < Base
|
|
13
|
+
FILENAME = CoverageJSONWriter::FILENAME
|
|
14
|
+
|
|
15
|
+
# `include_source:` defaults to `SimpleCov.source_in_json` (true
|
|
16
|
+
# by default) so the historical payload shape is unchanged.
|
|
17
|
+
# Callers that need the source array regardless of the global
|
|
18
|
+
# setting (the HTML formatter, which feeds the client-side
|
|
19
|
+
# viewer) pass `include_source: true` explicitly.
|
|
20
|
+
def self.build_hash(result, include_source: SimpleCov.source_in_json)
|
|
21
|
+
ResultHashFormatter.format(result, include_source: include_source)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def format(result)
|
|
25
|
+
CoverageJSONWriter.write(output_path, self.class.build_hash(result), result)
|
|
26
|
+
emit_status(result)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def message_prefix
|
|
32
|
+
"JSON "
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def entry_point_filename
|
|
36
|
+
FILENAME
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Loaded after the JSONFormatter class is defined so the nested
|
|
43
|
+
# `class JSONFormatter` reopen inside result_hash_formatter.rb doesn't
|
|
44
|
+
# accidentally create a JSONFormatter < Object before this file gets a
|
|
45
|
+
# chance to declare `JSONFormatter < Base`.
|
|
46
|
+
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.format(formatter, 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,20 @@ 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
|
-
result.groups.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
criterion = SimpleCov.coverage_statistics_key(SimpleCov.primary_coverage)
|
|
12
|
+
result.groups.map { |name, files| format_group(name, files, criterion) }.join
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def format_group(name, files, criterion)
|
|
18
|
+
header = "Group: #{name}\n#{'=' * 40}\n"
|
|
19
|
+
# The configured primary criterion is enabled, so every result file
|
|
20
|
+
# carries its corresponding statistic despite the nilable public API.
|
|
21
|
+
body = files.map do |file|
|
|
22
|
+
"#{file.filename} (coverage: #{(_ = file.covered_percent(criterion)).floor(2)}%)\n"
|
|
23
|
+
end.join
|
|
24
|
+
"#{header}#{body}\n"
|
|
22
25
|
end
|
|
23
26
|
end
|
|
24
27
|
end
|
data/lib/simplecov/formatter.rb
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
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
|
|
9
|
+
# Formatters can be configured either as classes (instantiated
|
|
10
|
+
# fresh for every report) or as ready-built instances — the only
|
|
11
|
+
# way to reach constructor options like
|
|
12
|
+
# `HTMLFormatter.new(silent: true)`. See #1240.
|
|
13
|
+
def self.instance_for(formatter)
|
|
14
|
+
formatter.is_a?(Class) ? formatter.new : formatter
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Normalize a class or instance, then dispatch the result to it.
|
|
18
|
+
def self.format(formatter, result)
|
|
19
|
+
instance_for(formatter).format(result)
|
|
20
|
+
end
|
|
6
21
|
end
|
|
7
22
|
end
|
|
8
23
|
|
|
9
24
|
require_relative "formatter/simple_formatter"
|
|
10
25
|
require_relative "formatter/multi_formatter"
|
|
26
|
+
require_relative "formatter/json_formatter"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
# Semantic names owned by result processing rather than user configuration.
|
|
5
|
+
module GroupNames
|
|
6
|
+
UNGROUPED = "Ungrouped"
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Group names are Hash keys, report labels, and JSON object keys all at
|
|
11
|
+
# once, so they are normalized to Strings up front: a Symbol spelling
|
|
12
|
+
# (`group :Controllers`) means the String, and anything else has no
|
|
13
|
+
# sensible serialized form. Normalizing before `validate!` also keeps
|
|
14
|
+
# `group :Ungrouped` from slipping past the reservation below.
|
|
15
|
+
def normalize(group_name)
|
|
16
|
+
case group_name
|
|
17
|
+
when String then group_name
|
|
18
|
+
when Symbol then group_name.to_s
|
|
19
|
+
else
|
|
20
|
+
raise SimpleCov::ConfigurationError,
|
|
21
|
+
"Group names must be Strings, got #{group_name.inspect} (#{group_name.class})"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate!(group_names)
|
|
26
|
+
return group_names unless group_names.include?(UNGROUPED)
|
|
27
|
+
|
|
28
|
+
raise SimpleCov::ConfigurationError,
|
|
29
|
+
"#{UNGROUPED.inspect} is reserved for files that do not match a configured group"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/simplecov/last_run.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "atomic_file"
|
|
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
|
|
@@ -15,13 +18,24 @@ module SimpleCov
|
|
|
15
18
|
json = File.read(last_run_path)
|
|
16
19
|
return nil if json.strip.empty?
|
|
17
20
|
|
|
18
|
-
JSON.parse(json, symbolize_names: true)
|
|
21
|
+
parsed = JSON.parse(json, symbolize_names: true)
|
|
22
|
+
# The maximum_coverage_drop check digs into a Hash, so anything
|
|
23
|
+
# else in a corrupt or hand-edited file counts as "no previous
|
|
24
|
+
# run" rather than crashing the at_exit hook.
|
|
25
|
+
parsed.is_a?(Hash) ? parsed : invalid_last_run
|
|
26
|
+
rescue JSON::ParserError
|
|
27
|
+
invalid_last_run
|
|
19
28
|
end
|
|
20
29
|
|
|
21
30
|
def write(json)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
AtomicFile.write(last_run_path, "#{JSON.pretty_generate(json)}\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def invalid_last_run
|
|
37
|
+
warn "[SimpleCov]: Warning! Parsing JSON content of .last_run.json failed, ignoring the previous run"
|
|
38
|
+
nil
|
|
25
39
|
end
|
|
26
40
|
end
|
|
27
41
|
end
|