simplecov 0.22.0 → 1.3.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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +173 -875
  4. data/exe/simplecov +8 -0
  5. data/lib/minitest/simplecov_plugin.rb +18 -7
  6. data/lib/simplecov/atomic_file.rb +67 -0
  7. data/lib/simplecov/autostart.rb +9 -0
  8. data/lib/simplecov/baseline/parser.rb +58 -0
  9. data/lib/simplecov/baseline.rb +170 -0
  10. data/lib/simplecov/cli/affected/changed_files.rb +57 -0
  11. data/lib/simplecov/cli/affected/selection.rb +117 -0
  12. data/lib/simplecov/cli/affected.rb +148 -0
  13. data/lib/simplecov/cli/annotations.rb +166 -0
  14. data/lib/simplecov/cli/badge/svg.rb +70 -0
  15. data/lib/simplecov/cli/badge.rb +62 -0
  16. data/lib/simplecov/cli/clean.rb +86 -0
  17. data/lib/simplecov/cli/command_helpers.rb +91 -0
  18. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  19. data/lib/simplecov/cli/completions.rb +83 -0
  20. data/lib/simplecov/cli/coverage.rb +83 -0
  21. data/lib/simplecov/cli/coverage_file.rb +109 -0
  22. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  23. data/lib/simplecov/cli/dead_code.rb +139 -0
  24. data/lib/simplecov/cli/diff/output.rb +45 -0
  25. data/lib/simplecov/cli/diff.rb +115 -0
  26. data/lib/simplecov/cli/dotfile.rb +156 -0
  27. data/lib/simplecov/cli/git.rb +41 -0
  28. data/lib/simplecov/cli/history/output.rb +135 -0
  29. data/lib/simplecov/cli/history.rb +67 -0
  30. data/lib/simplecov/cli/merge.rb +115 -0
  31. data/lib/simplecov/cli/open.rb +45 -0
  32. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  33. data/lib/simplecov/cli/patch/output.rb +151 -0
  34. data/lib/simplecov/cli/patch.rb +175 -0
  35. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  36. data/lib/simplecov/cli/ratchet.rb +83 -0
  37. data/lib/simplecov/cli/report.rb +91 -0
  38. data/lib/simplecov/cli/run.rb +35 -0
  39. data/lib/simplecov/cli/serve/report_preparer.rb +30 -0
  40. data/lib/simplecov/cli/serve/static_file_handler.rb +128 -0
  41. data/lib/simplecov/cli/serve.rb +88 -0
  42. data/lib/simplecov/cli/show/annotator.rb +87 -0
  43. data/lib/simplecov/cli/show/sweep.rb +36 -0
  44. data/lib/simplecov/cli/show.rb +133 -0
  45. data/lib/simplecov/cli/status/facts.rb +87 -0
  46. data/lib/simplecov/cli/status.rb +93 -0
  47. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  48. data/lib/simplecov/cli/tests.rb +148 -0
  49. data/lib/simplecov/cli/uncovered/misses.rb +33 -0
  50. data/lib/simplecov/cli/uncovered.rb +127 -0
  51. data/lib/simplecov/cli/usage.rb +153 -0
  52. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  53. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  54. data/lib/simplecov/cli/watch/poller.rb +45 -0
  55. data/lib/simplecov/cli/watch/session.rb +134 -0
  56. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  57. data/lib/simplecov/cli/watch.rb +85 -0
  58. data/lib/simplecov/cli.rb +114 -0
  59. data/lib/simplecov/color.rb +65 -0
  60. data/lib/simplecov/combine/branches_combiner.rb +44 -21
  61. data/lib/simplecov/combine/coverage_accumulator.rb +175 -0
  62. data/lib/simplecov/combine/identity_interner.rb +27 -0
  63. data/lib/simplecov/combine/interned_counts.rb +28 -0
  64. data/lib/simplecov/combine/lines_combiner.rb +35 -29
  65. data/lib/simplecov/combine/methods_combiner.rb +48 -0
  66. data/lib/simplecov/combine/results_combiner.rb +9 -49
  67. data/lib/simplecov/combine.rb +0 -24
  68. data/lib/simplecov/command_guesser.rb +99 -42
  69. data/lib/simplecov/configuration/baseline.rb +31 -0
  70. data/lib/simplecov/configuration/coverage.rb +184 -0
  71. data/lib/simplecov/configuration/coverage_criteria.rb +143 -0
  72. data/lib/simplecov/configuration/deprecations.rb +21 -0
  73. data/lib/simplecov/configuration/eval_coverage.rb +37 -0
  74. data/lib/simplecov/configuration/filters.rb +131 -0
  75. data/lib/simplecov/configuration/formatting.rb +129 -0
  76. data/lib/simplecov/configuration/groups.rb +48 -0
  77. data/lib/simplecov/configuration/history.rb +44 -0
  78. data/lib/simplecov/configuration/ignored_entries.rb +72 -0
  79. data/lib/simplecov/configuration/merging.rb +197 -0
  80. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  81. data/lib/simplecov/configuration/production.rb +23 -0
  82. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  83. data/lib/simplecov/configuration/thresholds.rb +150 -0
  84. data/lib/simplecov/configuration/view_coverage.rb +37 -0
  85. data/lib/simplecov/configuration.rb +100 -443
  86. data/lib/simplecov/configuration_error.rb +5 -0
  87. data/lib/simplecov/context_map/union.rb +81 -0
  88. data/lib/simplecov/context_map.rb +187 -0
  89. data/lib/simplecov/coverage_json.rb +21 -0
  90. data/lib/simplecov/coverage_statistics.rb +24 -28
  91. data/lib/simplecov/coverage_violations.rb +254 -0
  92. data/lib/simplecov/current_run.rb +61 -0
  93. data/lib/simplecov/defaults.rb +20 -28
  94. data/lib/simplecov/deprecation.rb +63 -0
  95. data/lib/simplecov/directive/erb.rb +44 -0
  96. data/lib/simplecov/directive/haml.rb +33 -0
  97. data/lib/simplecov/directive/indented_template.rb +37 -0
  98. data/lib/simplecov/directive/slim.rb +35 -0
  99. data/lib/simplecov/directive/template.rb +29 -0
  100. data/lib/simplecov/directive.rb +152 -0
  101. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  102. data/lib/simplecov/exit_codes/check.rb +39 -0
  103. data/lib/simplecov/exit_codes/exit_code_handling.rb +24 -5
  104. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +17 -67
  105. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  106. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  107. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +27 -0
  108. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +20 -38
  109. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +28 -0
  110. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -34
  111. data/lib/simplecov/exit_codes.rb +26 -0
  112. data/lib/simplecov/exit_handling.rb +163 -0
  113. data/lib/simplecov/file_list.rb +63 -48
  114. data/lib/simplecov/filter.rb +98 -42
  115. data/lib/simplecov/formatter/base.rb +100 -0
  116. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  117. data/lib/simplecov/formatter/coverage_json_writer.rb +95 -0
  118. data/lib/simplecov/formatter/html_formatter/public/index.html +93 -0
  119. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +153 -0
  120. data/lib/simplecov/formatter/html_formatter.rb +88 -0
  121. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +122 -0
  122. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  123. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +132 -0
  124. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +97 -0
  125. data/lib/simplecov/formatter/json_formatter.rb +40 -0
  126. data/lib/simplecov/formatter/multi_formatter.rb +8 -10
  127. data/lib/simplecov/formatter/simple_formatter.rb +12 -15
  128. data/lib/simplecov/formatter.rb +21 -1
  129. data/lib/simplecov/group_names.rb +33 -0
  130. data/lib/simplecov/history.rb +115 -0
  131. data/lib/simplecov/last_run.rb +16 -5
  132. data/lib/simplecov/lines_classifier.rb +67 -19
  133. data/lib/simplecov/load_global_config.rb +12 -4
  134. data/lib/simplecov/parallel_adapters/base.rb +50 -0
  135. data/lib/simplecov/parallel_adapters/generic.rb +33 -0
  136. data/lib/simplecov/parallel_adapters/parallel_tests.rb +72 -0
  137. data/lib/simplecov/parallel_adapters.rb +54 -0
  138. data/lib/simplecov/parallel_coordination.rb +124 -0
  139. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  140. data/lib/simplecov/parallel_result_merger.rb +171 -0
  141. data/lib/simplecov/process.rb +34 -12
  142. data/lib/simplecov/production/error.rb +11 -0
  143. data/lib/simplecov/production/file_sink.rb +130 -0
  144. data/lib/simplecov/production.rb +249 -0
  145. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  146. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  147. data/lib/simplecov/profiles/rails.rb +13 -10
  148. data/lib/simplecov/profiles/root_filter.rb +2 -5
  149. data/lib/simplecov/profiles/strict.rb +22 -0
  150. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  151. data/lib/simplecov/profiles.rb +26 -19
  152. data/lib/simplecov/report_deferral.rb +45 -0
  153. data/lib/simplecov/report_stamp.rb +25 -0
  154. data/lib/simplecov/result/filter_config.rb +19 -0
  155. data/lib/simplecov/result/missing_source_files_reporter.rb +48 -0
  156. data/lib/simplecov/result/serialization.rb +43 -0
  157. data/lib/simplecov/result/source_file_builder.rb +46 -0
  158. data/lib/simplecov/result.rb +123 -47
  159. data/lib/simplecov/result_adapter.rb +130 -11
  160. data/lib/simplecov/result_merger/contexts.rb +22 -0
  161. data/lib/simplecov/result_merger/legacy_format_adapter.rb +24 -0
  162. data/lib/simplecov/result_merger/resultset_file.rb +67 -0
  163. data/lib/simplecov/result_merger/resultset_run_identity.rb +65 -0
  164. data/lib/simplecov/result_merger/resultset_store.rb +62 -0
  165. data/lib/simplecov/result_merger/unloaded_files.rb +95 -0
  166. data/lib/simplecov/result_merger.rb +120 -124
  167. data/lib/simplecov/result_processing.rb +188 -0
  168. data/lib/simplecov/run_identity.rb +75 -0
  169. data/lib/simplecov/simulate_coverage.rb +72 -17
  170. data/lib/simplecov/source_file/branch.rb +8 -35
  171. data/lib/simplecov/source_file/branch_builder.rb +95 -0
  172. data/lib/simplecov/source_file/builder_context.rb +28 -0
  173. data/lib/simplecov/source_file/line.rb +17 -32
  174. data/lib/simplecov/source_file/line_builder.rb +37 -0
  175. data/lib/simplecov/source_file/method.rb +54 -0
  176. data/lib/simplecov/source_file/method_builder.rb +51 -0
  177. data/lib/simplecov/source_file/ruby_data_parser.rb +103 -0
  178. data/lib/simplecov/source_file/skip_chunks.rb +72 -0
  179. data/lib/simplecov/source_file/source_loader.rb +89 -0
  180. data/lib/simplecov/source_file/statistics.rb +76 -0
  181. data/lib/simplecov/source_file.rb +64 -260
  182. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +155 -0
  183. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +191 -0
  184. data/lib/simplecov/static_coverage_extractor/method_collector.rb +45 -0
  185. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +46 -0
  186. data/lib/simplecov/static_coverage_extractor/value_position.rb +61 -0
  187. data/lib/simplecov/static_coverage_extractor/visitor.rb +163 -0
  188. data/lib/simplecov/static_coverage_extractor.rb +78 -0
  189. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  190. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  191. data/lib/simplecov/test_tracker/delta.rb +69 -0
  192. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  193. data/lib/simplecov/test_tracker.rb +162 -0
  194. data/lib/simplecov/unloaded_file_injector.rb +62 -0
  195. data/lib/simplecov/useless_results_remover.rb +17 -7
  196. data/lib/simplecov/version.rb +1 -1
  197. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  198. data/lib/simplecov/view_coverage.rb +55 -0
  199. data/lib/simplecov-html.rb +5 -0
  200. data/lib/simplecov.rb +143 -394
  201. data/lib/simplecov_json_formatter.rb +5 -0
  202. data/man/simplecov.1 +354 -0
  203. data/sig/simplecov.rbs +1710 -0
  204. metadata +183 -61
  205. data/CHANGELOG.md +0 -191
  206. data/doc/alternate-formatters.md +0 -71
  207. data/doc/commercial-services.md +0 -25
  208. data/doc/editor-integration.md +0 -18
  209. data/lib/simplecov/combine/files_combiner.rb +0 -24
  210. data/lib/simplecov/default_formatter.rb +0 -20
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module Formatter
5
+ class JSONFormatter
6
+ class ErrorsFormatter
7
+ CRITERION_KEYS = {line: :lines, branch: :branches, method: :methods}.freeze
8
+ private_constant :CRITERION_KEYS
9
+
10
+ class << self
11
+ def call(result)
12
+ errors = {} #: Hash[Symbol, Hash[untyped, untyped]]
13
+ format_minimum_overall(result, errors)
14
+ format_minimum_by_file(result, errors)
15
+ format_baseline(result, errors)
16
+ format_minimum_by_group(result, errors)
17
+ format_maximum_overall(result, errors)
18
+ format_maximum_drop(result, errors)
19
+ format_maximum_missed(result, errors)
20
+ format_maximum_missed_per_file(result, errors)
21
+ errors
22
+ end
23
+
24
+ private
25
+
26
+ def format_minimum_overall(result, errors)
27
+ CoverageViolations.minimum_overall(result, SimpleCov.minimum_coverage).each do |violation|
28
+ bucket(errors, :minimum_coverage)[key_for(violation)] = expected_actual(violation)
29
+ end
30
+ end
31
+
32
+ def format_minimum_by_file(result, errors)
33
+ violations = CoverageViolations.minimum_by_file(
34
+ result, SimpleCov.minimum_coverage_by_file, SimpleCov.minimum_coverage_by_file_overrides,
35
+ baseline: SimpleCov.baseline
36
+ )
37
+ violations.each { |violation| record_by_file(violation, errors) }
38
+ end
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
+
54
+ def record_by_file(violation, errors)
55
+ by_file = bucket(errors, :minimum_coverage_by_file)
56
+ file_bucket = by_file[violation.fetch(:project_filename)] ||= {} #: Hash[untyped, untyped]
57
+ file_bucket[key_for(violation)] = expected_actual(violation)
58
+ end
59
+
60
+ def format_minimum_by_group(result, errors)
61
+ violations = CoverageViolations.minimum_by_group(result, SimpleCov.minimum_coverage_by_group)
62
+ return if violations.empty?
63
+
64
+ by_group = bucket(errors, :minimum_coverage_by_group)
65
+ violations.each do |violation|
66
+ group_bucket = by_group[violation.fetch(:group_name)] ||= {} #: Hash[untyped, untyped]
67
+ group_bucket[key_for(violation)] = expected_actual(violation)
68
+ end
69
+ end
70
+
71
+ def format_maximum_overall(result, errors)
72
+ CoverageViolations.maximum_overall(result, SimpleCov.maximum_coverage).each do |violation|
73
+ bucket(errors, :maximum_coverage)[key_for(violation)] = expected_actual(violation)
74
+ end
75
+ end
76
+
77
+ def format_maximum_drop(result, errors)
78
+ CoverageViolations.maximum_drop(result, SimpleCov.maximum_coverage_drop).each do |violation|
79
+ bucket(errors, :maximum_coverage_drop)[key_for(violation)] = maximum_actual(violation)
80
+ end
81
+ end
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.
103
+ def bucket(errors, name)
104
+ errors[name] ||= {} #: Hash[untyped, untyped]
105
+ end
106
+
107
+ def key_for(violation)
108
+ CRITERION_KEYS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion)))
109
+ end
110
+
111
+ def expected_actual(violation)
112
+ {expected: violation.fetch(:expected), actual: violation.fetch(:actual)}
113
+ end
114
+
115
+ def maximum_actual(violation)
116
+ {maximum: violation.fetch(:maximum), actual: violation.fetch(:actual)}
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ 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
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "time"
5
+ require_relative "errors_formatter"
6
+ require_relative "production_section_formatter"
7
+ require_relative "source_file_formatter"
8
+
9
+ module SimpleCov
10
+ module Formatter
11
+ class JSONFormatter
12
+ class ResultHashFormatter
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"
18
+ SCHEMA_URL = "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v#{SCHEMA_VERSION}.schema.json".freeze
19
+ private_constant :SCHEMA_VERSION, :SCHEMA_URL
20
+
21
+ class << self
22
+ def format(result, include_source: true)
23
+ document = {
24
+ :$schema => SCHEMA_URL,
25
+ :meta => format_meta(result),
26
+ :total => format_coverage_statistics(result.coverage_statistics),
27
+ :coverage => format_files(result, include_source: include_source),
28
+ :groups => format_groups(result),
29
+ :errors => ErrorsFormatter.call(result)
30
+ }
31
+ add_optional_sections(document, result)
32
+ document
33
+ end
34
+
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
48
+
49
+ def format_files(result, include_source:)
50
+ result.files.to_h do |source_file|
51
+ [source_file.project_filename,
52
+ SourceFileFormatter.call(source_file, include_source: include_source, contexts: result.contexts)]
53
+ end
54
+ end
55
+
56
+ def format_groups(result)
57
+ result.groups.to_h do |name, file_list|
58
+ stats = format_coverage_statistics(file_list.coverage_statistics)
59
+ [name, stats.merge(files: file_list.map(&:project_filename))]
60
+ end
61
+ end
62
+
63
+ def format_meta(result)
64
+ {
65
+ schema_version: SCHEMA_VERSION,
66
+ simplecov_version: VERSION,
67
+ command_name: result.command_name, command_names: result.command_names,
68
+ project_name: SimpleCov.project_name,
69
+ timestamp: result.created_at.iso8601(3),
70
+ root: SimpleCov.root,
71
+ commit: git_commit,
72
+ primary_coverage: SimpleCov.primary_coverage.to_s, **coverage_flags
73
+ }
74
+ end
75
+
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.
80
+ def git_commit
81
+ output, status = Open3.capture2e("git", "-C", SimpleCov.root, "rev-parse", "HEAD")
82
+ output.strip if status.success?
83
+ rescue
84
+ nil
85
+ end
86
+
87
+ def coverage_flags
88
+ {
89
+ line_coverage: SimpleCov.line_coverage?,
90
+ branch_coverage: SimpleCov.branch_coverage?,
91
+ method_coverage: SimpleCov.method_coverage?
92
+ }
93
+ end
94
+
95
+ # A criterion the run did not measure is absent from `statistics` entirely,
96
+ # and gets no section.
97
+ def format_coverage_statistics(statistics)
98
+ result = {} #: Hash[Symbol, untyped]
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
105
+ result
106
+ end
107
+
108
+ def format_line_statistic(stat)
109
+ {
110
+ covered: stat.covered,
111
+ missed: stat.missed,
112
+ omitted: stat.omitted,
113
+ total: stat.total,
114
+ percent: stat.percent,
115
+ strength: stat.strength
116
+ }
117
+ end
118
+
119
+ def format_single_statistic(stat)
120
+ {
121
+ covered: stat.covered,
122
+ missed: stat.missed,
123
+ total: stat.total,
124
+ percent: stat.percent,
125
+ strength: stat.strength
126
+ }
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module Formatter
5
+ class JSONFormatter
6
+ class SourceFileFormatter
7
+ class << self
8
+ def call(source_file, include_source: true, contexts: nil)
9
+ result = include_source ? format_source_code(source_file) : {} #: Hash[Symbol, untyped]
10
+ result.merge!(line_coverage_section(source_file)) if SimpleCov.line_coverage?
11
+ result.merge!(branch_coverage_section(source_file)) if SimpleCov.branch_coverage?
12
+ result.merge!(method_coverage_section(source_file)) if SimpleCov.method_coverage?
13
+ result.merge!(contexts_section(source_file, contexts)) if contexts
14
+ result
15
+ end
16
+
17
+ private
18
+
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.
30
+ def format_source_code(source_file)
31
+ {source: source_file.lines.map { |line| line.src.chomp }}
32
+ end
33
+
34
+ def line_coverage_section(source_file)
35
+ covered = source_file.covered_lines.size
36
+ missed = source_file.missed_lines.size
37
+ {
38
+ lines: source_file.lines.map { |line| format_line(line) },
39
+ lines_covered_percent: source_file.covered_percent,
40
+ covered_lines: covered,
41
+ missed_lines: missed,
42
+ omitted_lines: source_file.never_lines.size,
43
+ total_lines: covered + missed
44
+ }
45
+ end
46
+
47
+ def branch_coverage_section(source_file)
48
+ {
49
+ branches: source_file.branches.map { |branch| format_branch(branch) },
50
+ branches_covered_percent: source_file.covered_percent(:branch),
51
+ covered_branches: source_file.covered_branches.size,
52
+ missed_branches: source_file.missed_branches.size,
53
+ total_branches: source_file.total_branches.size
54
+ }
55
+ end
56
+
57
+ def method_coverage_section(source_file)
58
+ covered = source_file.covered_methods.size
59
+ missed = source_file.missed_methods.size
60
+
61
+ {
62
+ methods: source_file.methods.map { |method| format_method(method) },
63
+ methods_covered_percent: source_file.covered_percent(:method),
64
+ covered_methods: covered,
65
+ missed_methods: missed,
66
+ total_methods: covered + missed
67
+ }
68
+ end
69
+
70
+ def format_line(line)
71
+ line.skipped? ? "ignored" : line.coverage
72
+ end
73
+
74
+ def format_branch(branch)
75
+ {
76
+ type: branch.type,
77
+ start_line: branch.start_line,
78
+ end_line: branch.end_line,
79
+ coverage: format_line(branch),
80
+ inline: branch.inline?,
81
+ report_line: branch.report_line
82
+ }
83
+ end
84
+
85
+ def format_method(method)
86
+ {
87
+ name: method.to_s,
88
+ start_line: method.start_line,
89
+ end_line: method.end_line,
90
+ coverage: format_line(method)
91
+ }
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,40 @@
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
+ class JSONFormatter < Base
10
+ FILENAME = CoverageJSONWriter::FILENAME
11
+
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.
15
+ def self.build_hash(result, include_source: SimpleCov.source_in_json)
16
+ ResultHashFormatter.format(result, include_source: include_source)
17
+ end
18
+
19
+ def format(result)
20
+ CoverageJSONWriter.write(output_path, self.class.build_hash(result), result)
21
+ emit_status(result)
22
+ end
23
+
24
+ private
25
+
26
+ def message_prefix
27
+ "JSON "
28
+ end
29
+
30
+ def entry_point_filename
31
+ FILENAME
32
+ end
33
+ end
34
+ end
35
+ end
36
+
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`.
40
+ require_relative "json_formatter/result_hash_formatter"
@@ -6,27 +6,25 @@ module SimpleCov
6
6
  module InstanceMethods
7
7
  def format(result)
8
8
  formatters.map do |formatter|
9
- formatter.new.format(result)
10
- rescue StandardError => e
11
- warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
12
- nil
9
+ Formatter.format(formatter, result)
10
+ rescue => e
11
+ warn("Formatter #{formatter} failed with #{e.class}: #{e} (#{(_ = e.backtrace).first})")
13
12
  end
14
13
  end
15
14
  end
16
15
 
16
+ # Normalized eagerly and captured in the closure. `Array()` is pure for every
17
+ # accepted input shape, so this is equivalent to the historical lazy
18
+ # per-instance memoization.
17
19
  def self.new(formatters = nil)
20
+ formatter_list = Array(formatters)
18
21
  Class.new do
19
22
  define_method :formatters do
20
- @formatters ||= Array(formatters)
23
+ formatter_list
21
24
  end
22
25
  include InstanceMethods
23
26
  end
24
27
  end
25
-
26
- def self.[](*args)
27
- warn "#{Kernel.caller.first}: [DEPRECATION] ::[] is deprecated. Use ::new instead."
28
- new(Array(args))
29
- end
30
28
  end
31
29
  end
32
30
  end
@@ -2,23 +2,20 @@
2
2
 
3
3
  module SimpleCov
4
4
  module Formatter
5
- #
6
- # A ridiculously simple formatter for SimpleCov results.
7
- #
8
5
  class SimpleFormatter
9
- # Takes a SimpleCov::Result and generates a string out of it
10
6
  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
7
+ criterion = SimpleCov.coverage_statistics_key(SimpleCov.primary_coverage)
8
+ result.groups.map { |name, files| format_group(name, files, criterion) }.join
9
+ end
10
+
11
+ private
12
+
13
+ def format_group(name, files, criterion)
14
+ header = "Group: #{name}\n#{"=" * 40}\n"
15
+ body = files.map do |file|
16
+ "#{file.filename} (coverage: #{(_ = file.covered_percent(criterion)).floor(2)}%)\n"
17
+ end.join
18
+ "#{header}#{body}\n"
22
19
  end
23
20
  end
24
21
  end
@@ -1,10 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- # TODO: Documentation on how to build your own formatters
5
4
  module Formatter
5
+ # Formatters are configured either as classes, instantiated fresh for every
6
+ # report, or as ready-built instances, the only way to reach constructor
7
+ # options like `HTMLFormatter.new(silent: true)` (#1240).
8
+ def self.instance_for(formatter)
9
+ instantiable?(formatter) ? formatter.new : formatter
10
+ end
11
+
12
+ # mutant:disable
13
+ # `Class` cannot itself be subclassed, so `instance_of?(Class)` answers for
14
+ # every object exactly as `is_a?(Class)` does: a difference no example can
15
+ # show, which is why it is disabled here rather than pinned by a test.
16
+ def self.instantiable?(formatter)
17
+ formatter.is_a?(Class)
18
+ end
19
+ private_class_method :instantiable?
20
+
21
+ def self.format(formatter, result)
22
+ instance_for(formatter).format(result)
23
+ end
6
24
  end
7
25
  end
8
26
 
9
27
  require_relative "formatter/simple_formatter"
10
28
  require_relative "formatter/multi_formatter"
29
+ require_relative "formatter/json_formatter"
30
+ require_relative "formatter/baseline_formatter"
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module GroupNames
5
+ UNGROUPED = "Ungrouped"
6
+
7
+ extend self
8
+
9
+ # Group names are Hash keys, report labels, and JSON object keys all at once,
10
+ # so they are normalized to Strings up front: a Symbol spelling means the
11
+ # String, and anything else has no sensible serialized form. Normalizing
12
+ # before `validate!` also keeps `group :Ungrouped` from slipping past the
13
+ # reservation below.
14
+ def normalize(group_name)
15
+ case group_name
16
+ when String then group_name
17
+ when Symbol then group_name.to_s
18
+ else
19
+ raise ConfigurationError,
20
+ "Group names must be Strings, got #{group_name.inspect} (#{group_name.class})"
21
+ end
22
+ end
23
+
24
+ # Each name in turn, rather than asking the collection: a String asked
25
+ # whether it "includes" the reserved name answers about its own characters.
26
+ def validate!(group_names)
27
+ return group_names unless group_names.any? { |name| name.eql?(UNGROUPED) }
28
+
29
+ raise ConfigurationError,
30
+ "#{UNGROUPED.inspect} is reserved for files that do not match a configured group"
31
+ end
32
+ end
33
+ end