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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module Formatter
|
|
5
|
+
class JSONFormatter
|
|
6
|
+
class ErrorsFormatter
|
|
7
|
+
CRITERION_KEYS = {line: :lines, branch: :branches, method: :methods}.freeze
|
|
8
|
+
private_constant :CRITERION_KEYS
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def call(result)
|
|
12
|
+
errors = {} #: Hash[Symbol, Hash[untyped, untyped]]
|
|
13
|
+
format_minimum_overall(result, errors)
|
|
14
|
+
format_minimum_by_file(result, errors)
|
|
15
|
+
format_baseline(result, errors)
|
|
16
|
+
format_minimum_by_group(result, errors)
|
|
17
|
+
format_maximum_overall(result, errors)
|
|
18
|
+
format_maximum_drop(result, errors)
|
|
19
|
+
format_maximum_missed(result, errors)
|
|
20
|
+
format_maximum_missed_per_file(result, errors)
|
|
21
|
+
errors
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def format_minimum_overall(result, errors)
|
|
27
|
+
CoverageViolations.minimum_overall(result, SimpleCov.minimum_coverage).each do |violation|
|
|
28
|
+
bucket(errors, :minimum_coverage)[key_for(violation)] = expected_actual(violation)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def format_minimum_by_file(result, errors)
|
|
33
|
+
violations = CoverageViolations.minimum_by_file(
|
|
34
|
+
result, SimpleCov.minimum_coverage_by_file, SimpleCov.minimum_coverage_by_file_overrides,
|
|
35
|
+
baseline: SimpleCov.baseline
|
|
36
|
+
)
|
|
37
|
+
violations.each { |violation| record_by_file(violation, errors) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Baseline-floor violations keyed like minimum_coverage_by_file: filename,
|
|
41
|
+
# then criterion. The payload adds the two missed counts the floor was judged
|
|
42
|
+
# by; `allowed_missed` is absent for a percent-only floor.
|
|
43
|
+
def format_baseline(result, errors)
|
|
44
|
+
CoverageViolations.baseline(result, SimpleCov.baseline).each do |violation|
|
|
45
|
+
by_file = bucket(errors, :baseline)
|
|
46
|
+
file_bucket = by_file[violation.fetch(:project_filename)] ||= {} #: Hash[untyped, untyped]
|
|
47
|
+
payload = expected_actual(violation).merge(actual_missed: violation.fetch(:actual_missed))
|
|
48
|
+
allowed = violation.fetch(:allowed_missed)
|
|
49
|
+
payload[:allowed_missed] = allowed if allowed
|
|
50
|
+
file_bucket[key_for(violation)] = payload
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def record_by_file(violation, errors)
|
|
55
|
+
by_file = bucket(errors, :minimum_coverage_by_file)
|
|
56
|
+
file_bucket = by_file[violation.fetch(:project_filename)] ||= {} #: Hash[untyped, untyped]
|
|
57
|
+
file_bucket[key_for(violation)] = expected_actual(violation)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def format_minimum_by_group(result, errors)
|
|
61
|
+
violations = CoverageViolations.minimum_by_group(result, SimpleCov.minimum_coverage_by_group)
|
|
62
|
+
return if violations.empty?
|
|
63
|
+
|
|
64
|
+
by_group = bucket(errors, :minimum_coverage_by_group)
|
|
65
|
+
violations.each do |violation|
|
|
66
|
+
group_bucket = by_group[violation.fetch(:group_name)] ||= {} #: Hash[untyped, untyped]
|
|
67
|
+
group_bucket[key_for(violation)] = expected_actual(violation)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def format_maximum_overall(result, errors)
|
|
72
|
+
CoverageViolations.maximum_overall(result, SimpleCov.maximum_coverage).each do |violation|
|
|
73
|
+
bucket(errors, :maximum_coverage)[key_for(violation)] = expected_actual(violation)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def format_maximum_drop(result, errors)
|
|
78
|
+
CoverageViolations.maximum_drop(result, SimpleCov.maximum_coverage_drop).each do |violation|
|
|
79
|
+
bucket(errors, :maximum_coverage_drop)[key_for(violation)] = maximum_actual(violation)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def format_maximum_missed(result, errors)
|
|
84
|
+
CoverageViolations.maximum_missed(result, SimpleCov.maximum_missed).each do |violation|
|
|
85
|
+
bucket(errors, :maximum_missed)[key_for(violation)] = maximum_actual(violation)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def format_maximum_missed_per_file(result, errors)
|
|
90
|
+
violations = CoverageViolations.maximum_missed_by_file(
|
|
91
|
+
result, SimpleCov.maximum_missed_per_file, SimpleCov.maximum_missed_per_file_overrides,
|
|
92
|
+
baseline: SimpleCov.baseline
|
|
93
|
+
)
|
|
94
|
+
violations.each do |violation|
|
|
95
|
+
by_file = bucket(errors, :maximum_missed_per_file)
|
|
96
|
+
file_bucket = by_file[violation.fetch(:project_filename)] ||= {} #: Hash[untyped, untyped]
|
|
97
|
+
file_bucket[key_for(violation)] = maximum_actual(violation)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Lazily creates the errors key, so callers only touch it when there is a
|
|
102
|
+
# violation to record.
|
|
103
|
+
def bucket(errors, name)
|
|
104
|
+
errors[name] ||= {} #: Hash[untyped, untyped]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def key_for(violation)
|
|
108
|
+
CRITERION_KEYS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion)))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def expected_actual(violation)
|
|
112
|
+
{expected: violation.fetch(:expected), actual: violation.fetch(:actual)}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def maximum_actual(violation)
|
|
116
|
+
{maximum: violation.fetch(:maximum), actual: violation.fetch(:actual)}
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../production/file_sink"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module Formatter
|
|
7
|
+
class JSONFormatter
|
|
8
|
+
module ProductionSectionFormatter
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
# The section for the configured store, or nil when none is configured or it
|
|
12
|
+
# is unreadable. An unreadable store warns instead of raising: the report is
|
|
13
|
+
# generated at the end of a test run, and a missing night of production data
|
|
14
|
+
# must not fail the suite that measured the tests.
|
|
15
|
+
def call(path = SimpleCov.production_coverage)
|
|
16
|
+
return nil unless path
|
|
17
|
+
|
|
18
|
+
section(Production::FileSink.read(path))
|
|
19
|
+
rescue SystemCallError, Production::Error => e
|
|
20
|
+
warn "[SimpleCov] skipping production coverage (#{e})"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def section(store)
|
|
24
|
+
section = {} #: Hash[Symbol, untyped]
|
|
25
|
+
started_at = store["started_at"]
|
|
26
|
+
updated_at = store["updated_at"]
|
|
27
|
+
section[:started_at] = started_at if started_at
|
|
28
|
+
section[:updated_at] = updated_at if updated_at
|
|
29
|
+
section[:files] = files(store)
|
|
30
|
+
section
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# `coverage` and `last_seen` are the two keys a parsed store always carries,
|
|
34
|
+
# since `FileSink.parse` substitutes an empty Hash for either one a document
|
|
35
|
+
# is missing.
|
|
36
|
+
def files(store)
|
|
37
|
+
last_seen = store.fetch("last_seen")
|
|
38
|
+
store.fetch("coverage").sort.to_h do |file, lines|
|
|
39
|
+
entry = {lines: lines} #: Hash[Symbol, untyped]
|
|
40
|
+
stamp = last_seen[file]
|
|
41
|
+
entry[:last_seen] = stamp if stamp
|
|
42
|
+
[file, entry]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "errors_formatter"
|
|
6
|
+
require_relative "production_section_formatter"
|
|
7
|
+
require_relative "source_file_formatter"
|
|
8
|
+
|
|
9
|
+
module SimpleCov
|
|
10
|
+
module Formatter
|
|
11
|
+
class JSONFormatter
|
|
12
|
+
class ResultHashFormatter
|
|
13
|
+
# Bump this (and SCHEMA_URL) when the JSON shape changes: additive changes
|
|
14
|
+
# bump the minor segment, removals or shape changes the major. The versioned
|
|
15
|
+
# file at schemas/coverage-vX.Y.schema.json is the canonical artifact
|
|
16
|
+
# consumers should pin to.
|
|
17
|
+
SCHEMA_VERSION = "1.3"
|
|
18
|
+
SCHEMA_URL = "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v#{SCHEMA_VERSION}.schema.json".freeze
|
|
19
|
+
private_constant :SCHEMA_VERSION, :SCHEMA_URL
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def format(result, include_source: true)
|
|
23
|
+
document = {
|
|
24
|
+
:$schema => SCHEMA_URL,
|
|
25
|
+
:meta => format_meta(result),
|
|
26
|
+
:total => format_coverage_statistics(result.coverage_statistics),
|
|
27
|
+
:coverage => format_files(result, include_source: include_source),
|
|
28
|
+
:groups => format_groups(result),
|
|
29
|
+
:errors => ErrorsFormatter.call(result)
|
|
30
|
+
}
|
|
31
|
+
add_optional_sections(document, result)
|
|
32
|
+
document
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Contexts are present exactly when the result carried a complete map, so
|
|
38
|
+
# consumers can tell "recorded and empty" from "not recorded". History is
|
|
39
|
+
# present only when past runs are recorded, since one point is not a trend,
|
|
40
|
+
# and production coverage only when a store is configured and readable.
|
|
41
|
+
def add_optional_sections(document, result)
|
|
42
|
+
document[:contexts] = result.contexts.contexts if result.contexts
|
|
43
|
+
history = History.entries_with(result)
|
|
44
|
+
document[:history] = history if history.length > 1
|
|
45
|
+
production = ProductionSectionFormatter.call
|
|
46
|
+
document[:production] = production if production
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def format_files(result, include_source:)
|
|
50
|
+
result.files.to_h do |source_file|
|
|
51
|
+
[source_file.project_filename,
|
|
52
|
+
SourceFileFormatter.call(source_file, include_source: include_source, contexts: result.contexts)]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def format_groups(result)
|
|
57
|
+
result.groups.to_h do |name, file_list|
|
|
58
|
+
stats = format_coverage_statistics(file_list.coverage_statistics)
|
|
59
|
+
[name, stats.merge(files: file_list.map(&:project_filename))]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def format_meta(result)
|
|
64
|
+
{
|
|
65
|
+
schema_version: SCHEMA_VERSION,
|
|
66
|
+
simplecov_version: VERSION,
|
|
67
|
+
command_name: result.command_name, command_names: result.command_names,
|
|
68
|
+
project_name: SimpleCov.project_name,
|
|
69
|
+
timestamp: result.created_at.iso8601(3),
|
|
70
|
+
root: SimpleCov.root,
|
|
71
|
+
commit: git_commit,
|
|
72
|
+
primary_coverage: SimpleCov.primary_coverage.to_s, **coverage_flags
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Recorded so tools can recover the exact source a report was generated
|
|
77
|
+
# against, which matters most when `source_in_json false` drops the source
|
|
78
|
+
# text. stderr is captured rather than forwarded so a non-git project doesn't
|
|
79
|
+
# print git's diagnostics to the build.
|
|
80
|
+
def git_commit
|
|
81
|
+
output, status = Open3.capture2e("git", "-C", SimpleCov.root, "rev-parse", "HEAD")
|
|
82
|
+
output.strip if status.success?
|
|
83
|
+
rescue
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def coverage_flags
|
|
88
|
+
{
|
|
89
|
+
line_coverage: SimpleCov.line_coverage?,
|
|
90
|
+
branch_coverage: SimpleCov.branch_coverage?,
|
|
91
|
+
method_coverage: SimpleCov.method_coverage?
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# A criterion the run did not measure is absent from `statistics` entirely,
|
|
96
|
+
# and gets no section.
|
|
97
|
+
def format_coverage_statistics(statistics)
|
|
98
|
+
result = {} #: Hash[Symbol, untyped]
|
|
99
|
+
line = statistics[:line]
|
|
100
|
+
branch = statistics[:branch]
|
|
101
|
+
method_stat = statistics[:method]
|
|
102
|
+
result[:lines] = format_line_statistic(line) if line
|
|
103
|
+
result[:branches] = format_single_statistic(branch) if branch
|
|
104
|
+
result[:methods] = format_single_statistic(method_stat) if method_stat
|
|
105
|
+
result
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def format_line_statistic(stat)
|
|
109
|
+
{
|
|
110
|
+
covered: stat.covered,
|
|
111
|
+
missed: stat.missed,
|
|
112
|
+
omitted: stat.omitted,
|
|
113
|
+
total: stat.total,
|
|
114
|
+
percent: stat.percent,
|
|
115
|
+
strength: stat.strength
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def format_single_statistic(stat)
|
|
120
|
+
{
|
|
121
|
+
covered: stat.covered,
|
|
122
|
+
missed: stat.missed,
|
|
123
|
+
total: stat.total,
|
|
124
|
+
percent: stat.percent,
|
|
125
|
+
strength: stat.strength
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module Formatter
|
|
5
|
+
class JSONFormatter
|
|
6
|
+
class SourceFileFormatter
|
|
7
|
+
class << self
|
|
8
|
+
def call(source_file, include_source: true, contexts: nil)
|
|
9
|
+
result = include_source ? format_source_code(source_file) : {} #: Hash[Symbol, untyped]
|
|
10
|
+
result.merge!(line_coverage_section(source_file)) if SimpleCov.line_coverage?
|
|
11
|
+
result.merge!(branch_coverage_section(source_file)) if SimpleCov.branch_coverage?
|
|
12
|
+
result.merge!(method_coverage_section(source_file)) if SimpleCov.method_coverage?
|
|
13
|
+
result.merge!(contexts_section(source_file, contexts)) if contexts
|
|
14
|
+
result
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# An untouched file gets no key at all: under a present document-level
|
|
20
|
+
# `contexts` array, absence already says "no recorded context executed this
|
|
21
|
+
# file".
|
|
22
|
+
def contexts_section(source_file, contexts)
|
|
23
|
+
bitmaps = contexts.serialized_bitmaps_for(source_file.filename)
|
|
24
|
+
bitmaps.empty? ? {} : {contexts: bitmaps}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# No per-line encoding conversion: SourceLoader guarantees every line leaves
|
|
28
|
+
# it as valid UTF-8, and the converter copy this used to make per line was
|
|
29
|
+
# the single largest allocation source in formatting a large report.
|
|
30
|
+
def format_source_code(source_file)
|
|
31
|
+
{source: source_file.lines.map { |line| line.src.chomp }}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def line_coverage_section(source_file)
|
|
35
|
+
covered = source_file.covered_lines.size
|
|
36
|
+
missed = source_file.missed_lines.size
|
|
37
|
+
{
|
|
38
|
+
lines: source_file.lines.map { |line| format_line(line) },
|
|
39
|
+
lines_covered_percent: source_file.covered_percent,
|
|
40
|
+
covered_lines: covered,
|
|
41
|
+
missed_lines: missed,
|
|
42
|
+
omitted_lines: source_file.never_lines.size,
|
|
43
|
+
total_lines: covered + missed
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def branch_coverage_section(source_file)
|
|
48
|
+
{
|
|
49
|
+
branches: source_file.branches.map { |branch| format_branch(branch) },
|
|
50
|
+
branches_covered_percent: source_file.covered_percent(:branch),
|
|
51
|
+
covered_branches: source_file.covered_branches.size,
|
|
52
|
+
missed_branches: source_file.missed_branches.size,
|
|
53
|
+
total_branches: source_file.total_branches.size
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def method_coverage_section(source_file)
|
|
58
|
+
covered = source_file.covered_methods.size
|
|
59
|
+
missed = source_file.missed_methods.size
|
|
60
|
+
|
|
61
|
+
{
|
|
62
|
+
methods: source_file.methods.map { |method| format_method(method) },
|
|
63
|
+
methods_covered_percent: source_file.covered_percent(:method),
|
|
64
|
+
covered_methods: covered,
|
|
65
|
+
missed_methods: missed,
|
|
66
|
+
total_methods: covered + missed
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def format_line(line)
|
|
71
|
+
line.skipped? ? "ignored" : line.coverage
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def format_branch(branch)
|
|
75
|
+
{
|
|
76
|
+
type: branch.type,
|
|
77
|
+
start_line: branch.start_line,
|
|
78
|
+
end_line: branch.end_line,
|
|
79
|
+
coverage: format_line(branch),
|
|
80
|
+
inline: branch.inline?,
|
|
81
|
+
report_line: branch.report_line
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def format_method(method)
|
|
86
|
+
{
|
|
87
|
+
name: method.to_s,
|
|
88
|
+
start_line: method.start_line,
|
|
89
|
+
end_line: method.end_line,
|
|
90
|
+
coverage: format_line(method)
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
class JSONFormatter < Base
|
|
10
|
+
FILENAME = CoverageJSONWriter::FILENAME
|
|
11
|
+
|
|
12
|
+
# Callers that need the source array regardless of the global setting (the
|
|
13
|
+
# HTML formatter, which feeds the client-side viewer) pass
|
|
14
|
+
# `include_source: true` explicitly.
|
|
15
|
+
def self.build_hash(result, include_source: SimpleCov.source_in_json)
|
|
16
|
+
ResultHashFormatter.format(result, include_source: include_source)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def format(result)
|
|
20
|
+
CoverageJSONWriter.write(output_path, self.class.build_hash(result), result)
|
|
21
|
+
emit_status(result)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def message_prefix
|
|
27
|
+
"JSON "
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def entry_point_filename
|
|
31
|
+
FILENAME
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Loaded after the JSONFormatter class is defined so the nested reopen inside
|
|
38
|
+
# result_hash_formatter.rb doesn't create a JSONFormatter < Object before this
|
|
39
|
+
# file gets a chance to declare `JSONFormatter < Base`.
|
|
40
|
+
require_relative "json_formatter/result_hash_formatter"
|
|
@@ -6,27 +6,25 @@ module SimpleCov
|
|
|
6
6
|
module InstanceMethods
|
|
7
7
|
def format(result)
|
|
8
8
|
formatters.map do |formatter|
|
|
9
|
-
|
|
10
|
-
rescue
|
|
11
|
-
warn("Formatter #{formatter} failed with #{e.class}: #{e
|
|
12
|
-
nil
|
|
9
|
+
Formatter.format(formatter, result)
|
|
10
|
+
rescue => e
|
|
11
|
+
warn("Formatter #{formatter} failed with #{e.class}: #{e} (#{(_ = e.backtrace).first})")
|
|
13
12
|
end
|
|
14
13
|
end
|
|
15
14
|
end
|
|
16
15
|
|
|
16
|
+
# Normalized eagerly and captured in the closure. `Array()` is pure for every
|
|
17
|
+
# accepted input shape, so this is equivalent to the historical lazy
|
|
18
|
+
# per-instance memoization.
|
|
17
19
|
def self.new(formatters = nil)
|
|
20
|
+
formatter_list = Array(formatters)
|
|
18
21
|
Class.new do
|
|
19
22
|
define_method :formatters do
|
|
20
|
-
|
|
23
|
+
formatter_list
|
|
21
24
|
end
|
|
22
25
|
include InstanceMethods
|
|
23
26
|
end
|
|
24
27
|
end
|
|
25
|
-
|
|
26
|
-
def self.[](*args)
|
|
27
|
-
warn "#{Kernel.caller.first}: [DEPRECATION] ::[] is deprecated. Use ::new instead."
|
|
28
|
-
new(Array(args))
|
|
29
|
-
end
|
|
30
28
|
end
|
|
31
29
|
end
|
|
32
30
|
end
|
|
@@ -2,23 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module Formatter
|
|
5
|
-
#
|
|
6
|
-
# A ridiculously simple formatter for SimpleCov results.
|
|
7
|
-
#
|
|
8
5
|
class SimpleFormatter
|
|
9
|
-
# Takes a SimpleCov::Result and generates a string out of it
|
|
10
6
|
def format(result)
|
|
11
|
-
|
|
12
|
-
result.groups.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
7
|
+
criterion = SimpleCov.coverage_statistics_key(SimpleCov.primary_coverage)
|
|
8
|
+
result.groups.map { |name, files| format_group(name, files, criterion) }.join
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def format_group(name, files, criterion)
|
|
14
|
+
header = "Group: #{name}\n#{"=" * 40}\n"
|
|
15
|
+
body = files.map do |file|
|
|
16
|
+
"#{file.filename} (coverage: #{(_ = file.covered_percent(criterion)).floor(2)}%)\n"
|
|
17
|
+
end.join
|
|
18
|
+
"#{header}#{body}\n"
|
|
22
19
|
end
|
|
23
20
|
end
|
|
24
21
|
end
|
data/lib/simplecov/formatter.rb
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
|
-
# TODO: Documentation on how to build your own formatters
|
|
5
4
|
module Formatter
|
|
5
|
+
# Formatters are configured either as classes, instantiated fresh for every
|
|
6
|
+
# report, or as ready-built instances, the only way to reach constructor
|
|
7
|
+
# options like `HTMLFormatter.new(silent: true)` (#1240).
|
|
8
|
+
def self.instance_for(formatter)
|
|
9
|
+
instantiable?(formatter) ? formatter.new : formatter
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# mutant:disable
|
|
13
|
+
# `Class` cannot itself be subclassed, so `instance_of?(Class)` answers for
|
|
14
|
+
# every object exactly as `is_a?(Class)` does: a difference no example can
|
|
15
|
+
# show, which is why it is disabled here rather than pinned by a test.
|
|
16
|
+
def self.instantiable?(formatter)
|
|
17
|
+
formatter.is_a?(Class)
|
|
18
|
+
end
|
|
19
|
+
private_class_method :instantiable?
|
|
20
|
+
|
|
21
|
+
def self.format(formatter, result)
|
|
22
|
+
instance_for(formatter).format(result)
|
|
23
|
+
end
|
|
6
24
|
end
|
|
7
25
|
end
|
|
8
26
|
|
|
9
27
|
require_relative "formatter/simple_formatter"
|
|
10
28
|
require_relative "formatter/multi_formatter"
|
|
29
|
+
require_relative "formatter/json_formatter"
|
|
30
|
+
require_relative "formatter/baseline_formatter"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module GroupNames
|
|
5
|
+
UNGROUPED = "Ungrouped"
|
|
6
|
+
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
# Group names are Hash keys, report labels, and JSON object keys all at once,
|
|
10
|
+
# so they are normalized to Strings up front: a Symbol spelling means the
|
|
11
|
+
# String, and anything else has no sensible serialized form. Normalizing
|
|
12
|
+
# before `validate!` also keeps `group :Ungrouped` from slipping past the
|
|
13
|
+
# reservation below.
|
|
14
|
+
def normalize(group_name)
|
|
15
|
+
case group_name
|
|
16
|
+
when String then group_name
|
|
17
|
+
when Symbol then group_name.to_s
|
|
18
|
+
else
|
|
19
|
+
raise ConfigurationError,
|
|
20
|
+
"Group names must be Strings, got #{group_name.inspect} (#{group_name.class})"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Each name in turn, rather than asking the collection: a String asked
|
|
25
|
+
# whether it "includes" the reserved name answers about its own characters.
|
|
26
|
+
def validate!(group_names)
|
|
27
|
+
return group_names unless group_names.any? { |name| name.eql?(UNGROUPED) }
|
|
28
|
+
|
|
29
|
+
raise ConfigurationError,
|
|
30
|
+
"#{UNGROUPED.inspect} is reserved for files that do not match a configured group"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|