simplecov 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +165 -57
  3. data/exe/simplecov +0 -3
  4. data/lib/minitest/simplecov_plugin.rb +12 -9
  5. data/lib/simplecov/atomic_file.rb +16 -19
  6. data/lib/simplecov/autostart.rb +5 -7
  7. data/lib/simplecov/baseline/parser.rb +58 -0
  8. data/lib/simplecov/baseline.rb +170 -0
  9. data/lib/simplecov/cli/affected/changed_files.rb +57 -0
  10. data/lib/simplecov/cli/affected/selection.rb +117 -0
  11. data/lib/simplecov/cli/affected.rb +148 -0
  12. data/lib/simplecov/cli/badge/svg.rb +70 -0
  13. data/lib/simplecov/cli/badge.rb +62 -0
  14. data/lib/simplecov/cli/clean.rb +23 -24
  15. data/lib/simplecov/cli/command_helpers.rb +55 -19
  16. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  17. data/lib/simplecov/cli/completions.rb +83 -0
  18. data/lib/simplecov/cli/coverage.rb +25 -28
  19. data/lib/simplecov/cli/coverage_file.rb +58 -14
  20. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  21. data/lib/simplecov/cli/dead_code.rb +139 -0
  22. data/lib/simplecov/cli/diff/output.rb +45 -0
  23. data/lib/simplecov/cli/diff.rb +35 -67
  24. data/lib/simplecov/cli/dotfile.rb +83 -38
  25. data/lib/simplecov/cli/git.rb +41 -0
  26. data/lib/simplecov/cli/history/output.rb +135 -0
  27. data/lib/simplecov/cli/history.rb +67 -0
  28. data/lib/simplecov/cli/merge.rb +34 -39
  29. data/lib/simplecov/cli/open.rb +12 -15
  30. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  31. data/lib/simplecov/cli/patch/output.rb +135 -0
  32. data/lib/simplecov/cli/patch.rb +169 -0
  33. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  34. data/lib/simplecov/cli/ratchet.rb +83 -0
  35. data/lib/simplecov/cli/report.rb +18 -24
  36. data/lib/simplecov/cli/run.rb +8 -9
  37. data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
  38. data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
  39. data/lib/simplecov/cli/serve.rb +27 -24
  40. data/lib/simplecov/cli/show/annotator.rb +87 -0
  41. data/lib/simplecov/cli/show/sweep.rb +36 -0
  42. data/lib/simplecov/cli/show.rb +133 -0
  43. data/lib/simplecov/cli/status/facts.rb +87 -0
  44. data/lib/simplecov/cli/status.rb +93 -0
  45. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  46. data/lib/simplecov/cli/tests.rb +148 -0
  47. data/lib/simplecov/cli/uncovered/misses.rb +37 -0
  48. data/lib/simplecov/cli/uncovered.rb +70 -32
  49. data/lib/simplecov/cli/usage.rb +152 -0
  50. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  51. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  52. data/lib/simplecov/cli/watch/poller.rb +45 -0
  53. data/lib/simplecov/cli/watch/session.rb +134 -0
  54. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  55. data/lib/simplecov/cli/watch.rb +85 -0
  56. data/lib/simplecov/cli.rb +43 -92
  57. data/lib/simplecov/color.rb +21 -35
  58. data/lib/simplecov/combine/branches_combiner.rb +7 -31
  59. data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
  60. data/lib/simplecov/combine/identity_interner.rb +10 -13
  61. data/lib/simplecov/combine/interned_counts.rb +7 -9
  62. data/lib/simplecov/combine/lines_combiner.rb +20 -44
  63. data/lib/simplecov/combine/methods_combiner.rb +9 -35
  64. data/lib/simplecov/combine/results_combiner.rb +4 -21
  65. data/lib/simplecov/combine.rb +0 -6
  66. data/lib/simplecov/command_guesser.rb +20 -34
  67. data/lib/simplecov/configuration/baseline.rb +31 -0
  68. data/lib/simplecov/configuration/coverage.rb +91 -79
  69. data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
  70. data/lib/simplecov/configuration/deprecations.rb +21 -0
  71. data/lib/simplecov/configuration/eval_coverage.rb +6 -10
  72. data/lib/simplecov/configuration/filters.rb +40 -73
  73. data/lib/simplecov/configuration/formatting.rb +74 -83
  74. data/lib/simplecov/configuration/groups.rb +19 -13
  75. data/lib/simplecov/configuration/history.rb +44 -0
  76. data/lib/simplecov/configuration/ignored_entries.rb +31 -22
  77. data/lib/simplecov/configuration/merging.rb +94 -62
  78. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  79. data/lib/simplecov/configuration/production.rb +23 -0
  80. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  81. data/lib/simplecov/configuration/thresholds.rb +33 -62
  82. data/lib/simplecov/configuration/view_coverage.rb +38 -0
  83. data/lib/simplecov/configuration.rb +70 -76
  84. data/lib/simplecov/configuration_error.rb +5 -0
  85. data/lib/simplecov/context_map/union.rb +81 -0
  86. data/lib/simplecov/context_map.rb +187 -0
  87. data/lib/simplecov/coverage_json.rb +2 -5
  88. data/lib/simplecov/coverage_statistics.rb +14 -24
  89. data/lib/simplecov/coverage_violations.rb +157 -64
  90. data/lib/simplecov/current_run.rb +61 -0
  91. data/lib/simplecov/defaults.rb +4 -23
  92. data/lib/simplecov/deprecation.rb +45 -27
  93. data/lib/simplecov/directive.rb +46 -62
  94. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  95. data/lib/simplecov/exit_codes/check.rb +17 -11
  96. data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
  97. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
  98. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  99. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  100. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
  101. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
  102. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
  103. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
  104. data/lib/simplecov/exit_codes.rb +17 -7
  105. data/lib/simplecov/exit_handling.rb +94 -59
  106. data/lib/simplecov/file_list.rb +20 -51
  107. data/lib/simplecov/filter.rb +43 -66
  108. data/lib/simplecov/formatter/base.rb +38 -51
  109. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  110. data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
  111. data/lib/simplecov/formatter/html_formatter/public/index.html +17 -10
  112. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
  113. data/lib/simplecov/formatter/html_formatter.rb +24 -41
  114. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
  115. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  116. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
  117. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
  118. data/lib/simplecov/formatter/json_formatter.rb +7 -13
  119. data/lib/simplecov/formatter/multi_formatter.rb +5 -11
  120. data/lib/simplecov/formatter/simple_formatter.rb +2 -8
  121. data/lib/simplecov/formatter.rb +14 -10
  122. data/lib/simplecov/group_names.rb +13 -12
  123. data/lib/simplecov/history.rb +115 -0
  124. data/lib/simplecov/last_run.rb +6 -9
  125. data/lib/simplecov/lines_classifier.rb +31 -16
  126. data/lib/simplecov/load_global_config.rb +7 -7
  127. data/lib/simplecov/parallel_adapters/base.rb +13 -41
  128. data/lib/simplecov/parallel_adapters/generic.rb +7 -18
  129. data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
  130. data/lib/simplecov/parallel_adapters.rb +24 -53
  131. data/lib/simplecov/parallel_coordination.rb +39 -49
  132. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  133. data/lib/simplecov/parallel_result_merger.rb +67 -126
  134. data/lib/simplecov/process.rb +14 -32
  135. data/lib/simplecov/production/error.rb +11 -0
  136. data/lib/simplecov/production/file_sink.rb +130 -0
  137. data/lib/simplecov/production.rb +249 -0
  138. data/lib/simplecov/profiles/rails.rb +1 -12
  139. data/lib/simplecov/profiles/root_filter.rb +0 -4
  140. data/lib/simplecov/profiles/strict.rb +9 -19
  141. data/lib/simplecov/profiles.rb +11 -33
  142. data/lib/simplecov/report_deferral.rb +11 -15
  143. data/lib/simplecov/report_stamp.rb +6 -9
  144. data/lib/simplecov/result/filter_config.rb +19 -0
  145. data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
  146. data/lib/simplecov/result/serialization.rb +43 -0
  147. data/lib/simplecov/result/source_file_builder.rb +7 -12
  148. data/lib/simplecov/result.rb +68 -117
  149. data/lib/simplecov/result_adapter.rb +58 -68
  150. data/lib/simplecov/result_merger/contexts.rb +22 -0
  151. data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
  152. data/lib/simplecov/result_merger/resultset_file.rb +20 -27
  153. data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
  154. data/lib/simplecov/result_merger/resultset_store.rb +23 -14
  155. data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
  156. data/lib/simplecov/result_merger.rb +59 -58
  157. data/lib/simplecov/result_processing.rb +74 -119
  158. data/lib/simplecov/run_identity.rb +16 -18
  159. data/lib/simplecov/simulate_coverage.rb +35 -50
  160. data/lib/simplecov/source_file/branch.rb +8 -33
  161. data/lib/simplecov/source_file/branch_builder.rb +28 -47
  162. data/lib/simplecov/source_file/builder_context.rb +6 -13
  163. data/lib/simplecov/source_file/line.rb +13 -33
  164. data/lib/simplecov/source_file/line_builder.rb +9 -15
  165. data/lib/simplecov/source_file/method.rb +3 -7
  166. data/lib/simplecov/source_file/method_builder.rb +6 -15
  167. data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
  168. data/lib/simplecov/source_file/skip_chunks.rb +19 -28
  169. data/lib/simplecov/source_file/source_loader.rb +45 -35
  170. data/lib/simplecov/source_file/statistics.rb +10 -9
  171. data/lib/simplecov/source_file.rb +21 -51
  172. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
  173. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
  174. data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
  175. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
  176. data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
  177. data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
  178. data/lib/simplecov/static_coverage_extractor.rb +46 -62
  179. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  180. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  181. data/lib/simplecov/test_tracker/delta.rb +69 -0
  182. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  183. data/lib/simplecov/test_tracker.rb +162 -0
  184. data/lib/simplecov/unloaded_file_injector.rb +18 -31
  185. data/lib/simplecov/useless_results_remover.rb +7 -6
  186. data/lib/simplecov/version.rb +1 -1
  187. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  188. data/lib/simplecov/view_coverage.rb +55 -0
  189. data/lib/simplecov-html.rb +1 -0
  190. data/lib/simplecov.rb +59 -114
  191. data/lib/simplecov_json_formatter.rb +1 -0
  192. data/man/simplecov.1 +349 -0
  193. data/sig/simplecov.rbs +523 -595
  194. metadata +72 -7
  195. data/schemas/coverage-v1.0.schema.json +0 -306
  196. data/schemas/coverage.schema.json +0 -306
@@ -5,8 +5,6 @@ require "time"
5
5
  module SimpleCov
6
6
  module Formatter
7
7
  class HTMLFormatter
8
- # Validates the subset of coverage.json that the browser viewer
9
- # dereferences without defensive fallbacks.
10
8
  module ViewerDataValidator
11
9
  META_STRINGS = %w[simplecov_version command_name project_name timestamp].freeze
12
10
  COVERAGE_FLAGS = {
@@ -25,36 +23,95 @@ module SimpleCov
25
23
  validate_statistics!(data.fetch("total"), meta, "total")
26
24
  data.fetch("coverage").each { |filename, file| validate_file!(filename, file) }
27
25
  data.fetch("groups").each { |name, group| validate_group!(name, group, meta) }
26
+ validate_contexts!(data)
27
+ validate_production!(data)
28
28
  data
29
29
  end
30
30
 
31
- private
31
+ private
32
32
 
33
33
  def validate_section!(data, key)
34
- return if data[key].is_a?(Hash)
34
+ return if data[key].instance_of?(Hash)
35
35
 
36
- raise SimpleCov::CoverageJSON::Error, "#{key.inspect} must be an object"
36
+ raise CoverageJSON::Error, "#{key.inspect} must be an object"
37
37
  end
38
38
 
39
39
  def validate_file!(filename, file)
40
- unless file.is_a?(Hash)
41
- raise SimpleCov::CoverageJSON::Error, "coverage entry #{filename.inspect} must be an object"
40
+ unless file.instance_of?(Hash)
41
+ raise CoverageJSON::Error, "coverage entry #{filename.inspect} must be an object"
42
42
  end
43
43
 
44
44
  source = file["source"]
45
- return if source.is_a?(Array) && source.all?(String)
45
+ return if source.instance_of?(Array) && source.all?(String)
46
46
 
47
- raise SimpleCov::CoverageJSON::Error,
48
- "coverage entry #{filename.inspect} must include an array of source strings; " \
49
- "regenerate with source_in_json true"
47
+ raise CoverageJSON::Error,
48
+ "coverage entry #{filename.inspect} must include an array of source strings; " \
49
+ "regenerate with source_in_json true"
50
+ end
51
+
52
+ def validate_contexts!(data)
53
+ contexts = data["contexts"]
54
+ valid = contexts.nil? || (contexts.instance_of?(Array) && contexts.all?(String))
55
+ raise CoverageJSON::Error, '"contexts" must be an array of strings' unless valid
56
+
57
+ count = Array(contexts).size
58
+ data.fetch("coverage").each { |filename, file| validate_context_table!(filename, file["contexts"], count) }
59
+ end
60
+
61
+ def validate_context_table!(filename, table, count)
62
+ return if table.nil? || (table.instance_of?(Hash) && table.all? do |key, value|
63
+ context_entry?(key, value, count)
64
+ end)
65
+
66
+ raise CoverageJSON::Error,
67
+ "coverage entry #{filename.inspect} contexts must map recorded context indices to hex bitmaps"
68
+ end
69
+
70
+ def context_entry?(key, value, count)
71
+ key.instance_of?(String) && key.match?(/\A\d+\z/) && Integer(key) < count &&
72
+ value.instance_of?(String) && value.match?(/\A\h+\z/)
73
+ end
74
+
75
+ def validate_production!(data)
76
+ production = data["production"]
77
+ return if production.nil?
78
+
79
+ raise CoverageJSON::Error, '"production" must be an object' unless production.instance_of?(Hash)
80
+
81
+ files = validate_type!(production, "files", Hash, "production")
82
+ files.each { |filename, entry| validate_production_file!(filename, entry) }
83
+ end
84
+
85
+ def validate_production_file!(filename, entry)
86
+ unless production_lines?(entry)
87
+ raise CoverageJSON::Error,
88
+ "production entry #{filename.inspect} must list sorted line numbers"
89
+ end
90
+ last_seen = entry["last_seen"]
91
+ return if last_seen.nil? || last_seen.instance_of?(String)
92
+
93
+ raise CoverageJSON::Error, "production entry #{filename.inspect} last_seen must be a string"
94
+ end
95
+
96
+ def production_lines?(entry)
97
+ lines = entry["lines"] if entry.instance_of?(Hash)
98
+ lines.instance_of?(Array) && lines.all? { |line| line.instance_of?(Integer) && line.positive? }
50
99
  end
51
100
 
52
101
  def validate_meta!(meta)
53
102
  META_STRINGS.each { |key| validate_type!(meta, key, String, "meta") }
103
+ validate_command_names!(meta)
54
104
  Time.iso8601(meta.fetch("timestamp"))
55
105
  COVERAGE_FLAGS.each_key { |key| validate_boolean!(meta, key) }
56
106
  rescue ArgumentError
57
- raise SimpleCov::CoverageJSON::Error, "meta.timestamp must be an ISO 8601 date-time"
107
+ raise CoverageJSON::Error, "meta.timestamp must be an ISO 8601 date-time"
108
+ end
109
+
110
+ def validate_command_names!(meta)
111
+ names = meta["command_names"]
112
+ return if names.nil? || (names.instance_of?(Array) && names.all?(String))
113
+
114
+ raise CoverageJSON::Error, "meta.command_names must be an array of strings"
58
115
  end
59
116
 
60
117
  def validate_statistics!(statistics, meta, location)
@@ -67,11 +124,11 @@ module SimpleCov
67
124
  end
68
125
 
69
126
  def validate_group!(name, group, meta)
70
- raise SimpleCov::CoverageJSON::Error, "group #{name.inspect} must be an object" unless group.is_a?(Hash)
127
+ raise CoverageJSON::Error, "group #{name.inspect} must be an object" unless group.instance_of?(Hash)
71
128
 
72
129
  files = group["files"]
73
- unless files.is_a?(Array) && files.all?(String)
74
- raise SimpleCov::CoverageJSON::Error, "group #{name.inspect}.files must be an array of strings"
130
+ unless files.instance_of?(Array) && files.all?(String)
131
+ raise CoverageJSON::Error, "group #{name.inspect}.files must be an array of strings"
75
132
  end
76
133
 
77
134
  validate_statistics!(group, meta, "group #{name.inspect}")
@@ -81,13 +138,13 @@ module SimpleCov
81
138
  value = object[key]
82
139
  return value if value.is_a?(type)
83
140
 
84
- raise SimpleCov::CoverageJSON::Error, "#{location}.#{key} must be a #{type.name.downcase}"
141
+ raise CoverageJSON::Error, "#{location}.#{key} must be a #{type.name.downcase}"
85
142
  end
86
143
 
87
144
  def validate_boolean!(meta, key)
88
145
  return if [true, false].include?(meta[key])
89
146
 
90
- raise SimpleCov::CoverageJSON::Error, "meta.#{key} must be a boolean"
147
+ raise CoverageJSON::Error, "meta.#{key} must be a boolean"
91
148
  end
92
149
  end
93
150
  end
@@ -10,57 +10,45 @@ require_relative "json_formatter"
10
10
  module SimpleCov
11
11
  module Formatter
12
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.
13
+ # template already carries the viewer app's JS and CSS inline, so the report
14
+ # can be mailed, uploaded as one CI artifact, or opened from anywhere
15
+ # without sibling files. A coverage.json is written alongside from the same
16
+ # in-memory hash.
18
17
  class HTMLFormatter < Base
19
- # Placeholder in the compiled template where the report's data goes.
20
18
  DATA_MARKER = "<!-- SIMPLECOV_COVERAGE_DATA -->"
21
19
 
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
20
  LEGACY_REPORT_FILES = %w[
27
21
  coverage_data.js application.js application.css
28
22
  favicon_green.png favicon_red.png favicon_yellow.png
29
23
  ].freeze
30
24
 
31
25
  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
26
  viewer_hash = JSONFormatter.build_hash(result, include_source: true)
42
27
  json_hash = SimpleCov.source_in_json ? viewer_hash : source_less_hash(viewer_hash)
43
28
  write_report_files(json_hash, viewer_hash, result)
44
29
  emit_status(result)
45
30
  end
46
31
 
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.
32
+ # Generates HTML from a pre-existing coverage.json without a live Result or
33
+ # even a running test suite. The round-trip through parse/generate compacts
34
+ # the input and rejects invalid JSON here rather than at view time.
52
35
  def format_from_json(json_path, output_dir)
53
- data = ViewerDataValidator.call(SimpleCov::CoverageJSON.load(json_path))
36
+ data = ViewerDataValidator.call(CoverageJSON.load(json_path))
54
37
  json = JSON.generate(data)
55
38
  AtomicFile.write(File.join(output_dir, "index.html"), render_report(json), binary: true)
56
39
  end
57
40
 
58
- private
41
+ private
59
42
 
60
43
  def entry_point_filename
61
44
  "index.html"
62
45
  end
63
46
 
47
+ # The inlined report data feeds the client-side viewer, which renders source
48
+ # from the embedded array, so it always needs `source` regardless of
49
+ # `SimpleCov.source_in_json`. The side-file `coverage.json` honors the
50
+ # setting, and stays pretty-printed for human readers while the embedded
51
+ # copy is serialized compactly.
64
52
  def source_less_hash(hash)
65
53
  coverage = hash.fetch(:coverage).transform_values { |file| file.except(:source) }
66
54
  hash.merge(coverage: coverage)
@@ -72,31 +60,26 @@ module SimpleCov
72
60
  FileUtils.rm_f(LEGACY_REPORT_FILES.map { |name| File.join(output_path, name) })
73
61
  end
74
62
 
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.
63
+ # `<` is escaped as \u003c, valid JSON since `<` can only occur inside
64
+ # strings, so embedded source text containing "</script>" cannot terminate
65
+ # the surrounding element. Block forms keep gsub/sub from interpreting
66
+ # backslashes in the JSON as replacement-string back-references.
67
+ #
68
+ # The template is read as UTF-8 explicitly, not the locale's
69
+ # `Encoding.default_external`: substituting a UTF-8 payload into a template
70
+ # tagged US-ASCII raises Encoding::CompatibilityError.
81
71
  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
72
  template = File.read(File.join(public_dir, "index.html"), encoding: Encoding::UTF_8)
88
73
  unless template.include?(DATA_MARKER)
89
74
  raise "SimpleCov's HTML template is missing its #{DATA_MARKER.inspect} marker"
90
75
  end
91
76
 
92
- data_script = "<script>window.SIMPLECOV_DATA = #{json.gsub('<') { '\u003c' }};</script>"
77
+ data_script = "<script>window.SIMPLECOV_DATA = #{json.gsub("<") { '\u003c' }};</script>"
93
78
  template.sub(DATA_MARKER) { data_script }
94
79
  end
95
80
 
96
81
  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/")
82
+ "#{__dir__}/html_formatter/public/"
100
83
  end
101
84
  end
102
85
  end
@@ -3,11 +3,6 @@
3
3
  module SimpleCov
4
4
  module Formatter
5
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
6
  class ErrorsFormatter
12
7
  CRITERION_KEYS = {line: :lines, branch: :branches, method: :methods}.freeze
13
8
  private_constant :CRITERION_KEYS
@@ -17,27 +12,45 @@ module SimpleCov
17
12
  errors = {} #: Hash[Symbol, Hash[untyped, untyped]]
18
13
  format_minimum_overall(result, errors)
19
14
  format_minimum_by_file(result, errors)
15
+ format_baseline(result, errors)
20
16
  format_minimum_by_group(result, errors)
21
17
  format_maximum_overall(result, errors)
22
18
  format_maximum_drop(result, errors)
19
+ format_maximum_missed(result, errors)
20
+ format_maximum_missed_per_file(result, errors)
23
21
  errors
24
22
  end
25
23
 
26
- private
24
+ private
27
25
 
28
26
  def format_minimum_overall(result, errors)
29
- SimpleCov::CoverageViolations.minimum_overall(result, SimpleCov.minimum_coverage).each do |violation|
27
+ CoverageViolations.minimum_overall(result, SimpleCov.minimum_coverage).each do |violation|
30
28
  bucket(errors, :minimum_coverage)[key_for(violation)] = expected_actual(violation)
31
29
  end
32
30
  end
33
31
 
34
32
  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
33
+ violations = CoverageViolations.minimum_by_file(
34
+ result, SimpleCov.minimum_coverage_by_file, SimpleCov.minimum_coverage_by_file_overrides,
35
+ baseline: SimpleCov.baseline
37
36
  )
38
37
  violations.each { |violation| record_by_file(violation, errors) }
39
38
  end
40
39
 
40
+ # Baseline-floor violations keyed like minimum_coverage_by_file: filename,
41
+ # then criterion. The payload adds the two missed counts the floor was judged
42
+ # by; `allowed_missed` is absent for a percent-only floor.
43
+ def format_baseline(result, errors)
44
+ CoverageViolations.baseline(result, SimpleCov.baseline).each do |violation|
45
+ by_file = bucket(errors, :baseline)
46
+ file_bucket = by_file[violation.fetch(:project_filename)] ||= {} #: Hash[untyped, untyped]
47
+ payload = expected_actual(violation).merge(actual_missed: violation.fetch(:actual_missed))
48
+ allowed = violation.fetch(:allowed_missed)
49
+ payload[:allowed_missed] = allowed if allowed
50
+ file_bucket[key_for(violation)] = payload
51
+ end
52
+ end
53
+
41
54
  def record_by_file(violation, errors)
42
55
  by_file = bucket(errors, :minimum_coverage_by_file)
43
56
  file_bucket = by_file[violation.fetch(:project_filename)] ||= {} #: Hash[untyped, untyped]
@@ -45,11 +58,9 @@ module SimpleCov
45
58
  end
46
59
 
47
60
  def format_minimum_by_group(result, errors)
48
- violations = SimpleCov::CoverageViolations.minimum_by_group(result, SimpleCov.minimum_coverage_by_group)
61
+ violations = CoverageViolations.minimum_by_group(result, SimpleCov.minimum_coverage_by_group)
49
62
  return if violations.empty?
50
63
 
51
- # `bucket` lazily creates the errors key, so only touch it when
52
- # there is a violation to record.
53
64
  by_group = bucket(errors, :minimum_coverage_by_group)
54
65
  violations.each do |violation|
55
66
  group_bucket = by_group[violation.fetch(:group_name)] ||= {} #: Hash[untyped, untyped]
@@ -58,18 +69,37 @@ module SimpleCov
58
69
  end
59
70
 
60
71
  def format_maximum_overall(result, errors)
61
- SimpleCov::CoverageViolations.maximum_overall(result, SimpleCov.maximum_coverage).each do |violation|
72
+ CoverageViolations.maximum_overall(result, SimpleCov.maximum_coverage).each do |violation|
62
73
  bucket(errors, :maximum_coverage)[key_for(violation)] = expected_actual(violation)
63
74
  end
64
75
  end
65
76
 
66
77
  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)}
78
+ CoverageViolations.maximum_drop(result, SimpleCov.maximum_coverage_drop).each do |violation|
79
+ bucket(errors, :maximum_coverage_drop)[key_for(violation)] = maximum_actual(violation)
70
80
  end
71
81
  end
72
82
 
83
+ def format_maximum_missed(result, errors)
84
+ CoverageViolations.maximum_missed(result, SimpleCov.maximum_missed).each do |violation|
85
+ bucket(errors, :maximum_missed)[key_for(violation)] = maximum_actual(violation)
86
+ end
87
+ end
88
+
89
+ def format_maximum_missed_per_file(result, errors)
90
+ violations = CoverageViolations.maximum_missed_by_file(
91
+ result, SimpleCov.maximum_missed_per_file, SimpleCov.maximum_missed_per_file_overrides,
92
+ baseline: SimpleCov.baseline
93
+ )
94
+ violations.each do |violation|
95
+ by_file = bucket(errors, :maximum_missed_per_file)
96
+ file_bucket = by_file[violation.fetch(:project_filename)] ||= {} #: Hash[untyped, untyped]
97
+ file_bucket[key_for(violation)] = maximum_actual(violation)
98
+ end
99
+ end
100
+
101
+ # Lazily creates the errors key, so callers only touch it when there is a
102
+ # violation to record.
73
103
  def bucket(errors, name)
74
104
  errors[name] ||= {} #: Hash[untyped, untyped]
75
105
  end
@@ -81,6 +111,10 @@ module SimpleCov
81
111
  def expected_actual(violation)
82
112
  {expected: violation.fetch(:expected), actual: violation.fetch(:actual)}
83
113
  end
114
+
115
+ def maximum_actual(violation)
116
+ {maximum: violation.fetch(:maximum), actual: violation.fetch(:actual)}
117
+ end
84
118
  end
85
119
  end
86
120
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../production/file_sink"
4
+
5
+ module SimpleCov
6
+ module Formatter
7
+ class JSONFormatter
8
+ module ProductionSectionFormatter
9
+ extend self
10
+
11
+ # The section for the configured store, or nil when none is configured or it
12
+ # is unreadable. An unreadable store warns instead of raising: the report is
13
+ # generated at the end of a test run, and a missing night of production data
14
+ # must not fail the suite that measured the tests.
15
+ def call(path = SimpleCov.production_coverage)
16
+ return nil unless path
17
+
18
+ section(Production::FileSink.read(path))
19
+ rescue SystemCallError, Production::Error => e
20
+ warn "[SimpleCov] skipping production coverage (#{e})"
21
+ end
22
+
23
+ def section(store)
24
+ section = {} #: Hash[Symbol, untyped]
25
+ started_at = store["started_at"]
26
+ updated_at = store["updated_at"]
27
+ section[:started_at] = started_at if started_at
28
+ section[:updated_at] = updated_at if updated_at
29
+ section[:files] = files(store)
30
+ section
31
+ end
32
+
33
+ # `coverage` and `last_seen` are the two keys a parsed store always carries,
34
+ # since `FileSink.parse` substitutes an empty Hash for either one a document
35
+ # is missing.
36
+ def files(store)
37
+ last_seen = store.fetch("last_seen")
38
+ store.fetch("coverage").sort.to_h do |file, lines|
39
+ entry = {lines: lines} #: Hash[Symbol, untyped]
40
+ stamp = last_seen[file]
41
+ entry[:last_seen] = stamp if stamp
42
+ [file, entry]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -3,28 +3,24 @@
3
3
  require "open3"
4
4
  require "time"
5
5
  require_relative "errors_formatter"
6
+ require_relative "production_section_formatter"
6
7
  require_relative "source_file_formatter"
7
8
 
8
9
  module SimpleCov
9
10
  module Formatter
10
11
  class JSONFormatter
11
- # Builds the hash that JSONFormatter serializes to coverage.json:
12
- # meta, per-file coverage data, group totals, and aggregate stats.
13
12
  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"
13
+ # Bump this (and SCHEMA_URL) when the JSON shape changes: additive changes
14
+ # bump the minor segment, removals or shape changes the major. The versioned
15
+ # file at schemas/coverage-vX.Y.schema.json is the canonical artifact
16
+ # consumers should pin to.
17
+ SCHEMA_VERSION = "1.3"
22
18
  SCHEMA_URL = "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v#{SCHEMA_VERSION}.schema.json".freeze
23
19
  private_constant :SCHEMA_VERSION, :SCHEMA_URL
24
20
 
25
21
  class << self
26
22
  def format(result, include_source: true)
27
- {
23
+ document = {
28
24
  :$schema => SCHEMA_URL,
29
25
  :meta => format_meta(result),
30
26
  :total => format_coverage_statistics(result.coverage_statistics),
@@ -32,13 +28,28 @@ module SimpleCov
32
28
  :groups => format_groups(result),
33
29
  :errors => ErrorsFormatter.call(result)
34
30
  }
31
+ add_optional_sections(document, result)
32
+ document
35
33
  end
36
34
 
37
- private
35
+ private
36
+
37
+ # Contexts are present exactly when the result carried a complete map, so
38
+ # consumers can tell "recorded and empty" from "not recorded". History is
39
+ # present only when past runs are recorded, since one point is not a trend,
40
+ # and production coverage only when a store is configured and readable.
41
+ def add_optional_sections(document, result)
42
+ document[:contexts] = result.contexts.contexts if result.contexts
43
+ history = History.entries_with(result)
44
+ document[:history] = history if history.length > 1
45
+ production = ProductionSectionFormatter.call
46
+ document[:production] = production if production
47
+ end
38
48
 
39
49
  def format_files(result, include_source:)
40
50
  result.files.to_h do |source_file|
41
- [source_file.project_filename, SourceFileFormatter.call(source_file, include_source: include_source)]
51
+ [source_file.project_filename,
52
+ SourceFileFormatter.call(source_file, include_source: include_source, contexts: result.contexts)]
42
53
  end
43
54
  end
44
55
 
@@ -52,26 +63,24 @@ module SimpleCov
52
63
  def format_meta(result)
53
64
  {
54
65
  schema_version: SCHEMA_VERSION,
55
- simplecov_version: SimpleCov::VERSION,
56
- command_name: result.command_name,
66
+ simplecov_version: VERSION,
67
+ command_name: result.command_name, command_names: result.command_names,
57
68
  project_name: SimpleCov.project_name,
58
69
  timestamp: result.created_at.iso8601(3),
59
70
  root: SimpleCov.root,
60
71
  commit: git_commit,
61
- primary_coverage: SimpleCov.primary_coverage.to_s
62
- }.merge!(coverage_flags)
72
+ primary_coverage: SimpleCov.primary_coverage.to_s, **coverage_flags
73
+ }
63
74
  end
64
75
 
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.
76
+ # Recorded so tools can recover the exact source a report was generated
77
+ # against, which matters most when `source_in_json false` drops the source
78
+ # text. stderr is captured rather than forwarded so a non-git project doesn't
79
+ # print git's diagnostics to the build.
71
80
  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
81
+ output, status = Open3.capture2e("git", "-C", SimpleCov.root, "rev-parse", "HEAD")
82
+ output.strip if status.success?
83
+ rescue
75
84
  nil
76
85
  end
77
86
 
@@ -83,11 +92,16 @@ module SimpleCov
83
92
  }
84
93
  end
85
94
 
95
+ # A criterion the run did not measure is absent from `statistics` entirely,
96
+ # and gets no section.
86
97
  def format_coverage_statistics(statistics)
87
98
  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]
99
+ line = statistics[:line]
100
+ branch = statistics[:branch]
101
+ method_stat = statistics[:method]
102
+ result[:lines] = format_line_statistic(line) if line
103
+ result[:branches] = format_single_statistic(branch) if branch
104
+ result[:methods] = format_single_statistic(method_stat) if method_stat
91
105
  result
92
106
  end
93
107
 
@@ -3,26 +3,30 @@
3
3
  module SimpleCov
4
4
  module Formatter
5
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
6
  class SourceFileFormatter
10
7
  class << self
11
- def call(source_file, include_source: true)
8
+ def call(source_file, include_source: true, contexts: nil)
12
9
  result = include_source ? format_source_code(source_file) : {} #: Hash[Symbol, untyped]
13
10
  result.merge!(line_coverage_section(source_file)) if SimpleCov.line_coverage?
14
11
  result.merge!(branch_coverage_section(source_file)) if SimpleCov.branch_coverage?
15
12
  result.merge!(method_coverage_section(source_file)) if SimpleCov.method_coverage?
13
+ result.merge!(contexts_section(source_file, contexts)) if contexts
16
14
  result
17
15
  end
18
16
 
19
- private
17
+ private
20
18
 
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.
19
+ # An untouched file gets no key at all: under a present document-level
20
+ # `contexts` array, absence already says "no recorded context executed this
21
+ # file".
22
+ def contexts_section(source_file, contexts)
23
+ bitmaps = contexts.serialized_bitmaps_for(source_file.filename)
24
+ bitmaps.empty? ? {} : {contexts: bitmaps}
25
+ end
26
+
27
+ # No per-line encoding conversion: SourceLoader guarantees every line leaves
28
+ # it as valid UTF-8, and the converter copy this used to make per line was
29
+ # the single largest allocation source in formatting a large report.
26
30
  def format_source_code(source_file)
27
31
  {source: source_file.lines.map { |line| line.src.chomp }}
28
32
  end
@@ -6,17 +6,12 @@ require "json"
6
6
 
7
7
  module SimpleCov
8
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
9
  class JSONFormatter < Base
13
10
  FILENAME = CoverageJSONWriter::FILENAME
14
11
 
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.
12
+ # Callers that need the source array regardless of the global setting (the
13
+ # HTML formatter, which feeds the client-side viewer) pass
14
+ # `include_source: true` explicitly.
20
15
  def self.build_hash(result, include_source: SimpleCov.source_in_json)
21
16
  ResultHashFormatter.format(result, include_source: include_source)
22
17
  end
@@ -26,7 +21,7 @@ module SimpleCov
26
21
  emit_status(result)
27
22
  end
28
23
 
29
- private
24
+ private
30
25
 
31
26
  def message_prefix
32
27
  "JSON "
@@ -39,8 +34,7 @@ module SimpleCov
39
34
  end
40
35
  end
41
36
 
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`.
37
+ # Loaded after the JSONFormatter class is defined so the nested reopen inside
38
+ # result_hash_formatter.rb doesn't create a JSONFormatter < Object before this
39
+ # file gets a chance to declare `JSONFormatter < Base`.
46
40
  require_relative "json_formatter/result_hash_formatter"