simplecov 0.22.0 → 1.1.1

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