simplecov 1.0.3 → 1.1.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/README.md +65 -1439
- data/lib/simplecov/atomic_file.rb +70 -0
- data/lib/simplecov/cli/clean.rb +43 -3
- data/lib/simplecov/cli/command_helpers.rb +55 -0
- data/lib/simplecov/cli/coverage.rb +21 -28
- data/lib/simplecov/cli/coverage_file.rb +65 -0
- data/lib/simplecov/cli/diff.rb +43 -48
- data/lib/simplecov/cli/dotfile.rb +12 -6
- data/lib/simplecov/cli/merge.rb +12 -10
- data/lib/simplecov/cli/open.rb +3 -5
- data/lib/simplecov/cli/report.rb +31 -23
- 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 +30 -94
- data/lib/simplecov/cli/uncovered.rb +20 -25
- data/lib/simplecov/cli.rb +15 -2
- data/lib/simplecov/combine/branches_combiner.rb +43 -19
- data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
- data/lib/simplecov/combine/identity_interner.rb +30 -0
- data/lib/simplecov/combine/interned_counts.rb +30 -0
- data/lib/simplecov/combine/lines_combiner.rb +48 -20
- data/lib/simplecov/combine/methods_combiner.rb +36 -12
- data/lib/simplecov/combine/results_combiner.rb +12 -37
- data/lib/simplecov/combine.rb +5 -23
- data/lib/simplecov/command_guesser.rb +66 -9
- data/lib/simplecov/configuration/coverage.rb +12 -15
- data/lib/simplecov/configuration/coverage_criteria.rb +34 -38
- data/lib/simplecov/configuration/eval_coverage.rb +41 -0
- data/lib/simplecov/configuration/filters.rb +11 -44
- data/lib/simplecov/configuration/formatting.rb +6 -3
- data/lib/simplecov/configuration/groups.rb +42 -0
- data/lib/simplecov/configuration/merging.rb +8 -7
- data/lib/simplecov/configuration/thresholds.rb +14 -13
- data/lib/simplecov/configuration.rb +13 -43
- data/lib/simplecov/coverage_json.rb +24 -0
- data/lib/simplecov/coverage_statistics.rb +1 -1
- data/lib/simplecov/coverage_violations.rb +15 -5
- data/lib/simplecov/defaults.rb +7 -3
- data/lib/simplecov/directive.rb +1 -1
- data/lib/simplecov/exit_codes/check.rb +33 -0
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +11 -20
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +3 -16
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +14 -23
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +14 -25
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +4 -17
- data/lib/simplecov/exit_codes.rb +1 -0
- data/lib/simplecov/exit_handling.rb +11 -41
- data/lib/simplecov/file_list.rb +5 -10
- data/lib/simplecov/filter.rb +43 -9
- data/lib/simplecov/formatter/base.rb +11 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +35 -5
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
- data/lib/simplecov/formatter/html_formatter.rb +67 -47
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -56
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +78 -88
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +70 -76
- data/lib/simplecov/formatter/json_formatter.rb +5 -48
- data/lib/simplecov/formatter/multi_formatter.rb +1 -1
- data/lib/simplecov/formatter/simple_formatter.rb +8 -5
- data/lib/simplecov/formatter.rb +5 -0
- data/lib/simplecov/group_names.rb +32 -0
- data/lib/simplecov/last_run.rb +16 -9
- data/lib/simplecov/lines_classifier.rb +29 -8
- data/lib/simplecov/load_global_config.rb +5 -2
- data/lib/simplecov/parallel_adapters/base.rb +17 -0
- data/lib/simplecov/parallel_adapters/generic.rb +2 -2
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
- data/lib/simplecov/parallel_coordination.rb +6 -1
- data/lib/simplecov/parallel_result_merger.rb +230 -0
- data/lib/simplecov/report_deferral.rb +49 -0
- data/lib/simplecov/report_stamp.rb +28 -0
- data/lib/simplecov/result.rb +40 -10
- data/lib/simplecov/result_merger/resultset_file.rb +43 -7
- data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
- data/lib/simplecov/result_merger/resultset_store.rb +15 -12
- data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
- data/lib/simplecov/result_merger.rb +69 -42
- data/lib/simplecov/result_processing.rb +82 -43
- data/lib/simplecov/run_identity.rb +77 -0
- data/lib/simplecov/simulate_coverage.rb +35 -11
- data/lib/simplecov/source_file/method.rb +7 -1
- data/lib/simplecov/source_file/ruby_data_parser.rb +6 -2
- data/lib/simplecov/source_file/skip_chunks.rb +7 -10
- data/lib/simplecov/source_file/source_loader.rb +23 -7
- data/lib/simplecov/source_file/statistics.rb +24 -16
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +203 -13
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +19 -30
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +7 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +6 -14
- data/lib/simplecov/static_coverage_extractor/visitor.rb +23 -36
- data/lib/simplecov/unloaded_file_injector.rb +75 -0
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov.rb +11 -5
- data/schemas/coverage-v1.0.schema.json +2 -2
- data/schemas/coverage.schema.json +2 -2
- data/sig/simplecov.rbs +200 -66
- metadata +28 -14
- data/doc/alternate-formatters.md +0 -66
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -70
- data/lib/simplecov/formatter/html_formatter/public/application.css +0 -1
- data/lib/simplecov/formatter/html_formatter/public/application.js +0 -18
- data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
|
@@ -12,77 +12,75 @@ module SimpleCov
|
|
|
12
12
|
CRITERION_KEYS = {line: :lines, branch: :branches, method: :methods}.freeze
|
|
13
13
|
private_constant :CRITERION_KEYS
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
format_maximum_drop
|
|
26
|
-
@errors
|
|
27
|
-
end
|
|
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
|
|
28
25
|
|
|
29
|
-
|
|
26
|
+
private
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
34
32
|
end
|
|
35
|
-
end
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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?
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
60
58
|
end
|
|
61
|
-
end
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
66
64
|
end
|
|
67
|
-
end
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
73
71
|
end
|
|
74
|
-
end
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
def bucket(errors, name)
|
|
74
|
+
errors[name] ||= {} #: Hash[untyped, untyped]
|
|
75
|
+
end
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
def key_for(violation)
|
|
78
|
+
CRITERION_KEYS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion)))
|
|
79
|
+
end
|
|
83
80
|
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
def expected_actual(violation)
|
|
82
|
+
{expected: violation.fetch(:expected), actual: violation.fetch(:actual)}
|
|
83
|
+
end
|
|
86
84
|
end
|
|
87
85
|
end
|
|
88
86
|
end
|
|
@@ -22,105 +22,95 @@ module SimpleCov
|
|
|
22
22
|
SCHEMA_URL = "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v#{SCHEMA_VERSION}.schema.json".freeze
|
|
23
23
|
private_constant :SCHEMA_VERSION, :SCHEMA_URL
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
:groups => format_groups,
|
|
37
|
-
:errors => ErrorsFormatter.new(@result).call
|
|
38
|
-
}
|
|
39
|
-
end
|
|
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
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
private
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
46
43
|
end
|
|
47
|
-
end
|
|
48
44
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
53
50
|
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
51
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
81
64
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
89
77
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
96
85
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
104
93
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
115
104
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
124
114
|
end
|
|
125
115
|
end
|
|
126
116
|
end
|
|
@@ -7,91 +7,85 @@ module SimpleCov
|
|
|
7
7
|
# in coverage.json: source code plus per-enabled-criterion arrays
|
|
8
8
|
# and totals.
|
|
9
9
|
class SourceFileFormatter
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
result.merge!(branch_coverage_section) if SimpleCov.branch_coverage?
|
|
19
|
-
result.merge!(method_coverage_section) if SimpleCov.method_coverage?
|
|
20
|
-
result
|
|
21
|
-
end
|
|
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
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
private
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{source: @source_file.lines.map { |line| ensure_utf8(line.src.chomp) }}
|
|
34
|
-
end
|
|
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
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
total_lines: covered + missed
|
|
50
|
-
}
|
|
51
|
-
end
|
|
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
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
53
|
+
def method_coverage_section(source_file)
|
|
54
|
+
covered = source_file.covered_methods.size
|
|
55
|
+
missed = source_file.missed_methods.size
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
end
|
|
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
|
|
72
65
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
def format_line(line)
|
|
67
|
+
line.skipped? ? "ignored" : line.coverage
|
|
68
|
+
end
|
|
76
69
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
87
80
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
95
89
|
end
|
|
96
90
|
end
|
|
97
91
|
end
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base"
|
|
4
|
-
|
|
4
|
+
require_relative "coverage_json_writer"
|
|
5
5
|
require "json"
|
|
6
|
-
require "time"
|
|
7
6
|
|
|
8
7
|
module SimpleCov
|
|
9
8
|
module Formatter
|
|
@@ -11,7 +10,7 @@ module SimpleCov
|
|
|
11
10
|
# standalone, alongside the HTML formatter, or by external tools that
|
|
12
11
|
# consume SimpleCov output.
|
|
13
12
|
class JSONFormatter < Base
|
|
14
|
-
FILENAME =
|
|
13
|
+
FILENAME = CoverageJSONWriter::FILENAME
|
|
15
14
|
|
|
16
15
|
# `include_source:` defaults to `SimpleCov.source_in_json` (true
|
|
17
16
|
# by default) so the historical payload shape is unchanged.
|
|
@@ -19,19 +18,12 @@ module SimpleCov
|
|
|
19
18
|
# setting (the HTML formatter, which feeds the client-side
|
|
20
19
|
# viewer) pass `include_source: true` explicitly.
|
|
21
20
|
def self.build_hash(result, include_source: SimpleCov.source_in_json)
|
|
22
|
-
ResultHashFormatter.
|
|
21
|
+
ResultHashFormatter.format(result, include_source: include_source)
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
def format(result)
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
25
|
+
CoverageJSONWriter.write(output_path, self.class.build_hash(result), result)
|
|
26
|
+
emit_status(result)
|
|
35
27
|
end
|
|
36
28
|
|
|
37
29
|
private
|
|
@@ -43,41 +35,6 @@ module SimpleCov
|
|
|
43
35
|
def entry_point_filename
|
|
44
36
|
FILENAME
|
|
45
37
|
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
38
|
end
|
|
82
39
|
end
|
|
83
40
|
end
|
|
@@ -10,7 +10,7 @@ module SimpleCov
|
|
|
10
10
|
module InstanceMethods
|
|
11
11
|
def format(result)
|
|
12
12
|
formatters.map do |formatter|
|
|
13
|
-
Formatter.
|
|
13
|
+
Formatter.format(formatter, result)
|
|
14
14
|
rescue StandardError => e
|
|
15
15
|
warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{(_ = e.backtrace).first})")
|
|
16
16
|
nil
|
|
@@ -8,16 +8,19 @@ 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
|
-
|
|
11
|
+
criterion = SimpleCov.coverage_statistics_key(SimpleCov.primary_coverage)
|
|
12
|
+
result.groups.map { |name, files| format_group(name, files, criterion) }.join
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
private
|
|
15
16
|
|
|
16
|
-
def format_group(name, files)
|
|
17
|
+
def format_group(name, files, criterion)
|
|
17
18
|
header = "Group: #{name}\n#{'=' * 40}\n"
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
body
|
|
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
|
|
21
24
|
"#{header}#{body}\n"
|
|
22
25
|
end
|
|
23
26
|
end
|
data/lib/simplecov/formatter.rb
CHANGED
|
@@ -13,6 +13,11 @@ module SimpleCov
|
|
|
13
13
|
def self.instance_for(formatter)
|
|
14
14
|
formatter.is_a?(Class) ? formatter.new : formatter
|
|
15
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
|
|
16
21
|
end
|
|
17
22
|
end
|
|
18
23
|
|
|
@@ -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
|