simplecov 0.22.0 → 1.0.0.rc1
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/CHANGELOG.md +81 -1
- data/LICENSE +1 -1
- data/README.md +1009 -511
- data/doc/alternate-formatters.md +0 -5
- data/doc/commercial-services.md +5 -5
- data/exe/simplecov +11 -0
- data/lib/minitest/simplecov_plugin.rb +13 -5
- data/lib/simplecov/autostart.rb +11 -0
- data/lib/simplecov/cli/clean.rb +47 -0
- data/lib/simplecov/cli/coverage.rb +91 -0
- data/lib/simplecov/cli/diff.rb +151 -0
- data/lib/simplecov/cli/dotfile.rb +100 -0
- data/lib/simplecov/cli/merge.rb +116 -0
- data/lib/simplecov/cli/open.rb +50 -0
- data/lib/simplecov/cli/report.rb +84 -0
- data/lib/simplecov/cli/run.rb +36 -0
- data/lib/simplecov/cli/serve.rb +139 -0
- data/lib/simplecov/cli/uncovered.rb +107 -0
- data/lib/simplecov/cli.rb +150 -0
- data/lib/simplecov/color.rb +74 -0
- data/lib/simplecov/combine/branches_combiner.rb +3 -2
- data/lib/simplecov/combine/files_combiner.rb +7 -1
- data/lib/simplecov/combine/lines_combiner.rb +19 -17
- data/lib/simplecov/combine/methods_combiner.rb +26 -0
- data/lib/simplecov/combine/results_combiner.rb +5 -4
- data/lib/simplecov/command_guesser.rb +46 -32
- data/lib/simplecov/configuration/coverage.rb +171 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +156 -0
- data/lib/simplecov/configuration/filters.rb +195 -0
- data/lib/simplecov/configuration/formatting.rb +119 -0
- data/lib/simplecov/configuration/ignored_entries.rb +63 -0
- data/lib/simplecov/configuration/merging.rb +74 -0
- data/lib/simplecov/configuration/thresholds.rb +174 -0
- data/lib/simplecov/configuration.rb +79 -405
- data/lib/simplecov/coverage_statistics.rb +12 -9
- data/lib/simplecov/coverage_violations.rb +148 -0
- data/lib/simplecov/defaults.rb +27 -20
- data/lib/simplecov/directive.rb +162 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +19 -57
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -27
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +38 -21
- data/lib/simplecov/exit_codes.rb +3 -0
- data/lib/simplecov/exit_handling.rb +158 -0
- data/lib/simplecov/file_list.rb +61 -17
- data/lib/simplecov/filter.rb +69 -24
- data/lib/simplecov/formatter/base.rb +101 -0
- data/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
- data/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
- data/lib/simplecov/formatter/html_formatter.rb +79 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +84 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +127 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
- data/lib/simplecov/formatter/json_formatter.rb +77 -0
- data/lib/simplecov/formatter/multi_formatter.rb +4 -5
- data/lib/simplecov/formatter/simple_formatter.rb +9 -11
- data/lib/simplecov/formatter.rb +4 -0
- data/lib/simplecov/last_run.rb +10 -3
- data/lib/simplecov/lines_classifier.rb +26 -13
- data/lib/simplecov/load_global_config.rb +9 -4
- data/lib/simplecov/parallel_adapters/base.rb +51 -0
- data/lib/simplecov/parallel_adapters/generic.rb +42 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +77 -0
- data/lib/simplecov/parallel_adapters.rb +83 -0
- data/lib/simplecov/parallel_coordination.rb +95 -0
- data/lib/simplecov/process.rb +20 -14
- data/lib/simplecov/profiles/bundler_filter.rb +1 -1
- data/lib/simplecov/profiles/hidden_filter.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +24 -10
- data/lib/simplecov/profiles/root_filter.rb +6 -5
- data/lib/simplecov/profiles/strict.rb +32 -0
- data/lib/simplecov/profiles/test_frameworks.rb +1 -4
- data/lib/simplecov/profiles.rb +32 -3
- data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
- data/lib/simplecov/result/source_file_builder.rb +51 -0
- data/lib/simplecov/result.rb +97 -19
- data/lib/simplecov/result_adapter.rb +68 -6
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
- data/lib/simplecov/result_merger/resultset_file.rb +38 -0
- data/lib/simplecov/result_merger/resultset_store.rb +50 -0
- data/lib/simplecov/result_merger.rb +46 -90
- data/lib/simplecov/result_processing.rb +162 -0
- data/lib/simplecov/simulate_coverage.rb +54 -8
- data/lib/simplecov/source_file/branch.rb +1 -3
- data/lib/simplecov/source_file/branch_builder.rb +114 -0
- data/lib/simplecov/source_file/builder_context.rb +28 -0
- data/lib/simplecov/source_file/line.rb +7 -2
- data/lib/simplecov/source_file/line_builder.rb +43 -0
- data/lib/simplecov/source_file/method.rb +52 -0
- data/lib/simplecov/source_file/method_builder.rb +58 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
- data/lib/simplecov/source_file/skip_chunks.rb +77 -0
- data/lib/simplecov/source_file/source_loader.rb +63 -0
- data/lib/simplecov/source_file/statistics.rb +57 -0
- data/lib/simplecov/source_file.rb +66 -232
- data/lib/simplecov/static_coverage_extractor/visitor.rb +193 -0
- data/lib/simplecov/static_coverage_extractor.rb +111 -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 +131 -377
- data/lib/simplecov_json_formatter.rb +4 -0
- data/schemas/coverage-v1.0.schema.json +300 -0
- data/schemas/coverage.schema.json +300 -0
- metadata +88 -56
- data/lib/simplecov/default_formatter.rb +0 -20
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module ExitCodes
|
|
5
|
+
# Fails when the overall (project-wide) coverage for any criterion is
|
|
6
|
+
# below the configured minimum.
|
|
5
7
|
class MinimumOverallCoverageCheck
|
|
6
8
|
def initialize(result, minimum_coverage)
|
|
7
9
|
@result = result
|
|
@@ -9,18 +11,11 @@ module SimpleCov
|
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
def failing?
|
|
12
|
-
|
|
14
|
+
violations.any?
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def report
|
|
16
|
-
|
|
17
|
-
$stderr.printf(
|
|
18
|
-
"%<criterion>s coverage (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
|
|
19
|
-
covered: SimpleCov.round_coverage(violation.fetch(:actual)),
|
|
20
|
-
minimum_coverage: violation.fetch(:minimum_expected),
|
|
21
|
-
criterion: violation.fetch(:criterion).capitalize
|
|
22
|
-
)
|
|
23
|
-
end
|
|
18
|
+
violations.each { |violation| report_violation(violation) }
|
|
24
19
|
end
|
|
25
20
|
|
|
26
21
|
def exit_code
|
|
@@ -29,24 +24,46 @@ module SimpleCov
|
|
|
29
24
|
|
|
30
25
|
private
|
|
31
26
|
|
|
32
|
-
|
|
27
|
+
WORST_FILES_LIMIT = 5
|
|
28
|
+
private_constant :WORST_FILES_LIMIT
|
|
29
|
+
|
|
30
|
+
def violations
|
|
31
|
+
@violations ||= SimpleCov::CoverageViolations.minimum_overall(@result, @minimum_coverage)
|
|
32
|
+
end
|
|
33
33
|
|
|
34
|
-
def
|
|
35
|
-
|
|
34
|
+
def report_violation(violation)
|
|
35
|
+
criterion = violation.fetch(:criterion)
|
|
36
|
+
actual = violation.fetch(:actual)
|
|
37
|
+
warn format(
|
|
38
|
+
"%<criterion>s coverage (%<actual>s) is below the expected minimum coverage (%<expected>.2f%%).",
|
|
39
|
+
criterion: criterion.capitalize,
|
|
40
|
+
actual: SimpleCov::Color.colorize_percent(actual),
|
|
41
|
+
expected: violation.fetch(:expected)
|
|
42
|
+
)
|
|
43
|
+
report_worst_files(criterion)
|
|
36
44
|
end
|
|
37
45
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
def report_worst_files(criterion)
|
|
47
|
+
worst = worst_files_for(criterion)
|
|
48
|
+
return if worst.empty?
|
|
49
|
+
|
|
50
|
+
warn " Lowest-coverage files (#{criterion}):"
|
|
51
|
+
worst.each do |path, percent|
|
|
52
|
+
warn format(
|
|
53
|
+
" %<percent>s %<path>s",
|
|
54
|
+
percent: SimpleCov::Color.colorize_percent(percent, format("%6.2f%%", percent)),
|
|
55
|
+
path: path
|
|
56
|
+
)
|
|
45
57
|
end
|
|
58
|
+
end
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
def worst_files_for(criterion)
|
|
61
|
+
stats_key = SimpleCov.coverage_statistics_key(criterion)
|
|
62
|
+
with_stats = @result.files.filter_map do |source_file|
|
|
63
|
+
stats = source_file.coverage_statistics[stats_key]
|
|
64
|
+
[source_file.project_filename, stats.percent] if stats
|
|
49
65
|
end
|
|
66
|
+
with_stats.sort_by { |_path, percent| percent }.first(WORST_FILES_LIMIT)
|
|
50
67
|
end
|
|
51
68
|
end
|
|
52
69
|
end
|
data/lib/simplecov/exit_codes.rb
CHANGED
|
@@ -6,10 +6,13 @@ module SimpleCov
|
|
|
6
6
|
EXCEPTION = 1
|
|
7
7
|
MINIMUM_COVERAGE = 2
|
|
8
8
|
MAXIMUM_COVERAGE_DROP = 3
|
|
9
|
+
MAXIMUM_COVERAGE = 4
|
|
9
10
|
end
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
require_relative "exit_codes/exit_code_handling"
|
|
13
14
|
require_relative "exit_codes/maximum_coverage_drop_check"
|
|
15
|
+
require_relative "exit_codes/maximum_overall_coverage_check"
|
|
14
16
|
require_relative "exit_codes/minimum_coverage_by_file_check"
|
|
17
|
+
require_relative "exit_codes/minimum_coverage_by_group_check"
|
|
15
18
|
require_relative "exit_codes/minimum_overall_coverage_check"
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
|
|
5
|
+
# `at_exit` orchestration: post-suite report generation, threshold
|
|
6
|
+
# checks, deferral when a sibling subprocess already wrote a fresher
|
|
7
|
+
# report, and exit-status propagation.
|
|
8
|
+
module SimpleCov
|
|
9
|
+
class << self
|
|
10
|
+
# @api private
|
|
11
|
+
CoverageLimits = Struct.new(
|
|
12
|
+
:minimum_coverage,
|
|
13
|
+
:minimum_coverage_by_file,
|
|
14
|
+
:minimum_coverage_by_file_overrides,
|
|
15
|
+
:minimum_coverage_by_group,
|
|
16
|
+
:maximum_coverage,
|
|
17
|
+
:maximum_coverage_drop,
|
|
18
|
+
keyword_init: true
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def at_exit_behavior
|
|
22
|
+
# If we are in a different process than called start, don't interfere.
|
|
23
|
+
return if SimpleCov.pid != Process.pid
|
|
24
|
+
|
|
25
|
+
# If Coverage is no longer running (e.g. someone manually stopped it
|
|
26
|
+
# or a test consumed the result) then don't run exit tasks.
|
|
27
|
+
return unless Coverage.running?
|
|
28
|
+
|
|
29
|
+
# Stand down when we'd only clobber a fresher report. See
|
|
30
|
+
# `defer_to_existing_report?` and issue #581.
|
|
31
|
+
return if defer_to_existing_report?
|
|
32
|
+
|
|
33
|
+
SimpleCov.run_exit_tasks!
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns true when our process has no coverage data to contribute
|
|
37
|
+
# (after the resultset merge) and a newer report already exists on
|
|
38
|
+
# disk. Typically fires when `SimpleCov.start` ran in a parent
|
|
39
|
+
# process — e.g. a Rakefile or Rails' `Bundler.require` — that
|
|
40
|
+
# shelled out to the test runner. See issue #581.
|
|
41
|
+
def defer_to_existing_report?
|
|
42
|
+
return false unless existing_report_newer_than_us?
|
|
43
|
+
|
|
44
|
+
res = result
|
|
45
|
+
empty = res.nil? || res.files.empty?
|
|
46
|
+
warn_about_deferred_report if empty
|
|
47
|
+
empty
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def existing_report_newer_than_us?
|
|
51
|
+
return false unless process_start_time
|
|
52
|
+
|
|
53
|
+
last_run_path = File.join(coverage_path, ".last_run.json")
|
|
54
|
+
File.exist?(last_run_path) && File.mtime(last_run_path) > process_start_time
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def warn_about_deferred_report
|
|
58
|
+
return unless print_errors
|
|
59
|
+
|
|
60
|
+
warn SimpleCov::Color.colorize(
|
|
61
|
+
"Skipping SimpleCov report — this process tracked no application code and a newer " \
|
|
62
|
+
"report already exists at #{coverage_path}. This usually means SimpleCov.start ran in a " \
|
|
63
|
+
"parent process (e.g. a Rakefile or Rails' Bundler.require) that shelled out to the test " \
|
|
64
|
+
"runner. See https://github.com/simplecov-ruby/simplecov/issues/581.",
|
|
65
|
+
:yellow
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @api private — called from the at_exit block.
|
|
70
|
+
def run_exit_tasks!
|
|
71
|
+
error_exit_status = exit_status_from_exception
|
|
72
|
+
|
|
73
|
+
at_exit.call
|
|
74
|
+
|
|
75
|
+
exit_and_report_previous_error(error_exit_status) if previous_error?(error_exit_status)
|
|
76
|
+
process_results_and_report_error if ready_to_process_results?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @api private — returns the exit status from the exit exception.
|
|
80
|
+
def exit_status_from_exception
|
|
81
|
+
@exit_exception = $ERROR_INFO
|
|
82
|
+
return nil unless @exit_exception
|
|
83
|
+
|
|
84
|
+
if @exit_exception.is_a?(SystemExit)
|
|
85
|
+
@exit_exception.status
|
|
86
|
+
else
|
|
87
|
+
SimpleCov::ExitCodes::EXCEPTION
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @api private — strict boolean so rspec-mocks 4's predicate matcher
|
|
92
|
+
# accepts it. test_unit sets status 0 on success, so SUCCESS must
|
|
93
|
+
# also be treated as "not a previous error".
|
|
94
|
+
def previous_error?(error_exit_status)
|
|
95
|
+
!!(error_exit_status && error_exit_status != SimpleCov::ExitCodes::SUCCESS)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @api private
|
|
99
|
+
def exit_and_report_previous_error(exit_status)
|
|
100
|
+
if print_errors
|
|
101
|
+
warn SimpleCov::Color.colorize(
|
|
102
|
+
"Stopped processing SimpleCov as a previous error not related to SimpleCov has been detected",
|
|
103
|
+
:yellow
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
Kernel.exit(exit_status)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @api private — the first worker in a parallel run is the only
|
|
110
|
+
# one that reports against thresholds, and only when its
|
|
111
|
+
# `wait_for_other_processes` confirmed every sibling reported.
|
|
112
|
+
# When the wait times out, the merged total is partial and
|
|
113
|
+
# comparing it against `minimum_coverage` / `maximum_coverage`
|
|
114
|
+
# would surface a spurious "below minimum" violation about the
|
|
115
|
+
# missing slice rather than a real shortfall.
|
|
116
|
+
def ready_to_process_results?
|
|
117
|
+
final_result_process? && result? && parallel_results_complete?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def process_results_and_report_error
|
|
121
|
+
exit_status = process_result(result)
|
|
122
|
+
|
|
123
|
+
# Force exit with stored status (see github issue #5)
|
|
124
|
+
return unless exit_status.positive?
|
|
125
|
+
|
|
126
|
+
if print_errors
|
|
127
|
+
warn SimpleCov::Color.colorize(
|
|
128
|
+
"SimpleCov failed with exit #{exit_status} due to a coverage related error", :red
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
Kernel.exit exit_status
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @api private — `exit_status = SimpleCov.process_result(SimpleCov.result)`.
|
|
135
|
+
def process_result(result)
|
|
136
|
+
result_exit_status = result_exit_status(result)
|
|
137
|
+
write_last_run(result) if result_exit_status == SimpleCov::ExitCodes::SUCCESS
|
|
138
|
+
result_exit_status
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def result_exit_status(result)
|
|
142
|
+
ExitCodes::ExitCodeHandling.call(result, coverage_limits: build_coverage_limits)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def build_coverage_limits
|
|
148
|
+
CoverageLimits.new(
|
|
149
|
+
minimum_coverage: minimum_coverage,
|
|
150
|
+
minimum_coverage_by_file: minimum_coverage_by_file,
|
|
151
|
+
minimum_coverage_by_file_overrides: minimum_coverage_by_file_overrides,
|
|
152
|
+
minimum_coverage_by_group: minimum_coverage_by_group,
|
|
153
|
+
maximum_coverage: maximum_coverage,
|
|
154
|
+
maximum_coverage_drop: maximum_coverage_drop
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/simplecov/file_list.rb
CHANGED
|
@@ -23,8 +23,12 @@ module SimpleCov
|
|
|
23
23
|
@files = files
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# The per-criterion coverage statistics across all files. With no argument
|
|
27
|
+
# returns the `{line:, branch:, method:}` Hash; pass a criterion symbol
|
|
28
|
+
# (`:line` / `:branch` / `:method`) to get that one CoverageStatistics.
|
|
29
|
+
def coverage_statistics(criterion = nil)
|
|
27
30
|
@coverage_statistics ||= compute_coverage_statistics
|
|
31
|
+
criterion ? @coverage_statistics[criterion] : @coverage_statistics
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def coverage_statistics_by_file
|
|
@@ -45,14 +49,14 @@ module SimpleCov
|
|
|
45
49
|
def never_lines
|
|
46
50
|
return 0.0 if empty?
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
sum { |f| f.never_lines.size }
|
|
49
53
|
end
|
|
50
54
|
|
|
51
55
|
# Returns the count of skipped lines
|
|
52
56
|
def skipped_lines
|
|
53
57
|
return 0.0 if empty?
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
sum { |f| f.skipped_lines.size }
|
|
56
60
|
end
|
|
57
61
|
|
|
58
62
|
# Computes the coverage based upon lines covered and lines missed for each file
|
|
@@ -71,16 +75,18 @@ module SimpleCov
|
|
|
71
75
|
coverage_statistics[:line]&.total
|
|
72
76
|
end
|
|
73
77
|
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
# The coverage across all files in percent, for the given criterion (line
|
|
79
|
+
# by default). Returns nil if the criterion was not measured.
|
|
80
|
+
# @return [Float, nil]
|
|
81
|
+
def covered_percent(criterion = :line)
|
|
82
|
+
coverage_statistics(criterion)&.percent
|
|
78
83
|
end
|
|
79
84
|
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
# The strength (average hits per relevant unit) for the given criterion
|
|
86
|
+
# (line by default).
|
|
87
|
+
# @return [Float, nil]
|
|
88
|
+
def covered_strength(criterion = :line)
|
|
89
|
+
coverage_statistics(criterion)&.strength
|
|
84
90
|
end
|
|
85
91
|
|
|
86
92
|
# Return total count of branches in all files
|
|
@@ -102,19 +108,57 @@ module SimpleCov
|
|
|
102
108
|
coverage_statistics[:branch]&.percent
|
|
103
109
|
end
|
|
104
110
|
|
|
111
|
+
# Return total count of methods in all files
|
|
112
|
+
def total_methods
|
|
113
|
+
coverage_statistics[:method]&.total
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Return total count of covered methods
|
|
117
|
+
def covered_methods
|
|
118
|
+
coverage_statistics[:method]&.covered
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Return total count of missed methods
|
|
122
|
+
def missed_methods
|
|
123
|
+
coverage_statistics[:method]&.missed
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def method_covered_percent
|
|
127
|
+
coverage_statistics[:method]&.percent
|
|
128
|
+
end
|
|
129
|
+
|
|
105
130
|
private
|
|
106
131
|
|
|
132
|
+
# Seed the result hash with one entry per criterion the user
|
|
133
|
+
# enabled — so an empty FileList (e.g. a group with no files) still
|
|
134
|
+
# yields the right shape — then fold each file's stats into the
|
|
135
|
+
# matching bucket. `SourceFile#coverage_statistics` always reports
|
|
136
|
+
# all three criteria; FileList is the layer that filters to the
|
|
137
|
+
# enabled set so disabled criteria don't surface in totals, JSON,
|
|
138
|
+
# or the HTML report.
|
|
107
139
|
def compute_coverage_statistics_by_file
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
140
|
+
seed = enabled_criteria_for_reporting.to_h { |criterion| [criterion, []] }
|
|
141
|
+
@files.each_with_object(seed) do |file, together|
|
|
142
|
+
file.coverage_statistics.each do |criterion, stats|
|
|
143
|
+
together[criterion] << stats if together.key?(criterion)
|
|
144
|
+
end
|
|
111
145
|
end
|
|
112
146
|
end
|
|
113
147
|
|
|
114
148
|
def compute_coverage_statistics
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
149
|
+
coverage_statistics_by_file.transform_values { |stats| CoverageStatistics.from(stats) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# `:line` (or its `:oneshot_line` synonym) is reported when either
|
|
153
|
+
# criterion is enabled; the JRuby-gated branch/method criteria are
|
|
154
|
+
# reported when they pass their own engine-support check.
|
|
155
|
+
def enabled_criteria_for_reporting
|
|
156
|
+
criteria = []
|
|
157
|
+
criteria << :line if SimpleCov.coverage_criterion_enabled?(:line) ||
|
|
158
|
+
SimpleCov.coverage_criterion_enabled?(:oneshot_line)
|
|
159
|
+
criteria << :branch if SimpleCov.branch_coverage?
|
|
160
|
+
criteria << :method if SimpleCov.method_coverage?
|
|
161
|
+
criteria
|
|
118
162
|
end
|
|
119
163
|
end
|
|
120
164
|
end
|
data/lib/simplecov/filter.rb
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
#
|
|
5
5
|
# Base filter class. Inherit from this to create custom filters,
|
|
6
|
-
# and overwrite the
|
|
6
|
+
# and overwrite the matches?(source_file) instance method
|
|
7
7
|
#
|
|
8
8
|
# # A sample class that rejects all source files.
|
|
9
9
|
# class StupidFilter < SimpleCov::Filter
|
|
10
|
-
# def
|
|
10
|
+
# def matches?(source_file)
|
|
11
11
|
# false
|
|
12
12
|
# end
|
|
13
13
|
# end
|
|
@@ -20,12 +20,7 @@ module SimpleCov
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def matches?(_source_file)
|
|
23
|
-
raise "The base filter class is not intended for direct use"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def passes?(source_file)
|
|
27
|
-
warn "#{Kernel.caller.first}: [DEPRECATION] #passes? is deprecated. Use #matches? instead."
|
|
28
|
-
matches?(source_file)
|
|
23
|
+
raise NotImplementedError, "The base filter class is not intended for direct use"
|
|
29
24
|
end
|
|
30
25
|
|
|
31
26
|
def self.build_filter(filter_argument)
|
|
@@ -35,37 +30,75 @@ module SimpleCov
|
|
|
35
30
|
end
|
|
36
31
|
|
|
37
32
|
def self.class_for_argument(filter_argument)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
SimpleCov::StringFilter
|
|
41
|
-
when Regexp
|
|
42
|
-
SimpleCov::RegexFilter
|
|
43
|
-
when Array
|
|
44
|
-
SimpleCov::ArrayFilter
|
|
45
|
-
when Proc
|
|
46
|
-
SimpleCov::BlockFilter
|
|
47
|
-
else
|
|
48
|
-
raise ArgumentError, "You have provided an unrecognized filter type"
|
|
49
|
-
end
|
|
33
|
+
filter_classes_by_argument_type.find { |type, _| filter_argument.is_a?(type) }&.last ||
|
|
34
|
+
raise(SimpleCov::ConfigurationError, "You have provided an unrecognized filter type")
|
|
50
35
|
end
|
|
36
|
+
|
|
37
|
+
def self.filter_classes_by_argument_type
|
|
38
|
+
@filter_classes_by_argument_type ||= {
|
|
39
|
+
String => SimpleCov::StringFilter,
|
|
40
|
+
Regexp => SimpleCov::RegexFilter,
|
|
41
|
+
Array => SimpleCov::ArrayFilter,
|
|
42
|
+
Proc => SimpleCov::BlockFilter
|
|
43
|
+
}.freeze
|
|
44
|
+
end
|
|
45
|
+
private_class_method :filter_classes_by_argument_type
|
|
51
46
|
end
|
|
52
47
|
|
|
48
|
+
# Filter that matches when the source file's project path contains the
|
|
49
|
+
# configured string at a path-segment boundary.
|
|
53
50
|
class StringFilter < SimpleCov::Filter
|
|
54
51
|
# Returns true when the given source file's filename matches the
|
|
55
|
-
# string configured when initializing this Filter with StringFilter.new('somestring')
|
|
52
|
+
# string configured when initializing this Filter with StringFilter.new('somestring').
|
|
53
|
+
# Matching is path-segment-aware: the argument must appear immediately after a "/"
|
|
54
|
+
# and be followed by "/" or end-of-string, so "lib" matches "/lib/foo.rb" but not
|
|
55
|
+
# "/app/models/library.rb".
|
|
56
56
|
def matches?(source_file)
|
|
57
|
-
source_file.project_filename.
|
|
57
|
+
source_file.project_filename.match?(segment_pattern)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def segment_pattern
|
|
63
|
+
@segment_pattern ||= compute_segment_pattern
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def compute_segment_pattern
|
|
67
|
+
normalized = filter_argument.delete_prefix("/")
|
|
68
|
+
escaped = Regexp.escape(normalized)
|
|
69
|
+
boundary = '(?:\A|/)'
|
|
70
|
+
|
|
71
|
+
if normalized.include?(".")
|
|
72
|
+
# Filename pattern (e.g. "test.rb" matches "faked_test.rb"): allow
|
|
73
|
+
# substring match within the last path segment, anchored to a
|
|
74
|
+
# segment boundary.
|
|
75
|
+
%r{#{boundary}[^/]*#{escaped}}
|
|
76
|
+
elsif normalized.end_with?("/")
|
|
77
|
+
# Trailing slash signals directory-only matching.
|
|
78
|
+
/#{boundary}#{escaped}/
|
|
79
|
+
else
|
|
80
|
+
# Directory or path: require a segment-boundary match so "lib"
|
|
81
|
+
# matches "lib/" but not "library/".
|
|
82
|
+
%r{#{boundary}#{escaped}(?=[/.]|\z)}
|
|
83
|
+
end
|
|
58
84
|
end
|
|
59
85
|
end
|
|
60
86
|
|
|
87
|
+
# Filter that matches when the source file's project path matches the
|
|
88
|
+
# configured Regexp.
|
|
61
89
|
class RegexFilter < SimpleCov::Filter
|
|
62
90
|
# Returns true when the given source file's filename matches the
|
|
63
|
-
# regex configured when initializing this Filter with RegexFilter.new(/someregex/)
|
|
91
|
+
# regex configured when initializing this Filter with RegexFilter.new(/someregex/).
|
|
92
|
+
# Uses `Regexp#match?` so the predicate returns a real boolean — `=~`
|
|
93
|
+
# would return the match position (an Integer or nil), which trips
|
|
94
|
+
# rspec-mocks 4's stricter predicate-matcher type check.
|
|
64
95
|
def matches?(source_file)
|
|
65
|
-
(source_file.project_filename
|
|
96
|
+
filter_argument.match?(source_file.project_filename)
|
|
66
97
|
end
|
|
67
98
|
end
|
|
68
99
|
|
|
100
|
+
# Filter that matches when the configured block returns truthy for the
|
|
101
|
+
# source file.
|
|
69
102
|
class BlockFilter < SimpleCov::Filter
|
|
70
103
|
# Returns true if the block given when initializing this filter with BlockFilter.new {|src_file| ... }
|
|
71
104
|
# returns true for the given source file.
|
|
@@ -74,6 +107,18 @@ module SimpleCov
|
|
|
74
107
|
end
|
|
75
108
|
end
|
|
76
109
|
|
|
110
|
+
# Filter that matches when the source file's project path matches the
|
|
111
|
+
# configured shell glob (e.g. "lib/**/*.rb"). Used by `cover` and
|
|
112
|
+
# `skip` when callers want glob semantics instead of the substring
|
|
113
|
+
# match of `StringFilter`.
|
|
114
|
+
class GlobFilter < SimpleCov::Filter
|
|
115
|
+
def matches?(source_file)
|
|
116
|
+
File.fnmatch?(filter_argument, source_file.project_filename, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Filter that matches when any of its component filters (built from the
|
|
121
|
+
# array's elements) match the source file.
|
|
77
122
|
class ArrayFilter < SimpleCov::Filter
|
|
78
123
|
def initialize(filter_argument)
|
|
79
124
|
filter_objects = filter_argument.map do |arg|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module Formatter
|
|
7
|
+
# @api private
|
|
8
|
+
#
|
|
9
|
+
# Shared scaffolding for formatters that write a coverage report to
|
|
10
|
+
# an output directory and emit a "Coverage report generated for X
|
|
11
|
+
# to Y" summary on stderr (it's a status message, not data).
|
|
12
|
+
# Subclasses override `format` to do their actual writing, and may
|
|
13
|
+
# override `message_prefix` (e.g. JSON prepends "JSON ").
|
|
14
|
+
class Base
|
|
15
|
+
# `output_dir` defaults to `SimpleCov.coverage_path` so the at_exit
|
|
16
|
+
# pipeline keeps working unchanged. Pass it explicitly to write
|
|
17
|
+
# somewhere else (handy for tests that don't want to clobber the
|
|
18
|
+
# project's `coverage/` directory).
|
|
19
|
+
def initialize(silent: false, output_dir: nil)
|
|
20
|
+
@silent = silent
|
|
21
|
+
@output_dir = output_dir
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Subclasses override to prepend a marker (e.g. "JSON ") to the
|
|
27
|
+
# summary line. Default empty for the HTML formatter, which has
|
|
28
|
+
# historically been the unmarked default.
|
|
29
|
+
def message_prefix
|
|
30
|
+
""
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def output_path
|
|
34
|
+
@output_dir || SimpleCov.coverage_path
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The path shown in the "Coverage report generated for X to Y"
|
|
38
|
+
# status line. Renders relative to cwd when `output_path` lives
|
|
39
|
+
# inside cwd (e.g. `coverage` instead of `/Users/me/proj/coverage`)
|
|
40
|
+
# and appends the formatter's `entry_point_filename` so the line
|
|
41
|
+
# points at a concrete file the user (or a terminal that
|
|
42
|
+
# hyperlinks paths) can act on — e.g. `coverage/index.html`
|
|
43
|
+
# instead of the bare directory `coverage`. Paths outside cwd
|
|
44
|
+
# stay absolute; a `../../../tmp/cov` display would be more
|
|
45
|
+
# confusing than the absolute form. See issue #197.
|
|
46
|
+
def displayable_output_path
|
|
47
|
+
directory = relative_or_absolute_output_path
|
|
48
|
+
entry_point_filename ? File.join(directory, entry_point_filename) : directory
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def relative_or_absolute_output_path
|
|
52
|
+
absolute = output_path
|
|
53
|
+
relative = Pathname.new(absolute).relative_path_from(Pathname.pwd).to_s
|
|
54
|
+
relative.start_with?("..") ? absolute : relative
|
|
55
|
+
rescue ArgumentError
|
|
56
|
+
# Pathname#relative_path_from raises across mixed absolute/
|
|
57
|
+
# relative inputs (and across Windows drives) — keep the
|
|
58
|
+
# absolute form on any unresolvable case.
|
|
59
|
+
output_path
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Subclasses override to name the report's entry-point file
|
|
63
|
+
# (e.g. `index.html` for HTML, `coverage.json` for JSON), which
|
|
64
|
+
# gets appended to the directory in the status line. Default nil
|
|
65
|
+
# leaves the bare directory in place for any third-party formatter
|
|
66
|
+
# that has no single canonical entry point.
|
|
67
|
+
def entry_point_filename
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Emit one summary line per criterion that the run actually
|
|
72
|
+
# measured. The header line ("Coverage report generated for X
|
|
73
|
+
# to Y") is always first; per-criterion lines follow in the
|
|
74
|
+
# order of `result.coverage_statistics` (which is the same
|
|
75
|
+
# insertion order as `SourceFile#coverage_statistics`, which in
|
|
76
|
+
# turn reflects what the user enabled).
|
|
77
|
+
def output_message(result)
|
|
78
|
+
header = "#{message_prefix}Coverage report generated for #{result.command_name} to #{displayable_output_path}"
|
|
79
|
+
body = result.coverage_statistics.filter_map { |criterion, stat| stats_line(criterion, stat) }
|
|
80
|
+
[header, *body].join("\n")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns nil for branch/method criteria that have nothing to
|
|
84
|
+
# measure (e.g. a file with no branches under branch coverage).
|
|
85
|
+
# Showing "Branch coverage: 0 / 0 (100.00%)" is noise; the older
|
|
86
|
+
# output specifically suppressed it.
|
|
87
|
+
def stats_line(criterion, stat)
|
|
88
|
+
return if criterion != :line && !stat.total.positive?
|
|
89
|
+
|
|
90
|
+
percent = SimpleCov.round_coverage(stat.percent)
|
|
91
|
+
Kernel.format(
|
|
92
|
+
"%<label>s coverage: %<covered>d / %<total>d (%<percent>s)",
|
|
93
|
+
label: criterion.to_s.capitalize,
|
|
94
|
+
covered: stat.covered,
|
|
95
|
+
total: stat.total,
|
|
96
|
+
percent: SimpleCov::Color.colorize_percent(percent)
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,*:before,*:after{box-sizing:border-box}*{margin:0;padding:0}html{-moz-text-size-adjust:none;-webkit-text-size-adjust:none;text-size-adjust:none}body{min-height:100vh;line-height:1.5;-webkit-font-smoothing:antialiased}img,picture,svg{display:block;max-width:100%}input,button,textarea,select{font:inherit}h1,h2,h3,h4,h5,h6{overflow-wrap:break-word;text-wrap:balance}p{overflow-wrap:break-word;text-wrap:pretty}table{border-collapse:collapse;border-spacing:0}ul,ol{list-style:none}a{text-decoration-skip-ink:auto;color:currentColor}.hide{display:none}.hljs{color:#24292e}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#005cc5}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-comment,.hljs-code,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}@media(prefers-color-scheme:dark){:root:not(.light-mode) .hljs{color:#c9d1d9}:root:not(.light-mode) .hljs-doctag,:root:not(.light-mode) .hljs-keyword,:root:not(.light-mode) .hljs-meta .hljs-keyword,:root:not(.light-mode) .hljs-template-tag,:root:not(.light-mode) .hljs-template-variable,:root:not(.light-mode) .hljs-type,:root:not(.light-mode) .hljs-variable.language_{color:#ff7b72}:root:not(.light-mode) .hljs-title,:root:not(.light-mode) .hljs-title.class_,:root:not(.light-mode) .hljs-title.class_.inherited__,:root:not(.light-mode) .hljs-title.function_{color:#d2a8ff}:root:not(.light-mode) .hljs-attr,:root:not(.light-mode) .hljs-attribute,:root:not(.light-mode) .hljs-literal,:root:not(.light-mode) .hljs-meta,:root:not(.light-mode) .hljs-number,:root:not(.light-mode) .hljs-operator,:root:not(.light-mode) .hljs-variable,:root:not(.light-mode) .hljs-selector-attr,:root:not(.light-mode) .hljs-selector-class,:root:not(.light-mode) .hljs-selector-id{color:#79c0ff}:root:not(.light-mode) .hljs-regexp,:root:not(.light-mode) .hljs-string,:root:not(.light-mode) .hljs-meta .hljs-string{color:#a5d6ff}:root:not(.light-mode) .hljs-built_in,:root:not(.light-mode) .hljs-symbol{color:#ffa657}:root:not(.light-mode) .hljs-comment,:root:not(.light-mode) .hljs-code,:root:not(.light-mode) .hljs-formula{color:#8b949e}:root:not(.light-mode) .hljs-name,:root:not(.light-mode) .hljs-quote,:root:not(.light-mode) .hljs-selector-tag,:root:not(.light-mode) .hljs-selector-pseudo{color:#7ee787}:root:not(.light-mode) .hljs-subst{color:#c9d1d9}:root:not(.light-mode) .hljs-section{color:#1f6feb}}.dark-mode .hljs{color:#c9d1d9}.dark-mode .hljs-doctag,.dark-mode .hljs-keyword,.dark-mode .hljs-meta .hljs-keyword,.dark-mode .hljs-template-tag,.dark-mode .hljs-template-variable,.dark-mode .hljs-type,.dark-mode .hljs-variable.language_{color:#ff7b72}.dark-mode .hljs-title,.dark-mode .hljs-title.class_,.dark-mode .hljs-title.class_.inherited__,.dark-mode .hljs-title.function_{color:#d2a8ff}.dark-mode .hljs-attr,.dark-mode .hljs-attribute,.dark-mode .hljs-literal,.dark-mode .hljs-meta,.dark-mode .hljs-number,.dark-mode .hljs-operator,.dark-mode .hljs-variable,.dark-mode .hljs-selector-attr,.dark-mode .hljs-selector-class,.dark-mode .hljs-selector-id{color:#79c0ff}.dark-mode .hljs-regexp,.dark-mode .hljs-string,.dark-mode .hljs-meta .hljs-string{color:#a5d6ff}.dark-mode .hljs-built_in,.dark-mode .hljs-symbol{color:#ffa657}.dark-mode .hljs-comment,.dark-mode .hljs-code,.dark-mode .hljs-formula{color:#8b949e}.dark-mode .hljs-name,.dark-mode .hljs-quote,.dark-mode .hljs-selector-tag,.dark-mode .hljs-selector-pseudo{color:#7ee787}.dark-mode .hljs-subst{color:#c9d1d9}.dark-mode .hljs-section{color:#1f6feb}:root{--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--font-mono: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace;--sp-1: 4px;--sp-2: 8px;--sp-3: 12px;--sp-4: 16px;--sp-5: 20px;--sp-6: 24px;--sp-8: 32px;--radius-sm: 8px;--radius-md: 12px;--radius-lg: 16px;--bg: #f0f1f3;--surface: #fff;--surface-alt: #f0f1f3;--text: #111;--text-secondary: #333;--text-tertiary: #444;--zebra: #f4f5f7;--border: #c0c5cc;--border-strong: #999;--accent: #0550ae;--accent-hover: #033d8b;--accent-subtle: #ddf4ff;--green: #116329;--red: #a40e26;--yellow: #7a5200;--orange: #953800;--covered-bg: #ccf5d0;--covered-line-num-bg: #9ae6a4;--missed-bg: #ffd8d5;--missed-line-num-bg: #ffb8b3;--never-bg: #fff;--never-line-num-bg: #f0f1f3;--skipped-bg: #fff0a0;--skipped-line-num-bg: #eed860;--missed-branch-bg: #ffd0a0;--missed-branch-line-num-bg: #ffb060;--missed-branch-text: #b45309;--missed-method-bg: #e8d0ff;--missed-method-line-num-bg: #d4b0ff;--missed-method-text: #7b2d8e;--source-bg: #fff;--source-border: #c0c5cc;--source-line-number: #444;--source-line-num-bg: #f0f1f3;--hits-bg: #e0e3e8;--hits-color: #333;--overlay-bg: rgba(0, 0, 0, .5);--bar-bg: #d0d7de;--bar-height: 6px}@media(prefers-color-scheme:dark){:root:not(.light-mode){--bg: #010409;--surface: #0d1117;--surface-alt: #161b22;--text: #f0f3f6;--text-secondary: #b0b8c4;--text-tertiary: #9aa5b1;--zebra: #161b22;--border: #3d444d;--border-strong: #555e68;--accent: #6cb6ff;--accent-hover: #96ccff;--accent-subtle: #121d2f;--green: #56d364;--red: #ff6b61;--yellow: #e3b341;--orange: #f0883e;--covered-bg: #122d1e;--covered-line-num-bg: #1e4430;--missed-bg: #351418;--missed-line-num-bg: #4e1d20;--never-bg: #0d1117;--never-line-num-bg: #161b22;--skipped-bg: #302818;--skipped-line-num-bg: #443920;--missed-branch-bg: #322218;--missed-branch-line-num-bg: #483020;--missed-branch-text: #ffb86c;--missed-method-bg: #1e1830;--missed-method-line-num-bg: #2a2044;--missed-method-text: #dcb8ff;--source-bg: #0d1117;--source-border: #3d444d;--source-line-number: #9aa5b1;--source-line-num-bg: #161b22;--hits-bg: #262c34;--hits-color: #b0b8c4;--overlay-bg: rgba(1, 4, 9, .8);--bar-bg: #3d444d}}.dark-mode{--bg: #010409;--surface: #0d1117;--surface-alt: #161b22;--text: #f0f3f6;--text-secondary: #b0b8c4;--text-tertiary: #9aa5b1;--zebra: #161b22;--border: #3d444d;--border-strong: #555e68;--accent: #6cb6ff;--accent-hover: #96ccff;--accent-subtle: #121d2f;--green: #56d364;--red: #ff6b61;--yellow: #e3b341;--orange: #f0883e;--covered-bg: #122d1e;--covered-line-num-bg: #1e4430;--missed-bg: #351418;--missed-line-num-bg: #4e1d20;--never-bg: #0d1117;--never-line-num-bg: #161b22;--skipped-bg: #302818;--skipped-line-num-bg: #443920;--missed-branch-bg: #322218;--missed-branch-line-num-bg: #483020;--missed-branch-text: #ffb86c;--missed-method-bg: #1e1830;--missed-method-line-num-bg: #2a2044;--missed-method-text: #dcb8ff;--source-bg: #0d1117;--source-border: #3d444d;--source-line-number: #9aa5b1;--source-line-num-bg: #161b22;--hits-bg: #262c34;--hits-color: #b0b8c4;--overlay-bg: rgba(1, 4, 9, .8);--bar-bg: #3d444d}body{font-family:var(--font-sans);font-size:18px;color:var(--text);background:var(--bg);padding:var(--sp-6)}a{color:var(--accent);text-decoration:none;transition:color .15s}a:hover{color:var(--accent-hover)}strong,b{font-weight:600}#loading{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:var(--bg);z-index:9999}#loading-inner{width:280px;text-align:center}#loading-bar-track{width:100%;height:6px;background:var(--bar-bg);border-radius:6px;overflow:hidden}#loading-bar-fill{width:0%;height:100%;background:var(--accent);border-radius:6px;transition:width .15s ease-out}#loading-text{margin-top:var(--sp-3);font-size:18px;color:var(--text-tertiary)}#wrapper{margin:0 auto}.tab-bar{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--sp-4);margin-bottom:-1px;position:relative;z-index:1}abbr.timeago{text-decoration:none;border:none}.group_tabs{display:flex;align-self:flex-end;gap:var(--sp-1);overflow-x:auto}.group_tabs li a{display:block;padding:var(--sp-2) var(--sp-4);font-size:18px;font-weight:500;color:var(--text-secondary);background:transparent;border:1px solid transparent;border-bottom:none;border-radius:var(--radius-md) var(--radius-md) 0 0;white-space:nowrap;transition:color .15s,background .15s}.group_tabs li a:hover{color:var(--text);background:var(--surface-alt);text-decoration:none}.group_tabs li.active a{color:var(--accent);background:var(--surface);border-color:var(--border);font-weight:600}#content{background:var(--surface);border:1px solid var(--border);border-radius:0 var(--radius-lg) var(--radius-lg) var(--radius-lg);padding:var(--sp-6)}.file_list_container h2{font-size:24px;font-weight:600;color:var(--text);margin:0}.file_list_container h2 .covered_percent{font-weight:600}.summary-stats{display:flex;flex-direction:column;gap:var(--sp-1);font-size:18px;color:var(--text-secondary)}.summary-stats b{color:var(--text)}.summary-stats .missed-branch-text b,.summary-stats .missed-method-text-color b,.summary-stats .green b,.summary-stats .red b{color:inherit}.summary-stats .green{color:var(--green)}.summary-stats .red{color:var(--red)}.coverage-disabled{color:var(--text-tertiary);font-style:italic}.th-with-filter{display:flex;align-items:center;gap:var(--sp-2)}table.file_list th.cell--coverage .th-with-filter{white-space:nowrap;justify-content:flex-end}.th-with-filter .th-label{white-space:nowrap}.col-filter--name{width:100%;min-width:200px;border:1px solid var(--border);border-radius:999px;padding:var(--sp-1) var(--sp-4);font-size:14px;background:var(--surface);color:var(--text);outline:none}.col-filter--name:focus{border-color:var(--accent)}.col-filter__coverage{display:flex;gap:var(--sp-1)}.col-filter__op{border:1px solid var(--border);border-radius:var(--radius-sm);padding:var(--sp-1) var(--sp-1);font-size:14px;background:var(--surface);color:var(--text);cursor:pointer}.col-filter__value{border:1px solid var(--border);border-radius:var(--radius-sm);padding:var(--sp-1) var(--sp-2);font-size:14px;background:var(--surface);color:var(--text);width:60px;outline:none}.col-filter__value:focus{border-color:var(--accent)}.file_list--responsive{overflow-x:auto}table.file_list{width:100%;font-size:18px}table.file_list{border-collapse:separate;border-spacing:0}table.file_list thead th{font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--text-tertiary);background:var(--surface);padding:var(--sp-2) var(--sp-2);border-bottom:2px solid var(--border-strong);white-space:nowrap}table.file_list tbody tr{background:var(--surface);cursor:pointer}table.file_list tbody tr:nth-child(2n){background:var(--zebra)}table.file_list tbody tr:hover{background:var(--accent-subtle)}table.file_list tbody tr.keyboard-focus{background:var(--accent-subtle);outline:2px solid var(--accent);outline-offset:-2px}table.file_list tbody td{padding:var(--sp-2) var(--sp-2);border-bottom:1px solid var(--border);color:var(--text)}table.file_list td.cell--number{text-align:right;font-variant-numeric:tabular-nums;color:var(--text)}table.file_list th.cell--left,table.file_list th.cell--coverage{text-align:left}table.file_list th.cell--number{text-align:right}table.file_list th.cell--numerator{text-align:right;padding-right:0}table.file_list th.cell--denominator{text-align:left;padding-left:0}table.file_list td.strong{font-weight:600;color:var(--text)}table.file_list td.t-file__name{white-space:nowrap}a.src_link{color:var(--accent);font-weight:500;word-break:break-all}table.file_list td.cell--coverage{white-space:nowrap}.coverage-cell{display:flex;flex-wrap:nowrap;align-items:center;justify-content:flex-end;gap:10px}.coverage-cell .coverage-pct{flex:0 0 4.5em;font-variant-numeric:tabular-nums}.coverage-cell .bar-sizer{flex:0 0 auto;width:240px;min-width:160px;max-width:240px}table.file_list td.cell--numerator{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap;padding-left:var(--sp-1);padding-right:0}table.file_list td.cell--denominator{text-align:left;font-variant-numeric:tabular-nums;white-space:nowrap;padding-left:0;padding-right:var(--sp-1)}table.file_list .totals-row td.cell--numerator{color:var(--text);padding-right:0}table.file_list .totals-row td.cell--denominator{color:var(--text);padding-left:0}.coverage-cell__fraction{font-variant-numeric:tabular-nums;color:var(--text-secondary);font-size:14px;white-space:nowrap}.coverage-bar{width:100%;height:var(--bar-height);background:var(--bar-bg);border-radius:6px;overflow:hidden}.coverage-bar__fill{height:100%;border-radius:6px}.coverage-bar__fill--green{background:var(--green)}.coverage-bar__fill--yellow{background:var(--yellow)}.coverage-bar__fill--red{background:var(--red)}.green,table.file_list td.green{color:var(--green)}.red,table.file_list td.red{color:var(--red)}.yellow,table.file_list td.yellow{color:var(--yellow)}.missed-branch-text{color:var(--missed-branch-text)}.missed-method-text-color{color:var(--missed-method-text)}dialog.source-dialog{position:fixed;inset:0;width:100%;height:100%;max-width:100%;max-height:100%;border:none;padding:0;background:var(--bg);color:var(--text);overflow:hidden}dialog.source-dialog::backdrop{background:var(--overlay-bg)}dialog.source-dialog[open]{display:flex;flex-direction:column}.source-dialog__header{display:flex;align-items:flex-start;justify-content:space-between;padding:var(--sp-4) var(--sp-6);background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0}.source-dialog__title{flex:1;min-width:0}.source-dialog__title h2{font-size:22px;font-weight:700;color:var(--text);margin-bottom:var(--sp-2);word-break:break-all}.source-legend{display:flex;flex-wrap:wrap;gap:var(--sp-2) var(--sp-4);align-items:center;align-self:flex-end;flex-shrink:0;margin-left:auto;padding-left:var(--sp-6)}.source-legend__item{display:flex;align-items:center;gap:var(--sp-1);font-size:13px;color:var(--text-secondary);white-space:nowrap}.source-legend__swatch{display:inline-block;width:14px;height:14px;border-radius:3px;border:1px solid var(--border)}.source-legend__swatch--covered{background:var(--covered-bg);border-color:var(--covered-line-num-bg)}.source-legend__swatch--missed{background:var(--missed-bg);border-color:var(--missed-line-num-bg)}.source-legend__swatch--skipped{background:var(--skipped-bg);border-color:var(--skipped-line-num-bg)}.source-legend__swatch--missed-branch{background:var(--missed-branch-bg);border-color:var(--missed-branch-line-num-bg)}.source-legend__swatch--missed-method{background:var(--missed-method-bg);border-color:var(--missed-method-line-num-bg)}.source-dialog__close{appearance:none;background:none;border:1px solid var(--border);border-radius:50%;width:34px;height:34px;font-size:0;color:var(--text-secondary);cursor:pointer;position:relative;flex-shrink:0;margin-left:var(--sp-4);transition:color .15s,border-color .15s}.source-dialog__close:before,.source-dialog__close:after{content:"";position:absolute;top:50%;left:50%;width:14px;height:2px;background:currentColor;border-radius:1px}.source-dialog__close:before{transform:translate(-50%,-50%) rotate(45deg)}.source-dialog__close:after{transform:translate(-50%,-50%) rotate(-45deg)}.source-dialog__close:hover{color:var(--text);border-color:var(--border-strong)}.source-dialog__body{flex:1;overflow:auto}.source_table .header{padding:var(--sp-4) var(--sp-6);background:var(--surface)}.source_table .header h2{font-size:22px;font-weight:700;color:var(--text);margin-bottom:var(--sp-2)}table.file_list .totals-row td{padding:var(--sp-2) var(--sp-2);font-weight:600;border-bottom:2px solid var(--border-strong);background:var(--surface-alt)}.totals-row .t-file-count{font-size:18px;font-weight:700;color:var(--text)}.t-missed-method-toggle{color:var(--missed-method-text);font-weight:600;cursor:pointer;text-decoration:none}.t-missed-method-toggle:hover{text-decoration:underline;color:var(--missed-method-text)}.t-missed-method-list ul{padding-left:2em;margin-top:var(--sp-1);max-height:200px;overflow-y:auto}.t-missed-method-list li{list-style:none}.source_table pre{margin:0;padding:0;white-space:normal;color:var(--text);font-family:var(--font-mono);font-size:16px;line-height:24px;background:var(--source-bg);border:1px solid var(--source-border);border-top:none}.source_table code{color:inherit;font-family:var(--font-mono)}.source_table pre ol{margin:0;padding:0;list-style:none;counter-reset:linenumber}.source_table pre li{display:flex;counter-increment:linenumber;background:var(--never-bg)}.source_table pre li:before{content:counter(linenumber);flex-shrink:0;width:50px;padding:0 8px;text-align:right;color:var(--source-line-number);background:var(--source-line-num-bg);border-right:1px solid var(--source-border);-webkit-user-select:none;user-select:none;cursor:pointer}.source_table pre li:hover:before{color:var(--text)}.source_table pre li:hover{cursor:pointer}.source_table pre li code{order:1;flex:1;min-width:0;padding:0 12px;white-space:pre-wrap}.source_table pre .hits{order:2;flex-shrink:0;padding:0 var(--sp-2);background:var(--hits-bg);color:var(--hits-color);font-family:var(--font-mono);font-size:14px;text-align:center;line-height:24px;border-left:1px solid var(--source-border);-webkit-user-select:none;user-select:none}.source_table pre .hits:after{content:attr(data-content)}.source_table .covered{background-color:var(--covered-bg)}.source_table .missed{background-color:var(--missed-bg)}.source_table .never{background-color:var(--never-bg)}.source_table .skipped{background-color:var(--skipped-bg)}.source_table .missed-branch{background-color:var(--missed-branch-bg)}.source_table .missed-method{background-color:var(--missed-method-bg)}.source_table .covered:before{background-color:var(--covered-line-num-bg)}.source_table .missed:before{background-color:var(--missed-line-num-bg)}.source_table .never:before{background-color:var(--never-line-num-bg)}.source_table .skipped:before{background-color:var(--skipped-line-num-bg)}.source_table .missed-branch:before{background-color:var(--missed-branch-line-num-bg)}.source_table .missed-method:before{background-color:var(--missed-method-line-num-bg)}#dark-mode-toggle{appearance:none;background:var(--surface);color:var(--text-secondary);border:1px solid var(--border);border-radius:999px;padding:var(--sp-1) var(--sp-4);font-size:16px;font-family:var(--font-sans);cursor:pointer;white-space:nowrap;transition:color .15s,border-color .15s}#dark-mode-toggle:hover{color:var(--text);border-color:var(--border-strong)}#footer{color:var(--text-tertiary);font-size:16px;margin-top:var(--sp-5);text-align:center}#footer a{color:var(--text-secondary);text-decoration:underline}#footer a:hover{color:var(--text)}table.file_list thead th.sorting,table.file_list thead th.sorting_asc,table.file_list thead th.sorting_desc{cursor:pointer;position:relative;padding-right:12px}table.file_list thead th.sorting:after,table.file_list thead th.sorting_asc:after,table.file_list thead th.sorting_desc:after{position:absolute;right:2px;top:50%;transform:translateY(-50%);font-size:14px;color:var(--text-tertiary)}table.file_list thead th.sorting:after{content:"\2195"}table.file_list thead th.sorting_asc:after{content:"\2191"}table.file_list thead th.sorting_desc:after{content:"\2193"}@media print{:root,.dark-mode{--bg: #fff;--surface: #fff;--surface-alt: #f4f5f7;--text: #111;--text-secondary: #333;--text-tertiary: #444;--zebra: #f4f5f7;--border: #c0c5cc;--border-strong: #999;--accent: #0550ae;--green: #116329;--red: #a40e26;--yellow: #7a5200;--orange: #953800;--bar-bg: #d0d7de;--covered-bg: #ccf5d0;--covered-line-num-bg: #9ae6a4;--missed-bg: #ffd8d5;--missed-line-num-bg: #ffb8b3;--never-bg: #fff;--never-line-num-bg: #f0f1f3;--skipped-bg: #fff0a0;--skipped-line-num-bg: #eed860;--missed-branch-bg: #ffd0a0;--missed-branch-line-num-bg: #ffb060;--missed-branch-text: #b45309;--missed-method-bg: #e8d0ff;--missed-method-line-num-bg: #d4b0ff;--missed-method-text: #7b2d8e;--source-bg: #fff;--source-border: #c0c5cc;--source-line-number: #444;--source-line-num-bg: #f0f1f3;--hits-bg: #e0e3e8;--hits-color: #333}body{padding:0;font-size:12pt}#loading,#dark-mode-toggle,.tab-bar,.source_files,.col-filter--name,.col-filter__coverage,table.file_list thead th.sorting:after,table.file_list thead th.sorting_asc:after,table.file_list thead th.sorting_desc:after{display:none!important}#wrapper,.file_list_container{display:block!important}body:has(.source-dialog[open]) #wrapper{display:none!important}#content{border:none;border-radius:0;padding:0}.file_list_container .group_name{display:inline!important;font-size:16pt;font-weight:700}.file_list_container .covered_percent{display:inline!important;font-weight:600}.file_list_container+.file_list_container{margin-top:24pt}dialog.source-dialog{display:none}dialog.source-dialog[open]{display:block!important;position:static;width:100%;height:auto;max-height:none;overflow:visible;background:#fff}.source-dialog__close{display:none!important}.source-dialog__header{flex-wrap:wrap;border-bottom:none;padding:0 0 8pt}.source-dialog__title{flex:0 0 100%}.source-dialog__title h2{word-break:normal;overflow-wrap:break-word}.source-legend{margin-left:0;padding-left:0;padding-top:var(--sp-2)}.source-dialog__body{overflow:visible}.source_table pre{font-size:8pt;line-height:1.4}.source_table pre li{-webkit-print-color-adjust:exact;print-color-adjust:exact}.source_table pre li:before{-webkit-print-color-adjust:exact;print-color-adjust:exact}.source_table pre .hits{-webkit-print-color-adjust:exact;print-color-adjust:exact}.coverage-bar{border:1px solid var(--border);-webkit-print-color-adjust:exact;print-color-adjust:exact}.coverage-bar__fill{-webkit-print-color-adjust:exact;print-color-adjust:exact}table.file_list tbody tr:nth-child(2n){-webkit-print-color-adjust:exact;print-color-adjust:exact}table.file_list{font-size:10pt}table.file_list thead th{font-size:9pt}table.file_list{page-break-inside:auto}table.file_list tr{page-break-inside:avoid}table.file_list thead{display:table-header-group}#footer{font-size:10pt;margin-top:12pt}#footer a:after{content:" (" attr(href) ")";font-size:9pt;color:var(--text-tertiary)}}
|