padded_cell 0.1.0 → 0.3.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/CHANGELOG.md +38 -0
- data/Cargo.lock +16 -2
- data/Cargo.toml +2 -0
- data/README.md +170 -8
- data/ext/padded_cell_native/core/Cargo.toml +2 -1
- data/ext/padded_cell_native/core/src/chart/directives/tests.rs +56 -0
- data/ext/padded_cell_native/core/src/chart/directives.rs +144 -0
- data/ext/padded_cell_native/core/src/chart/json/tests.rs +46 -0
- data/ext/padded_cell_native/core/src/chart/json.rs +175 -0
- data/ext/padded_cell_native/core/src/chart/lcov.rs +50 -0
- data/ext/padded_cell_native/core/src/chart/markdown/tests.rs +93 -0
- data/ext/padded_cell_native/core/src/chart/markdown.rs +201 -0
- data/ext/padded_cell_native/core/src/chart/methods/tests.rs +38 -0
- data/ext/padded_cell_native/core/src/chart/methods.rs +62 -0
- data/ext/padded_cell_native/core/src/chart/output/tests.rs +45 -0
- data/ext/padded_cell_native/core/src/chart/output.rs +103 -0
- data/ext/padded_cell_native/core/src/chart/results/tests.rs +96 -0
- data/ext/padded_cell_native/core/src/chart/results.rs +379 -0
- data/ext/padded_cell_native/core/src/chart/source/tests.rs +64 -0
- data/ext/padded_cell_native/core/src/chart/source.rs +153 -0
- data/ext/padded_cell_native/core/src/chart/stats/tests.rs +52 -0
- data/ext/padded_cell_native/core/src/chart/stats.rs +218 -0
- data/ext/padded_cell_native/core/src/chart/test_support.rs +93 -0
- data/ext/padded_cell_native/core/src/chart/tests.rs +97 -0
- data/ext/padded_cell_native/core/src/chart/thresholds/tests.rs +36 -0
- data/ext/padded_cell_native/core/src/chart/thresholds.rs +116 -0
- data/ext/padded_cell_native/core/src/chart.rs +302 -0
- data/ext/padded_cell_native/core/src/discover/tests.rs +2 -0
- data/ext/padded_cell_native/core/src/discover.rs +7 -53
- data/ext/padded_cell_native/core/src/error.rs +24 -0
- data/ext/padded_cell_native/core/src/lib.rs +10 -3
- data/ext/padded_cell_native/core/src/parallel.rs +43 -0
- data/ext/padded_cell_native/core/src/restarts/tests.rs +29 -0
- data/ext/padded_cell_native/core/src/restarts.rs +64 -0
- data/ext/padded_cell_native/ffi/Cargo.toml +2 -1
- data/ext/padded_cell_native/ffi/src/chart.rs +205 -0
- data/ext/padded_cell_native/ffi/src/lib.rs +5 -0
- data/ext/padded_cell_native/ffi/src/limits.rs +83 -0
- data/lib/padded_cell/asylum.rb +93 -0
- data/lib/padded_cell/cell.rb +23 -14
- data/lib/padded_cell/chart/accumulator.rb +103 -0
- data/lib/padded_cell/chart/config.rb +129 -0
- data/lib/padded_cell/chart/delta.rb +148 -0
- data/lib/padded_cell/chart/document.rb +64 -0
- data/lib/padded_cell/chart/filter.rb +55 -0
- data/lib/padded_cell/chart/merge.rb +63 -0
- data/lib/padded_cell/chart/probe.rb +70 -0
- data/lib/padded_cell/chart/raw.rb +48 -0
- data/lib/padded_cell/chart/report.rb +128 -0
- data/lib/padded_cell/chart/source_files.rb +90 -0
- data/lib/padded_cell/chart.rb +38 -0
- data/lib/padded_cell/cli.rb +70 -4
- data/lib/padded_cell/limits.rb +45 -0
- data/lib/padded_cell/loader.rb +5 -1
- data/lib/padded_cell/pool.rb +82 -58
- data/lib/padded_cell/reporters.rb +20 -3
- data/lib/padded_cell/results.rb +13 -0
- data/lib/padded_cell/runner.rb +150 -22
- data/lib/padded_cell/version.rb +1 -1
- data/lib/padded_cell/worker.rb +63 -0
- data/lib/padded_cell.rb +8 -0
- metadata +43 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module PaddedCell
|
|
8
|
+
module Chart
|
|
9
|
+
# Writes the reports (natively), prints the summary, and reports the
|
|
10
|
+
# thresholds missed.
|
|
11
|
+
class Report
|
|
12
|
+
Stats = Data.define(:covered, :missed, :omitted, :hits, :percent, :strength) do
|
|
13
|
+
def total = covered + missed
|
|
14
|
+
end
|
|
15
|
+
CRITERIA = { lines: :line, branches: :branch, methods: :method }.freeze
|
|
16
|
+
WORST_FILES = 5
|
|
17
|
+
|
|
18
|
+
def initialize(document, dir:, io:)
|
|
19
|
+
@document = document
|
|
20
|
+
@settings = document.settings
|
|
21
|
+
@dir = dir
|
|
22
|
+
@io = io
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# check: a whole, passing run: thresholds decide, and .last_run.json
|
|
26
|
+
# records it. False when a threshold is missed.
|
|
27
|
+
def call(check:)
|
|
28
|
+
result = PaddedCellNative.chart_report(@document.rows, @document.group_rows, options(check))
|
|
29
|
+
@total = stats(result[:total])
|
|
30
|
+
@files = result[:files].map { |name, file| [name, stats(file)] }
|
|
31
|
+
summary
|
|
32
|
+
warn_lost
|
|
33
|
+
return true unless check
|
|
34
|
+
|
|
35
|
+
result[:violations].each { report_violation(*it) }
|
|
36
|
+
result[:violations].empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def options(check)
|
|
42
|
+
now = Time.now
|
|
43
|
+
root = @document.root
|
|
44
|
+
{
|
|
45
|
+
dir: File.expand_path(@dir), formats: @settings[:formats], nocov_token: @settings[:nocov_token],
|
|
46
|
+
criteria: criteria.map(&:to_s), write_last_run: check, last_run: last_run,
|
|
47
|
+
minimum: limits(@settings[:minimum]), minimum_by_file: limits(@settings[:minimum_by_file]),
|
|
48
|
+
minimum_by_group: @settings[:minimum_by_group].map { |name, minimums| [name.to_s, limits(minimums)] },
|
|
49
|
+
maximum_drop: limits(@settings[:maximum_drop]),
|
|
50
|
+
root:, timestamp: now.iso8601(3), created_at: now.to_f, commit: commit(root),
|
|
51
|
+
project_name: File.basename(root).capitalize.tr('_', ' '), generator: "padded_cell #{VERSION}"
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# [line, branch, method], nil where unset.
|
|
56
|
+
def limits(values) = %i[line branch method].map { values[it]&.to_f }
|
|
57
|
+
|
|
58
|
+
# The previous whole run's percents.
|
|
59
|
+
def last_run
|
|
60
|
+
previous = JSON.parse(File.read(File.join(@dir, '.last_run.json')), symbolize_names: true)[:result] || {}
|
|
61
|
+
limits({ line: previous[:line] || previous[:covered_percent], branch: previous[:branch],
|
|
62
|
+
method: previous[:method] })
|
|
63
|
+
rescue Errno::ENOENT, JSON::ParserError
|
|
64
|
+
[nil, nil, nil]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def commit(root)
|
|
68
|
+
out, status = Open3.capture2e('git', '-C', root, 'rev-parse', 'HEAD')
|
|
69
|
+
out.strip if status.success?
|
|
70
|
+
rescue SystemCallError
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def stats(row) = row.transform_values { Stats.new(*it) }
|
|
75
|
+
|
|
76
|
+
# The criteria measured, as stats keys.
|
|
77
|
+
def criteria = @document.criteria.filter_map { CRITERIA[it.to_sym] }
|
|
78
|
+
|
|
79
|
+
def summary
|
|
80
|
+
@io.puts "\nCoverage report generated for padded_cell to #{@dir}"
|
|
81
|
+
criteria.each do |criterion|
|
|
82
|
+
stat = @total[criterion]
|
|
83
|
+
next if criterion != :line && stat.total.zero?
|
|
84
|
+
|
|
85
|
+
@io.puts format('%<label>s coverage: %<covered>d / %<total>d (%<percent>.2f%%)',
|
|
86
|
+
label: criterion.capitalize, covered: stat.covered, total: stat.total,
|
|
87
|
+
percent: stat.percent.floor(2))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def warn_lost
|
|
92
|
+
lost = @document.lost
|
|
93
|
+
unmeasured = @document.unmeasured
|
|
94
|
+
if lost[:workers].positive? || lost[:cells].positive?
|
|
95
|
+
@io.puts "Coverage is partial: #{lost[:workers]} workers and #{lost[:cells]} cells died before handing " \
|
|
96
|
+
'over what they measured'
|
|
97
|
+
end
|
|
98
|
+
return if unmeasured.empty?
|
|
99
|
+
|
|
100
|
+
@io.puts "#{unmeasured.size} tracked files were loaded before coverage started, and are not reported: " \
|
|
101
|
+
"#{unmeasured.first(3).join(', ')}#{', ...' if unmeasured.size > 3}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def report_violation(scope, name, criterion, limit, actual)
|
|
105
|
+
label = criterion.capitalize
|
|
106
|
+
@io.puts case scope
|
|
107
|
+
when :drop
|
|
108
|
+
format('%<label>s coverage has dropped by %<actual>.2f%% since the last time ' \
|
|
109
|
+
'(maximum allowed: %<limit>.2f%%).', label:, actual:, limit:)
|
|
110
|
+
when :total then format('%<label>s coverage (%<actual>.2f%%) is below the expected minimum ' \
|
|
111
|
+
'coverage (%<limit>.2f%%).', label:, actual:, limit:)
|
|
112
|
+
else format('%<label>s coverage by %<scope>s (%<actual>.2f%%) is below the expected minimum ' \
|
|
113
|
+
'coverage (%<limit>.2f%%) in %<name>s.', label:, scope:, actual:, limit:, name:)
|
|
114
|
+
end
|
|
115
|
+
worst_files(criterion) if scope == :total
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def worst_files(criterion)
|
|
119
|
+
worst = @files.map { |name, file| [name, file[criterion].percent] }.select { it[1] < 100 }
|
|
120
|
+
.min_by(WORST_FILES) { it[1] }
|
|
121
|
+
return if worst.empty?
|
|
122
|
+
|
|
123
|
+
@io.puts " Lowest-coverage files (#{criterion}):"
|
|
124
|
+
worst.each { |name, percent| @io.puts format(' %<percent>6.2f%% %<name>s', percent:, name:) }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaddedCell
|
|
4
|
+
module Chart
|
|
5
|
+
# The files of a report: those measured and those tracked but never
|
|
6
|
+
# loaded, minus the filtered ones, in path order; and their groups.
|
|
7
|
+
class SourceFiles
|
|
8
|
+
# Tracked templates are not Ruby files Coverage could stub.
|
|
9
|
+
TEMPLATES = /\.(?:erb|haml|slim)\z/
|
|
10
|
+
|
|
11
|
+
# Tracked files loaded before measuring started (by a Rails rake task,
|
|
12
|
+
# Bundler.require...): their coverage is unknown, not 0%.
|
|
13
|
+
attr_reader :unmeasured
|
|
14
|
+
|
|
15
|
+
# files: Raw's {relative path => {lines:, branches:, methods:}}
|
|
16
|
+
def initialize(files, config:, root:)
|
|
17
|
+
@config = config
|
|
18
|
+
@root = root
|
|
19
|
+
loaded = loaded_features
|
|
20
|
+
never, @unmeasured = tracked(files).partition { !loaded.include?(File.join(root, it)) }
|
|
21
|
+
@rows = (files.map { |name, entry| row(name, entry) } + stubs(never))
|
|
22
|
+
.reject { |path, name| skipped?(path, name) }
|
|
23
|
+
.sort_by { it[1] }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# [[absolute path, name, lines, branches, methods, loaded], ...], for
|
|
27
|
+
# PaddedCellNative.chart_report.
|
|
28
|
+
attr_reader :rows
|
|
29
|
+
|
|
30
|
+
# [[name, [row index, ...]], ...]: the groups with files, then
|
|
31
|
+
# Ungrouped. None without groups.
|
|
32
|
+
def groups
|
|
33
|
+
return [] if @config.groups.empty?
|
|
34
|
+
|
|
35
|
+
files = @rows.map { |path, name| SourceFile.new(path, name) }
|
|
36
|
+
grouped = @config.groups.map { |name, filter| [name, files.each_index.select { filter.call(files[it]) }] }
|
|
37
|
+
ungrouped = files.each_index.to_a - grouped.flat_map(&:last)
|
|
38
|
+
(grouped + [['Ungrouped', ungrouped]]).reject { |_, members| members.empty? }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def row(name, entry) = [File.join(@root, name), name, entry[:lines], entry[:branches], entry[:methods], true]
|
|
44
|
+
|
|
45
|
+
# Files never loaded. Ruby compiles them, without running them: while
|
|
46
|
+
# Coverage runs, that gives their lines and branches as if loaded, none
|
|
47
|
+
# covered. Their methods only exist once run: the native side finds
|
|
48
|
+
# them in the source. A file that does not compile has every line but
|
|
49
|
+
# blank and comment ones missed.
|
|
50
|
+
def stubs(names)
|
|
51
|
+
paths = names.map { File.join(@root, it) }
|
|
52
|
+
compiled = paths.select { compile(it) }
|
|
53
|
+
measured = compiled.empty? ? {} : ::Coverage.peek_result
|
|
54
|
+
paths.zip(names).map do |path, name|
|
|
55
|
+
data = measured[path]
|
|
56
|
+
next [path, name, File.readlines(path).map { 0 }, nil, nil, false] unless data
|
|
57
|
+
|
|
58
|
+
entry = Raw.file(Delta.dense(data))
|
|
59
|
+
[path, name, entry[:lines], entry[:branches], nil, false]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def compile(path)
|
|
64
|
+
RubyVM::InstructionSequence.compile_file(path)
|
|
65
|
+
rescue SyntaxError
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def tracked(files)
|
|
70
|
+
@config.tracked.flat_map { Dir.glob(it, base: @root) }.uniq.reject do |name|
|
|
71
|
+
files.key?(name) || TEMPLATES.match?(name) || !File.file?(File.join(@root, name))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def skipped?(path, name)
|
|
76
|
+
file = SourceFile.new(path, name)
|
|
77
|
+
@config.filters.any? { it.call(file) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Required files, also under the root's real path.
|
|
81
|
+
def loaded_features
|
|
82
|
+
pwd = "#{Dir.pwd}/"
|
|
83
|
+
$LOADED_FEATURES.each_with_object(Set.new) do |feature, set|
|
|
84
|
+
set << feature
|
|
85
|
+
set << File.join(@root, feature.delete_prefix(pwd)) if feature.start_with?(pwd)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'chart/delta'
|
|
4
|
+
require_relative 'chart/probe'
|
|
5
|
+
require_relative 'chart/raw'
|
|
6
|
+
require_relative 'chart/accumulator'
|
|
7
|
+
require_relative 'chart/filter'
|
|
8
|
+
require_relative 'chart/config'
|
|
9
|
+
require_relative 'chart/source_files'
|
|
10
|
+
require_relative 'chart/document'
|
|
11
|
+
require_relative 'chart/merge'
|
|
12
|
+
require_relative 'chart/report'
|
|
13
|
+
|
|
14
|
+
module PaddedCell
|
|
15
|
+
# Built-in coverage: Ruby's Coverage measured in the
|
|
16
|
+
# runner, every worker and every cell, summed exactly in the runner. The
|
|
17
|
+
# chart, as the asylum keeps one per patient: inside, a name that is not
|
|
18
|
+
# Ruby's own Coverage; outside, it is all --coverage.
|
|
19
|
+
#
|
|
20
|
+
# Measuring starts before the suite loads, so what is measured comes from
|
|
21
|
+
# the command line (--coverage=lines,branches,methods,eval); the report is
|
|
22
|
+
# configured in the test helper, with PaddedCell.coverage (see Config).
|
|
23
|
+
module Chart
|
|
24
|
+
CRITERIA = %i[lines branches methods eval].freeze
|
|
25
|
+
DEFAULT_CRITERIA = %i[lines branches].freeze
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# Every PaddedCell.coverage block, in one Config.
|
|
29
|
+
def config = @config ||= Config.new
|
|
30
|
+
|
|
31
|
+
def configure(profile = nil, &block)
|
|
32
|
+
config.profile(profile) if profile
|
|
33
|
+
config.instance_eval(&block) if block
|
|
34
|
+
config
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/padded_cell/cli.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'optparse'
|
|
4
4
|
require_relative 'runner'
|
|
5
|
+
require_relative 'chart'
|
|
5
6
|
|
|
6
7
|
module PaddedCell
|
|
7
8
|
# The padded_cell command.
|
|
@@ -18,22 +19,33 @@ module PaddedCell
|
|
|
18
19
|
|
|
19
20
|
module_function
|
|
20
21
|
|
|
21
|
-
# Exit status: 0 passed, 1 failed, 2 usage or load error,
|
|
22
|
+
# Exit status: 0 passed, 1 failed, 2 usage or load error, 3 coverage below
|
|
23
|
+
# a minimum, 130 interrupted.
|
|
22
24
|
def start(argv, io: $stdout)
|
|
23
|
-
options = { io: }
|
|
25
|
+
options = { io:, coverage: coverage_criteria(ENV.fetch('PADDED_CELL_COVERAGE', nil)), **report_paths }
|
|
24
26
|
parser(options).parse!(argv)
|
|
27
|
+
return merge(options) if options[:coverage_merge]
|
|
28
|
+
|
|
25
29
|
options[:paths] = argv
|
|
26
30
|
Runner.new(**options).run
|
|
27
|
-
rescue OptionParser::ParseError, ArgumentError, LoadFailure, Unsupported, SystemCallError => e
|
|
31
|
+
rescue OptionParser::ParseError, ArgumentError, RegexpError, LoadFailure, Unsupported, SystemCallError => e
|
|
28
32
|
warn "padded_cell: #{e.message}"
|
|
29
33
|
2
|
|
30
34
|
end
|
|
31
35
|
|
|
36
|
+
# Report paths a CI runner can set without touching the command line;
|
|
37
|
+
# --json and --junit still win.
|
|
38
|
+
def report_paths
|
|
39
|
+
{ json: ENV.fetch('PADDED_CELL_JSON', nil), junit: ENV.fetch('PADDED_CELL_JUNIT', nil) }
|
|
40
|
+
.reject { |_, path| path.nil? || path.empty? }
|
|
41
|
+
end
|
|
42
|
+
|
|
32
43
|
def parser(options)
|
|
33
44
|
OptionParser.new do |opts|
|
|
34
45
|
opts.banner = BANNER
|
|
35
46
|
opts.version = VERSION
|
|
36
47
|
runner_options(opts, options)
|
|
48
|
+
coverage_options(opts, options)
|
|
37
49
|
output_options(opts, options)
|
|
38
50
|
opts.on('--version', 'Print the version') do
|
|
39
51
|
options[:io].puts "padded_cell #{VERSION}"
|
|
@@ -50,7 +62,7 @@ module PaddedCell
|
|
|
50
62
|
opts.on('-j', '--jobs N', Integer, "Workers (default: #{Etc.nprocessors})") do
|
|
51
63
|
options[:jobs] = positive(it)
|
|
52
64
|
end
|
|
53
|
-
opts.on('-s', '--seed SEED', Integer, 'Order seed (default: random, or $SEED)') { options[:seed] = it }
|
|
65
|
+
opts.on('-s', '--seed SEED', Integer, 'Order seed (default: random, or $SEED)') { options[:seed] = seed(it) }
|
|
54
66
|
opts.on('-n', '-i', '--name', '--include PATTERN', 'Only tests matching (/regexp/ or exact name)') do
|
|
55
67
|
options[:include] = it
|
|
56
68
|
end
|
|
@@ -65,10 +77,41 @@ module PaddedCell
|
|
|
65
77
|
opts.on('--timeout SECONDS', Float, 'Kill and fail a test running longer') do
|
|
66
78
|
options[:timeout] = positive(it)
|
|
67
79
|
end
|
|
80
|
+
opts.on('--budget SECONDS', Float, 'Stop dispatching once the run has taken SECONDS (and kill and ' \
|
|
81
|
+
'fail what runs past it); tests left out fail the run') do
|
|
82
|
+
options[:budget] = positive(it)
|
|
83
|
+
end
|
|
68
84
|
opts.on('--fail-fast', 'Stop dispatching after the first failure') { options[:fail_fast] = true }
|
|
85
|
+
opts.on('--asylum[=N]', Integer, 'Retry failed tests up to N times (default 2) after the run, alone, ' \
|
|
86
|
+
'each in a fresh cell; report the flaky ones') do
|
|
87
|
+
options[:asylum] = it.nil? ? 2 : positive(it)
|
|
88
|
+
end
|
|
69
89
|
opts.on('--list', 'List the tests that would run (static, loads nothing)') { options[:list] = true }
|
|
70
90
|
end
|
|
71
91
|
|
|
92
|
+
def coverage_options(opts, options)
|
|
93
|
+
opts.on('--[no-]coverage[=CRITERIA]', 'Measure coverage: lines, branches, methods, eval ' \
|
|
94
|
+
"(default: #{Chart::DEFAULT_CRITERIA.join(',')}; " \
|
|
95
|
+
'or $PADDED_CELL_COVERAGE)') do
|
|
96
|
+
options[:coverage] = coverage_criteria(it.to_s)
|
|
97
|
+
end
|
|
98
|
+
opts.on('--coverage-dir DIR', 'Write coverage reports to DIR (default: coverage, or the test helper\'s)') do
|
|
99
|
+
options[:coverage_dir] = it
|
|
100
|
+
end
|
|
101
|
+
opts.on('--coverage-merge DIR,DIR', Array, 'Merge the coverage of shards (their --coverage-dir) ' \
|
|
102
|
+
'into one report; runs no test') do
|
|
103
|
+
options[:coverage_merge] = it
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# --coverage-merge: 0, or 3 when a threshold is missed.
|
|
108
|
+
def merge(options)
|
|
109
|
+
document = Chart::Merge.call(options[:coverage_merge])
|
|
110
|
+
dir = options[:coverage_dir] || 'coverage'
|
|
111
|
+
document.write(dir)
|
|
112
|
+
Chart::Report.new(document, dir:, io: options[:io]).call(check: document.complete?) ? 0 : 3
|
|
113
|
+
end
|
|
114
|
+
|
|
72
115
|
def output_options(opts, options)
|
|
73
116
|
opts.on('-v', '--verbose', 'One line per test, with timings') { options[:verbose] = true }
|
|
74
117
|
opts.on('--show-skips', 'Show skipped tests at the end') { options[:show_skips] = true }
|
|
@@ -78,6 +121,13 @@ module PaddedCell
|
|
|
78
121
|
opts.on('--no-timings', 'Neither read nor write timings') { options[:timings] = nil }
|
|
79
122
|
end
|
|
80
123
|
|
|
124
|
+
# The planner takes an unsigned 64-bit seed.
|
|
125
|
+
def seed(value)
|
|
126
|
+
raise OptionParser::InvalidArgument, "#{value} (must be 0 to 2**64 - 1)" unless value.between?(0, (2**64) - 1)
|
|
127
|
+
|
|
128
|
+
value
|
|
129
|
+
end
|
|
130
|
+
|
|
81
131
|
# OptionParser names the switch in the message.
|
|
82
132
|
def positive(value)
|
|
83
133
|
raise OptionParser::InvalidArgument, "#{value} (must be positive)" unless value.positive?
|
|
@@ -85,6 +135,22 @@ module PaddedCell
|
|
|
85
135
|
value
|
|
86
136
|
end
|
|
87
137
|
|
|
138
|
+
# "branches,methods" => [:lines, :branches, :methods] (lines always);
|
|
139
|
+
# "1" or "true" => the defaults; nil, "", "0" or "false" => off.
|
|
140
|
+
def coverage_criteria(value)
|
|
141
|
+
value = value.to_s.strip.downcase
|
|
142
|
+
return if ['', '0', 'false'].include?(value)
|
|
143
|
+
return Chart::DEFAULT_CRITERIA if %w[1 true].include?(value)
|
|
144
|
+
|
|
145
|
+
names = value.split(',').map { it.strip.to_sym }
|
|
146
|
+
unknown = names - Chart::CRITERIA
|
|
147
|
+
unless unknown.empty?
|
|
148
|
+
raise OptionParser::InvalidArgument, "coverage #{unknown.join(',')} (known: #{Chart::CRITERIA.join(', ')})"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
[:lines] | names
|
|
152
|
+
end
|
|
153
|
+
|
|
88
154
|
def shard(value)
|
|
89
155
|
index, count = value.split('/', 2).map { Integer(it, 10, exception: false) }
|
|
90
156
|
unless index && count&.positive? && index.between?(1, count)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaddedCell
|
|
4
|
+
# What cuts a test short: its --timeout, and what is left of the run's
|
|
5
|
+
# --budget. A test's deadline never outlives the run's. Deadlines are
|
|
6
|
+
# PaddedCellNative::Deadline values in monotonic nanoseconds: one clock for
|
|
7
|
+
# the runner, its workers and their cells.
|
|
8
|
+
class Limits
|
|
9
|
+
NS = 1_000_000_000
|
|
10
|
+
|
|
11
|
+
def self.now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
12
|
+
|
|
13
|
+
# Seconds left before deadline (0.0 once passed), nil when never.
|
|
14
|
+
def self.remaining(deadline, now = self.now) = deadline.remaining_ns(now)&./(NS.to_f)
|
|
15
|
+
|
|
16
|
+
attr_reader :timeout, :budget
|
|
17
|
+
|
|
18
|
+
# timeout: seconds per test, or nil
|
|
19
|
+
# budget: seconds for the whole run from started, or nil
|
|
20
|
+
def initialize(timeout: nil, budget: nil, started: Limits.now)
|
|
21
|
+
@timeout = timeout
|
|
22
|
+
@budget = budget
|
|
23
|
+
@run = budget ? PaddedCellNative::Deadline.after(started, nanoseconds(budget)) : PaddedCellNative::Deadline.never
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The deadline of a test starting now.
|
|
27
|
+
def for_test(now = Limits.now) = @run.derive(now, @timeout && nanoseconds(@timeout))
|
|
28
|
+
|
|
29
|
+
# True once the run's budget is gone: nothing more gets dispatched.
|
|
30
|
+
def spent?(now = Limits.now) = @run.expired?(now)
|
|
31
|
+
|
|
32
|
+
# Why a test that reached deadline was cut short.
|
|
33
|
+
def message(deadline)
|
|
34
|
+
if @budget && deadline.expires_at == @run.expires_at
|
|
35
|
+
"run budget of #{@budget}s spent"
|
|
36
|
+
else
|
|
37
|
+
"timed out after #{@timeout}s"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def nanoseconds(seconds) = (seconds * NS).round
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/padded_cell/loader.rb
CHANGED
|
@@ -53,11 +53,15 @@ module PaddedCell
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def call(klass, name)
|
|
56
|
+
matches_name?(klass, name) && (!@selection.line_filtered? || line_selected?(klass, name))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def matches_name?(klass, name)
|
|
56
60
|
id = "#{klass}##{name}"
|
|
57
61
|
return false if @include && !matches?(@include, name, id)
|
|
58
62
|
return false if @exclude && matches?(@exclude, name, id)
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
true
|
|
61
65
|
end
|
|
62
66
|
|
|
63
67
|
private
|