simplecov 1.1.1 → 1.2.0
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 +4 -4
- data/README.md +148 -5
- data/exe/simplecov +0 -3
- data/lib/minitest/simplecov_plugin.rb +12 -9
- data/lib/simplecov/atomic_file.rb +16 -19
- data/lib/simplecov/autostart.rb +5 -7
- data/lib/simplecov/baseline/parser.rb +58 -0
- data/lib/simplecov/baseline.rb +170 -0
- data/lib/simplecov/cli/affected/changed_files.rb +57 -0
- data/lib/simplecov/cli/affected/selection.rb +117 -0
- data/lib/simplecov/cli/affected.rb +148 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +23 -24
- data/lib/simplecov/cli/command_helpers.rb +55 -19
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +25 -28
- data/lib/simplecov/cli/coverage_file.rb +58 -14
- data/lib/simplecov/cli/dead_code/output.rb +100 -0
- data/lib/simplecov/cli/dead_code.rb +139 -0
- data/lib/simplecov/cli/diff/output.rb +45 -0
- data/lib/simplecov/cli/diff.rb +35 -67
- data/lib/simplecov/cli/dotfile.rb +83 -38
- data/lib/simplecov/cli/git.rb +41 -0
- data/lib/simplecov/cli/history/output.rb +135 -0
- data/lib/simplecov/cli/history.rb +67 -0
- data/lib/simplecov/cli/merge.rb +34 -39
- data/lib/simplecov/cli/open.rb +12 -15
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +135 -0
- data/lib/simplecov/cli/patch.rb +169 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +18 -24
- data/lib/simplecov/cli/run.rb +8 -9
- data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
- data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
- data/lib/simplecov/cli/serve.rb +27 -24
- data/lib/simplecov/cli/show/annotator.rb +87 -0
- data/lib/simplecov/cli/show/sweep.rb +36 -0
- data/lib/simplecov/cli/show.rb +133 -0
- data/lib/simplecov/cli/status/facts.rb +87 -0
- data/lib/simplecov/cli/status.rb +93 -0
- data/lib/simplecov/cli/tests/redundancy.rb +78 -0
- data/lib/simplecov/cli/tests.rb +148 -0
- data/lib/simplecov/cli/uncovered/misses.rb +37 -0
- data/lib/simplecov/cli/uncovered.rb +70 -32
- data/lib/simplecov/cli/usage.rb +152 -0
- data/lib/simplecov/cli/watch/live_report.rb +73 -0
- data/lib/simplecov/cli/watch/narrator.rb +55 -0
- data/lib/simplecov/cli/watch/poller.rb +45 -0
- data/lib/simplecov/cli/watch/session.rb +134 -0
- data/lib/simplecov/cli/watch/test_plan.rb +43 -0
- data/lib/simplecov/cli/watch.rb +85 -0
- data/lib/simplecov/cli.rb +43 -92
- data/lib/simplecov/color.rb +19 -37
- data/lib/simplecov/combine/branches_combiner.rb +7 -31
- data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
- data/lib/simplecov/combine/identity_interner.rb +10 -13
- data/lib/simplecov/combine/interned_counts.rb +7 -9
- data/lib/simplecov/combine/lines_combiner.rb +20 -44
- data/lib/simplecov/combine/methods_combiner.rb +9 -35
- data/lib/simplecov/combine/results_combiner.rb +4 -21
- data/lib/simplecov/combine.rb +0 -6
- data/lib/simplecov/command_guesser.rb +20 -34
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +91 -79
- data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +6 -10
- data/lib/simplecov/configuration/filters.rb +40 -73
- data/lib/simplecov/configuration/formatting.rb +74 -83
- data/lib/simplecov/configuration/groups.rb +19 -13
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +31 -22
- data/lib/simplecov/configuration/merging.rb +94 -62
- data/lib/simplecov/configuration/missed_caps.rb +51 -0
- data/lib/simplecov/configuration/production.rb +23 -0
- data/lib/simplecov/configuration/test_tracking.rb +48 -0
- data/lib/simplecov/configuration/thresholds.rb +33 -62
- data/lib/simplecov/configuration/view_coverage.rb +38 -0
- data/lib/simplecov/configuration.rb +70 -76
- data/lib/simplecov/configuration_error.rb +5 -0
- data/lib/simplecov/context_map/union.rb +81 -0
- data/lib/simplecov/context_map.rb +187 -0
- data/lib/simplecov/coverage_json.rb +2 -5
- data/lib/simplecov/coverage_statistics.rb +14 -24
- data/lib/simplecov/coverage_violations.rb +157 -64
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +4 -23
- data/lib/simplecov/deprecation.rb +45 -27
- data/lib/simplecov/directive.rb +46 -62
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +17 -11
- data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
- data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
- data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
- data/lib/simplecov/exit_codes.rb +16 -10
- data/lib/simplecov/exit_handling.rb +94 -59
- data/lib/simplecov/file_list.rb +20 -51
- data/lib/simplecov/filter.rb +43 -66
- data/lib/simplecov/formatter/base.rb +37 -55
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
- data/lib/simplecov/formatter/html_formatter/public/index.html +15 -9
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
- data/lib/simplecov/formatter/html_formatter.rb +24 -41
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
- data/lib/simplecov/formatter/json_formatter.rb +7 -13
- data/lib/simplecov/formatter/multi_formatter.rb +5 -11
- data/lib/simplecov/formatter/simple_formatter.rb +2 -8
- data/lib/simplecov/formatter.rb +14 -10
- data/lib/simplecov/group_names.rb +13 -12
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +6 -9
- data/lib/simplecov/lines_classifier.rb +31 -16
- data/lib/simplecov/load_global_config.rb +7 -7
- data/lib/simplecov/parallel_adapters/base.rb +13 -41
- data/lib/simplecov/parallel_adapters/generic.rb +7 -19
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
- data/lib/simplecov/parallel_adapters.rb +24 -55
- data/lib/simplecov/parallel_coordination.rb +39 -49
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +67 -126
- data/lib/simplecov/process.rb +14 -32
- data/lib/simplecov/production/error.rb +11 -0
- data/lib/simplecov/production/file_sink.rb +130 -0
- data/lib/simplecov/production.rb +249 -0
- data/lib/simplecov/profiles/rails.rb +1 -12
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +9 -19
- data/lib/simplecov/profiles.rb +11 -33
- data/lib/simplecov/report_deferral.rb +11 -15
- data/lib/simplecov/report_stamp.rb +6 -9
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +7 -12
- data/lib/simplecov/result.rb +68 -117
- data/lib/simplecov/result_adapter.rb +56 -68
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
- data/lib/simplecov/result_merger/resultset_file.rb +20 -27
- data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
- data/lib/simplecov/result_merger/resultset_store.rb +23 -14
- data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
- data/lib/simplecov/result_merger.rb +59 -58
- data/lib/simplecov/result_processing.rb +74 -119
- data/lib/simplecov/run_identity.rb +16 -18
- data/lib/simplecov/simulate_coverage.rb +34 -50
- data/lib/simplecov/source_file/branch.rb +8 -33
- data/lib/simplecov/source_file/branch_builder.rb +28 -47
- data/lib/simplecov/source_file/builder_context.rb +6 -13
- data/lib/simplecov/source_file/line.rb +13 -33
- data/lib/simplecov/source_file/line_builder.rb +9 -15
- data/lib/simplecov/source_file/method.rb +3 -7
- data/lib/simplecov/source_file/method_builder.rb +6 -15
- data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
- data/lib/simplecov/source_file/skip_chunks.rb +19 -28
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +10 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
- data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
- data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
- data/lib/simplecov/static_coverage_extractor.rb +46 -62
- data/lib/simplecov/test_tracker/accessors.rb +34 -0
- data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
- data/lib/simplecov/test_tracker/delta.rb +69 -0
- data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
- data/lib/simplecov/test_tracker.rb +162 -0
- data/lib/simplecov/unloaded_file_injector.rb +18 -31
- data/lib/simplecov/useless_results_remover.rb +7 -6
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
- data/lib/simplecov/view_coverage.rb +55 -0
- data/lib/simplecov-html.rb +1 -0
- data/lib/simplecov.rb +57 -113
- data/lib/simplecov_json_formatter.rb +1 -0
- data/man/simplecov.1 +349 -0
- data/sig/simplecov.rbs +523 -595
- metadata +72 -5
|
@@ -2,27 +2,21 @@
|
|
|
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.
|
|
7
5
|
class MultiFormatter
|
|
8
|
-
# Shared `#format` implementation; included into individual
|
|
9
|
-
# MultiFormatter subclasses built by `MultiFormatter.new`.
|
|
10
6
|
module InstanceMethods
|
|
11
7
|
def format(result)
|
|
12
8
|
formatters.map do |formatter|
|
|
13
9
|
Formatter.format(formatter, result)
|
|
14
|
-
rescue
|
|
15
|
-
warn("Formatter #{formatter} failed with #{e.class}: #{e
|
|
16
|
-
nil
|
|
10
|
+
rescue => e
|
|
11
|
+
warn("Formatter #{formatter} failed with #{e.class}: #{e} (#{(_ = e.backtrace).first})")
|
|
17
12
|
end
|
|
18
13
|
end
|
|
19
14
|
end
|
|
20
15
|
|
|
16
|
+
# Normalized eagerly and captured in the closure. `Array()` is pure for every
|
|
17
|
+
# accepted input shape, so this is equivalent to the historical lazy
|
|
18
|
+
# per-instance memoization.
|
|
21
19
|
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
20
|
formatter_list = Array(formatters)
|
|
27
21
|
Class.new do
|
|
28
22
|
define_method :formatters do
|
|
@@ -2,22 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module Formatter
|
|
5
|
-
#
|
|
6
|
-
# A ridiculously simple formatter for SimpleCov results.
|
|
7
|
-
#
|
|
8
5
|
class SimpleFormatter
|
|
9
|
-
# Takes a SimpleCov::Result and generates a string out of it
|
|
10
6
|
def format(result)
|
|
11
7
|
criterion = SimpleCov.coverage_statistics_key(SimpleCov.primary_coverage)
|
|
12
8
|
result.groups.map { |name, files| format_group(name, files, criterion) }.join
|
|
13
9
|
end
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
private
|
|
16
12
|
|
|
17
13
|
def format_group(name, files, criterion)
|
|
18
|
-
header = "Group: #{name}\n#{
|
|
19
|
-
# The configured primary criterion is enabled, so every result file
|
|
20
|
-
# carries its corresponding statistic despite the nilable public API.
|
|
14
|
+
header = "Group: #{name}\n#{"=" * 40}\n"
|
|
21
15
|
body = files.map do |file|
|
|
22
16
|
"#{file.filename} (coverage: #{(_ = file.covered_percent(criterion)).floor(2)}%)\n"
|
|
23
17
|
end.join
|
data/lib/simplecov/formatter.rb
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
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=`.
|
|
7
|
-
# TODO: Documentation on how to build your own formatters
|
|
8
4
|
module Formatter
|
|
9
|
-
# Formatters
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# `HTMLFormatter.new(silent: true)`. See #1240.
|
|
5
|
+
# Formatters are configured either as classes, instantiated fresh for every
|
|
6
|
+
# report, or as ready-built instances, the only way to reach constructor
|
|
7
|
+
# options like `HTMLFormatter.new(silent: true)` (#1240).
|
|
13
8
|
def self.instance_for(formatter)
|
|
14
|
-
|
|
9
|
+
instantiable?(formatter) ? formatter.new : formatter
|
|
15
10
|
end
|
|
16
11
|
|
|
17
|
-
#
|
|
12
|
+
# mutant:disable
|
|
13
|
+
# `Class` cannot itself be subclassed, so `instance_of?(Class)` answers for
|
|
14
|
+
# every object exactly as `is_a?(Class)` does: a difference no example can
|
|
15
|
+
# show, which is why it is disabled here rather than pinned by a test.
|
|
16
|
+
def self.instantiable?(formatter)
|
|
17
|
+
formatter.is_a?(Class)
|
|
18
|
+
end
|
|
19
|
+
private_class_method :instantiable?
|
|
20
|
+
|
|
18
21
|
def self.format(formatter, result)
|
|
19
22
|
instance_for(formatter).format(result)
|
|
20
23
|
end
|
|
@@ -24,3 +27,4 @@ end
|
|
|
24
27
|
require_relative "formatter/simple_formatter"
|
|
25
28
|
require_relative "formatter/multi_formatter"
|
|
26
29
|
require_relative "formatter/json_formatter"
|
|
30
|
+
require_relative "formatter/baseline_formatter"
|
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
|
-
# Semantic names owned by result processing rather than user configuration.
|
|
5
4
|
module GroupNames
|
|
6
5
|
UNGROUPED = "Ungrouped"
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
extend self
|
|
9
8
|
|
|
10
|
-
# Group names are Hash keys, report labels, and JSON object keys all at
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
9
|
+
# Group names are Hash keys, report labels, and JSON object keys all at once,
|
|
10
|
+
# so they are normalized to Strings up front: a Symbol spelling means the
|
|
11
|
+
# String, and anything else has no sensible serialized form. Normalizing
|
|
12
|
+
# before `validate!` also keeps `group :Ungrouped` from slipping past the
|
|
13
|
+
# reservation below.
|
|
15
14
|
def normalize(group_name)
|
|
16
15
|
case group_name
|
|
17
16
|
when String then group_name
|
|
18
17
|
when Symbol then group_name.to_s
|
|
19
18
|
else
|
|
20
|
-
raise
|
|
21
|
-
|
|
19
|
+
raise ConfigurationError,
|
|
20
|
+
"Group names must be Strings, got #{group_name.inspect} (#{group_name.class})"
|
|
22
21
|
end
|
|
23
22
|
end
|
|
24
23
|
|
|
24
|
+
# Each name in turn, rather than asking the collection: a String asked
|
|
25
|
+
# whether it "includes" the reserved name answers about its own characters.
|
|
25
26
|
def validate!(group_names)
|
|
26
|
-
return group_names unless group_names.
|
|
27
|
+
return group_names unless group_names.any? { |name| name.eql?(UNGROUPED) }
|
|
27
28
|
|
|
28
|
-
raise
|
|
29
|
-
|
|
29
|
+
raise ConfigurationError,
|
|
30
|
+
"#{UNGROUPED.inspect} is reserved for files that do not match a configured group"
|
|
30
31
|
end
|
|
31
32
|
end
|
|
32
33
|
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "time"
|
|
6
|
+
require_relative "atomic_file"
|
|
7
|
+
|
|
8
|
+
module SimpleCov
|
|
9
|
+
# A bounded record of past runs, coverage/.history.json (#1267): one entry
|
|
10
|
+
# per reported run carrying the per-criterion totals, the per-group and
|
|
11
|
+
# per-file percentages, a timestamp, and the branch and commit when the
|
|
12
|
+
# project is a git checkout. It is what turns "did the number move since the
|
|
13
|
+
# very last run" into direction of travel.
|
|
14
|
+
#
|
|
15
|
+
# Entries are capped and written atomically, and a corrupt or foreign file
|
|
16
|
+
# degrades to an empty history with a warning rather than crashing the
|
|
17
|
+
# at_exit hook, the same tolerance `.last_run.json` has. The file is
|
|
18
|
+
# deliberately plain committable JSON.
|
|
19
|
+
module History
|
|
20
|
+
ENVELOPE = "simplecov_history"
|
|
21
|
+
FORMAT_VERSION = 1
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def history_path
|
|
25
|
+
File.join(SimpleCov.coverage_path, ".history.json")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def record(result)
|
|
29
|
+
limit = SimpleCov.history_limit
|
|
30
|
+
return if limit.zero?
|
|
31
|
+
|
|
32
|
+
entries = read
|
|
33
|
+
entries << entry_for(result)
|
|
34
|
+
write(entries.last(limit))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def read
|
|
38
|
+
return [] unless File.exist?(history_path)
|
|
39
|
+
|
|
40
|
+
content = File.read(history_path)
|
|
41
|
+
return [] if content.match?(/\A\s*\z/)
|
|
42
|
+
|
|
43
|
+
entries = JSON.parse(content).dig(ENVELOPE, "entries")
|
|
44
|
+
entries.instance_of?(Array) ? entries : invalid_history
|
|
45
|
+
rescue JSON::ParserError
|
|
46
|
+
invalid_history
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The recorded entries plus `result` as the newest, capped, and without
|
|
50
|
+
# writing anything: what the report artifacts embed, so the embedded history
|
|
51
|
+
# includes the run being reported even though recording happens later.
|
|
52
|
+
def entries_with(result)
|
|
53
|
+
(read << entry_for(result)).last(SimpleCov.history_limit)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A detached HEAD reports its branch as nil rather than the literal "HEAD"
|
|
57
|
+
# most CI checkouts would record.
|
|
58
|
+
def git_info
|
|
59
|
+
branch = git("rev-parse", "--abbrev-ref", "HEAD")
|
|
60
|
+
branch = nil if branch.eql?("HEAD")
|
|
61
|
+
[branch, git("rev-parse", "HEAD")]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def entry_for(result)
|
|
67
|
+
branch, commit = git_info
|
|
68
|
+
{
|
|
69
|
+
# getutc, not utc: the destructive variant would rezone the Time the result
|
|
70
|
+
# still holds.
|
|
71
|
+
"created_at" => result.created_at.getutc.iso8601,
|
|
72
|
+
"branch" => branch,
|
|
73
|
+
"commit" => commit,
|
|
74
|
+
"totals" => measured_percents(result),
|
|
75
|
+
"groups" => result.groups.transform_values { |files| measured_percents(files) },
|
|
76
|
+
"files" => files_percents(result)
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def measured_percents(stats_source)
|
|
81
|
+
stats_source.coverage_statistics.to_h do |criterion, stats|
|
|
82
|
+
[criterion.to_s, SimpleCov.round_coverage(stats.percent)]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def files_percents(result)
|
|
87
|
+
measured = result.coverage_statistics
|
|
88
|
+
initial = {} #: Hash[String, Hash[String, Numeric]]
|
|
89
|
+
result.files.each_with_object(initial) do |file, percents|
|
|
90
|
+
stats = file.coverage_statistics
|
|
91
|
+
percents[file.project_filename] = measured.to_h do |criterion, _run_total|
|
|
92
|
+
[criterion.to_s, SimpleCov.round_coverage(stats.fetch(criterion).percent)]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def write(entries)
|
|
98
|
+
payload = {ENVELOPE => {"format_version" => FORMAT_VERSION, "entries" => entries}}
|
|
99
|
+
AtomicFile.write(history_path, "#{JSON.pretty_generate(payload)}\n")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def invalid_history
|
|
103
|
+
warn "[SimpleCov]: Warning! Parsing JSON content of .history.json failed, starting a fresh history"
|
|
104
|
+
[]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def git(*)
|
|
108
|
+
output, status = Open3.capture2e("git", "-C", SimpleCov.root.to_s, *)
|
|
109
|
+
output.rstrip if status.success?
|
|
110
|
+
rescue
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/simplecov/last_run.rb
CHANGED
|
@@ -4,25 +4,23 @@ require "json"
|
|
|
4
4
|
require_relative "atomic_file"
|
|
5
5
|
|
|
6
6
|
module SimpleCov
|
|
7
|
-
# Reads and writes coverage/.last_run.json — the previous run's coverage
|
|
8
|
-
# percentages used by MaximumCoverageDropCheck.
|
|
9
7
|
module LastRun
|
|
10
8
|
class << self
|
|
11
9
|
def last_run_path
|
|
12
10
|
File.join(SimpleCov.coverage_path, ".last_run.json")
|
|
13
11
|
end
|
|
14
12
|
|
|
13
|
+
# The maximum_coverage_drop check digs into a Hash, so anything else in a
|
|
14
|
+
# corrupt or hand-edited file counts as "no previous run" rather than
|
|
15
|
+
# crashing the at_exit hook.
|
|
15
16
|
def read
|
|
16
17
|
return nil unless File.exist?(last_run_path)
|
|
17
18
|
|
|
18
19
|
json = File.read(last_run_path)
|
|
19
|
-
return nil if json.
|
|
20
|
+
return nil if json.match?(/\A\s*\z/)
|
|
20
21
|
|
|
21
22
|
parsed = JSON.parse(json, symbolize_names: true)
|
|
22
|
-
|
|
23
|
-
# else in a corrupt or hand-edited file counts as "no previous
|
|
24
|
-
# run" rather than crashing the at_exit hook.
|
|
25
|
-
parsed.is_a?(Hash) ? parsed : invalid_last_run
|
|
23
|
+
parsed.instance_of?(Hash) ? parsed : invalid_last_run
|
|
26
24
|
rescue JSON::ParserError
|
|
27
25
|
invalid_last_run
|
|
28
26
|
end
|
|
@@ -31,11 +29,10 @@ module SimpleCov
|
|
|
31
29
|
AtomicFile.write(last_run_path, "#{JSON.pretty_generate(json)}\n")
|
|
32
30
|
end
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
private
|
|
35
33
|
|
|
36
34
|
def invalid_last_run
|
|
37
35
|
warn "[SimpleCov]: Warning! Parsing JSON content of .last_run.json failed, ignoring the previous run"
|
|
38
|
-
nil
|
|
39
36
|
end
|
|
40
37
|
end
|
|
41
38
|
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
require_relative "directive"
|
|
4
4
|
|
|
5
5
|
module SimpleCov
|
|
6
|
-
# Classifies whether lines are relevant for code coverage analysis.
|
|
7
|
-
#
|
|
6
|
+
# Classifies whether lines are relevant for code coverage analysis. Comments,
|
|
7
|
+
# whitespace lines, and disabled blocks are considered not relevant.
|
|
8
8
|
class LinesClassifier
|
|
9
9
|
RELEVANT = 0
|
|
10
10
|
NOT_RELEVANT = nil
|
|
@@ -27,45 +27,60 @@ module SimpleCov
|
|
|
27
27
|
/^(\s*)#(\s*)(:#{SimpleCov.current_nocov_token}:)/o
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# A marker is itself a comment, so the cheap whitespace-or-comment test gates
|
|
31
|
+
# the token match: a line of real code cannot be a marker, and no longer pays
|
|
32
|
+
# to be checked against one. That matters because this runs per line of every
|
|
33
|
+
# tracked-but-unloaded file, once in every process of a parallel run.
|
|
30
34
|
def self.no_cov_line?(line)
|
|
31
35
|
no_cov_line.match?(line)
|
|
32
36
|
rescue ArgumentError
|
|
33
|
-
# E.g., line contains an invalid byte sequence in UTF-8
|
|
34
37
|
false
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
def self.whitespace_line?(line)
|
|
38
41
|
WHITESPACE_OR_COMMENT_LINE.match?(line)
|
|
39
42
|
rescue ArgumentError
|
|
40
|
-
# E.g., line contains an invalid byte sequence in UTF-8
|
|
41
43
|
false
|
|
42
44
|
end
|
|
43
45
|
|
|
46
|
+
class SkipState
|
|
47
|
+
def initialize
|
|
48
|
+
@skipping = false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def skipping?
|
|
52
|
+
@skipping
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def toggle
|
|
56
|
+
@skipping = !@skipping
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The skip state lives in a one-slot box owned by this call rather than in an
|
|
61
|
+
# ivar, so one classifier instance can serve concurrent `classify` calls
|
|
62
|
+
# without the toggles interleaving.
|
|
44
63
|
def classify(lines)
|
|
45
64
|
lines = lines.to_a
|
|
46
65
|
directive_disabled = directive_disabled_line_set(lines)
|
|
47
|
-
|
|
48
|
-
# call, not in an ivar, so one classifier instance can serve
|
|
49
|
-
# concurrent `classify` calls without the toggles interleaving.
|
|
50
|
-
skip_state = [false]
|
|
66
|
+
skip_state = SkipState.new
|
|
51
67
|
|
|
52
68
|
lines.map.with_index(1) do |line, line_number|
|
|
53
69
|
classify_line(line, line_number, directive_disabled, skip_state)
|
|
54
70
|
end
|
|
55
71
|
end
|
|
56
72
|
|
|
57
|
-
|
|
73
|
+
private
|
|
58
74
|
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
# tracked-but-unloaded file, once in every process of a parallel run.
|
|
75
|
+
# mutant:disable — `NOT_RELEVANT` is nil, so naming it and writing
|
|
76
|
+
# the literal are the same value, and so is an `unless` that falls
|
|
77
|
+
# off its end. The name is kept because it says which of the two
|
|
78
|
+
# verdicts a line got, which nil alone does not.
|
|
64
79
|
def classify_line(line, line_number, directive_disabled, skip_state)
|
|
65
80
|
if self.class.whitespace_line?(line)
|
|
66
|
-
skip_state
|
|
81
|
+
skip_state.toggle if self.class.no_cov_line?(line)
|
|
67
82
|
NOT_RELEVANT
|
|
68
|
-
elsif skip_state
|
|
83
|
+
elsif skip_state.skipping? || directive_disabled.include?(line_number)
|
|
69
84
|
NOT_RELEVANT
|
|
70
85
|
else
|
|
71
86
|
RELEVANT
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# `~/.simplecov` was historically resolved
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
|
|
3
|
+
# `~/.simplecov` was historically resolved through a three-step fallback chain
|
|
4
|
+
# for hostile container environments circa 2017. Modern CRuby and JRuby both set
|
|
5
|
+
# HOME reliably, so trust it and skip silently when it isn't there. An ENV check
|
|
6
|
+
# rather than `Dir.home`: JRuby raises from `Dir.home` with HOME unset, and
|
|
7
|
+
# `File.expand_path("~")` raises for a set-but-empty HOME, which must not break
|
|
8
|
+
# `require "simplecov"`.
|
|
9
|
+
|
|
10
10
|
unless ENV.fetch("HOME", "").empty?
|
|
11
11
|
# simplecov:disable — only fires when ~/.simplecov exists, which is
|
|
12
12
|
# developer-machine-dependent (we can't rely on it for the dogfood).
|
|
@@ -3,73 +3,45 @@
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module ParallelAdapters
|
|
5
5
|
# Default no-op implementations for a parallel-test-runner adapter.
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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.
|
|
6
|
+
# Everything not overridden falls back to behaving like a single-process
|
|
7
|
+
# run. Adapters are classes, used as singletons and never instantiated.
|
|
15
8
|
class Base
|
|
16
9
|
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
10
|
def active?
|
|
21
11
|
false
|
|
22
12
|
end
|
|
23
13
|
|
|
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
14
|
def first_worker?
|
|
29
15
|
true
|
|
30
16
|
end
|
|
31
17
|
|
|
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
18
|
def wait_for_siblings
|
|
39
|
-
# No-op default; polling fallback handles correctness.
|
|
40
19
|
end
|
|
41
20
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# `parallel_wait_timeout` for workers that produced no coverage.
|
|
47
|
-
# Defaults to false (no native wait; the poll is the only signal).
|
|
21
|
+
# Whether `wait_for_siblings` blocks until every sibling process has exited,
|
|
22
|
+
# so no further resultset can appear. When true, the reporting worker can
|
|
23
|
+
# accept a settled resultset count below `expected_worker_count` as final
|
|
24
|
+
# instead of waiting out the whole `parallel_wait_timeout`.
|
|
48
25
|
def native_wait?
|
|
49
26
|
false
|
|
50
27
|
end
|
|
51
28
|
|
|
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
29
|
def expected_worker_count
|
|
56
30
|
1
|
|
57
31
|
end
|
|
58
32
|
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
33
|
+
# mutant:disable — `false` is a singleton, so comparing against
|
|
34
|
+
# it answers the same through ==, eql? and equal?, and no
|
|
35
|
+
# example can tell the three apart.
|
|
62
36
|
def forced_off?
|
|
63
37
|
SimpleCov.parallel_tests == false
|
|
64
38
|
end
|
|
65
39
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
# an unparseable value must not yield 0 workers, which would end
|
|
70
|
-
# the sibling wait before it started.
|
|
40
|
+
# Unset, empty, non-numeric, and non-positive values all mean 1: an
|
|
41
|
+
# unparseable value must not yield 0 workers, which would end the sibling
|
|
42
|
+
# wait before it started.
|
|
71
43
|
def parallel_test_groups_count
|
|
72
|
-
count = Integer(ENV.fetch("PARALLEL_TEST_GROUPS",
|
|
44
|
+
count = Integer(ENV.fetch("PARALLEL_TEST_GROUPS", nil), exception: false)
|
|
73
45
|
count&.positive? ? count : 1
|
|
74
46
|
end
|
|
75
47
|
end
|
|
@@ -5,22 +5,10 @@ require_relative "base"
|
|
|
5
5
|
module SimpleCov
|
|
6
6
|
module ParallelAdapters
|
|
7
7
|
# Catch-all adapter for parallel test runners that follow the
|
|
8
|
-
# `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` env-var convention but
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# doesn't require any specific gem to be loaded.
|
|
13
|
-
#
|
|
14
|
-
# Heuristic for `first_worker?`: the worker whose `TEST_ENV_NUMBER`
|
|
15
|
-
# is `""` (parallel_tests/parallel_rspec convention, rspec-conductor's
|
|
16
|
-
# default) or `"1"` (runners that number from 1, like rspec-conductor
|
|
17
|
-
# with `--first-is-1`). Any other value is treated as a non-first
|
|
18
|
-
# worker.
|
|
19
|
-
#
|
|
20
|
-
# `wait_for_siblings` is inherited from Base as a no-op — without a
|
|
21
|
-
# runner-provided API the only synchronization available is polling
|
|
22
|
-
# the resultset cache, which `SimpleCov.wait_for_parallel_results`
|
|
23
|
-
# does after the no-op returns.
|
|
8
|
+
# `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` env-var convention but ship no
|
|
9
|
+
# Ruby API for SimpleCov to hook. `wait_for_siblings` is inherited from Base
|
|
10
|
+
# as a no-op: without a runner-provided API the only synchronization
|
|
11
|
+
# available is polling the resultset cache.
|
|
24
12
|
class GenericAdapter < Base
|
|
25
13
|
class << self
|
|
26
14
|
def active?
|
|
@@ -29,9 +17,9 @@ module SimpleCov
|
|
|
29
17
|
ENV.key?("TEST_ENV_NUMBER")
|
|
30
18
|
end
|
|
31
19
|
|
|
32
|
-
# parallel_tests sets the first worker's TEST_ENV_NUMBER to ""
|
|
33
|
-
#
|
|
34
|
-
#
|
|
20
|
+
# parallel_tests sets the first worker's TEST_ENV_NUMBER to "", and runners
|
|
21
|
+
# that number from 1 use "1". Both shapes match; any other value is a
|
|
22
|
+
# non-first worker.
|
|
35
23
|
def first_worker?
|
|
36
24
|
["", "1"].include?(ENV.fetch("TEST_ENV_NUMBER", nil))
|
|
37
25
|
end
|
|
@@ -4,47 +4,37 @@ require_relative "base"
|
|
|
4
4
|
|
|
5
5
|
module SimpleCov
|
|
6
6
|
module ParallelAdapters
|
|
7
|
-
# Adapter for
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# path is required because the native wait API reads it with `ENV.fetch`.
|
|
14
|
-
# When a runner only provides the env-var convention, GenericAdapter is
|
|
15
|
-
# the correct coordination path.
|
|
7
|
+
# Adapter for grosser/parallel_tests, the historical default and still the
|
|
8
|
+
# most precise option for projects on it. Detection requires the full native
|
|
9
|
+
# coordination contract: the `ParallelTests` constant loaded,
|
|
10
|
+
# `TEST_ENV_NUMBER` set, and `PARALLEL_PID_FILE` set, because the native
|
|
11
|
+
# wait API reads the pid-file path with `ENV.fetch`. A runner that only
|
|
12
|
+
# provides the env-var convention belongs to GenericAdapter.
|
|
16
13
|
class ParallelTestsAdapter < Base
|
|
17
14
|
class << self
|
|
18
15
|
def active?
|
|
19
16
|
return false if forced_off?
|
|
20
17
|
|
|
21
18
|
ensure_loaded
|
|
22
|
-
# !! to coerce `defined?` (returns nil or "constant") to a proper bool.
|
|
23
19
|
!!(defined?(::ParallelTests) && native_parallel_tests_environment?)
|
|
24
20
|
end
|
|
25
21
|
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# side avoids the cross-process deadlock #922 reported. Also
|
|
33
|
-
# handles `PARALLEL_TEST_GROUPS=1` naturally (the only worker's
|
|
34
|
-
# `TEST_ENV_NUMBER` is "" and `first_process?` tests for that
|
|
35
|
-
# empty string).
|
|
22
|
+
# The first started process, not the last. The parallel_tests README
|
|
23
|
+
# recommends `first_process?` for "do something once after every worker
|
|
24
|
+
# finishes" hooks, so user code with its own
|
|
25
|
+
# `wait_for_other_processes_to_finish` overwhelmingly waits in the first
|
|
26
|
+
# process, and picking the same side avoids the cross-process deadlock #922
|
|
27
|
+
# reported.
|
|
36
28
|
def first_worker?
|
|
37
|
-
|
|
29
|
+
ParallelTests.first_process?
|
|
38
30
|
end
|
|
39
31
|
|
|
40
32
|
def wait_for_siblings
|
|
41
33
|
return unless native_parallel_tests_environment?
|
|
42
34
|
|
|
43
|
-
|
|
35
|
+
ParallelTests.wait_for_other_processes_to_finish
|
|
44
36
|
end
|
|
45
37
|
|
|
46
|
-
# The native wait blocks until every sibling process exits, but only
|
|
47
|
-
# when the pid-file contract is present (see `wait_for_siblings`).
|
|
48
38
|
def native_wait?
|
|
49
39
|
native_parallel_tests_environment?
|
|
50
40
|
end
|
|
@@ -53,30 +43,19 @@ module SimpleCov
|
|
|
53
43
|
parallel_test_groups_count
|
|
54
44
|
end
|
|
55
45
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
# for other reasons (custom subprocess coordination, CI sharding,
|
|
62
|
-
# the parallel_rspec gem which intentionally mirrors the env-var
|
|
63
|
-
# convention), so a missing gem is treated as "user isn't using
|
|
64
|
-
# parallel_tests" — silently skip and let GenericAdapter handle
|
|
65
|
-
# it. Users who want to override the auto-detect can set
|
|
66
|
-
# `SimpleCov.parallel_tests true` (force on) or `false` (force
|
|
67
|
-
# off). See #1018.
|
|
46
|
+
# parallel_tests is an optional dependency, and `TEST_ENV_NUMBER` /
|
|
47
|
+
# `PARALLEL_TEST_GROUPS` are commonly set for other reasons, so a missing
|
|
48
|
+
# gem is treated as "user isn't using parallel_tests": stay quiet and let
|
|
49
|
+
# GenericAdapter handle it. Warning here regressed users who use those env
|
|
50
|
+
# vars for their own subprocess coordination (#1018).
|
|
68
51
|
def ensure_loaded
|
|
69
|
-
return if defined?(::ParallelTests)
|
|
70
|
-
return if
|
|
71
|
-
# simplecov:disable — env-var-only path
|
|
52
|
+
return if defined?(::ParallelTests)
|
|
53
|
+
return if forced_off?
|
|
72
54
|
return unless SimpleCov.parallel_tests || env_suggests_parallel_tests?
|
|
73
55
|
|
|
74
56
|
# simplecov:disable — only fires under a real parallel_tests setup
|
|
75
57
|
require "parallel_tests"
|
|
76
58
|
rescue LoadError
|
|
77
|
-
# Gem isn't installed; stay quiet — warning here regressed
|
|
78
|
-
# users who use those env vars for their own subprocess
|
|
79
|
-
# coordination.
|
|
80
59
|
# simplecov:enable
|
|
81
60
|
end
|
|
82
61
|
|