simplecov 0.22.0 → 1.0.2

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +1058 -508
  4. data/doc/alternate-formatters.md +0 -5
  5. data/doc/commercial-services.md +5 -5
  6. data/exe/simplecov +11 -0
  7. data/lib/minitest/simplecov_plugin.rb +13 -5
  8. data/lib/simplecov/autostart.rb +11 -0
  9. data/lib/simplecov/cli/clean.rb +47 -0
  10. data/lib/simplecov/cli/coverage.rb +93 -0
  11. data/lib/simplecov/cli/diff.rb +152 -0
  12. data/lib/simplecov/cli/dotfile.rb +105 -0
  13. data/lib/simplecov/cli/merge.rb +118 -0
  14. data/lib/simplecov/cli/open.rb +50 -0
  15. data/lib/simplecov/cli/report.rb +89 -0
  16. data/lib/simplecov/cli/run.rb +36 -0
  17. data/lib/simplecov/cli/serve.rb +149 -0
  18. data/lib/simplecov/cli/uncovered.rb +108 -0
  19. data/lib/simplecov/cli.rb +150 -0
  20. data/lib/simplecov/color.rb +79 -0
  21. data/lib/simplecov/combine/branches_combiner.rb +28 -5
  22. data/lib/simplecov/combine/files_combiner.rb +47 -1
  23. data/lib/simplecov/combine/lines_combiner.rb +19 -17
  24. data/lib/simplecov/combine/methods_combiner.rb +49 -0
  25. data/lib/simplecov/combine/results_combiner.rb +7 -5
  26. data/lib/simplecov/command_guesser.rb +46 -32
  27. data/lib/simplecov/configuration/coverage.rb +175 -0
  28. data/lib/simplecov/configuration/coverage_criteria.rb +158 -0
  29. data/lib/simplecov/configuration/filters.rb +197 -0
  30. data/lib/simplecov/configuration/formatting.rb +129 -0
  31. data/lib/simplecov/configuration/ignored_entries.rb +63 -0
  32. data/lib/simplecov/configuration/merging.rb +164 -0
  33. data/lib/simplecov/configuration/thresholds.rb +178 -0
  34. data/lib/simplecov/configuration.rb +105 -412
  35. data/lib/simplecov/coverage_statistics.rb +15 -9
  36. data/lib/simplecov/coverage_violations.rb +149 -0
  37. data/lib/simplecov/defaults.rb +29 -22
  38. data/lib/simplecov/deprecation.rb +45 -0
  39. data/lib/simplecov/directive.rb +167 -0
  40. data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
  41. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +19 -57
  42. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
  43. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -27
  44. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
  45. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +38 -21
  46. data/lib/simplecov/exit_codes.rb +15 -0
  47. data/lib/simplecov/exit_handling.rb +158 -0
  48. data/lib/simplecov/file_list.rb +70 -19
  49. data/lib/simplecov/filter.rb +69 -24
  50. data/lib/simplecov/formatter/base.rb +102 -0
  51. data/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
  52. data/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
  53. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  54. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  55. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
  56. data/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
  57. data/lib/simplecov/formatter/html_formatter.rb +85 -0
  58. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +90 -0
  59. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +128 -0
  60. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
  61. data/lib/simplecov/formatter/json_formatter.rb +89 -0
  62. data/lib/simplecov/formatter/multi_formatter.rb +11 -7
  63. data/lib/simplecov/formatter/simple_formatter.rb +11 -11
  64. data/lib/simplecov/formatter.rb +4 -0
  65. data/lib/simplecov/last_run.rb +10 -3
  66. data/lib/simplecov/lines_classifier.rb +25 -13
  67. data/lib/simplecov/load_global_config.rb +9 -4
  68. data/lib/simplecov/parallel_adapters/base.rb +61 -0
  69. data/lib/simplecov/parallel_adapters/generic.rb +44 -0
  70. data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
  71. data/lib/simplecov/parallel_adapters.rb +83 -0
  72. data/lib/simplecov/parallel_coordination.rb +129 -0
  73. data/lib/simplecov/process.rb +52 -12
  74. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  75. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  76. data/lib/simplecov/profiles/rails.rb +24 -10
  77. data/lib/simplecov/profiles/root_filter.rb +6 -5
  78. data/lib/simplecov/profiles/strict.rb +32 -0
  79. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  80. data/lib/simplecov/profiles.rb +32 -3
  81. data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
  82. data/lib/simplecov/result/source_file_builder.rb +51 -0
  83. data/lib/simplecov/result.rb +108 -19
  84. data/lib/simplecov/result_adapter.rb +105 -8
  85. data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
  86. data/lib/simplecov/result_merger/resultset_file.rb +38 -0
  87. data/lib/simplecov/result_merger/resultset_store.rb +50 -0
  88. data/lib/simplecov/result_merger.rb +59 -91
  89. data/lib/simplecov/result_processing.rb +179 -0
  90. data/lib/simplecov/simulate_coverage.rb +54 -8
  91. data/lib/simplecov/source_file/branch.rb +1 -3
  92. data/lib/simplecov/source_file/branch_builder.rb +114 -0
  93. data/lib/simplecov/source_file/builder_context.rb +28 -0
  94. data/lib/simplecov/source_file/line.rb +9 -4
  95. data/lib/simplecov/source_file/line_builder.rb +43 -0
  96. data/lib/simplecov/source_file/method.rb +52 -0
  97. data/lib/simplecov/source_file/method_builder.rb +60 -0
  98. data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
  99. data/lib/simplecov/source_file/skip_chunks.rb +75 -0
  100. data/lib/simplecov/source_file/source_loader.rb +63 -0
  101. data/lib/simplecov/source_file/statistics.rb +57 -0
  102. data/lib/simplecov/source_file.rb +66 -232
  103. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +49 -0
  104. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +247 -0
  105. data/lib/simplecov/static_coverage_extractor/method_collector.rb +55 -0
  106. data/lib/simplecov/static_coverage_extractor/value_position.rb +82 -0
  107. data/lib/simplecov/static_coverage_extractor/visitor.rb +196 -0
  108. data/lib/simplecov/static_coverage_extractor.rb +110 -0
  109. data/lib/simplecov/useless_results_remover.rb +16 -7
  110. data/lib/simplecov/version.rb +1 -1
  111. data/lib/simplecov-html.rb +4 -0
  112. data/lib/simplecov.rb +178 -380
  113. data/lib/simplecov_json_formatter.rb +4 -0
  114. data/schemas/coverage-v1.0.schema.json +306 -0
  115. data/schemas/coverage.schema.json +306 -0
  116. data/sig/simplecov.rbs +1638 -0
  117. metadata +94 -57
  118. data/CHANGELOG.md +0 -191
  119. data/lib/simplecov/default_formatter.rb +0 -20
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "time"
5
+ require_relative "errors_formatter"
6
+ require_relative "source_file_formatter"
7
+
8
+ module SimpleCov
9
+ module Formatter
10
+ class JSONFormatter
11
+ # Builds the hash that JSONFormatter serializes to coverage.json:
12
+ # meta, per-file coverage data, group totals, and aggregate stats.
13
+ class ResultHashFormatter
14
+ # Bump SCHEMA_VERSION (and SCHEMA_URL) when the JSON shape
15
+ # changes. Additive changes bump the minor segment, removals or
16
+ # shape changes bump the major segment. The versioned file at
17
+ # schemas/coverage-vX.Y.schema.json is the canonical artifact
18
+ # consumers should pin to, schemas/coverage.schema.json is a
19
+ # convenience alias that always tracks the latest. See the
20
+ # `coverage.json` schema section of the README for the rationale.
21
+ SCHEMA_VERSION = "1.0"
22
+ SCHEMA_URL = "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v#{SCHEMA_VERSION}.schema.json".freeze
23
+ private_constant :SCHEMA_VERSION, :SCHEMA_URL
24
+
25
+ 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
40
+
41
+ private
42
+
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]
46
+ end
47
+ end
48
+
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))]
53
+ 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
+
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
81
+
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
89
+
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
96
+
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
104
+
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
115
+
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
+ }
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module Formatter
5
+ class JSONFormatter
6
+ # Renders a single `SimpleCov::SourceFile` as the per-file payload
7
+ # in coverage.json: source code plus per-enabled-criterion arrays
8
+ # and totals.
9
+ class SourceFileFormatter
10
+ 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
22
+
23
+ private
24
+
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
35
+
36
+ def ensure_utf8(str)
37
+ str.encode("UTF-8", invalid: :replace, undef: :replace)
38
+ end
39
+
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
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
62
+
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
72
+
73
+ def format_line(line)
74
+ line.skipped? ? "ignored" : line.coverage
75
+ end
76
+
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
87
+
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
+ }
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require "fileutils"
5
+ require "json"
6
+ require "time"
7
+
8
+ module SimpleCov
9
+ module Formatter
10
+ # Writes coverage results as JSON to coverage/coverage.json. Used
11
+ # standalone, alongside the HTML formatter, or by external tools that
12
+ # consume SimpleCov output.
13
+ class JSONFormatter < Base
14
+ FILENAME = "coverage.json"
15
+
16
+ # `include_source:` defaults to `SimpleCov.source_in_json` (true
17
+ # by default) so the historical payload shape is unchanged.
18
+ # Callers that need the source array regardless of the global
19
+ # setting (the HTML formatter, which feeds the client-side
20
+ # viewer) pass `include_source: true` explicitly.
21
+ def self.build_hash(result, include_source: SimpleCov.source_in_json)
22
+ ResultHashFormatter.new(result, include_source: include_source).format
23
+ end
24
+
25
+ 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`. And
32
+ # $stderr.puts, not `warn`: a status line should not reach
33
+ # `Warning.warn` hooks or vanish under `-W0` (see #1225).
34
+ $stderr.puts output_message(result) unless @silent # rubocop:disable Style/StderrPuts
35
+ end
36
+
37
+ private
38
+
39
+ def message_prefix
40
+ "JSON "
41
+ end
42
+
43
+ def entry_point_filename
44
+ FILENAME
45
+ end
46
+
47
+ # Warns when the existing coverage.json has a timestamp newer than this
48
+ # process's start time — a strong signal that a sibling test process
49
+ # (e.g., parallel_tests) wrote it while we were running, and that our
50
+ # write is about to clobber their data.
51
+ def warn_if_concurrent_overwrite(path, result)
52
+ start_time = SimpleCov.process_start_time or return
53
+ existing = existing_meta(path) or return
54
+ return unless existing[:timestamp] > start_time
55
+
56
+ # The HTML formatter also writes coverage.json (it shares the file as
57
+ # a side artifact), so when both formatters are configured the file we
58
+ # find was just written by our own run, not a concurrent one. A
59
+ # matching command_name means the same merged result, so there's
60
+ # nothing to lose by overwriting. See issue #1171.
61
+ return if existing[:command_name] == result.command_name
62
+
63
+ warn "simplecov: #{path} was written at #{existing[:timestamp].iso8601} — after " \
64
+ "this process started at #{start_time.iso8601}. Overwriting " \
65
+ "likely loses coverage data from a concurrent test run. For " \
66
+ "parallel test setups, use SimpleCov::ResultMerger or run a single " \
67
+ "collation step after all workers finish."
68
+ end
69
+
70
+ def existing_meta(path)
71
+ return nil unless File.exist?(path)
72
+
73
+ meta = JSON.parse(File.read(path), symbolize_names: true)
74
+ timestamp = meta.dig(:meta, :timestamp)
75
+ return nil unless timestamp
76
+
77
+ {timestamp: Time.iso8601(timestamp), command_name: meta.dig(:meta, :command_name)}
78
+ rescue JSON::ParserError, ArgumentError
79
+ nil
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ # Loaded after the JSONFormatter class is defined so the nested
86
+ # `class JSONFormatter` reopen inside result_hash_formatter.rb doesn't
87
+ # accidentally create a JSONFormatter < Object before this file gets a
88
+ # chance to declare `JSONFormatter < Base`.
89
+ require_relative "json_formatter/result_hash_formatter"
@@ -2,31 +2,35 @@
2
2
 
3
3
  module SimpleCov
4
4
  module Formatter
5
+ # Wraps multiple formatters so SimpleCov.formatter can drive several
6
+ # output formats (HTML + JSON, etc.) in a single run.
5
7
  class MultiFormatter
8
+ # Shared `#format` implementation; included into individual
9
+ # MultiFormatter subclasses built by `MultiFormatter.new`.
6
10
  module InstanceMethods
7
11
  def format(result)
8
12
  formatters.map do |formatter|
9
13
  formatter.new.format(result)
10
14
  rescue StandardError => e
11
- warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
15
+ warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{(_ = e.backtrace).first})")
12
16
  nil
13
17
  end
14
18
  end
15
19
  end
16
20
 
17
21
  def self.new(formatters = nil)
22
+ # Normalize eagerly and capture the list in the closure. Array()
23
+ # is pure for every accepted input shape (nil, single formatter,
24
+ # array, or another MultiFormatter class), so this is equivalent
25
+ # to the historical lazy per-instance memoization.
26
+ formatter_list = Array(formatters)
18
27
  Class.new do
19
28
  define_method :formatters do
20
- @formatters ||= Array(formatters)
29
+ formatter_list
21
30
  end
22
31
  include InstanceMethods
23
32
  end
24
33
  end
25
-
26
- def self.[](*args)
27
- warn "#{Kernel.caller.first}: [DEPRECATION] ::[] is deprecated. Use ::new instead."
28
- new(Array(args))
29
- end
30
34
  end
31
35
  end
32
36
  end
@@ -8,17 +8,17 @@ module SimpleCov
8
8
  class SimpleFormatter
9
9
  # Takes a SimpleCov::Result and generates a string out of it
10
10
  def format(result)
11
- output = +""
12
- result.groups.each do |name, files|
13
- output << "Group: #{name}\n"
14
- output << "=" * 40
15
- output << "\n"
16
- files.each do |file|
17
- output << "#{file.filename} (coverage: #{file.covered_percent.round(2)}%)\n"
18
- end
19
- output << "\n"
20
- end
21
- output
11
+ result.groups.map { |name, files| format_group(name, files) }.join
12
+ end
13
+
14
+ private
15
+
16
+ def format_group(name, files)
17
+ header = "Group: #{name}\n#{'=' * 40}\n"
18
+ # `covered_percent` is nilable across criteria, but line stats are
19
+ # always measured, so the no-argument call can't return nil here.
20
+ body = files.map { |file| "#{file.filename} (coverage: #{(_ = file.covered_percent).floor(2)}%)\n" }.join
21
+ "#{header}#{body}\n"
22
22
  end
23
23
  end
24
24
  end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
+ # Namespace for SimpleCov result formatters. Built-in formatters live
5
+ # below this module; custom formatters should respond to `#format(result)`
6
+ # and can be wired up via `SimpleCov.formatter=`.
4
7
  # TODO: Documentation on how to build your own formatters
5
8
  module Formatter
6
9
  end
@@ -8,3 +11,4 @@ end
8
11
 
9
12
  require_relative "formatter/simple_formatter"
10
13
  require_relative "formatter/multi_formatter"
14
+ require_relative "formatter/json_formatter"
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
3
4
  require "json"
4
5
 
5
6
  module SimpleCov
7
+ # Reads and writes coverage/.last_run.json — the previous run's coverage
8
+ # percentages used by MaximumCoverageDropCheck.
6
9
  module LastRun
7
10
  class << self
8
11
  def last_run_path
@@ -18,10 +21,14 @@ module SimpleCov
18
21
  JSON.parse(json, symbolize_names: true)
19
22
  end
20
23
 
24
+ # Write to a process-private temp file, then atomically rename, so a
25
+ # concurrent reader (e.g. another parallel-tests worker checking
26
+ # MaximumCoverageDrop) never sees a half-written file.
21
27
  def write(json)
22
- File.open(last_run_path, "w+") do |f|
23
- f.puts JSON.pretty_generate(json)
24
- end
28
+ FileUtils.mkdir_p(SimpleCov.coverage_path)
29
+ temp_path = "#{last_run_path}.#{Process.pid}.tmp"
30
+ File.open(temp_path, "w") { |f| f.puts JSON.pretty_generate(json) }
31
+ File.rename(temp_path, last_run_path)
25
32
  end
26
33
  end
27
34
  end
@@ -1,19 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "directive"
4
+
3
5
  module SimpleCov
4
6
  # Classifies whether lines are relevant for code coverage analysis.
5
7
  # Comments & whitespace lines, and :nocov: token blocks, are considered not relevant.
6
-
7
8
  class LinesClassifier
8
9
  RELEVANT = 0
9
10
  NOT_RELEVANT = nil
10
11
 
11
- WHITESPACE_LINE = /^\s*$/.freeze
12
- COMMENT_LINE = /^\s*#/.freeze
12
+ WHITESPACE_LINE = /^\s*$/
13
+ COMMENT_LINE = /^\s*#/
13
14
  WHITESPACE_OR_COMMENT_LINE = Regexp.union(WHITESPACE_LINE, COMMENT_LINE)
14
15
 
15
16
  def self.no_cov_line
16
- /^(\s*)#(\s*)(:#{SimpleCov.nocov_token}:)/o
17
+ /^(\s*)#(\s*)(:#{SimpleCov.current_nocov_token}:)/o
17
18
  end
18
19
 
19
20
  def self.no_cov_line?(line)
@@ -31,17 +32,28 @@ module SimpleCov
31
32
  end
32
33
 
33
34
  def classify(lines)
35
+ lines = lines.to_a
36
+ directive_disabled = directive_disabled_line_set(lines)
34
37
  skipping = false
35
38
 
36
- lines.map do |line|
37
- if self.class.no_cov_line?(line)
38
- skipping = !skipping
39
- NOT_RELEVANT
40
- elsif skipping || self.class.whitespace_line?(line)
41
- NOT_RELEVANT
42
- else
43
- RELEVANT
44
- end
39
+ lines.map.with_index(1) do |line, line_number|
40
+ skipping = !skipping if self.class.no_cov_line?(line)
41
+ not_relevant_line?(line, line_number, skipping, directive_disabled) ? NOT_RELEVANT : RELEVANT
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def not_relevant_line?(line, line_number, skipping, directive_disabled)
48
+ skipping ||
49
+ self.class.no_cov_line?(line) ||
50
+ directive_disabled.include?(line_number) ||
51
+ self.class.whitespace_line?(line)
52
+ end
53
+
54
+ def directive_disabled_line_set(lines)
55
+ Directive.disabled_ranges(lines).fetch(:line).each_with_object(Set.new) do |range, set|
56
+ range.each { |line_number| set.add(line_number) }
45
57
  end
46
58
  end
47
59
  end
@@ -1,8 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "etc"
4
- home_dir = (ENV["HOME"] && File.expand_path("~")) || Etc.getpwuid.dir || (ENV["USER"] && File.expand_path("~#{ENV['USER']}"))
5
- if home_dir
6
- global_config_path = File.join(home_dir, ".simplecov")
3
+ # `~/.simplecov` was historically resolved via a three-step fallback chain
4
+ # (HOME, then `Etc.getpwuid.dir`, then `~$USER`) for hostile container
5
+ # environments circa 2017. Modern CRuby/JRuby/TruffleRuby all set HOME
6
+ # reliably, so trust it and skip silently when it isn't there.
7
+ if ENV.fetch("HOME", nil)
8
+ # simplecov:disable — only fires when ~/.simplecov exists, which is
9
+ # developer-machine-dependent (we can't rely on it for the dogfood).
10
+ global_config_path = File.join(File.expand_path("~"), ".simplecov")
7
11
  load global_config_path if File.exist?(global_config_path)
12
+ # simplecov:enable
8
13
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ParallelAdapters
5
+ # Default no-op implementations for a parallel-test-runner adapter.
6
+ # Real adapters subclass and override what they need; everything else
7
+ # falls back to "behave like a single-process run."
8
+ #
9
+ # Adapters are classes (used as singletons, never instantiated) — they
10
+ # answer a small fixed set of questions about whether THIS worker
11
+ # process is the one that should do final-result work, and provide an
12
+ # optional hook for waiting on sibling workers.
13
+ #
14
+ # @see SimpleCov::ParallelAdapters for the registry and selection.
15
+ class Base
16
+ class << self
17
+ # Should this adapter be selected for the current process? Adapters
18
+ # are tried in registration order; the first one whose `active?`
19
+ # returns true is chosen. Inactive adapters return `false`.
20
+ def active?
21
+ false
22
+ end
23
+
24
+ # Among the parallel workers in this run, should THIS worker do
25
+ # the final-result work (wait for siblings, merge resultsets,
26
+ # run threshold checks, format the report)? Default is `true`
27
+ # for the single-process case.
28
+ def first_worker?
29
+ true
30
+ end
31
+
32
+ # Optional: block until sibling workers have finished writing
33
+ # their resultsets. An adapter that wraps a parallel-test runner
34
+ # with a native synchronization primitive (e.g., `parallel_tests`'s
35
+ # `wait_for_other_processes_to_finish`) implements this for
36
+ # lower latency; otherwise SimpleCov polls the resultset cache
37
+ # as a fallback (see `SimpleCov.wait_for_parallel_results`).
38
+ def wait_for_siblings
39
+ # No-op default; polling fallback handles correctness.
40
+ end
41
+
42
+ # Does `wait_for_siblings` block until every sibling PROCESS has
43
+ # exited (so no further resultset can appear)? When true, the
44
+ # reporting worker can accept a settled resultset count below
45
+ # `expected_worker_count` as final instead of waiting out the whole
46
+ # `parallel_wait_timeout` for workers that produced no coverage.
47
+ # Defaults to false (no native wait; the poll is the only signal).
48
+ def native_wait?
49
+ false
50
+ end
51
+
52
+ # How many parallel workers are participating in this run. Used
53
+ # by the polling fallback to know how many resultset entries to
54
+ # expect. Defaults to 1 (single-process).
55
+ def expected_worker_count
56
+ 1
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SimpleCov
6
+ module ParallelAdapters
7
+ # Catch-all adapter for parallel test runners that follow the
8
+ # `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` env-var convention but
9
+ # don't ship a Ruby API for SimpleCov to hook (parallel_rspec,
10
+ # knapsack-style splitters, custom CI sharding scripts). Activates
11
+ # when `TEST_ENV_NUMBER` is set; doesn't require any specific gem to
12
+ # be loaded.
13
+ #
14
+ # Heuristic for `first_worker?`: the worker whose `TEST_ENV_NUMBER`
15
+ # is `""` (parallel_tests/parallel_rspec convention) or `"1"`
16
+ # (zero-based runners that start at 1). Any other value is treated
17
+ # as a non-first worker.
18
+ #
19
+ # `wait_for_siblings` is inherited from Base as a no-op — without a
20
+ # runner-provided API the only synchronization available is polling
21
+ # the resultset cache, which `SimpleCov.wait_for_parallel_results`
22
+ # does after the no-op returns.
23
+ class GenericAdapter < Base
24
+ class << self
25
+ def active?
26
+ return false if SimpleCov.parallel_tests == false
27
+
28
+ ENV.key?("TEST_ENV_NUMBER")
29
+ end
30
+
31
+ # parallel_tests sets the first worker's TEST_ENV_NUMBER to "";
32
+ # parallel_rspec inherits that. Runners that number from 1 use
33
+ # "1" for the first worker. Both shapes match.
34
+ def first_worker?
35
+ ["", "1"].include?(ENV.fetch("TEST_ENV_NUMBER", nil))
36
+ end
37
+
38
+ def expected_worker_count
39
+ ENV["PARALLEL_TEST_GROUPS"]&.to_i || 1
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end