simplecov 1.1.0 → 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 +165 -57
- 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 +21 -35
- 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 +17 -7
- 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 +38 -51
- 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 +17 -10
- 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 -18
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
- data/lib/simplecov/parallel_adapters.rb +24 -53
- 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 +58 -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 +35 -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 +59 -114
- data/lib/simplecov_json_formatter.rb +1 -0
- data/man/simplecov.1 +349 -0
- data/sig/simplecov.rbs +523 -595
- metadata +72 -7
- data/schemas/coverage-v1.0.schema.json +0 -306
- data/schemas/coverage.schema.json +0 -306
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module Production
|
|
5
|
+
# Configuration and store-format failures. Sink errors raised from `store` are
|
|
6
|
+
# not wrapped in this: the runtime rescues them, warns, and retries on the
|
|
7
|
+
# next interval.
|
|
8
|
+
class Error < StandardError
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
require_relative "error"
|
|
7
|
+
|
|
8
|
+
module SimpleCov
|
|
9
|
+
module Production
|
|
10
|
+
# The bundled sink: a single JSON file, union-merged under an exclusive
|
|
11
|
+
# lock so any number of processes on the same host can share it. This is
|
|
12
|
+
# the reference for the sink contract:
|
|
13
|
+
#
|
|
14
|
+
# store(coverage) # {"lib/foo.rb" => [1, 3, 12], ...} of
|
|
15
|
+
# # root-relative paths to sorted line numbers
|
|
16
|
+
#
|
|
17
|
+
# A sink must merge (never replace: many processes each hold only a
|
|
18
|
+
# slice), must be idempotent (the same lines may arrive twice), and
|
|
19
|
+
# signals failure by raising, which makes the runtime keep the delta and
|
|
20
|
+
# retry next interval.
|
|
21
|
+
#
|
|
22
|
+
# `last_seen` stamps each file with the last store that carried it.
|
|
23
|
+
# Oneshot clears on drain, so still-running code re-reports every interval
|
|
24
|
+
# and the stamp tracks real recency, not first sighting. The field is
|
|
25
|
+
# optional on read: a v1 store written before it existed simply has no
|
|
26
|
+
# recency evidence to offer.
|
|
27
|
+
class FileSink
|
|
28
|
+
# The envelope key, which is also how `read` tells a production coverage
|
|
29
|
+
# file from an arbitrary JSON document it must not clobber or misread.
|
|
30
|
+
ENVELOPE = "simplecov_production"
|
|
31
|
+
FORMAT_VERSION = 1
|
|
32
|
+
|
|
33
|
+
attr_reader :path
|
|
34
|
+
|
|
35
|
+
def initialize(path:)
|
|
36
|
+
@path = File.expand_path(path)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def store(coverage)
|
|
40
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
41
|
+
with_exclusive_lock do |file|
|
|
42
|
+
existing = self.class.parse(file.read, path)
|
|
43
|
+
rewrite(file, envelope(existing, coverage))
|
|
44
|
+
end
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Truncates any leftover tail from a previously larger document.
|
|
49
|
+
def rewrite(file, payload)
|
|
50
|
+
file.rewind
|
|
51
|
+
file.write(JSON.generate(payload))
|
|
52
|
+
file.truncate(file.pos)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Raises `Error` for anything that is not a production coverage file,
|
|
56
|
+
# naming the path: misreading an arbitrary JSON file as empty coverage
|
|
57
|
+
# would quietly report every line dead.
|
|
58
|
+
def self.read(path)
|
|
59
|
+
parse(File.read(path), path)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @api private -- shared by `read` and `store`'s read-modify-write. An
|
|
63
|
+
# empty or missing file is a fresh store. JSON answers plain hashes,
|
|
64
|
+
# arrays, and scalars, never a subclass of one, so the shape checks ask
|
|
65
|
+
# about the class itself.
|
|
66
|
+
def self.parse(content, path)
|
|
67
|
+
return {"coverage" => {}, "last_seen" => {}, "started_at" => nil} if content.match?(/\A\s*\z/)
|
|
68
|
+
|
|
69
|
+
inner = envelope_of(JSON.parse(content), path)
|
|
70
|
+
inner["coverage"] = {} unless inner["coverage"].instance_of?(Hash)
|
|
71
|
+
inner["last_seen"] = {} unless inner["last_seen"].instance_of?(Hash)
|
|
72
|
+
inner
|
|
73
|
+
rescue JSON::ParserError => e
|
|
74
|
+
# One line of the parser's complaint, which for some inputs quotes the
|
|
75
|
+
# document back and runs long.
|
|
76
|
+
raise Error, "#{path} is not valid JSON (#{e.message.lines.first.to_s.rstrip})"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The refusal is what keeps an arbitrary JSON file from being misread as
|
|
80
|
+
# empty coverage.
|
|
81
|
+
def self.envelope_of(document, path)
|
|
82
|
+
inner = document[ENVELOPE] if document.instance_of?(Hash)
|
|
83
|
+
raise Error, "#{path} is not a SimpleCov production coverage file" unless inner.instance_of?(Hash)
|
|
84
|
+
|
|
85
|
+
inner
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Both halves of the read-modify-write happen through the one handle, so
|
|
91
|
+
# processes sharing the file take turns rather than overwriting each
|
|
92
|
+
# other. The open flags are summed, which for disjoint bits is the same
|
|
93
|
+
# number OR would build and leaves no spelling of the combination
|
|
94
|
+
# without a witness.
|
|
95
|
+
def with_exclusive_lock
|
|
96
|
+
open_file(path, File::RDWR + File::CREAT, 0o644) do |file|
|
|
97
|
+
file.flock(File::LOCK_EX)
|
|
98
|
+
yield file
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# mutant:disable — Ruby 4.0 removed Kernel#open's leading-pipe
|
|
103
|
+
# command mode, so no test can tell `File.open` from `open` here
|
|
104
|
+
# any more. The explicit receiver is kept: on older rubies it is
|
|
105
|
+
# what refuses to run a store path as a command.
|
|
106
|
+
def open_file(name, mode, perm, &)
|
|
107
|
+
File.open(name, mode, perm, &)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def merge(existing, incoming)
|
|
111
|
+
incoming.each_with_object(existing) do |(file, lines), merged|
|
|
112
|
+
merged[file] = ((merged[file] || []) | lines).sort
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def envelope(existing, incoming)
|
|
117
|
+
now = Time.now.utc.iso8601
|
|
118
|
+
{
|
|
119
|
+
ENVELOPE => {
|
|
120
|
+
"format_version" => FORMAT_VERSION,
|
|
121
|
+
"started_at" => existing["started_at"] || now,
|
|
122
|
+
"updated_at" => now,
|
|
123
|
+
"coverage" => merge(existing.fetch("coverage"), incoming).sort.to_h,
|
|
124
|
+
"last_seen" => existing.fetch("last_seen").merge(incoming.transform_values { now }).sort.to_h
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "coverage"
|
|
4
|
+
require_relative "production/error"
|
|
5
|
+
|
|
6
|
+
#
|
|
7
|
+
# Production coverage mode (#1271). Deliberately loadable on its own:
|
|
8
|
+
# `require "simplecov/production"` pulls in none of the reporting machinery,
|
|
9
|
+
# because a production process wants a measurement tap and nothing else.
|
|
10
|
+
# Nothing here runs unless `SimpleCov::Production.start` is called explicitly.
|
|
11
|
+
#
|
|
12
|
+
module SimpleCov
|
|
13
|
+
module Production
|
|
14
|
+
class << self
|
|
15
|
+
# Answers false, with a warning naming why, when it safely declines
|
|
16
|
+
# (already running, another Coverage owner such as a test suite, or a
|
|
17
|
+
# runtime without oneshot lines). Configuration mistakes raise `Error`
|
|
18
|
+
# instead: a typo'd interval should fail deployment, not quietly measure
|
|
19
|
+
# wrong.
|
|
20
|
+
#
|
|
21
|
+
# In a forking server, call this from the worker-boot hook (Puma's
|
|
22
|
+
# `on_worker_boot`, Unicorn's `after_fork`): the flush thread does not
|
|
23
|
+
# survive a fork, and `start` in the child picks the inherited
|
|
24
|
+
# measurement back up.
|
|
25
|
+
def start(root: Dir.pwd, sink: nil, flush_interval: 60, flush_jitter: nil, sample_rate: 1.0,
|
|
26
|
+
max_buffered_lines: 1_000_000)
|
|
27
|
+
validate!(flush_interval, flush_jitter, sample_rate, max_buffered_lines)
|
|
28
|
+
return warn_decline("SimpleCov::Production is already running") if running?
|
|
29
|
+
return false unless claim_coverage
|
|
30
|
+
|
|
31
|
+
configure(root, sink, flush_interval: flush_interval, flush_jitter: flush_jitter,
|
|
32
|
+
sample_rate: sample_rate, max_buffered_lines: max_buffered_lines)
|
|
33
|
+
@running = true
|
|
34
|
+
@pid = Process.pid
|
|
35
|
+
install_at_exit
|
|
36
|
+
spawn_flush_thread
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Both are small Integers, so identity settles it: a measurement belongs to
|
|
41
|
+
# the process that started it and to no other.
|
|
42
|
+
def running?
|
|
43
|
+
!!@running && @pid.equal?(Process.pid)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Called by the flush thread every interval; public so a deploy hook (or a
|
|
47
|
+
# signal handler) can force one. An empty delta counts as accepted.
|
|
48
|
+
def flush
|
|
49
|
+
return false unless running?
|
|
50
|
+
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
pull_pending
|
|
53
|
+
push_pending
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def stop
|
|
58
|
+
return false unless running?
|
|
59
|
+
|
|
60
|
+
@running = false
|
|
61
|
+
wake_flush_thread
|
|
62
|
+
@thread&.join
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
pull_pending
|
|
65
|
+
push_pending
|
|
66
|
+
Coverage.result(stop: true, clear: true)
|
|
67
|
+
end
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @api private -- clears module state between tests. The at_exit
|
|
72
|
+
# registration flag survives on purpose: one hook per process.
|
|
73
|
+
def reset!
|
|
74
|
+
@running = false
|
|
75
|
+
wake_flush_thread if @thread
|
|
76
|
+
@thread&.join
|
|
77
|
+
@thread = nil
|
|
78
|
+
@pending = {}
|
|
79
|
+
@pid = nil
|
|
80
|
+
@started_coverage = false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def validate!(flush_interval, flush_jitter, sample_rate, max_buffered_lines)
|
|
86
|
+
raise Error, "flush_interval must be a positive number of seconds" unless flush_interval.positive?
|
|
87
|
+
|
|
88
|
+
validate_jitter!(flush_jitter)
|
|
89
|
+
raise Error, "sample_rate must be within (0, 1]" unless sample_rate.positive? && sample_rate <= 1
|
|
90
|
+
raise Error, "max_buffered_lines must be positive" unless max_buffered_lines.positive?
|
|
91
|
+
return unless sample_rate < 1 && !Coverage.respond_to?(:suspend)
|
|
92
|
+
|
|
93
|
+
raise Error, "sample_rate below 1.0 needs Coverage.suspend (Ruby 3.2 or later)"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def validate_jitter!(flush_jitter)
|
|
97
|
+
return if flush_jitter.nil? || (flush_jitter.is_a?(Numeric) && flush_jitter >= 0)
|
|
98
|
+
|
|
99
|
+
raise Error, "flush_jitter must be a non-negative number of seconds"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# A running Coverage belongs to someone else, unless this very module
|
|
103
|
+
# started it before a fork, in which case the child inherits the
|
|
104
|
+
# measurement and only needs its own flush thread.
|
|
105
|
+
def claim_coverage
|
|
106
|
+
if Coverage.running?
|
|
107
|
+
return true if @started_coverage && @pid && !@pid.equal?(Process.pid)
|
|
108
|
+
|
|
109
|
+
return warn_decline("Coverage is already running (a test suite or another tool owns it)")
|
|
110
|
+
end
|
|
111
|
+
return warn_decline("this runtime has no oneshot lines support") unless oneshot_supported?
|
|
112
|
+
|
|
113
|
+
Coverage.start(oneshot_lines: true)
|
|
114
|
+
@started_coverage = true
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def oneshot_supported?
|
|
118
|
+
!Coverage.respond_to?(:supported?) || Coverage.supported?(:oneshot_lines)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def configure(root, sink, flush_interval:, flush_jitter:, sample_rate:, max_buffered_lines:)
|
|
122
|
+
@root_prefix = File.expand_path(root) + File::SEPARATOR
|
|
123
|
+
@sink = sink || default_sink(root)
|
|
124
|
+
@flush_interval = flush_interval
|
|
125
|
+
@flush_jitter = flush_jitter || (flush_interval / 10.0)
|
|
126
|
+
@sample_rate = sample_rate
|
|
127
|
+
@max_buffered_lines = max_buffered_lines
|
|
128
|
+
@pending = {} #: Hash[String, Set[Integer]]
|
|
129
|
+
@measuring = true
|
|
130
|
+
@mutex = Mutex.new
|
|
131
|
+
@waiter = ConditionVariable.new
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def default_sink(root)
|
|
135
|
+
FileSink.new(path: File.join(root, "tmp", "simplecov", "production.json"))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def spawn_flush_thread
|
|
139
|
+
thread = Thread.new { flush_loop }
|
|
140
|
+
thread.name = "SimpleCov::Production flusher"
|
|
141
|
+
@thread = thread
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The condition variable rather than a bare sleep is what lets `stop`
|
|
145
|
+
# return promptly instead of waiting out a long interval.
|
|
146
|
+
def flush_loop
|
|
147
|
+
@mutex.synchronize do
|
|
148
|
+
while @running
|
|
149
|
+
@waiter.wait(@mutex, interval_with_jitter)
|
|
150
|
+
cycle if @running
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# A fresh share is drawn per wait: a fleet of workers booted together
|
|
156
|
+
# would otherwise contend on the shared sink at the same instant every
|
|
157
|
+
# interval, forever.
|
|
158
|
+
def interval_with_jitter
|
|
159
|
+
@flush_interval + (rand * @flush_jitter)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def wake_flush_thread
|
|
163
|
+
@mutex.synchronize { @waiter.signal }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def cycle
|
|
167
|
+
resample
|
|
168
|
+
pull_pending
|
|
169
|
+
push_pending
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Duty-cycle sampling: with `sample_rate` 0.25, roughly one interval in
|
|
173
|
+
# four measures and the rest are suspended, capping steady-state overhead
|
|
174
|
+
# while every line still gets its chance over time.
|
|
175
|
+
def resample
|
|
176
|
+
return unless @sample_rate < 1
|
|
177
|
+
|
|
178
|
+
wanted = rand < @sample_rate
|
|
179
|
+
if wanted && !@measuring
|
|
180
|
+
Coverage.resume
|
|
181
|
+
elsif !wanted && @measuring
|
|
182
|
+
Coverage.suspend
|
|
183
|
+
end
|
|
184
|
+
@measuring = wanted
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Clearing on read means a line executed again later re-reports, which is
|
|
188
|
+
# what lets a dropped buffer heal for still-hot code.
|
|
189
|
+
def pull_pending
|
|
190
|
+
Coverage.result(stop: false, clear: true).each do |path, data|
|
|
191
|
+
lines = data[:oneshot_lines]
|
|
192
|
+
next if lines.nil? || lines.empty?
|
|
193
|
+
|
|
194
|
+
relative = relativize(path)
|
|
195
|
+
next unless relative
|
|
196
|
+
|
|
197
|
+
(@pending[relative] ||= Set.new).merge(lines)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# On failure the buffer is kept for the next interval, bounded by the
|
|
202
|
+
# ceiling so a long outage cannot grow memory without limit.
|
|
203
|
+
def push_pending
|
|
204
|
+
return true if @pending.empty?
|
|
205
|
+
|
|
206
|
+
@sink.store(@pending.transform_values(&:sort))
|
|
207
|
+
@pending.clear
|
|
208
|
+
true
|
|
209
|
+
rescue => e
|
|
210
|
+
warn "[SimpleCov::Production] flush failed (#{e.class}: #{e}); retrying next interval"
|
|
211
|
+
enforce_ceiling
|
|
212
|
+
false
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Oneshot's re-reporting is what makes dropping safe-ish: lines still being
|
|
216
|
+
# executed report again after the drop, and only code that ran during the
|
|
217
|
+
# outage and never again is lost.
|
|
218
|
+
def enforce_ceiling
|
|
219
|
+
return if @pending.sum { |_path, lines| lines.size } <= @max_buffered_lines
|
|
220
|
+
|
|
221
|
+
@pending.clear
|
|
222
|
+
warn "[SimpleCov::Production] buffer ceiling (#{@max_buffered_lines} lines) exceeded " \
|
|
223
|
+
"while the sink was unreachable; dropping buffered coverage"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def relativize(path)
|
|
227
|
+
return nil unless path.start_with?(@root_prefix)
|
|
228
|
+
|
|
229
|
+
path[@root_prefix.length..]
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def warn_decline(reason)
|
|
233
|
+
warn "[SimpleCov::Production] not starting: #{reason}"
|
|
234
|
+
false
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def install_at_exit
|
|
238
|
+
return if @at_exit_installed
|
|
239
|
+
|
|
240
|
+
@at_exit_installed = true
|
|
241
|
+
at_exit do
|
|
242
|
+
stop
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
require_relative "production/file_sink"
|
|
@@ -11,22 +11,11 @@ SimpleCov.profiles.define "rails" do
|
|
|
11
11
|
group "Models", "app/models"
|
|
12
12
|
group "Mailers", "app/mailers"
|
|
13
13
|
group "Helpers", "app/helpers"
|
|
14
|
+
group "Views", "app/views"
|
|
14
15
|
group "Jobs", %w[app/jobs app/workers]
|
|
15
16
|
group "Libraries", "lib/"
|
|
16
17
|
|
|
17
|
-
# Preserve the legacy `track_files` semantics (additive disk-discovery
|
|
18
|
-
# without restricting the report's universe): write the ivar directly
|
|
19
|
-
# so loading the profile doesn't emit the public-API deprecation. Users
|
|
20
|
-
# migrating their own configs should prefer `cover "{app,lib}/**/*.rb"`,
|
|
21
|
-
# which both injects unloaded files and scopes the report to the match
|
|
22
|
-
# set — usually the intended behavior for a Rails project.
|
|
23
18
|
@tracked_files = "{app,lib}/**/*.rb"
|
|
24
19
|
|
|
25
|
-
# `parallelize(workers: ...)` forks worker processes that each run a
|
|
26
|
-
# slice of the suite. Without subprocess support, the workers' coverage
|
|
27
|
-
# is dropped on the floor and the parent records 0% for everything they
|
|
28
|
-
# touched. Hooking `Process._fork` makes each worker re-call
|
|
29
|
-
# `SimpleCov.start` with a unique command_name so the resultsets merge
|
|
30
|
-
# correctly.
|
|
31
20
|
merge_subprocesses true
|
|
32
21
|
end
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
SimpleCov.profiles.define "root_filter" do
|
|
4
|
-
# Exclude all files outside of simplecov root. Shares the regex with
|
|
5
|
-
# SimpleCov::UselessResultsRemover so the root-prefix logic lives in one
|
|
6
|
-
# place; this profile is the user-facing entry point that tools like
|
|
7
|
-
# `SimpleCov.filtered` apply.
|
|
8
4
|
skip do |src|
|
|
9
5
|
src.filename !~ SimpleCov::UselessResultsRemover.root_regex
|
|
10
6
|
end
|
|
@@ -1,26 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# an opt-in profile rather than the default — most projects can't
|
|
7
|
-
# realistically start at 100% on day one — so a team that has paid down
|
|
8
|
-
# the debt can flip the strict switch without re-typing the threshold
|
|
9
|
-
# trio every time.
|
|
3
|
+
# Every coverage criterion enabled and held to 100%. An opt-in profile rather
|
|
4
|
+
# than the default (#1061), so a team that has paid down the debt can flip the
|
|
5
|
+
# strict switch without re-typing the threshold trio every time.
|
|
10
6
|
#
|
|
11
|
-
# JRuby gracefully degrades: `enable_coverage :branch` / `:method` are
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# materialize. `CoverageViolations` skips threshold lookups for
|
|
15
|
-
# criteria not in the stats, so the `:branch` / `:method` clauses below
|
|
16
|
-
# silently no-op on JRuby and only `:line` is enforced.
|
|
7
|
+
# JRuby gracefully degrades: `enable_coverage :branch` / `:method` are accepted
|
|
8
|
+
# but the runtime cannot measure them, and `CoverageViolations` skips threshold
|
|
9
|
+
# lookups for criteria not in the stats, so only `:line` is enforced there.
|
|
17
10
|
#
|
|
18
|
-
# `:eval`
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
# the profile stays quiet on Ruby < 3.2, where `enable_coverage :eval`
|
|
22
|
-
# would otherwise warn about the missing runtime support every time
|
|
23
|
-
# the profile loads.
|
|
11
|
+
# `:eval` is guarded on `coverage_for_eval_supported?` so the profile stays
|
|
12
|
+
# quiet on Ruby < 3.2, where enabling it would warn every time it loads.
|
|
13
|
+
|
|
24
14
|
SimpleCov.profiles.define "strict" do
|
|
25
15
|
enable_coverage :branch
|
|
26
16
|
enable_coverage :method
|
data/lib/simplecov/profiles.rb
CHANGED
|
@@ -1,63 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# loaded using SimpleCov.start :rails and defined using
|
|
7
|
-
# SimpleCov.profiles.define :foo do
|
|
8
|
-
# # SimpleCov configuration here, same as in SimpleCov.configure
|
|
9
|
-
# end
|
|
10
|
-
#
|
|
4
|
+
# Configuration procs loaded with `SimpleCov.start :rails` and defined with
|
|
5
|
+
# `SimpleCov.profiles.define`.
|
|
11
6
|
class Profiles < Hash
|
|
12
|
-
#
|
|
13
|
-
# Define a SimpleCov profile:
|
|
14
|
-
# SimpleCov.profiles.define 'rails' do
|
|
15
|
-
# # Same as SimpleCov.configure do .. here
|
|
16
|
-
# end
|
|
17
|
-
#
|
|
18
7
|
def define(name, &blk)
|
|
19
8
|
name = name.to_sym
|
|
20
|
-
raise
|
|
9
|
+
raise ConfigurationError, "SimpleCov Profile '#{name}' is already defined" unless self[name].nil?
|
|
21
10
|
|
|
22
11
|
self[name] = blk
|
|
23
12
|
end
|
|
24
13
|
|
|
25
|
-
#
|
|
26
|
-
# Applies the profile of given name on SimpleCov.configure
|
|
27
|
-
#
|
|
28
14
|
def load(name)
|
|
29
15
|
SimpleCov.configure(&fetch_proc(name))
|
|
30
16
|
end
|
|
31
17
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# cannot be located.
|
|
36
|
-
#
|
|
37
|
-
# Lookup order:
|
|
38
|
-
# 1. already registered via #define
|
|
39
|
-
# 2. require "simplecov/profiles/<name>" (bundled profiles)
|
|
40
|
-
# 3. require "simplecov-profile-<name>" (third-party plugin gems)
|
|
41
|
-
#
|
|
18
|
+
# Lookup order: already registered via #define, then
|
|
19
|
+
# `require "simplecov/profiles/<name>"` for the bundled profiles, then
|
|
20
|
+
# `require "simplecov-profile-<name>"` for third-party plugin gems.
|
|
42
21
|
def fetch_proc(name)
|
|
43
22
|
name = name.to_sym
|
|
44
23
|
autoload_profile(name) unless key?(name)
|
|
45
|
-
return
|
|
24
|
+
return fetch(name) if key?(name)
|
|
46
25
|
|
|
47
|
-
raise
|
|
26
|
+
raise ConfigurationError, "Could not find SimpleCov Profile called '#{name}'"
|
|
48
27
|
end
|
|
49
28
|
|
|
50
|
-
|
|
29
|
+
private
|
|
51
30
|
|
|
52
31
|
def autoload_profile(name)
|
|
53
32
|
require "simplecov/profiles/#{name}"
|
|
54
33
|
rescue LoadError
|
|
55
34
|
begin
|
|
56
|
-
# simplecov:disable — third-party gem fallback (no such gem in test env)
|
|
57
35
|
require "simplecov-profile-#{name}"
|
|
58
|
-
# simplecov:enable
|
|
59
36
|
rescue LoadError
|
|
60
|
-
#
|
|
37
|
+
# Neither a bundled profile nor a plugin gem; `fetch_proc` raises the
|
|
38
|
+
# user-facing error.
|
|
61
39
|
end
|
|
62
40
|
end
|
|
63
41
|
end
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# The clobber-prevention backstop: an empty process stands down instead
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
3
|
+
# The clobber-prevention backstop: an empty process stands down instead of
|
|
4
|
+
# formatting over a fresher report a sibling produced. Typically fires when
|
|
5
|
+
# `SimpleCov.start` ran in a parent process that shelled out to the test runner
|
|
6
|
+
# (#581).
|
|
7
|
+
|
|
7
8
|
module SimpleCov
|
|
8
9
|
class << self
|
|
9
|
-
# Returns true when our process has no coverage data to contribute
|
|
10
|
-
# (after the resultset merge) and a newer report already exists on
|
|
11
|
-
# disk. Typically fires when `SimpleCov.start` ran in a parent
|
|
12
|
-
# process that shelled out to the test runner. See issue #581.
|
|
13
10
|
def defer_to_existing_report?
|
|
14
11
|
return false unless existing_report_newer_than_us?
|
|
15
12
|
|
|
@@ -19,15 +16,14 @@ module SimpleCov
|
|
|
19
16
|
empty
|
|
20
17
|
end
|
|
21
18
|
|
|
22
|
-
# `.last_run.json` only exists after fully successful runs, so alone
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# rescue covers a file vanishing mid-at_exit (`rm -rf coverage`).
|
|
19
|
+
# `.last_run.json` only exists after fully successful runs, so alone it left
|
|
20
|
+
# the backstop inert when the child run failed, the case where clobbering its
|
|
21
|
+
# report hurts most. The report stamp is touched by every formatting process
|
|
22
|
+
# regardless of exit status. The rescue covers a file vanishing mid-at_exit.
|
|
27
23
|
def existing_report_newer_than_us?
|
|
28
24
|
return false unless process_start_time
|
|
29
25
|
|
|
30
|
-
[
|
|
26
|
+
[LastRun.last_run_path, ReportStamp.path].any? do |path|
|
|
31
27
|
File.mtime(path) > process_start_time
|
|
32
28
|
rescue SystemCallError
|
|
33
29
|
false
|
|
@@ -37,7 +33,7 @@ module SimpleCov
|
|
|
37
33
|
def warn_about_deferred_report
|
|
38
34
|
return unless print_errors
|
|
39
35
|
|
|
40
|
-
ExitCodes.print_error
|
|
36
|
+
ExitCodes.print_error Color.colorize(
|
|
41
37
|
"Skipping SimpleCov report — this process tracked no application code and a newer " \
|
|
42
38
|
"report already exists at #{coverage_path}. This usually means SimpleCov.start ran in a " \
|
|
43
39
|
"parent process (e.g. a Rakefile or Rails' Bundler.require) that shelled out to the test " \
|
|
@@ -3,24 +3,21 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
|
|
5
5
|
module SimpleCov
|
|
6
|
-
# Touches coverage/.report_stamp whenever a report is formatted, no
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# that file is only written after fully successful runs, which left
|
|
11
|
-
# the backstop inert exactly when the child run failed and its report
|
|
12
|
-
# mattered most.
|
|
6
|
+
# Touches coverage/.report_stamp whenever a report is formatted, no matter how
|
|
7
|
+
# the run ends. The clobber-prevention backstop needs an on-disk signal that a
|
|
8
|
+
# fresher report exists, and `.last_run.json` can't be it alone: that file is
|
|
9
|
+
# only written after fully successful runs.
|
|
13
10
|
module ReportStamp
|
|
14
11
|
class << self
|
|
15
12
|
def path
|
|
16
13
|
File.join(SimpleCov.coverage_path, ".report_stamp")
|
|
17
14
|
end
|
|
18
15
|
|
|
16
|
+
# A read-only or vanished coverage dir must not crash reporting; the stamp
|
|
17
|
+
# only powers the deferral heuristic.
|
|
19
18
|
def touch
|
|
20
19
|
FileUtils.touch(path)
|
|
21
20
|
rescue SystemCallError
|
|
22
|
-
# A read-only or vanished coverage dir must not crash reporting;
|
|
23
|
-
# the stamp only powers the deferral heuristic.
|
|
24
21
|
nil
|
|
25
22
|
end
|
|
26
23
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class Result
|
|
5
|
+
# The filter and grouping configuration a Result applies to its source files
|
|
6
|
+
# after building them. Each field defaults to the SimpleCov singleton's
|
|
7
|
+
# configuration, so ordinary callers never construct one; tests pass a custom
|
|
8
|
+
# instance to opt out of the project's filters or groups.
|
|
9
|
+
class FilterConfig
|
|
10
|
+
attr_reader :filters, :cover_filters, :groups
|
|
11
|
+
|
|
12
|
+
def initialize(filters: SimpleCov.filters, cover_filters: SimpleCov.cover_filters, groups: SimpleCov.groups)
|
|
13
|
+
@filters = filters
|
|
14
|
+
@cover_filters = cover_filters
|
|
15
|
+
@groups = groups
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|