simplecov 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +56 -1451
  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 +23 -28
  7. data/lib/simplecov/cli/coverage_file.rb +65 -0
  8. data/lib/simplecov/cli/diff.rb +43 -47
  9. data/lib/simplecov/cli/dotfile.rb +21 -10
  10. data/lib/simplecov/cli/merge.rb +17 -13
  11. data/lib/simplecov/cli/open.rb +3 -5
  12. data/lib/simplecov/cli/report.rb +38 -25
  13. data/lib/simplecov/cli/run.rb +1 -1
  14. data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
  15. data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
  16. data/lib/simplecov/cli/serve.rb +45 -99
  17. data/lib/simplecov/cli/uncovered.rb +20 -24
  18. data/lib/simplecov/cli.rb +15 -2
  19. data/lib/simplecov/color.rb +10 -1
  20. data/lib/simplecov/combine/branches_combiner.rb +43 -18
  21. data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
  22. data/lib/simplecov/combine/identity_interner.rb +30 -0
  23. data/lib/simplecov/combine/interned_counts.rb +30 -0
  24. data/lib/simplecov/combine/lines_combiner.rb +48 -20
  25. data/lib/simplecov/combine/methods_combiner.rb +53 -5
  26. data/lib/simplecov/combine/results_combiner.rb +12 -36
  27. data/lib/simplecov/combine.rb +5 -23
  28. data/lib/simplecov/command_guesser.rb +66 -9
  29. data/lib/simplecov/configuration/coverage.rb +17 -16
  30. data/lib/simplecov/configuration/coverage_criteria.rb +37 -39
  31. data/lib/simplecov/configuration/eval_coverage.rb +41 -0
  32. data/lib/simplecov/configuration/filters.rb +11 -44
  33. data/lib/simplecov/configuration/formatting.rb +33 -14
  34. data/lib/simplecov/configuration/groups.rb +42 -0
  35. data/lib/simplecov/configuration/merging.rb +8 -7
  36. data/lib/simplecov/configuration/thresholds.rb +27 -22
  37. data/lib/simplecov/configuration.rb +21 -42
  38. data/lib/simplecov/coverage_json.rb +24 -0
  39. data/lib/simplecov/coverage_statistics.rb +5 -2
  40. data/lib/simplecov/coverage_violations.rb +17 -6
  41. data/lib/simplecov/defaults.rb +10 -6
  42. data/lib/simplecov/directive.rb +9 -4
  43. data/lib/simplecov/exit_codes/check.rb +33 -0
  44. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +11 -20
  45. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +4 -17
  46. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +14 -23
  47. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +14 -25
  48. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +7 -20
  49. data/lib/simplecov/exit_codes.rb +17 -0
  50. data/lib/simplecov/exit_handling.rb +13 -43
  51. data/lib/simplecov/file_list.rb +14 -12
  52. data/lib/simplecov/filter.rb +43 -9
  53. data/lib/simplecov/formatter/base.rb +18 -1
  54. data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
  55. data/lib/simplecov/formatter/html_formatter/public/index.html +36 -5
  56. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
  57. data/lib/simplecov/formatter/html_formatter.rb +71 -45
  58. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -50
  59. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +78 -88
  60. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +70 -76
  61. data/lib/simplecov/formatter/json_formatter.rb +5 -46
  62. data/lib/simplecov/formatter/multi_formatter.rb +8 -3
  63. data/lib/simplecov/formatter/simple_formatter.rb +8 -3
  64. data/lib/simplecov/formatter.rb +12 -0
  65. data/lib/simplecov/group_names.rb +32 -0
  66. data/lib/simplecov/last_run.rb +16 -9
  67. data/lib/simplecov/lines_classifier.rb +29 -8
  68. data/lib/simplecov/load_global_config.rb +5 -2
  69. data/lib/simplecov/parallel_adapters/base.rb +17 -0
  70. data/lib/simplecov/parallel_adapters/generic.rb +9 -8
  71. data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
  72. data/lib/simplecov/parallel_adapters.rb +4 -2
  73. data/lib/simplecov/parallel_coordination.rb +6 -1
  74. data/lib/simplecov/parallel_result_merger.rb +230 -0
  75. data/lib/simplecov/process.rb +22 -0
  76. data/lib/simplecov/report_deferral.rb +49 -0
  77. data/lib/simplecov/report_stamp.rb +28 -0
  78. data/lib/simplecov/result.rb +40 -10
  79. data/lib/simplecov/result_adapter.rb +79 -10
  80. data/lib/simplecov/result_merger/resultset_file.rb +43 -7
  81. data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
  82. data/lib/simplecov/result_merger/resultset_store.rb +15 -12
  83. data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
  84. data/lib/simplecov/result_merger.rb +69 -42
  85. data/lib/simplecov/result_processing.rb +85 -46
  86. data/lib/simplecov/run_identity.rb +77 -0
  87. data/lib/simplecov/simulate_coverage.rb +37 -12
  88. data/lib/simplecov/source_file/line.rb +2 -2
  89. data/lib/simplecov/source_file/method.rb +8 -2
  90. data/lib/simplecov/source_file/method_builder.rb +3 -1
  91. data/lib/simplecov/source_file/ruby_data_parser.rb +25 -3
  92. data/lib/simplecov/source_file/skip_chunks.rb +7 -10
  93. data/lib/simplecov/source_file/source_loader.rb +25 -9
  94. data/lib/simplecov/source_file/statistics.rb +24 -16
  95. data/lib/simplecov/source_file.rb +2 -2
  96. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +239 -0
  97. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +236 -0
  98. data/lib/simplecov/static_coverage_extractor/method_collector.rb +7 -0
  99. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
  100. data/lib/simplecov/static_coverage_extractor/value_position.rb +74 -0
  101. data/lib/simplecov/static_coverage_extractor/visitor.rb +74 -62
  102. data/lib/simplecov/unloaded_file_injector.rb +75 -0
  103. data/lib/simplecov/version.rb +1 -1
  104. data/lib/simplecov.rb +37 -17
  105. data/sig/simplecov.rbs +1777 -0
  106. metadata +32 -16
  107. data/doc/alternate-formatters.md +0 -66
  108. data/doc/commercial-services.md +0 -25
  109. data/doc/editor-integration.md +0 -18
  110. data/lib/simplecov/combine/files_combiner.rb +0 -30
  111. data/lib/simplecov/formatter/html_formatter/public/application.css +0 -1
  112. data/lib/simplecov/formatter/html_formatter/public/application.js +0 -18
  113. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  114. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  115. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
  116. data/schemas/coverage-v1.0.schema.json +0 -306
  117. data/schemas/coverage.schema.json +0 -306
@@ -12,71 +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
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
19
25
 
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
26
+ private
28
27
 
29
- private
28
+ def format_minimum_overall(result, errors)
29
+ SimpleCov::CoverageViolations.minimum_overall(result, SimpleCov.minimum_coverage).each do |violation|
30
+ bucket(errors, :minimum_coverage)[key_for(violation)] = expected_actual(violation)
31
+ end
32
+ end
30
33
 
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)
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) }
34
39
  end
35
- end
36
40
 
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
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
43
46
 
44
- def record_by_file(violation)
45
- file_bucket = bucket(:minimum_coverage_by_file)[violation.fetch(:project_filename)] ||= {}
46
- file_bucket[key_for(violation)] = expected_actual(violation)
47
- end
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?
48
50
 
49
- def format_minimum_by_group
50
- violations = SimpleCov::CoverageViolations.minimum_by_group(@result, SimpleCov.minimum_coverage_by_group)
51
- violations.each do |violation|
52
- group_bucket = bucket(:minimum_coverage_by_group)[violation.fetch(:group_name)] ||= {}
53
- 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
54
58
  end
55
- end
56
59
 
57
- def format_maximum_overall
58
- SimpleCov::CoverageViolations.maximum_overall(@result, SimpleCov.maximum_coverage).each do |violation|
59
- 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
60
64
  end
61
- end
62
65
 
63
- def format_maximum_drop
64
- SimpleCov::CoverageViolations.maximum_drop(@result, SimpleCov.maximum_coverage_drop).each do |violation|
65
- bucket(:maximum_coverage_drop)[key_for(violation)] =
66
- {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
67
71
  end
68
- end
69
72
 
70
- def bucket(name)
71
- @errors[name] ||= {}
72
- end
73
+ def bucket(errors, name)
74
+ errors[name] ||= {} #: Hash[untyped, untyped]
75
+ end
73
76
 
74
- def key_for(violation)
75
- CRITERION_KEYS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion)))
76
- end
77
+ def key_for(violation)
78
+ CRITERION_KEYS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion)))
79
+ end
77
80
 
78
- def expected_actual(violation)
79
- {expected: violation.fetch(:expected), actual: violation.fetch(:actual)}
81
+ def expected_actual(violation)
82
+ {expected: violation.fetch(:expected), actual: violation.fetch(:actual)}
83
+ end
80
84
  end
81
85
  end
82
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 = {}
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 : {}
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
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base"
4
- require "fileutils"
4
+ require_relative "coverage_json_writer"
5
5
  require "json"
6
- require "time"
7
6
 
8
7
  module SimpleCov
9
8
  module Formatter
@@ -11,7 +10,7 @@ module SimpleCov
11
10
  # standalone, alongside the HTML formatter, or by external tools that
12
11
  # consume SimpleCov output.
13
12
  class JSONFormatter < Base
14
- FILENAME = "coverage.json"
13
+ FILENAME = CoverageJSONWriter::FILENAME
15
14
 
16
15
  # `include_source:` defaults to `SimpleCov.source_in_json` (true
17
16
  # by default) so the historical payload shape is unchanged.
@@ -19,17 +18,12 @@ module SimpleCov
19
18
  # setting (the HTML formatter, which feeds the client-side
20
19
  # viewer) pass `include_source: true` explicitly.
21
20
  def self.build_hash(result, include_source: SimpleCov.source_in_json)
22
- ResultHashFormatter.new(result, include_source: include_source).format
21
+ ResultHashFormatter.format(result, include_source: include_source)
23
22
  end
24
23
 
25
24
  def format(result)
26
- FileUtils.mkdir_p(output_path)
27
- path = File.join(output_path, FILENAME)
28
- warn_if_concurrent_overwrite(path, result)
29
- File.write(path, JSON.pretty_generate(self.class.build_hash(result)))
30
- # stderr, not stdout: this is a status message, not the program's
31
- # output. Keeps the line out of pipelines like `rspec -f json`.
32
- warn output_message(result) unless @silent
25
+ CoverageJSONWriter.write(output_path, self.class.build_hash(result), result)
26
+ emit_status(result)
33
27
  end
34
28
 
35
29
  private
@@ -41,41 +35,6 @@ module SimpleCov
41
35
  def entry_point_filename
42
36
  FILENAME
43
37
  end
44
-
45
- # Warns when the existing coverage.json has a timestamp newer than this
46
- # process's start time — a strong signal that a sibling test process
47
- # (e.g., parallel_tests) wrote it while we were running, and that our
48
- # write is about to clobber their data.
49
- def warn_if_concurrent_overwrite(path, result)
50
- start_time = SimpleCov.process_start_time or return
51
- existing = existing_meta(path) or return
52
- return unless existing[:timestamp] > start_time
53
-
54
- # The HTML formatter also writes coverage.json (it shares the file as
55
- # a side artifact), so when both formatters are configured the file we
56
- # find was just written by our own run, not a concurrent one. A
57
- # matching command_name means the same merged result, so there's
58
- # nothing to lose by overwriting. See issue #1171.
59
- return if existing[:command_name] == result.command_name
60
-
61
- warn "simplecov: #{path} was written at #{existing[:timestamp].iso8601} — after " \
62
- "this process started at #{start_time.iso8601}. Overwriting " \
63
- "likely loses coverage data from a concurrent test run. For " \
64
- "parallel test setups, use SimpleCov::ResultMerger or run a single " \
65
- "collation step after all workers finish."
66
- end
67
-
68
- def existing_meta(path)
69
- return nil unless File.exist?(path)
70
-
71
- meta = JSON.parse(File.read(path), symbolize_names: true)
72
- timestamp = meta.dig(:meta, :timestamp)
73
- return nil unless timestamp
74
-
75
- {timestamp: Time.iso8601(timestamp), command_name: meta.dig(:meta, :command_name)}
76
- rescue JSON::ParserError, ArgumentError
77
- nil
78
- end
79
38
  end
80
39
  end
81
40
  end
@@ -10,18 +10,23 @@ module SimpleCov
10
10
  module InstanceMethods
11
11
  def format(result)
12
12
  formatters.map do |formatter|
13
- formatter.new.format(result)
13
+ Formatter.format(formatter, result)
14
14
  rescue StandardError => e
15
- warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
15
+ warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{(_ = e.backtrace).first})")
16
16
  nil
17
17
  end
18
18
  end
19
19
  end
20
20
 
21
21
  def self.new(formatters = nil)
22
+ # Normalize eagerly and capture the list in the closure. Array()
23
+ # is pure for every accepted input shape (nil, single formatter,
24
+ # array, or another MultiFormatter class), so this is equivalent
25
+ # to the historical lazy per-instance memoization.
26
+ formatter_list = Array(formatters)
22
27
  Class.new do
23
28
  define_method :formatters do
24
- @formatters ||= Array(formatters)
29
+ formatter_list
25
30
  end
26
31
  include InstanceMethods
27
32
  end
@@ -8,14 +8,19 @@ module SimpleCov
8
8
  class SimpleFormatter
9
9
  # Takes a SimpleCov::Result and generates a string out of it
10
10
  def format(result)
11
- result.groups.map { |name, files| format_group(name, files) }.join
11
+ criterion = SimpleCov.coverage_statistics_key(SimpleCov.primary_coverage)
12
+ result.groups.map { |name, files| format_group(name, files, criterion) }.join
12
13
  end
13
14
 
14
15
  private
15
16
 
16
- def format_group(name, files)
17
+ def format_group(name, files, criterion)
17
18
  header = "Group: #{name}\n#{'=' * 40}\n"
18
- body = files.map { |file| "#{file.filename} (coverage: #{file.covered_percent.floor(2)}%)\n" }.join
19
+ # The configured primary criterion is enabled, so every result file
20
+ # carries its corresponding statistic despite the nilable public API.
21
+ body = files.map do |file|
22
+ "#{file.filename} (coverage: #{(_ = file.covered_percent(criterion)).floor(2)}%)\n"
23
+ end.join
19
24
  "#{header}#{body}\n"
20
25
  end
21
26
  end
@@ -6,6 +6,18 @@ module SimpleCov
6
6
  # and can be wired up via `SimpleCov.formatter=`.
7
7
  # TODO: Documentation on how to build your own formatters
8
8
  module Formatter
9
+ # Formatters can be configured either as classes (instantiated
10
+ # fresh for every report) or as ready-built instances — the only
11
+ # way to reach constructor options like
12
+ # `HTMLFormatter.new(silent: true)`. See #1240.
13
+ def self.instance_for(formatter)
14
+ formatter.is_a?(Class) ? formatter.new : formatter
15
+ end
16
+
17
+ # Normalize a class or instance, then dispatch the result to it.
18
+ def self.format(formatter, result)
19
+ instance_for(formatter).format(result)
20
+ end
9
21
  end
10
22
  end
11
23