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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -1
  3. data/LICENSE +1 -1
  4. data/README.md +1009 -511
  5. data/doc/alternate-formatters.md +0 -5
  6. data/doc/commercial-services.md +5 -5
  7. data/exe/simplecov +11 -0
  8. data/lib/minitest/simplecov_plugin.rb +13 -5
  9. data/lib/simplecov/autostart.rb +11 -0
  10. data/lib/simplecov/cli/clean.rb +47 -0
  11. data/lib/simplecov/cli/coverage.rb +91 -0
  12. data/lib/simplecov/cli/diff.rb +151 -0
  13. data/lib/simplecov/cli/dotfile.rb +100 -0
  14. data/lib/simplecov/cli/merge.rb +116 -0
  15. data/lib/simplecov/cli/open.rb +50 -0
  16. data/lib/simplecov/cli/report.rb +84 -0
  17. data/lib/simplecov/cli/run.rb +36 -0
  18. data/lib/simplecov/cli/serve.rb +139 -0
  19. data/lib/simplecov/cli/uncovered.rb +107 -0
  20. data/lib/simplecov/cli.rb +150 -0
  21. data/lib/simplecov/color.rb +74 -0
  22. data/lib/simplecov/combine/branches_combiner.rb +3 -2
  23. data/lib/simplecov/combine/files_combiner.rb +7 -1
  24. data/lib/simplecov/combine/lines_combiner.rb +19 -17
  25. data/lib/simplecov/combine/methods_combiner.rb +26 -0
  26. data/lib/simplecov/combine/results_combiner.rb +5 -4
  27. data/lib/simplecov/command_guesser.rb +46 -32
  28. data/lib/simplecov/configuration/coverage.rb +171 -0
  29. data/lib/simplecov/configuration/coverage_criteria.rb +156 -0
  30. data/lib/simplecov/configuration/filters.rb +195 -0
  31. data/lib/simplecov/configuration/formatting.rb +119 -0
  32. data/lib/simplecov/configuration/ignored_entries.rb +63 -0
  33. data/lib/simplecov/configuration/merging.rb +74 -0
  34. data/lib/simplecov/configuration/thresholds.rb +174 -0
  35. data/lib/simplecov/configuration.rb +79 -405
  36. data/lib/simplecov/coverage_statistics.rb +12 -9
  37. data/lib/simplecov/coverage_violations.rb +148 -0
  38. data/lib/simplecov/defaults.rb +27 -20
  39. data/lib/simplecov/directive.rb +162 -0
  40. data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
  41. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +19 -57
  42. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
  43. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -27
  44. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
  45. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +38 -21
  46. data/lib/simplecov/exit_codes.rb +3 -0
  47. data/lib/simplecov/exit_handling.rb +158 -0
  48. data/lib/simplecov/file_list.rb +61 -17
  49. data/lib/simplecov/filter.rb +69 -24
  50. data/lib/simplecov/formatter/base.rb +101 -0
  51. data/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
  52. data/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
  53. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  54. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  55. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
  56. data/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
  57. data/lib/simplecov/formatter/html_formatter.rb +79 -0
  58. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +84 -0
  59. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +127 -0
  60. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
  61. data/lib/simplecov/formatter/json_formatter.rb +77 -0
  62. data/lib/simplecov/formatter/multi_formatter.rb +4 -5
  63. data/lib/simplecov/formatter/simple_formatter.rb +9 -11
  64. data/lib/simplecov/formatter.rb +4 -0
  65. data/lib/simplecov/last_run.rb +10 -3
  66. data/lib/simplecov/lines_classifier.rb +26 -13
  67. data/lib/simplecov/load_global_config.rb +9 -4
  68. data/lib/simplecov/parallel_adapters/base.rb +51 -0
  69. data/lib/simplecov/parallel_adapters/generic.rb +42 -0
  70. data/lib/simplecov/parallel_adapters/parallel_tests.rb +77 -0
  71. data/lib/simplecov/parallel_adapters.rb +83 -0
  72. data/lib/simplecov/parallel_coordination.rb +95 -0
  73. data/lib/simplecov/process.rb +20 -14
  74. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  75. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  76. data/lib/simplecov/profiles/rails.rb +24 -10
  77. data/lib/simplecov/profiles/root_filter.rb +6 -5
  78. data/lib/simplecov/profiles/strict.rb +32 -0
  79. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  80. data/lib/simplecov/profiles.rb +32 -3
  81. data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
  82. data/lib/simplecov/result/source_file_builder.rb +51 -0
  83. data/lib/simplecov/result.rb +97 -19
  84. data/lib/simplecov/result_adapter.rb +68 -6
  85. data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
  86. data/lib/simplecov/result_merger/resultset_file.rb +38 -0
  87. data/lib/simplecov/result_merger/resultset_store.rb +50 -0
  88. data/lib/simplecov/result_merger.rb +46 -90
  89. data/lib/simplecov/result_processing.rb +162 -0
  90. data/lib/simplecov/simulate_coverage.rb +54 -8
  91. data/lib/simplecov/source_file/branch.rb +1 -3
  92. data/lib/simplecov/source_file/branch_builder.rb +114 -0
  93. data/lib/simplecov/source_file/builder_context.rb +28 -0
  94. data/lib/simplecov/source_file/line.rb +7 -2
  95. data/lib/simplecov/source_file/line_builder.rb +43 -0
  96. data/lib/simplecov/source_file/method.rb +52 -0
  97. data/lib/simplecov/source_file/method_builder.rb +58 -0
  98. data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
  99. data/lib/simplecov/source_file/skip_chunks.rb +77 -0
  100. data/lib/simplecov/source_file/source_loader.rb +63 -0
  101. data/lib/simplecov/source_file/statistics.rb +57 -0
  102. data/lib/simplecov/source_file.rb +66 -232
  103. data/lib/simplecov/static_coverage_extractor/visitor.rb +193 -0
  104. data/lib/simplecov/static_coverage_extractor.rb +111 -0
  105. data/lib/simplecov/useless_results_remover.rb +16 -7
  106. data/lib/simplecov/version.rb +1 -1
  107. data/lib/simplecov-html.rb +4 -0
  108. data/lib/simplecov.rb +131 -377
  109. data/lib/simplecov_json_formatter.rb +4 -0
  110. data/schemas/coverage-v1.0.schema.json +300 -0
  111. data/schemas/coverage.schema.json +300 -0
  112. metadata +88 -56
  113. 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
- minimum_violations.any?
14
+ violations.any?
13
15
  end
14
16
 
15
17
  def report
16
- minimum_violations.each do |violation|
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
- attr_reader :result, :minimum_coverage
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 minimum_violations
35
- @minimum_violations ||= calculate_minimum_violations
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 calculate_minimum_violations
39
- coverage_achieved = minimum_coverage.map do |criterion, percent|
40
- {
41
- criterion: criterion,
42
- minimum_expected: percent,
43
- actual: result.coverage_statistics.fetch(criterion).percent
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
- coverage_achieved.select do |achieved|
48
- achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
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
@@ -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
@@ -23,8 +23,12 @@ module SimpleCov
23
23
  @files = files
24
24
  end
25
25
 
26
- def coverage_statistics
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
- map { |f| f.never_lines.count }.inject(:+)
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
- map { |f| f.skipped_lines.count }.inject(:+)
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
- # Computes the coverage based upon lines covered and lines missed
75
- # @return [Float]
76
- def covered_percent
77
- coverage_statistics[:line]&.percent
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
- # Computes the strength (hits / line) based upon lines covered and lines missed
81
- # @return [Float]
82
- def covered_strength
83
- coverage_statistics[:line]&.strength
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
- @files.each_with_object(line: [], branch: []) do |file, together|
109
- together[:line] << file.coverage_statistics.fetch(:line)
110
- together[:branch] << file.coverage_statistics.fetch(:branch) if SimpleCov.branch_coverage?
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
- coverage_statistics = {line: CoverageStatistics.from(coverage_statistics_by_file[:line])}
116
- coverage_statistics[:branch] = CoverageStatistics.from(coverage_statistics_by_file[:branch]) if SimpleCov.branch_coverage?
117
- coverage_statistics
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
@@ -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 passes?(source_file) instance method
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 passes?(source_file)
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
- case filter_argument
39
- when String
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.include?(filter_argument)
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 =~ filter_argument)
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)}}