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.
- checksums.yaml +7 -0
- data/simplecov-1.0.0/LICENSE +20 -0
- data/simplecov-1.0.0/README.md +1524 -0
- data/simplecov-1.0.0/doc/alternate-formatters.md +66 -0
- data/simplecov-1.0.0/doc/commercial-services.md +25 -0
- data/simplecov-1.0.0/doc/editor-integration.md +18 -0
- data/simplecov-1.0.0/exe/simplecov +11 -0
- data/simplecov-1.0.0/lib/minitest/simplecov_plugin.rb +171 -0
- data/simplecov-1.0.0/lib/simplecov/autostart.rb +11 -0
- data/simplecov-1.0.0/lib/simplecov/cli/clean.rb +47 -0
- data/simplecov-1.0.0/lib/simplecov/cli/coverage.rb +91 -0
- data/simplecov-1.0.0/lib/simplecov/cli/diff.rb +151 -0
- data/simplecov-1.0.0/lib/simplecov/cli/dotfile.rb +100 -0
- data/simplecov-1.0.0/lib/simplecov/cli/merge.rb +116 -0
- data/simplecov-1.0.0/lib/simplecov/cli/open.rb +50 -0
- data/simplecov-1.0.0/lib/simplecov/cli/report.rb +84 -0
- data/simplecov-1.0.0/lib/simplecov/cli/run.rb +36 -0
- data/simplecov-1.0.0/lib/simplecov/cli/serve.rb +139 -0
- data/simplecov-1.0.0/lib/simplecov/cli/uncovered.rb +107 -0
- data/simplecov-1.0.0/lib/simplecov/cli.rb +150 -0
- data/simplecov-1.0.0/lib/simplecov/color.rb +74 -0
- data/simplecov-1.0.0/lib/simplecov/combine/branches_combiner.rb +54 -0
- data/simplecov-1.0.0/lib/simplecov/combine/files_combiner.rb +30 -0
- data/simplecov-1.0.0/lib/simplecov/combine/lines_combiner.rb +45 -0
- data/simplecov-1.0.0/lib/simplecov/combine/methods_combiner.rb +26 -0
- data/simplecov-1.0.0/lib/simplecov/combine/results_combiner.rb +61 -0
- data/simplecov-1.0.0/lib/simplecov/combine.rb +30 -0
- data/simplecov-1.0.0/lib/simplecov/command_guesser.rb +78 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/coverage.rb +171 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/coverage_criteria.rb +156 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/filters.rb +197 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/formatting.rb +119 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/ignored_entries.rb +63 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/merging.rb +164 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/thresholds.rb +174 -0
- data/simplecov-1.0.0/lib/simplecov/configuration.rb +184 -0
- data/simplecov-1.0.0/lib/simplecov/coverage_statistics.rb +59 -0
- data/simplecov-1.0.0/lib/simplecov/coverage_violations.rb +148 -0
- data/simplecov-1.0.0/lib/simplecov/defaults.rb +60 -0
- data/simplecov-1.0.0/lib/simplecov/deprecation.rb +45 -0
- data/simplecov-1.0.0/lib/simplecov/directive.rb +162 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes/exit_code_handling.rb +35 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +45 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +44 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +70 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes.rb +18 -0
- data/simplecov-1.0.0/lib/simplecov/exit_handling.rb +158 -0
- data/simplecov-1.0.0/lib/simplecov/file_list.rb +164 -0
- data/simplecov-1.0.0/lib/simplecov/filter.rb +139 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/base.rb +101 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter.rb +79 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter/errors_formatter.rb +84 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +128 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter.rb +87 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/multi_formatter.rb +31 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/simple_formatter.rb +23 -0
- data/simplecov-1.0.0/lib/simplecov/formatter.rb +14 -0
- data/simplecov-1.0.0/lib/simplecov/last_run.rb +35 -0
- data/simplecov-1.0.0/lib/simplecov/lines_classifier.rb +60 -0
- data/simplecov-1.0.0/lib/simplecov/load_global_config.rb +13 -0
- data/simplecov-1.0.0/lib/simplecov/no_defaults.rb +4 -0
- data/simplecov-1.0.0/lib/simplecov/parallel_adapters/base.rb +61 -0
- data/simplecov-1.0.0/lib/simplecov/parallel_adapters/generic.rb +44 -0
- data/simplecov-1.0.0/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
- data/simplecov-1.0.0/lib/simplecov/parallel_adapters.rb +83 -0
- data/simplecov-1.0.0/lib/simplecov/parallel_coordination.rb +129 -0
- data/simplecov-1.0.0/lib/simplecov/process.rb +37 -0
- data/simplecov-1.0.0/lib/simplecov/profiles/bundler_filter.rb +5 -0
- data/simplecov-1.0.0/lib/simplecov/profiles/hidden_filter.rb +5 -0
- data/simplecov-1.0.0/lib/simplecov/profiles/rails.rb +32 -0
- data/simplecov-1.0.0/lib/simplecov/profiles/root_filter.rb +11 -0
- data/simplecov-1.0.0/lib/simplecov/profiles/strict.rb +32 -0
- data/simplecov-1.0.0/lib/simplecov/profiles/test_frameworks.rb +5 -0
- data/simplecov-1.0.0/lib/simplecov/profiles.rb +64 -0
- data/simplecov-1.0.0/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
- data/simplecov-1.0.0/lib/simplecov/result/source_file_builder.rb +51 -0
- data/simplecov-1.0.0/lib/simplecov/result.rb +183 -0
- data/simplecov-1.0.0/lib/simplecov/result_adapter.rb +92 -0
- data/simplecov-1.0.0/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
- data/simplecov-1.0.0/lib/simplecov/result_merger/resultset_file.rb +38 -0
- data/simplecov-1.0.0/lib/simplecov/result_merger/resultset_store.rb +50 -0
- data/simplecov-1.0.0/lib/simplecov/result_merger.rb +162 -0
- data/simplecov-1.0.0/lib/simplecov/result_processing.rb +179 -0
- data/simplecov-1.0.0/lib/simplecov/simulate_coverage.rb +75 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/branch.rb +82 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/branch_builder.rb +114 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/builder_context.rb +28 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/line.rb +77 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/line_builder.rb +43 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/method.rb +52 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/method_builder.rb +58 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/skip_chunks.rb +75 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/source_loader.rb +63 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/statistics.rb +57 -0
- data/simplecov-1.0.0/lib/simplecov/source_file.rb +189 -0
- data/simplecov-1.0.0/lib/simplecov/static_coverage_extractor/method_collector.rb +55 -0
- data/simplecov-1.0.0/lib/simplecov/static_coverage_extractor/visitor.rb +171 -0
- data/simplecov-1.0.0/lib/simplecov/static_coverage_extractor.rb +110 -0
- data/simplecov-1.0.0/lib/simplecov/useless_results_remover.rb +27 -0
- data/simplecov-1.0.0/lib/simplecov/version.rb +5 -0
- data/simplecov-1.0.0/lib/simplecov-html.rb +4 -0
- data/simplecov-1.0.0/lib/simplecov.rb +255 -0
- data/simplecov-1.0.0/lib/simplecov_json_formatter.rb +4 -0
- data/simplecov-1.0.0/schemas/coverage-v1.0.schema.json +306 -0
- data/simplecov-1.0.0/schemas/coverage.schema.json +306 -0
- data/ultra-pro-pkg.gemspec +12 -0
- 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
|