ultra-pro-pkg 0.0.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 +7 -0
  2. data/simplecov-1.0.0/LICENSE +20 -0
  3. data/simplecov-1.0.0/README.md +1524 -0
  4. data/simplecov-1.0.0/doc/alternate-formatters.md +66 -0
  5. data/simplecov-1.0.0/doc/commercial-services.md +25 -0
  6. data/simplecov-1.0.0/doc/editor-integration.md +18 -0
  7. data/simplecov-1.0.0/exe/simplecov +11 -0
  8. data/simplecov-1.0.0/lib/minitest/simplecov_plugin.rb +171 -0
  9. data/simplecov-1.0.0/lib/simplecov/autostart.rb +11 -0
  10. data/simplecov-1.0.0/lib/simplecov/cli/clean.rb +47 -0
  11. data/simplecov-1.0.0/lib/simplecov/cli/coverage.rb +91 -0
  12. data/simplecov-1.0.0/lib/simplecov/cli/diff.rb +151 -0
  13. data/simplecov-1.0.0/lib/simplecov/cli/dotfile.rb +100 -0
  14. data/simplecov-1.0.0/lib/simplecov/cli/merge.rb +116 -0
  15. data/simplecov-1.0.0/lib/simplecov/cli/open.rb +50 -0
  16. data/simplecov-1.0.0/lib/simplecov/cli/report.rb +84 -0
  17. data/simplecov-1.0.0/lib/simplecov/cli/run.rb +36 -0
  18. data/simplecov-1.0.0/lib/simplecov/cli/serve.rb +139 -0
  19. data/simplecov-1.0.0/lib/simplecov/cli/uncovered.rb +107 -0
  20. data/simplecov-1.0.0/lib/simplecov/cli.rb +150 -0
  21. data/simplecov-1.0.0/lib/simplecov/color.rb +74 -0
  22. data/simplecov-1.0.0/lib/simplecov/combine/branches_combiner.rb +54 -0
  23. data/simplecov-1.0.0/lib/simplecov/combine/files_combiner.rb +30 -0
  24. data/simplecov-1.0.0/lib/simplecov/combine/lines_combiner.rb +45 -0
  25. data/simplecov-1.0.0/lib/simplecov/combine/methods_combiner.rb +26 -0
  26. data/simplecov-1.0.0/lib/simplecov/combine/results_combiner.rb +61 -0
  27. data/simplecov-1.0.0/lib/simplecov/combine.rb +30 -0
  28. data/simplecov-1.0.0/lib/simplecov/command_guesser.rb +78 -0
  29. data/simplecov-1.0.0/lib/simplecov/configuration/coverage.rb +171 -0
  30. data/simplecov-1.0.0/lib/simplecov/configuration/coverage_criteria.rb +156 -0
  31. data/simplecov-1.0.0/lib/simplecov/configuration/filters.rb +197 -0
  32. data/simplecov-1.0.0/lib/simplecov/configuration/formatting.rb +119 -0
  33. data/simplecov-1.0.0/lib/simplecov/configuration/ignored_entries.rb +63 -0
  34. data/simplecov-1.0.0/lib/simplecov/configuration/merging.rb +164 -0
  35. data/simplecov-1.0.0/lib/simplecov/configuration/thresholds.rb +174 -0
  36. data/simplecov-1.0.0/lib/simplecov/configuration.rb +184 -0
  37. data/simplecov-1.0.0/lib/simplecov/coverage_statistics.rb +59 -0
  38. data/simplecov-1.0.0/lib/simplecov/coverage_violations.rb +148 -0
  39. data/simplecov-1.0.0/lib/simplecov/defaults.rb +60 -0
  40. data/simplecov-1.0.0/lib/simplecov/deprecation.rb +45 -0
  41. data/simplecov-1.0.0/lib/simplecov/directive.rb +162 -0
  42. data/simplecov-1.0.0/lib/simplecov/exit_codes/exit_code_handling.rb +35 -0
  43. data/simplecov-1.0.0/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +45 -0
  44. data/simplecov-1.0.0/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
  45. data/simplecov-1.0.0/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +44 -0
  46. data/simplecov-1.0.0/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
  47. data/simplecov-1.0.0/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +70 -0
  48. data/simplecov-1.0.0/lib/simplecov/exit_codes.rb +18 -0
  49. data/simplecov-1.0.0/lib/simplecov/exit_handling.rb +158 -0
  50. data/simplecov-1.0.0/lib/simplecov/file_list.rb +164 -0
  51. data/simplecov-1.0.0/lib/simplecov/filter.rb +139 -0
  52. data/simplecov-1.0.0/lib/simplecov/formatter/base.rb +101 -0
  53. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
  54. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
  55. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  56. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  57. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
  58. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
  59. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter.rb +79 -0
  60. data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter/errors_formatter.rb +84 -0
  61. data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +128 -0
  62. data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
  63. data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter.rb +87 -0
  64. data/simplecov-1.0.0/lib/simplecov/formatter/multi_formatter.rb +31 -0
  65. data/simplecov-1.0.0/lib/simplecov/formatter/simple_formatter.rb +23 -0
  66. data/simplecov-1.0.0/lib/simplecov/formatter.rb +14 -0
  67. data/simplecov-1.0.0/lib/simplecov/last_run.rb +35 -0
  68. data/simplecov-1.0.0/lib/simplecov/lines_classifier.rb +60 -0
  69. data/simplecov-1.0.0/lib/simplecov/load_global_config.rb +13 -0
  70. data/simplecov-1.0.0/lib/simplecov/no_defaults.rb +4 -0
  71. data/simplecov-1.0.0/lib/simplecov/parallel_adapters/base.rb +61 -0
  72. data/simplecov-1.0.0/lib/simplecov/parallel_adapters/generic.rb +44 -0
  73. data/simplecov-1.0.0/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
  74. data/simplecov-1.0.0/lib/simplecov/parallel_adapters.rb +83 -0
  75. data/simplecov-1.0.0/lib/simplecov/parallel_coordination.rb +129 -0
  76. data/simplecov-1.0.0/lib/simplecov/process.rb +37 -0
  77. data/simplecov-1.0.0/lib/simplecov/profiles/bundler_filter.rb +5 -0
  78. data/simplecov-1.0.0/lib/simplecov/profiles/hidden_filter.rb +5 -0
  79. data/simplecov-1.0.0/lib/simplecov/profiles/rails.rb +32 -0
  80. data/simplecov-1.0.0/lib/simplecov/profiles/root_filter.rb +11 -0
  81. data/simplecov-1.0.0/lib/simplecov/profiles/strict.rb +32 -0
  82. data/simplecov-1.0.0/lib/simplecov/profiles/test_frameworks.rb +5 -0
  83. data/simplecov-1.0.0/lib/simplecov/profiles.rb +64 -0
  84. data/simplecov-1.0.0/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
  85. data/simplecov-1.0.0/lib/simplecov/result/source_file_builder.rb +51 -0
  86. data/simplecov-1.0.0/lib/simplecov/result.rb +183 -0
  87. data/simplecov-1.0.0/lib/simplecov/result_adapter.rb +92 -0
  88. data/simplecov-1.0.0/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
  89. data/simplecov-1.0.0/lib/simplecov/result_merger/resultset_file.rb +38 -0
  90. data/simplecov-1.0.0/lib/simplecov/result_merger/resultset_store.rb +50 -0
  91. data/simplecov-1.0.0/lib/simplecov/result_merger.rb +162 -0
  92. data/simplecov-1.0.0/lib/simplecov/result_processing.rb +179 -0
  93. data/simplecov-1.0.0/lib/simplecov/simulate_coverage.rb +75 -0
  94. data/simplecov-1.0.0/lib/simplecov/source_file/branch.rb +82 -0
  95. data/simplecov-1.0.0/lib/simplecov/source_file/branch_builder.rb +114 -0
  96. data/simplecov-1.0.0/lib/simplecov/source_file/builder_context.rb +28 -0
  97. data/simplecov-1.0.0/lib/simplecov/source_file/line.rb +77 -0
  98. data/simplecov-1.0.0/lib/simplecov/source_file/line_builder.rb +43 -0
  99. data/simplecov-1.0.0/lib/simplecov/source_file/method.rb +52 -0
  100. data/simplecov-1.0.0/lib/simplecov/source_file/method_builder.rb +58 -0
  101. data/simplecov-1.0.0/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
  102. data/simplecov-1.0.0/lib/simplecov/source_file/skip_chunks.rb +75 -0
  103. data/simplecov-1.0.0/lib/simplecov/source_file/source_loader.rb +63 -0
  104. data/simplecov-1.0.0/lib/simplecov/source_file/statistics.rb +57 -0
  105. data/simplecov-1.0.0/lib/simplecov/source_file.rb +189 -0
  106. data/simplecov-1.0.0/lib/simplecov/static_coverage_extractor/method_collector.rb +55 -0
  107. data/simplecov-1.0.0/lib/simplecov/static_coverage_extractor/visitor.rb +171 -0
  108. data/simplecov-1.0.0/lib/simplecov/static_coverage_extractor.rb +110 -0
  109. data/simplecov-1.0.0/lib/simplecov/useless_results_remover.rb +27 -0
  110. data/simplecov-1.0.0/lib/simplecov/version.rb +5 -0
  111. data/simplecov-1.0.0/lib/simplecov-html.rb +4 -0
  112. data/simplecov-1.0.0/lib/simplecov.rb +255 -0
  113. data/simplecov-1.0.0/lib/simplecov_json_formatter.rb +4 -0
  114. data/simplecov-1.0.0/schemas/coverage-v1.0.schema.json +306 -0
  115. data/simplecov-1.0.0/schemas/coverage.schema.json +306 -0
  116. data/ultra-pro-pkg.gemspec +12 -0
  117. metadata +156 -0
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ #
5
+ # Responsible for adapting the format of the coverage result whether it's default or with statistics
6
+ #
7
+ class ResultAdapter
8
+ attr_reader :result
9
+
10
+ def initialize(result)
11
+ @result = result
12
+ end
13
+
14
+ def self.call(*)
15
+ new(*).adapt
16
+ end
17
+
18
+ def adapt
19
+ return unless result
20
+
21
+ result.to_h do |file_name, cover_statistic|
22
+ [file_name, adapt_one(file_name, cover_statistic)]
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # Pre-0.18 resultsets pointed each filename straight at a line-coverage
29
+ # array; everything since uses the `{lines:, branches:, methods:}`
30
+ # shape. Newer entries also need their methods table massaged before
31
+ # downstream code merges across processes.
32
+ def adapt_one(file_name, cover_statistic)
33
+ return {"lines" => cover_statistic} if cover_statistic.is_a?(Array)
34
+
35
+ adapt_oneshot_lines_if_needed(file_name, cover_statistic)
36
+ normalize_method_keys(cover_statistic)
37
+ cover_statistic
38
+ end
39
+
40
+ # Normalize memory addresses in method coverage keys so that results
41
+ # from different processes can be merged. Anonymous class names like
42
+ # "#<Class:0x00007ff19ab24790>" get inconsistent addresses across runs.
43
+ # Address widths vary by runtime (32-bit hosts: 8 hex chars; 64-bit
44
+ # CRuby: 16; some JVM/TruffleRuby formats may differ), so match any
45
+ # length of hex digits and collapse to a single placeholder.
46
+ ADDRESS_PATTERN = /0x\h+/
47
+ private_constant :ADDRESS_PATTERN
48
+
49
+ ADDRESS_PLACEHOLDER = "0x0"
50
+ private_constant :ADDRESS_PLACEHOLDER
51
+
52
+ # Strip the `#<Class:Foo>` wrapper Ruby's Coverage adds to singleton-class
53
+ # method keys. `module_function` and class methods get recorded both as
54
+ # singleton (`[#<Class:Foo>, :m, …]`) and instance/module (`[Foo, :m, …]`)
55
+ # entries pointing at the same source location; only one of the two is
56
+ # ever reachable at runtime, so we merge them. Only applies to named
57
+ # constants — anonymous-class addresses like `#<Class:0x0>` are left
58
+ # alone (handled by ADDRESS_PATTERN above).
59
+ SINGLETON_WRAPPER_PATTERN = /\A#<Class:([A-Z_][\w:]*)>\z/
60
+ private_constant :SINGLETON_WRAPPER_PATTERN
61
+
62
+ def normalize_method_keys(cover_statistic)
63
+ methods = cover_statistic[:methods]
64
+ return unless methods
65
+
66
+ cover_statistic[:methods] = methods.each_with_object({}) do |(key, count), normalized|
67
+ normalized_key = key.dup
68
+ normalized_key[0] = key[0].to_s
69
+ .gsub(ADDRESS_PATTERN, ADDRESS_PLACEHOLDER)
70
+ .sub(SINGLETON_WRAPPER_PATTERN, '\1')
71
+ # Keys may collide after normalization (anonymous classes sharing a
72
+ # method name, or singleton + instance forms of a module_function method).
73
+ normalized[normalized_key] = normalized.fetch(normalized_key, 0) + count
74
+ end
75
+ end
76
+
77
+ def adapt_oneshot_lines_if_needed(file_name, cover_statistic)
78
+ return unless cover_statistic.key?(:oneshot_lines)
79
+
80
+ oneshot_lines = cover_statistic.delete(:oneshot_lines)
81
+ line_stub = build_line_stub(file_name, oneshot_lines)
82
+ oneshot_lines.each { |covered_line| line_stub[covered_line - 1] = 1 }
83
+ cover_statistic[:lines] = line_stub
84
+ end
85
+
86
+ def build_line_stub(file_name, oneshot_lines)
87
+ Coverage.line_stub(file_name)
88
+ rescue Errno::ENOENT, SyntaxError
89
+ Array.new(oneshot_lines.max || 0, nil)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ResultMerger
5
+ # We changed the format of the raw result data in simplecov, as people
6
+ # are likely to have "old" resultsets lying around (but not too old so
7
+ # that they're still considered we can adapt them). See
8
+ # https://github.com/simplecov-ruby/simplecov/pull/824#issuecomment-576049747
9
+ module LegacyFormatAdapter
10
+ module_function
11
+
12
+ def call(result)
13
+ pre_0_18?(result) ? upgrade(result) : result
14
+ end
15
+
16
+ # Pre-0.18 coverage data pointed from file directly to an array of
17
+ # line coverage rather than a `{"lines" => [...]}` hash.
18
+ def pre_0_18?(result)
19
+ _key, data = result.first
20
+ data.is_a?(Array)
21
+ end
22
+
23
+ def upgrade(result)
24
+ result.transform_values { |line_coverage_data| {"lines" => line_coverage_data} }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SimpleCov
6
+ module ResultMerger
7
+ # Read + parse a `.resultset.json` file with the same tolerance the
8
+ # historical `ResultMerger` had: missing file returns `{}`, an empty
9
+ # or unparseable file warns and returns `{}`, parse success returns
10
+ # the decoded Hash.
11
+ module ResultsetFile
12
+ module_function
13
+
14
+ def parse(path)
15
+ data = read(path)
16
+ decode(data)
17
+ end
18
+
19
+ def read(path)
20
+ return unless File.exist?(path)
21
+
22
+ data = File.read(path)
23
+ return if data.nil? || data.length < 2
24
+
25
+ data
26
+ end
27
+
28
+ def decode(content)
29
+ return {} unless content
30
+
31
+ JSON.parse(content) || {}
32
+ rescue StandardError
33
+ warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
34
+ {}
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+
6
+ module SimpleCov
7
+ module ResultMerger
8
+ # Reads and writes the persistent `.resultset.json` cache, including
9
+ # file-lock synchronization between processes and atomic temp-file
10
+ # renames so concurrent readers don't observe a truncated file.
11
+ module ResultsetStore
12
+ module_function
13
+
14
+ def resultset_path
15
+ File.join(SimpleCov.coverage_path, ".resultset.json")
16
+ end
17
+
18
+ def writelock_path
19
+ File.join(SimpleCov.coverage_path, ".resultset.json.lock")
20
+ end
21
+
22
+ def write(resultset)
23
+ FileUtils.mkdir_p(SimpleCov.coverage_path)
24
+ temp_path = "#{resultset_path}.#{Process.pid}.tmp"
25
+ File.open(temp_path, "w") { |f| f.puts JSON.pretty_generate(resultset) }
26
+ File.rename(temp_path, resultset_path)
27
+ end
28
+
29
+ # Ensure only one process is reading or writing the resultset at
30
+ # any given time. Reentrant: the lock is acquired once per outer
31
+ # call no matter how deeply nested.
32
+ def synchronize(&)
33
+ return yield if @locked
34
+
35
+ @locked = true
36
+ with_flock(&)
37
+ ensure
38
+ @locked = false
39
+ end
40
+
41
+ def with_flock
42
+ FileUtils.mkdir_p(SimpleCov.coverage_path)
43
+ File.open(writelock_path, "w+") do |f|
44
+ f.flock(File::LOCK_EX)
45
+ yield
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "result_merger/legacy_format_adapter"
4
+ require_relative "result_merger/resultset_file"
5
+ require_relative "result_merger/resultset_store"
6
+
7
+ module SimpleCov
8
+ #
9
+ # Singleton that is responsible for caching, loading and merging
10
+ # SimpleCov::Results into a single result for coverage analysis based
11
+ # upon multiple test suites.
12
+ #
13
+ module ResultMerger
14
+ class << self
15
+ def resultset_path
16
+ ResultsetStore.resultset_path
17
+ end
18
+
19
+ def merge_and_store(*file_paths, ignore_timeout: false)
20
+ result = merge_results(*file_paths, ignore_timeout: ignore_timeout)
21
+ store_result(result) if result
22
+ result
23
+ end
24
+
25
+ def merge_results(*file_paths, ignore_timeout: false)
26
+ # It is intentional here that files are only read in and parsed one at a time.
27
+ #
28
+ # In big CI setups you might deal with 100s of CI jobs and each one producing Megabytes
29
+ # of data. Reading them all in easily produces Gigabytes of memory consumption which
30
+ # we want to avoid.
31
+ #
32
+ # For similar reasons a SimpleCov::Result is only created in the end as that'd create
33
+ # even more data especially when it also reads in all source files.
34
+ initial_memo = valid_results(file_paths.shift, ignore_timeout: ignore_timeout)
35
+
36
+ command_names, coverage = file_paths.reduce(initial_memo) do |memo, file_path|
37
+ merge_coverage(memo, valid_results(file_path, ignore_timeout: ignore_timeout))
38
+ end
39
+
40
+ create_result(command_names, coverage)
41
+ end
42
+
43
+ def valid_results(file_path, ignore_timeout: false)
44
+ merge_valid_results(ResultsetFile.parse(file_path), ignore_timeout: ignore_timeout)
45
+ end
46
+
47
+ def merge_valid_results(results, ignore_timeout: false)
48
+ results = drop_expired_results(results) unless ignore_timeout
49
+
50
+ command_plus_coverage = results.map do |command_name, data|
51
+ [[command_name], LegacyFormatAdapter.call(data.fetch("coverage"))]
52
+ end
53
+
54
+ # one file itself _might_ include multiple test runs
55
+ merge_coverage(*command_plus_coverage)
56
+ end
57
+
58
+ def drop_expired_results(results)
59
+ fresh, expired = results.partition { |_command_name, data| within_merge_timeout?(data) }
60
+ return results if expired.empty?
61
+
62
+ warn_about_expired_results(expired.map(&:first))
63
+ fresh.to_h
64
+ end
65
+
66
+ def within_merge_timeout?(data)
67
+ (Time.now - Time.at(data.fetch("timestamp"))) < SimpleCov.merge_timeout
68
+ end
69
+
70
+ def warn_about_expired_results(expired_command_names)
71
+ # Subprocesses merge the resultset too (each forked worker calls
72
+ # `SimpleCov.result` to store its slice), and the default `at_fork`
73
+ # sets `print_errors false` for them. Without this guard the warning
74
+ # is emitted once per worker — N copies of the same message for an
75
+ # N-worker run. Gate on `print_errors` like every other SimpleCov
76
+ # warning so only the reporting process speaks up.
77
+ return unless SimpleCov.print_errors
78
+
79
+ warn "[SimpleCov]: Excluded #{expired_command_names.size} result(s) older than " \
80
+ "merge_timeout (#{SimpleCov.merge_timeout}s) from the merged report: " \
81
+ "#{expired_command_names.sort.join(', ')}. " \
82
+ "Increase SimpleCov.merge_timeout to include them."
83
+ end
84
+
85
+ def create_result(command_names, coverage)
86
+ return nil unless coverage
87
+
88
+ command_name = command_names.reject(&:empty?).sort.join(", ")
89
+ # The merged result is the authoritative one users actually see, so
90
+ # it's the one that warns about source files dropped because they no
91
+ # longer exist on disk (issue #980). The per-process slices built in
92
+ # `process_coverage_result` stay quiet to avoid one warning per worker.
93
+ SimpleCov::Result.new(coverage, command_name: command_name, report: true)
94
+ end
95
+
96
+ def merge_coverage(*results)
97
+ return [[""], nil] if results.empty?
98
+ return results.first if results.size == 1
99
+
100
+ results.reduce do |(memo_command, memo_coverage), (command, coverage)|
101
+ # timestamp is dropped here, which is intentional (we merge it, it gets a new time stamp as of now)
102
+ merged_coverage = Combine.combine(Combine::ResultsCombiner, memo_coverage, coverage)
103
+ [memo_command + command, merged_coverage]
104
+ end
105
+ end
106
+
107
+ #
108
+ # Gets all SimpleCov::Results stored in resultset, merges them and produces a new
109
+ # SimpleCov::Result with merged coverage data and the command_name
110
+ # for the result consisting of a join on all source result's names
111
+ def merged_result
112
+ command_names, coverage = merge_valid_results(read_resultset)
113
+ create_result(command_names, coverage)
114
+ end
115
+
116
+ def read_resultset
117
+ content = synchronize_resultset { ResultsetFile.read(resultset_path) }
118
+ ResultsetFile.decode(content)
119
+ end
120
+
121
+ # Saves the given SimpleCov::Result in the resultset cache
122
+ def store_result(result) # rubocop:disable Naming/PredicateMethod
123
+ synchronize_resultset do
124
+ # Ensure we have the latest, in case it was already cached
125
+ new_resultset = read_resultset
126
+
127
+ # A single result only ever has one command_name, see `SimpleCov::Result#to_hash`
128
+ command_name, data = result.to_hash.first
129
+ new_resultset[command_name] = merged_entry(new_resultset[command_name], data)
130
+
131
+ ResultsetStore.write(new_resultset)
132
+ end
133
+ true
134
+ end
135
+
136
+ # If an entry with the same command_name was written AFTER our process
137
+ # started, a sibling test runner (typically a subprocess our parent
138
+ # process shelled out to) wrote it. Combine coverage data rather than
139
+ # overwriting, so an empty parent-process result doesn't clobber the
140
+ # subprocess's real data. See https://github.com/simplecov-ruby/simplecov/issues/581.
141
+ def merged_entry(existing, incoming)
142
+ return incoming unless concurrent_runner_entry?(existing)
143
+
144
+ incoming.merge(
145
+ "coverage" => Combine.combine(Combine::ResultsCombiner, existing["coverage"], incoming["coverage"])
146
+ )
147
+ end
148
+
149
+ def concurrent_runner_entry?(entry)
150
+ return false unless entry.is_a?(Hash)
151
+
152
+ timestamp = entry["timestamp"]
153
+ process_start = SimpleCov.process_start_time
154
+ timestamp && process_start && timestamp.to_i >= process_start.to_i
155
+ end
156
+
157
+ def synchronize_resultset(&)
158
+ ResultsetStore.synchronize(&)
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Result-building façade: turns the raw `Coverage.result` hash into a
4
+ # `SimpleCov::Result`, applies filters and groups, drives merging
5
+ # across test suites via `SimpleCov::ResultMerger`, and exposes the
6
+ # `collate` entry point for stitching disparate resultsets together.
7
+ module SimpleCov
8
+ class << self
9
+ #
10
+ # Collate a series of SimpleCov result files into a single SimpleCov output.
11
+ #
12
+ # See README for usage. By default `collate` ignores the merge_timeout
13
+ # so all results in all files specified will be merged. Pass
14
+ # `ignore_timeout: false` to honor it.
15
+ #
16
+ def collate(result_filenames, profile = nil, ignore_timeout: true, &)
17
+ raise ArgumentError, "There are no reports to be merged" if result_filenames.empty?
18
+
19
+ initial_setup(profile, &)
20
+
21
+ # Use the ResultMerger to produce a single, merged result, ready to use.
22
+ @result = ResultMerger.merge_and_store(*result_filenames, ignore_timeout: ignore_timeout)
23
+
24
+ @collating_result = true
25
+ run_exit_tasks!
26
+ ensure
27
+ @collating_result = false
28
+ end
29
+
30
+ #
31
+ # Returns the result for the current coverage run, merging it across test suites
32
+ # from cache using SimpleCov::ResultMerger if use_merging is activated (default)
33
+ #
34
+ def result
35
+ return @result if result?
36
+
37
+ use_merging = merging
38
+
39
+ # Collect our coverage result. When merging is off there is no merge
40
+ # step, so this per-process result is the final one and reports any
41
+ # dropped source files; otherwise the merged result does the reporting.
42
+ process_coverage_result(report: !use_merging) if defined?(Coverage) && Coverage.running?
43
+
44
+ # If we're using merging of results, store the current result
45
+ # first (if there is one), then merge the results and return those
46
+ if use_merging
47
+ SimpleCov::ResultMerger.store_result(@result) if result?
48
+ return @result unless finalize_merge?
49
+
50
+ wait_for_other_processes
51
+ @result = SimpleCov::ResultMerger.merged_result
52
+ end
53
+
54
+ @result
55
+ end
56
+
57
+ # Returns nil if the result has not been computed, otherwise the result.
58
+ def result?
59
+ defined?(@result) && @result
60
+ end
61
+
62
+ # @api private — true while `SimpleCov.collate` is running its finalizer.
63
+ def collating_result?
64
+ defined?(@collating_result) && @collating_result
65
+ end
66
+
67
+ # Applies the configured filters to the given array of SimpleCov::SourceFile items
68
+ def filtered(files)
69
+ result = files.clone
70
+ filters.each do |filter|
71
+ result = result.reject { |source_file| filter.matches?(source_file) }
72
+ end
73
+ SimpleCov::FileList.new result
74
+ end
75
+
76
+ # Bin the given source files by group filter. `groups:` defaults to
77
+ # `SimpleCov.groups`; pass a Hash explicitly to bin against a
78
+ # different group config (e.g., the snapshot a Result captured at
79
+ # construction). Files matched by no group fall into the implicit
80
+ # "Ungrouped" bucket.
81
+ def grouped(files, groups: SimpleCov.groups)
82
+ return {} if groups.empty?
83
+
84
+ grouped = groups.transform_values do |filter|
85
+ SimpleCov::FileList.new(files.select { |source_file| filter.matches?(source_file) })
86
+ end
87
+
88
+ in_group = grouped_file_set(grouped)
89
+ ungrouped = files.reject { |source_file| in_group.include?(source_file) }
90
+ grouped["Ungrouped"] = SimpleCov::FileList.new(ungrouped) if ungrouped.any?
91
+
92
+ grouped
93
+ end
94
+
95
+ # Applies the profile of given name on SimpleCov configuration
96
+ def load_profile(name)
97
+ profiles.load(name)
98
+ end
99
+
100
+ # Clear out the previously cached .result. Primarily useful in testing.
101
+ def clear_result
102
+ @result = nil
103
+ end
104
+
105
+ # @api private — persist the per-criterion coverage percentages
106
+ # rounded down (see #679) so the next run can compute drift.
107
+ def write_last_run(result)
108
+ SimpleCov::LastRun.write(
109
+ result: result.coverage_statistics.transform_values { |stats| round_coverage(stats.percent) }
110
+ )
111
+ end
112
+
113
+ # @api private — round down to two decimals to be extra strict.
114
+ def round_coverage(coverage)
115
+ coverage.floor(2)
116
+ end
117
+
118
+ private
119
+
120
+ def initial_setup(profile, &block)
121
+ load_profile(profile) if profile
122
+ configure(&block) if block
123
+ end
124
+
125
+ def grouped_file_set(grouped)
126
+ grouped.values.each_with_object(Set.new) { |file_list, set| set.merge(file_list) }
127
+ end
128
+
129
+ # Finds files that were to be tracked but were not loaded, and
130
+ # initializes their line-by-line coverage to zero (or nil for
131
+ # comments / whitespace).
132
+ def add_not_loaded_files(result)
133
+ globs = unloaded_file_discovery_globs
134
+ return [result, Set.new] if globs.empty?
135
+
136
+ inject_unloaded_files(result.dup, discover_unloaded_paths(globs))
137
+ end
138
+
139
+ # Globs to expand on disk when injecting unloaded files into the
140
+ # result. Combines the legacy `track_files` glob (additive only)
141
+ # with every string glob declared via `cover` (also restrictive,
142
+ # but the restriction lives in `Result#apply_cover_filters!`).
143
+ def unloaded_file_discovery_globs
144
+ [tracked_files, *cover_globs].compact
145
+ end
146
+
147
+ # Expand the given globs relative to SimpleCov.root, not Dir.pwd —
148
+ # test runners that chdir (or CI scripts that invoke the suite
149
+ # from a subdir) would otherwise silently miss the unloaded-file
150
+ # injection and produce a different file set per environment. See
151
+ # issue #1106.
152
+ def discover_unloaded_paths(globs)
153
+ globs.flat_map { |glob| Dir.glob(glob, base: root) }.uniq
154
+ end
155
+
156
+ def inject_unloaded_files(result, candidate_paths)
157
+ not_loaded_files = candidate_paths.each_with_object(Set.new) do |file, set|
158
+ absolute_path = File.expand_path(file, root)
159
+ next if result.key?(absolute_path)
160
+
161
+ result[absolute_path] = SimulateCoverage.call(absolute_path)
162
+ set << absolute_path
163
+ end
164
+
165
+ [result, not_loaded_files]
166
+ end
167
+
168
+ # Run all the steps that handle processing the raw coverage result.
169
+ # `report:` is true only when this slice is the final result (merging
170
+ # off); with merging on the merged result reports dropped source files,
171
+ # so the per-process slice stays quiet to avoid one warning per worker.
172
+ def process_coverage_result(report:)
173
+ @result = SimpleCov::UselessResultsRemover.call(Coverage.result)
174
+ @result = SimpleCov::ResultAdapter.call(@result)
175
+ result, not_loaded_files = add_not_loaded_files(@result)
176
+ @result = SimpleCov::Result.new(result, not_loaded_files: not_loaded_files, report: report)
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "static_coverage_extractor"
4
+
5
+ module SimpleCov
6
+ #
7
+ # Responsible for producing file coverage metrics.
8
+ #
9
+ module SimulateCoverage
10
+ module_function
11
+
12
+ #
13
+ # Simulate a file coverage report for a file that was tracked but never
14
+ # required. Returns the same hash shape as `Coverage.result` (lines,
15
+ # branches, methods).
16
+ #
17
+ # The line classification comes from `Coverage.line_stub` — the same
18
+ # classification the runtime would have produced if the file had been
19
+ # required — overlaid with SimpleCov's `# :nocov:` toggles and
20
+ # `# simplecov:disable line` directive ranges, which `Coverage` doesn't
21
+ # know about. This keeps "relevant lines" identical whether a file was
22
+ # loaded or just tracked, fixing the multi-line statement discrepancy
23
+ # in https://github.com/simplecov-ruby/simplecov/issues/654.
24
+ #
25
+ # Branches and methods are enumerated by static analysis (via
26
+ # `StaticCoverageExtractor`, which uses Prism). Earlier behavior left
27
+ # both as empty hashes, which made unloaded files invisible to the
28
+ # branch/method denominators while their lines DID count — so a
29
+ # `track_files`/`cover` glob that picked up files without specs
30
+ # silently inflated branch% relative to line%. See
31
+ # https://github.com/simplecov-ruby/simplecov/issues/1059. When Prism
32
+ # isn't loadable (Ruby < 3.3 without the prism gem) or the file
33
+ # can't be parsed, fall back to the old empty hashes — old behavior,
34
+ # old tradeoff.
35
+ #
36
+ # @return [Hash]
37
+ #
38
+ def call(absolute_path)
39
+ source_lines = read_lines(absolute_path)
40
+ lines = coverage_stub(absolute_path, source_lines) ||
41
+ LinesClassifier.new.classify(source_lines)
42
+ synthesized = StaticCoverageExtractor.call(source_lines.join) ||
43
+ {"branches" => {}, "methods" => {}}
44
+
45
+ {
46
+ "lines" => lines,
47
+ "branches" => synthesized["branches"],
48
+ "methods" => synthesized["methods"]
49
+ }
50
+ end
51
+
52
+ def read_lines(path)
53
+ File.readlines(path)
54
+ rescue Errno::ENOENT
55
+ []
56
+ end
57
+
58
+ # Combine `Coverage.line_stub` (which gets multi-line statements right)
59
+ # with `LinesClassifier` (which knows about `# :nocov:` toggles and
60
+ # `# simplecov:disable line` ranges). Returns nil — and the caller
61
+ # falls back to `LinesClassifier` alone — when `Coverage` can't read
62
+ # or parse the file, or when the runtime doesn't expose `line_stub`
63
+ # (JRuby and TruffleRuby).
64
+ def coverage_stub(path, source_lines)
65
+ return nil unless Coverage.respond_to?(:line_stub)
66
+
67
+ stub = Coverage.line_stub(path)
68
+ classifier_output = LinesClassifier.new.classify(source_lines)
69
+ stub.each_index { |idx| stub[idx] = nil if classifier_output[idx].nil? }
70
+ stub
71
+ rescue Errno::ENOENT, SyntaxError
72
+ nil
73
+ end
74
+ end
75
+ end