simplecov 1.0.2 → 1.1.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -1425
  3. data/lib/simplecov/atomic_file.rb +70 -0
  4. data/lib/simplecov/cli/clean.rb +43 -3
  5. data/lib/simplecov/cli/command_helpers.rb +55 -0
  6. data/lib/simplecov/cli/coverage.rb +21 -28
  7. data/lib/simplecov/cli/coverage_file.rb +65 -0
  8. data/lib/simplecov/cli/diff.rb +43 -48
  9. data/lib/simplecov/cli/dotfile.rb +12 -6
  10. data/lib/simplecov/cli/merge.rb +12 -10
  11. data/lib/simplecov/cli/open.rb +3 -5
  12. data/lib/simplecov/cli/report.rb +31 -23
  13. data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
  14. data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
  15. data/lib/simplecov/cli/serve.rb +30 -94
  16. data/lib/simplecov/cli/uncovered.rb +20 -25
  17. data/lib/simplecov/cli.rb +15 -2
  18. data/lib/simplecov/combine/branches_combiner.rb +43 -19
  19. data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
  20. data/lib/simplecov/combine/identity_interner.rb +30 -0
  21. data/lib/simplecov/combine/interned_counts.rb +30 -0
  22. data/lib/simplecov/combine/lines_combiner.rb +48 -20
  23. data/lib/simplecov/combine/methods_combiner.rb +46 -21
  24. data/lib/simplecov/combine/results_combiner.rb +12 -37
  25. data/lib/simplecov/combine.rb +5 -23
  26. data/lib/simplecov/command_guesser.rb +66 -9
  27. data/lib/simplecov/configuration/coverage.rb +12 -15
  28. data/lib/simplecov/configuration/coverage_criteria.rb +34 -38
  29. data/lib/simplecov/configuration/eval_coverage.rb +41 -0
  30. data/lib/simplecov/configuration/filters.rb +11 -44
  31. data/lib/simplecov/configuration/formatting.rb +18 -9
  32. data/lib/simplecov/configuration/groups.rb +42 -0
  33. data/lib/simplecov/configuration/merging.rb +8 -7
  34. data/lib/simplecov/configuration/thresholds.rb +14 -13
  35. data/lib/simplecov/configuration.rb +13 -43
  36. data/lib/simplecov/coverage_json.rb +24 -0
  37. data/lib/simplecov/coverage_statistics.rb +1 -1
  38. data/lib/simplecov/coverage_violations.rb +15 -5
  39. data/lib/simplecov/defaults.rb +7 -3
  40. data/lib/simplecov/directive.rb +1 -1
  41. data/lib/simplecov/exit_codes/check.rb +33 -0
  42. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +11 -20
  43. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +3 -16
  44. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +14 -23
  45. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +14 -25
  46. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +4 -17
  47. data/lib/simplecov/exit_codes.rb +1 -0
  48. data/lib/simplecov/exit_handling.rb +11 -41
  49. data/lib/simplecov/file_list.rb +5 -10
  50. data/lib/simplecov/filter.rb +43 -9
  51. data/lib/simplecov/formatter/base.rb +11 -0
  52. data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
  53. data/lib/simplecov/formatter/html_formatter/public/index.html +35 -5
  54. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
  55. data/lib/simplecov/formatter/html_formatter.rb +67 -47
  56. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -56
  57. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +78 -88
  58. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +70 -76
  59. data/lib/simplecov/formatter/json_formatter.rb +5 -48
  60. data/lib/simplecov/formatter/multi_formatter.rb +1 -1
  61. data/lib/simplecov/formatter/simple_formatter.rb +8 -5
  62. data/lib/simplecov/formatter.rb +12 -0
  63. data/lib/simplecov/group_names.rb +32 -0
  64. data/lib/simplecov/last_run.rb +16 -9
  65. data/lib/simplecov/lines_classifier.rb +29 -8
  66. data/lib/simplecov/load_global_config.rb +5 -2
  67. data/lib/simplecov/parallel_adapters/base.rb +17 -0
  68. data/lib/simplecov/parallel_adapters/generic.rb +2 -2
  69. data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
  70. data/lib/simplecov/parallel_coordination.rb +6 -1
  71. data/lib/simplecov/parallel_result_merger.rb +230 -0
  72. data/lib/simplecov/report_deferral.rb +49 -0
  73. data/lib/simplecov/report_stamp.rb +28 -0
  74. data/lib/simplecov/result.rb +40 -10
  75. data/lib/simplecov/result_adapter.rb +48 -16
  76. data/lib/simplecov/result_merger/resultset_file.rb +43 -7
  77. data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
  78. data/lib/simplecov/result_merger/resultset_store.rb +15 -12
  79. data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
  80. data/lib/simplecov/result_merger.rb +69 -42
  81. data/lib/simplecov/result_processing.rb +82 -43
  82. data/lib/simplecov/run_identity.rb +77 -0
  83. data/lib/simplecov/simulate_coverage.rb +35 -11
  84. data/lib/simplecov/source_file/method.rb +7 -1
  85. data/lib/simplecov/source_file/ruby_data_parser.rb +25 -3
  86. data/lib/simplecov/source_file/skip_chunks.rb +7 -10
  87. data/lib/simplecov/source_file/source_loader.rb +23 -7
  88. data/lib/simplecov/source_file/statistics.rb +24 -16
  89. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +203 -13
  90. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +19 -30
  91. data/lib/simplecov/static_coverage_extractor/method_collector.rb +7 -0
  92. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
  93. data/lib/simplecov/static_coverage_extractor/value_position.rb +6 -14
  94. data/lib/simplecov/static_coverage_extractor/visitor.rb +23 -36
  95. data/lib/simplecov/unloaded_file_injector.rb +75 -0
  96. data/lib/simplecov/version.rb +1 -1
  97. data/lib/simplecov.rb +11 -5
  98. data/schemas/coverage-v1.0.schema.json +2 -2
  99. data/schemas/coverage.schema.json +2 -2
  100. data/sig/simplecov.rbs +206 -67
  101. metadata +28 -14
  102. data/doc/alternate-formatters.md +0 -66
  103. data/doc/commercial-services.md +0 -25
  104. data/doc/editor-integration.md +0 -18
  105. data/lib/simplecov/combine/files_combiner.rb +0 -70
  106. data/lib/simplecov/formatter/html_formatter/public/application.css +0 -1
  107. data/lib/simplecov/formatter/html_formatter/public/application.js +0 -18
  108. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  109. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  110. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
@@ -3,47 +3,56 @@
3
3
  require "fileutils"
4
4
  require "json"
5
5
  require_relative "base"
6
+ require_relative "../atomic_file"
7
+ require_relative "../coverage_json"
6
8
  require_relative "json_formatter"
7
9
 
8
10
  module SimpleCov
9
11
  module Formatter
10
- # Generates an HTML coverage report by writing a coverage_data.js file
11
- # alongside pre-compiled static assets (index.html, application.js/css).
12
- # Uses JSONFormatter.build_hash to serialize the result, then writes both
13
- # coverage.json and coverage_data.js from the same in-memory hash.
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.
14
18
  class HTMLFormatter < Base
15
- DATA_FILENAME = "coverage_data.js"
19
+ # Placeholder in the compiled template where the report's data goes.
20
+ DATA_MARKER = "<!-- SIMPLECOV_COVERAGE_DATA -->"
16
21
 
17
- def format(result)
18
- # `coverage_data.js` feeds the client-side viewer, which renders
19
- # source from the embedded array it always needs `source`,
20
- # regardless of `SimpleCov.source_in_json`. The side-file
21
- # `coverage.json` honors the setting so downstream tools that
22
- # read source from disk can opt into a smaller payload. When
23
- # the setting is at its default (true), the two files share a
24
- # single serialization.
25
- FileUtils.mkdir_p(output_path)
26
- viewer_json = JSON.pretty_generate(JSONFormatter.build_hash(result, include_source: true))
27
- coverage_json = SimpleCov.source_in_json ? viewer_json : JSON.pretty_generate(JSONFormatter.build_hash(result))
28
-
29
- atomic_write(File.join(output_path, JSONFormatter::FILENAME), coverage_json)
30
- atomic_write(File.join(output_path, DATA_FILENAME), "window.SIMPLECOV_DATA = #{viewer_json};\n")
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
31
30
 
32
- copy_static_assets
33
- # stderr, not stdout: this is a status message, not the program's
34
- # output. Keeps the line out of pipelines like `rspec -f json`. And
35
- # $stderr.puts, not `warn`: a status line should not reach
36
- # `Warning.warn` hooks or vanish under `-W0` (see #1225).
37
- $stderr.puts output_message(result) unless @silent # rubocop:disable Style/StderrPuts
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)
38
45
  end
39
46
 
40
47
  # Generate HTML from a pre-existing coverage.json file without
41
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.
42
52
  def format_from_json(json_path, output_dir)
43
- FileUtils.mkdir_p(output_dir)
44
- json = File.read(json_path)
45
- atomic_write(File.join(output_dir, DATA_FILENAME), "window.SIMPLECOV_DATA = #{json};\n")
46
- copy_static_assets(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)
47
56
  end
48
57
 
49
58
  private
@@ -52,27 +61,36 @@ module SimpleCov
52
61
  "index.html"
53
62
  end
54
63
 
55
- def copy_static_assets(dest_dir = output_path)
56
- Dir[File.join(public_dir, "*")].each do |src|
57
- atomic_write(File.join(dest_dir, File.basename(src)), File.binread(src))
58
- end
64
+ def source_less_hash(hash)
65
+ coverage = hash.fetch(:coverage).transform_values { |file| file.except(:source) }
66
+ hash.merge(coverage: coverage)
59
67
  end
60
68
 
61
- # Write `content` at `dest` via a uniquely-named temp file in the
62
- # same directory, then `File.rename` onto the final path. rename is
63
- # atomic and overwrite-safe, so:
64
- # - parallel writers can't race on an unlink-then-write window, and
65
- # - read-only existing files (e.g. assets shipped at 0444 from
66
- # /nix/store) are replaced cleanly instead of triggering EACCES
67
- # from opening the existing path for writing.
68
- def atomic_write(dest, content)
69
- temp = "#{dest}.#{Process.pid}.#{rand(2**32).to_s(36)}"
70
- begin
71
- File.binwrite(temp, content)
72
- File.rename(temp, dest)
73
- ensure
74
- FileUtils.rm_f(temp)
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"
75
90
  end
91
+
92
+ data_script = "<script>window.SIMPLECOV_DATA = #{json.gsub('<') { '\u003c' }};</script>"
93
+ template.sub(DATA_MARKER) { data_script }
76
94
  end
77
95
 
78
96
  def public_dir
@@ -83,3 +101,5 @@ module SimpleCov
83
101
  end
84
102
  end
85
103
  end
104
+
105
+ require_relative "html_formatter/viewer_data_validator"
@@ -12,77 +12,75 @@ module SimpleCov
12
12
  CRITERION_KEYS = {line: :lines, branch: :branches, method: :methods}.freeze
13
13
  private_constant :CRITERION_KEYS
14
14
 
15
- def initialize(result)
16
- @result = result
17
- @errors = {}
18
- end
19
-
20
- def call
21
- format_minimum_overall
22
- format_minimum_by_file
23
- format_minimum_by_group
24
- format_maximum_overall
25
- format_maximum_drop
26
- @errors
27
- end
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
28
25
 
29
- private
26
+ private
30
27
 
31
- def format_minimum_overall
32
- SimpleCov::CoverageViolations.minimum_overall(@result, SimpleCov.minimum_coverage).each do |violation|
33
- bucket(:minimum_coverage)[key_for(violation)] = expected_actual(violation)
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
34
32
  end
35
- end
36
33
 
37
- def format_minimum_by_file
38
- violations = SimpleCov::CoverageViolations.minimum_by_file(
39
- @result, SimpleCov.minimum_coverage_by_file, SimpleCov.minimum_coverage_by_file_overrides
40
- )
41
- violations.each { |violation| record_by_file(violation) }
42
- end
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
43
40
 
44
- def record_by_file(violation)
45
- by_file = bucket(:minimum_coverage_by_file)
46
- file_bucket = by_file[violation.fetch(:project_filename)] ||= {} #: Hash[untyped, untyped]
47
- file_bucket[key_for(violation)] = expected_actual(violation)
48
- end
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
49
46
 
50
- def format_minimum_by_group
51
- violations = SimpleCov::CoverageViolations.minimum_by_group(@result, SimpleCov.minimum_coverage_by_group)
52
- return if violations.empty?
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?
53
50
 
54
- # `bucket` lazily creates the errors key, so only touch it when
55
- # there is a violation to record.
56
- by_group = bucket(:minimum_coverage_by_group)
57
- violations.each do |violation|
58
- group_bucket = by_group[violation.fetch(:group_name)] ||= {} #: Hash[untyped, untyped]
59
- group_bucket[key_for(violation)] = expected_actual(violation)
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
60
58
  end
61
- end
62
59
 
63
- def format_maximum_overall
64
- SimpleCov::CoverageViolations.maximum_overall(@result, SimpleCov.maximum_coverage).each do |violation|
65
- bucket(:maximum_coverage)[key_for(violation)] = expected_actual(violation)
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
66
64
  end
67
- end
68
65
 
69
- def format_maximum_drop
70
- SimpleCov::CoverageViolations.maximum_drop(@result, SimpleCov.maximum_coverage_drop).each do |violation|
71
- bucket(:maximum_coverage_drop)[key_for(violation)] =
72
- {maximum: violation.fetch(:maximum), actual: violation.fetch(:actual)}
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
73
71
  end
74
- end
75
72
 
76
- def bucket(name)
77
- @errors[name] ||= {} #: Hash[untyped, untyped]
78
- end
73
+ def bucket(errors, name)
74
+ errors[name] ||= {} #: Hash[untyped, untyped]
75
+ end
79
76
 
80
- def key_for(violation)
81
- CRITERION_KEYS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion)))
82
- end
77
+ def key_for(violation)
78
+ CRITERION_KEYS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion)))
79
+ end
83
80
 
84
- def expected_actual(violation)
85
- {expected: violation.fetch(:expected), actual: violation.fetch(:actual)}
81
+ def expected_actual(violation)
82
+ {expected: violation.fetch(:expected), actual: violation.fetch(:actual)}
83
+ end
86
84
  end
87
85
  end
88
86
  end
@@ -22,105 +22,95 @@ module SimpleCov
22
22
  SCHEMA_URL = "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v#{SCHEMA_VERSION}.schema.json".freeze
23
23
  private_constant :SCHEMA_VERSION, :SCHEMA_URL
24
24
 
25
- def initialize(result, include_source: true)
26
- @result = result
27
- @include_source = include_source
28
- end
29
-
30
- def format
31
- {
32
- :$schema => SCHEMA_URL,
33
- :meta => format_meta,
34
- :total => format_coverage_statistics(@result.coverage_statistics),
35
- :coverage => format_files,
36
- :groups => format_groups,
37
- :errors => ErrorsFormatter.new(@result).call
38
- }
39
- end
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
40
36
 
41
- private
37
+ private
42
38
 
43
- def format_files
44
- @result.files.to_h do |source_file|
45
- [source_file.project_filename, SourceFileFormatter.new(source_file, include_source: @include_source).call]
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
46
43
  end
47
- end
48
44
 
49
- def format_groups
50
- @result.groups.to_h do |name, file_list|
51
- stats = format_coverage_statistics(file_list.coverage_statistics)
52
- [name, stats.merge(files: file_list.map(&:project_filename))]
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
53
50
  end
54
- end
55
-
56
- def format_meta
57
- {
58
- schema_version: SCHEMA_VERSION,
59
- simplecov_version: SimpleCov::VERSION,
60
- command_name: @result.command_name,
61
- project_name: SimpleCov.project_name,
62
- timestamp: @result.created_at.iso8601(3),
63
- root: SimpleCov.root,
64
- commit: git_commit,
65
- primary_coverage: SimpleCov.primary_coverage.to_s
66
- }.merge!(coverage_flags)
67
- end
68
51
 
69
- # Full git commit SHA of `SimpleCov.root`'s HEAD, or nil when the
70
- # project isn't a git checkout or git isn't on PATH. Recorded so tools
71
- # can recover the exact source a report was generated against, which
72
- # matters most when `source_in_json false` drops the source text from
73
- # coverage.json. stderr is captured (not forwarded) so a non-git project
74
- # doesn't print git's diagnostics to the build.
75
- def git_commit
76
- output, status = Open3.capture2e("git", "-C", SimpleCov.root.to_s, "rev-parse", "HEAD")
77
- status.success? ? output.strip : nil
78
- rescue StandardError
79
- nil
80
- end
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
81
64
 
82
- def coverage_flags
83
- {
84
- line_coverage: line_coverage_enabled?,
85
- branch_coverage: SimpleCov.branch_coverage?,
86
- method_coverage: SimpleCov.method_coverage?
87
- }
88
- end
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
89
77
 
90
- # Mirrors SourceFileFormatter's predicate so meta.line_coverage
91
- # tracks exactly which configurations cause the formatter to
92
- # emit line stats.
93
- def line_coverage_enabled?
94
- SimpleCov.coverage_criterion_enabled?(:line) || SimpleCov.coverage_criterion_enabled?(:oneshot_line)
95
- end
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
96
85
 
97
- def format_coverage_statistics(statistics)
98
- result = {} #: Hash[Symbol, untyped]
99
- result[:lines] = format_line_statistic(statistics[:line]) if statistics[:line]
100
- result[:branches] = format_single_statistic(statistics[:branch]) if statistics[:branch]
101
- result[:methods] = format_single_statistic(statistics[:method]) if statistics[:method]
102
- result
103
- end
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
104
93
 
105
- def format_line_statistic(stat)
106
- {
107
- covered: stat.covered,
108
- missed: stat.missed,
109
- omitted: stat.omitted,
110
- total: stat.total,
111
- percent: stat.percent,
112
- strength: stat.strength
113
- }
114
- end
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
115
104
 
116
- def format_single_statistic(stat)
117
- {
118
- covered: stat.covered,
119
- missed: stat.missed,
120
- total: stat.total,
121
- percent: stat.percent,
122
- strength: stat.strength
123
- }
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
124
114
  end
125
115
  end
126
116
  end
@@ -7,91 +7,85 @@ module SimpleCov
7
7
  # in coverage.json: source code plus per-enabled-criterion arrays
8
8
  # and totals.
9
9
  class SourceFileFormatter
10
- def initialize(source_file, include_source: true)
11
- @source_file = source_file
12
- @include_source = include_source
13
- end
14
-
15
- def call
16
- result = @include_source ? format_source_code : {} #: Hash[Symbol, untyped]
17
- result.merge!(line_coverage_section) if line_coverage_enabled?
18
- result.merge!(branch_coverage_section) if SimpleCov.branch_coverage?
19
- result.merge!(method_coverage_section) if SimpleCov.method_coverage?
20
- result
21
- end
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
22
18
 
23
- private
19
+ private
24
20
 
25
- # `:oneshot_line` is a synonym for `:line` for stats purposes
26
- # (see `SimpleCov.coverage_statistics_key`), so treat either as
27
- # "line coverage is on" for the line-block emit decisions.
28
- def line_coverage_enabled?
29
- SimpleCov.coverage_criterion_enabled?(:line) || SimpleCov.coverage_criterion_enabled?(:oneshot_line)
30
- end
31
-
32
- def format_source_code
33
- {source: @source_file.lines.map { |line| ensure_utf8(line.src.chomp) }}
34
- end
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
35
29
 
36
- def ensure_utf8(str)
37
- str.encode("UTF-8", invalid: :replace, undef: :replace)
38
- end
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
39
42
 
40
- def line_coverage_section
41
- covered = @source_file.covered_lines.size
42
- missed = @source_file.missed_lines.size
43
- {
44
- lines: @source_file.lines.map { |line| format_line(line) },
45
- lines_covered_percent: @source_file.covered_percent,
46
- covered_lines: covered,
47
- missed_lines: missed,
48
- omitted_lines: @source_file.never_lines.size,
49
- total_lines: covered + missed
50
- }
51
- end
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
52
 
53
- def branch_coverage_section
54
- {
55
- branches: @source_file.branches.map { |branch| format_branch(branch) },
56
- branches_covered_percent: @source_file.covered_percent(:branch),
57
- covered_branches: @source_file.covered_branches.size,
58
- missed_branches: @source_file.missed_branches.size,
59
- total_branches: @source_file.total_branches.size
60
- }
61
- end
53
+ def method_coverage_section(source_file)
54
+ covered = source_file.covered_methods.size
55
+ missed = source_file.missed_methods.size
62
56
 
63
- def method_coverage_section
64
- {
65
- methods: @source_file.methods.map { |method| format_method(method) },
66
- methods_covered_percent: @source_file.covered_percent(:method),
67
- covered_methods: @source_file.covered_methods.size,
68
- missed_methods: @source_file.missed_methods.size,
69
- total_methods: @source_file.methods.size
70
- }
71
- end
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
72
65
 
73
- def format_line(line)
74
- line.skipped? ? "ignored" : line.coverage
75
- end
66
+ def format_line(line)
67
+ line.skipped? ? "ignored" : line.coverage
68
+ end
76
69
 
77
- def format_branch(branch)
78
- {
79
- type: branch.type,
80
- start_line: branch.start_line,
81
- end_line: branch.end_line,
82
- coverage: format_line(branch),
83
- inline: branch.inline?,
84
- report_line: branch.report_line
85
- }
86
- end
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
87
80
 
88
- def format_method(method)
89
- {
90
- name: method.to_s,
91
- start_line: method.start_line,
92
- end_line: method.end_line,
93
- coverage: format_line(method)
94
- }
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
95
89
  end
96
90
  end
97
91
  end