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
@@ -1,15 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
+ # Exit statuses SimpleCov sets when coverage checks fail, and the output
5
+ # helper the enforcement machinery reports through.
4
6
  module ExitCodes
5
7
  SUCCESS = 0
6
8
  EXCEPTION = 1
7
9
  MINIMUM_COVERAGE = 2
8
10
  MAXIMUM_COVERAGE_DROP = 3
9
11
  MAXIMUM_COVERAGE = 4
12
+
13
+ # Threshold-violation reports and exit-status notices are the output of
14
+ # the enforcement feature, not Ruby warnings: routing them through
15
+ # `Kernel#warn` made `-W0` swallow the explanation for a failing exit
16
+ # code, let `Warning.warn` hooks (warning trackers, raise-on-warning
17
+ # test setups) intercept them mid-`at_exit`, and fed colorized text to
18
+ # warning logs. `print_errors false` remains the intended opt-out.
19
+ def self.print_error(message)
20
+ $stderr.puts message # rubocop:disable Style/StderrPuts
21
+ rescue IOError
22
+ # A parallel runner can close a worker's stderr before its at_exit
23
+ # hooks run (rspec-conductor does). The violation still has to set
24
+ # the exit status even when its explanation cannot be printed.
25
+ end
10
26
  end
11
27
  end
12
28
 
29
+ require_relative "exit_codes/check"
13
30
  require_relative "exit_codes/exit_code_handling"
14
31
  require_relative "exit_codes/maximum_coverage_drop_check"
15
32
  require_relative "exit_codes/maximum_overall_coverage_check"
@@ -25,50 +25,20 @@ module SimpleCov
25
25
  # or a test consumed the result) then don't run exit tasks.
26
26
  return unless Coverage.running?
27
27
 
28
- # Stand down when we'd only clobber a fresher report. See
29
- # `defer_to_existing_report?` and issue #581.
30
- return if defer_to_existing_report?
31
-
32
- SimpleCov.run_exit_tasks!
33
- end
34
-
35
- # Returns true when our process has no coverage data to contribute
36
- # (after the resultset merge) and a newer report already exists on
37
- # disk. Typically fires when `SimpleCov.start` ran in a parent
38
- # process — e.g. a Rakefile or Rails' `Bundler.require` — that
39
- # shelled out to the test runner. See issue #581.
40
- def defer_to_existing_report?
41
- return false unless existing_report_newer_than_us?
42
-
43
- res = result
44
- empty = res.nil? || res.files.empty?
45
- warn_about_deferred_report if empty
46
- empty
47
- end
48
-
49
- def existing_report_newer_than_us?
50
- return false unless process_start_time
51
-
52
- last_run_path = File.join(coverage_path, ".last_run.json")
53
- File.exist?(last_run_path) && File.mtime(last_run_path) > process_start_time
54
- end
28
+ # Capture the suite's exit status BEFORE the deferral probe: its
29
+ # freshness check rescues filesystem errors, and completing any
30
+ # rescue inside an at_exit handler resets $ERROR_INFO to nil.
31
+ error_exit_status = exit_status_from_exception
55
32
 
56
- def warn_about_deferred_report
57
- return unless print_errors
33
+ # Stand down when we'd only clobber a fresher report (#581).
34
+ return if defer_to_existing_report?
58
35
 
59
- warn SimpleCov::Color.colorize(
60
- "Skipping SimpleCov report — this process tracked no application code and a newer " \
61
- "report already exists at #{coverage_path}. This usually means SimpleCov.start ran in a " \
62
- "parent process (e.g. a Rakefile or Rails' Bundler.require) that shelled out to the test " \
63
- "runner. See https://github.com/simplecov-ruby/simplecov/issues/581.",
64
- :yellow
65
- )
36
+ SimpleCov.run_exit_tasks!(error_exit_status)
66
37
  end
67
38
 
68
- # @api private — called from the at_exit block.
69
- def run_exit_tasks!
70
- error_exit_status = exit_status_from_exception
71
-
39
+ # @api private — called from the at_exit block (which pre-captures
40
+ # the status, see there) and by `collate` (the default argument).
41
+ def run_exit_tasks!(error_exit_status = exit_status_from_exception)
72
42
  at_exit.call
73
43
 
74
44
  exit_and_report_previous_error(error_exit_status) if previous_error?(error_exit_status)
@@ -97,7 +67,7 @@ module SimpleCov
97
67
  # @api private
98
68
  def exit_and_report_previous_error(exit_status)
99
69
  if print_errors
100
- warn SimpleCov::Color.colorize(
70
+ ExitCodes.print_error SimpleCov::Color.colorize(
101
71
  "Stopped processing SimpleCov as a previous error not related to SimpleCov has been detected",
102
72
  :yellow
103
73
  )
@@ -113,7 +83,7 @@ module SimpleCov
113
83
  # would surface a spurious "below minimum" violation about the
114
84
  # missing slice rather than a real shortfall.
115
85
  def ready_to_process_results?
116
- merge_finalization_owner? && final_result_process? && result? &&
86
+ merge_finalization_owner? && result? &&
117
87
  (collating_result? || parallel_results_complete?)
118
88
  end
119
89
 
@@ -124,7 +94,7 @@ module SimpleCov
124
94
  return unless exit_status.positive?
125
95
 
126
96
  if print_errors
127
- warn SimpleCov::Color.colorize(
97
+ ExitCodes.print_error SimpleCov::Color.colorize(
128
98
  "SimpleCov failed with exit #{exit_status} due to a coverage related error", :red
129
99
  )
130
100
  end
@@ -27,8 +27,8 @@ module SimpleCov
27
27
  # returns the `{line:, branch:, method:}` Hash; pass a criterion symbol
28
28
  # (`:line` / `:branch` / `:method`) to get that one CoverageStatistics.
29
29
  def coverage_statistics(criterion = nil)
30
- @coverage_statistics ||= compute_coverage_statistics
31
- criterion ? @coverage_statistics[criterion] : @coverage_statistics
30
+ stats = (@coverage_statistics ||= compute_coverage_statistics)
31
+ criterion ? stats[criterion] : stats
32
32
  end
33
33
 
34
34
  def coverage_statistics_by_file
@@ -47,15 +47,11 @@ module SimpleCov
47
47
 
48
48
  # Returns the count of lines that are not relevant for coverage
49
49
  def never_lines
50
- return 0.0 if empty?
51
-
52
50
  sum { |f| f.never_lines.size }
53
51
  end
54
52
 
55
53
  # Returns the count of skipped lines
56
54
  def skipped_lines
57
- return 0.0 if empty?
58
-
59
55
  sum { |f| f.skipped_lines.size }
60
56
  end
61
57
 
@@ -65,9 +61,13 @@ module SimpleCov
65
61
  map(&:covered_percent)
66
62
  end
67
63
 
68
- # Finds the least covered file and returns that file's name
64
+ # Finds the least covered file and returns that file's name, or nil
65
+ # for an empty list (e.g. a fully filtered result).
69
66
  def least_covered_file
70
- min_by(&:covered_percent).filename
67
+ # `covered_percent` is nil only for an unmeasured criterion, and :line
68
+ # is always measured, so the `|| 0.0` arm never fires at runtime; it
69
+ # exists to satisfy min_by's Comparable requirement.
70
+ min_by { |file| file.covered_percent || 0.0 }&.filename
71
71
  end
72
72
 
73
73
  # Returns the overall amount of relevant lines of code across all files in this list
@@ -137,7 +137,10 @@ module SimpleCov
137
137
  # enabled set so disabled criteria don't surface in totals, JSON,
138
138
  # or the HTML report.
139
139
  def compute_coverage_statistics_by_file
140
- seed = enabled_criteria_for_reporting.to_h { |criterion| [criterion, []] }
140
+ seed = enabled_criteria_for_reporting.to_h do |criterion|
141
+ bucket = [] #: Array[CoverageStatistics]
142
+ [criterion, bucket]
143
+ end
141
144
  @files.each_with_object(seed) do |file, together|
142
145
  file.coverage_statistics.each do |criterion, stats|
143
146
  together[criterion] << stats if together.key?(criterion)
@@ -153,9 +156,8 @@ module SimpleCov
153
156
  # criterion is enabled; the JRuby-gated branch/method criteria are
154
157
  # reported when they pass their own engine-support check.
155
158
  def enabled_criteria_for_reporting
156
- criteria = []
157
- criteria << :line if SimpleCov.coverage_criterion_enabled?(:line) ||
158
- SimpleCov.coverage_criterion_enabled?(:oneshot_line)
159
+ criteria = [] #: Array[SimpleCov::criterion]
160
+ criteria << :line if SimpleCov.line_coverage?
159
161
  criteria << :branch if SimpleCov.branch_coverage?
160
162
  criteria << :method if SimpleCov.method_coverage?
161
163
  criteria
@@ -23,10 +23,26 @@ module SimpleCov
23
23
  raise NotImplementedError, "The base filter class is not intended for direct use"
24
24
  end
25
25
 
26
- def self.build_filter(filter_argument)
27
- return filter_argument if filter_argument.is_a?(SimpleCov::Filter)
26
+ # Whether this filter's verdict depends only on the file's path, so it can
27
+ # be decided before that file has any coverage. Used when recording which
28
+ # tracked files a process did not load, where no coverage exists yet.
29
+ # Defaults to false so a custom filter is never guessed at. See #1250.
30
+ def path_only?
31
+ false
32
+ end
28
33
 
29
- class_for_argument(filter_argument).new(filter_argument)
34
+ # `string_filter` selects the semantics of bare String arguments —
35
+ # StringFilter's segment-substring match for `add_filter`/`skip`,
36
+ # GlobFilter for `cover` — and threads through Array elements so a
37
+ # list gets the same treatment as its members.
38
+ def self.build_filter(filter_argument, string_filter: SimpleCov::StringFilter)
39
+ case filter_argument
40
+ when SimpleCov::Filter then filter_argument
41
+ when String then string_filter.new(filter_argument)
42
+ when Array
43
+ SimpleCov::ArrayFilter.new(filter_argument.map { |arg| build_filter(arg, string_filter: string_filter) })
44
+ else class_for_argument(filter_argument).new(filter_argument)
45
+ end
30
46
  end
31
47
 
32
48
  def self.class_for_argument(filter_argument)
@@ -57,6 +73,10 @@ module SimpleCov
57
73
  source_file.project_filename.match?(segment_pattern)
58
74
  end
59
75
 
76
+ def path_only?
77
+ true
78
+ end
79
+
60
80
  private
61
81
 
62
82
  def segment_pattern
@@ -68,14 +88,15 @@ module SimpleCov
68
88
  escaped = Regexp.escape(normalized)
69
89
  boundary = '(?:\A|/)'
70
90
 
71
- if normalized.include?(".")
72
- # Filename pattern (e.g. "test.rb" matches "faked_test.rb"): allow
73
- # substring match within the last path segment, anchored to a
74
- # segment boundary.
75
- %r{#{boundary}[^/]*#{escaped}}
76
- elsif normalized.end_with?("/")
91
+ if normalized.end_with?("/")
77
92
  # Trailing slash signals directory-only matching.
78
93
  /#{boundary}#{escaped}/
94
+ elsif normalized.include?(".") && !normalized.include?("/")
95
+ # Bare filename pattern (e.g. "test.rb" matches "faked_test.rb"):
96
+ # allow a substring match within a single path segment. Multi-segment
97
+ # arguments must not get this relaxation, or "app/models/user.rb"
98
+ # would also match "webapp/models/user.rb".
99
+ %r{#{boundary}[^/]*#{escaped}(?=[/.]|\z)}
79
100
  else
80
101
  # Directory or path: require a segment-boundary match so "lib"
81
102
  # matches "lib/" but not "library/".
@@ -95,6 +116,10 @@ module SimpleCov
95
116
  def matches?(source_file)
96
117
  filter_argument.match?(source_file.project_filename)
97
118
  end
119
+
120
+ def path_only?
121
+ true
122
+ end
98
123
  end
99
124
 
100
125
  # Filter that matches when the configured block returns truthy for the
@@ -115,6 +140,10 @@ module SimpleCov
115
140
  def matches?(source_file)
116
141
  File.fnmatch?(filter_argument, source_file.project_filename, File::FNM_PATHNAME | File::FNM_EXTGLOB)
117
142
  end
143
+
144
+ def path_only?
145
+ true
146
+ end
118
147
  end
119
148
 
120
149
  # Filter that matches when any of its component filters (built from the
@@ -128,6 +157,11 @@ module SimpleCov
128
157
  super(filter_objects)
129
158
  end
130
159
 
160
+ # Path-decidable only when every component filter is.
161
+ def path_only?
162
+ filter_argument.all?(&:path_only?)
163
+ end
164
+
131
165
  # Returns true if any of the filters in the array match the given source file.
132
166
  # Configure this Filter like StringFilter.new(['some/path', /^some_regex/, Proc.new {|src_file| ... }])
133
167
  def matches?(source_files_list)
@@ -23,6 +23,22 @@ module SimpleCov
23
23
 
24
24
  private
25
25
 
26
+ # The one home of the "status lines go to stderr, not through warn"
27
+ # decision (see #1225). stderr rather than stdout because this is a
28
+ # status message, not the program's output, so it stays out of
29
+ # pipelines like `rspec -f json`. And `$stderr.puts` rather than
30
+ # `warn` so the line neither reaches `Warning.warn` hooks (warning
31
+ # trackers, raise-on-warning test setups) nor vanishes under `-W0`.
32
+ # Subclasses call this at the end of their `format`.
33
+ def emit_status(result)
34
+ $stderr.puts output_message(result) unless @silent # rubocop:disable Style/StderrPuts
35
+ rescue IOError
36
+ # A parallel runner can close a worker's stderr before its at_exit
37
+ # hooks run (rspec-conductor does). Losing the status line must not
38
+ # abort the exit tasks that follow report generation, i.e. the
39
+ # threshold checks and the .last_run.json write.
40
+ end
41
+
26
42
  # Subclasses override to prepend a marker (e.g. "JSON ") to the
27
43
  # summary line. Default empty for the HTML formatter, which has
28
44
  # historically been the unmarked default.
@@ -45,7 +61,8 @@ module SimpleCov
45
61
  # confusing than the absolute form. See issue #197.
46
62
  def displayable_output_path
47
63
  directory = relative_or_absolute_output_path
48
- entry_point_filename ? File.join(directory, entry_point_filename) : directory
64
+ entry_point = entry_point_filename
65
+ entry_point ? File.join(directory, entry_point) : directory
49
66
  end
50
67
 
51
68
  def relative_or_absolute_output_path
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require_relative "../atomic_file"
6
+
7
+ module SimpleCov
8
+ module Formatter
9
+ # Shared writer for the coverage.json artifact, used by JSONFormatter
10
+ # (as its report) and HTMLFormatter (as the side file feeding
11
+ # `simplecov serve` and external tools). Centralizing the write keeps
12
+ # the two copies byte-identical (same pretty printing, same binary
13
+ # mode, so no CRLF translation on Windows depending on which
14
+ # formatter wrote last) and gives both formatters the
15
+ # concurrent-overwrite warning of issue #1171.
16
+ module CoverageJSONWriter
17
+ FILENAME = "coverage.json"
18
+
19
+ # The previous report can embed the project's entire source text,
20
+ # so the overwrite check bounds how much of it is read looking for
21
+ # the meta object before falling back to a full parse.
22
+ META_SCAN_BYTES = 64 * 1024
23
+ private_constant :META_SCAN_BYTES
24
+
25
+ module_function
26
+
27
+ def write(output_path, hash, result)
28
+ path = File.join(output_path, FILENAME)
29
+ warn_if_concurrent_overwrite(path, result)
30
+ AtomicFile.write(path, JSON.pretty_generate(hash), binary: true)
31
+ path
32
+ end
33
+
34
+ # Warns when the existing coverage.json has a timestamp newer than this
35
+ # process's start time — a strong signal that a sibling test process
36
+ # (e.g., parallel_tests) wrote it while we were running, and that our
37
+ # write is about to clobber their data.
38
+ def warn_if_concurrent_overwrite(path, result)
39
+ start_time = SimpleCov.process_start_time or return
40
+ existing = existing_meta(path) or return
41
+ return unless existing[:timestamp] > start_time
42
+
43
+ # Both formatters write coverage.json through this method, so when
44
+ # they are configured together the file found here was just written
45
+ # by our own run, not a concurrent one. A matching command_name
46
+ # means the same merged result, so there's nothing to lose by
47
+ # overwriting. See issue #1171.
48
+ return if existing[:command_name] == result.command_name
49
+
50
+ warn "simplecov: #{path} was written at #{existing[:timestamp].iso8601} — after " \
51
+ "this process started at #{start_time.iso8601}. Overwriting " \
52
+ "likely loses coverage data from a concurrent test run. For " \
53
+ "parallel test setups, use SimpleCov::ResultMerger or run a single " \
54
+ "collation step after all workers finish."
55
+ end
56
+
57
+ def existing_meta(path)
58
+ return nil unless File.exist?(path)
59
+
60
+ meta = parse_meta(path) or return nil
61
+ timestamp = meta[:timestamp] or return nil
62
+
63
+ {timestamp: Time.iso8601(timestamp), command_name: meta[:command_name]}
64
+ rescue ArgumentError
65
+ # An unparseable timestamp disables the check for this file.
66
+ nil
67
+ end
68
+
69
+ def parse_meta(path)
70
+ parse_meta_head(path) || parse_meta_full(path)
71
+ end
72
+
73
+ # The meta object is flat and sits at the head of every file this
74
+ # module writes, so the common case parses just that slice instead
75
+ # of a multi-megabyte report. A miss (foreign key order, a brace
76
+ # inside a meta string) falls back to the full parse.
77
+ def parse_meta_head(path)
78
+ head = File.read(path, META_SCAN_BYTES).to_s
79
+ slice = head[/"meta"\s*:\s*(\{.*?\})/m, 1]
80
+ return nil unless slice
81
+
82
+ # The captured slice is always a JSON object, so a successful parse
83
+ # is always a Hash.
84
+ JSON.parse(slice, symbolize_names: true)
85
+ rescue JSON::ParserError
86
+ nil
87
+ end
88
+
89
+ def parse_meta_full(path)
90
+ parsed = JSON.parse(File.read(path), symbolize_names: true)
91
+ parsed.is_a?(Hash) ? parsed[:meta] : nil
92
+ rescue JSON::ParserError
93
+ nil
94
+ end
95
+ end
96
+ end
97
+ end