ultra-pro-pkg 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/simplecov-1.0.0/LICENSE +20 -0
- data/simplecov-1.0.0/README.md +1524 -0
- data/simplecov-1.0.0/doc/alternate-formatters.md +66 -0
- data/simplecov-1.0.0/doc/commercial-services.md +25 -0
- data/simplecov-1.0.0/doc/editor-integration.md +18 -0
- data/simplecov-1.0.0/exe/simplecov +11 -0
- data/simplecov-1.0.0/lib/minitest/simplecov_plugin.rb +171 -0
- data/simplecov-1.0.0/lib/simplecov/autostart.rb +11 -0
- data/simplecov-1.0.0/lib/simplecov/cli/clean.rb +47 -0
- data/simplecov-1.0.0/lib/simplecov/cli/coverage.rb +91 -0
- data/simplecov-1.0.0/lib/simplecov/cli/diff.rb +151 -0
- data/simplecov-1.0.0/lib/simplecov/cli/dotfile.rb +100 -0
- data/simplecov-1.0.0/lib/simplecov/cli/merge.rb +116 -0
- data/simplecov-1.0.0/lib/simplecov/cli/open.rb +50 -0
- data/simplecov-1.0.0/lib/simplecov/cli/report.rb +84 -0
- data/simplecov-1.0.0/lib/simplecov/cli/run.rb +36 -0
- data/simplecov-1.0.0/lib/simplecov/cli/serve.rb +139 -0
- data/simplecov-1.0.0/lib/simplecov/cli/uncovered.rb +107 -0
- data/simplecov-1.0.0/lib/simplecov/cli.rb +150 -0
- data/simplecov-1.0.0/lib/simplecov/color.rb +74 -0
- data/simplecov-1.0.0/lib/simplecov/combine/branches_combiner.rb +54 -0
- data/simplecov-1.0.0/lib/simplecov/combine/files_combiner.rb +30 -0
- data/simplecov-1.0.0/lib/simplecov/combine/lines_combiner.rb +45 -0
- data/simplecov-1.0.0/lib/simplecov/combine/methods_combiner.rb +26 -0
- data/simplecov-1.0.0/lib/simplecov/combine/results_combiner.rb +61 -0
- data/simplecov-1.0.0/lib/simplecov/combine.rb +30 -0
- data/simplecov-1.0.0/lib/simplecov/command_guesser.rb +78 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/coverage.rb +171 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/coverage_criteria.rb +156 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/filters.rb +197 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/formatting.rb +119 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/ignored_entries.rb +63 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/merging.rb +164 -0
- data/simplecov-1.0.0/lib/simplecov/configuration/thresholds.rb +174 -0
- data/simplecov-1.0.0/lib/simplecov/configuration.rb +184 -0
- data/simplecov-1.0.0/lib/simplecov/coverage_statistics.rb +59 -0
- data/simplecov-1.0.0/lib/simplecov/coverage_violations.rb +148 -0
- data/simplecov-1.0.0/lib/simplecov/defaults.rb +60 -0
- data/simplecov-1.0.0/lib/simplecov/deprecation.rb +45 -0
- data/simplecov-1.0.0/lib/simplecov/directive.rb +162 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes/exit_code_handling.rb +35 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +45 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +44 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +70 -0
- data/simplecov-1.0.0/lib/simplecov/exit_codes.rb +18 -0
- data/simplecov-1.0.0/lib/simplecov/exit_handling.rb +158 -0
- data/simplecov-1.0.0/lib/simplecov/file_list.rb +164 -0
- data/simplecov-1.0.0/lib/simplecov/filter.rb +139 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/base.rb +101 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter.rb +79 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter/errors_formatter.rb +84 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +128 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter.rb +87 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/multi_formatter.rb +31 -0
- data/simplecov-1.0.0/lib/simplecov/formatter/simple_formatter.rb +23 -0
- data/simplecov-1.0.0/lib/simplecov/formatter.rb +14 -0
- data/simplecov-1.0.0/lib/simplecov/last_run.rb +35 -0
- data/simplecov-1.0.0/lib/simplecov/lines_classifier.rb +60 -0
- data/simplecov-1.0.0/lib/simplecov/load_global_config.rb +13 -0
- data/simplecov-1.0.0/lib/simplecov/no_defaults.rb +4 -0
- data/simplecov-1.0.0/lib/simplecov/parallel_adapters/base.rb +61 -0
- data/simplecov-1.0.0/lib/simplecov/parallel_adapters/generic.rb +44 -0
- data/simplecov-1.0.0/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
- data/simplecov-1.0.0/lib/simplecov/parallel_adapters.rb +83 -0
- data/simplecov-1.0.0/lib/simplecov/parallel_coordination.rb +129 -0
- data/simplecov-1.0.0/lib/simplecov/process.rb +37 -0
- data/simplecov-1.0.0/lib/simplecov/profiles/bundler_filter.rb +5 -0
- data/simplecov-1.0.0/lib/simplecov/profiles/hidden_filter.rb +5 -0
- data/simplecov-1.0.0/lib/simplecov/profiles/rails.rb +32 -0
- data/simplecov-1.0.0/lib/simplecov/profiles/root_filter.rb +11 -0
- data/simplecov-1.0.0/lib/simplecov/profiles/strict.rb +32 -0
- data/simplecov-1.0.0/lib/simplecov/profiles/test_frameworks.rb +5 -0
- data/simplecov-1.0.0/lib/simplecov/profiles.rb +64 -0
- data/simplecov-1.0.0/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
- data/simplecov-1.0.0/lib/simplecov/result/source_file_builder.rb +51 -0
- data/simplecov-1.0.0/lib/simplecov/result.rb +183 -0
- data/simplecov-1.0.0/lib/simplecov/result_adapter.rb +92 -0
- data/simplecov-1.0.0/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
- data/simplecov-1.0.0/lib/simplecov/result_merger/resultset_file.rb +38 -0
- data/simplecov-1.0.0/lib/simplecov/result_merger/resultset_store.rb +50 -0
- data/simplecov-1.0.0/lib/simplecov/result_merger.rb +162 -0
- data/simplecov-1.0.0/lib/simplecov/result_processing.rb +179 -0
- data/simplecov-1.0.0/lib/simplecov/simulate_coverage.rb +75 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/branch.rb +82 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/branch_builder.rb +114 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/builder_context.rb +28 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/line.rb +77 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/line_builder.rb +43 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/method.rb +52 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/method_builder.rb +58 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/skip_chunks.rb +75 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/source_loader.rb +63 -0
- data/simplecov-1.0.0/lib/simplecov/source_file/statistics.rb +57 -0
- data/simplecov-1.0.0/lib/simplecov/source_file.rb +189 -0
- data/simplecov-1.0.0/lib/simplecov/static_coverage_extractor/method_collector.rb +55 -0
- data/simplecov-1.0.0/lib/simplecov/static_coverage_extractor/visitor.rb +171 -0
- data/simplecov-1.0.0/lib/simplecov/static_coverage_extractor.rb +110 -0
- data/simplecov-1.0.0/lib/simplecov/useless_results_remover.rb +27 -0
- data/simplecov-1.0.0/lib/simplecov/version.rb +5 -0
- data/simplecov-1.0.0/lib/simplecov-html.rb +4 -0
- data/simplecov-1.0.0/lib/simplecov.rb +255 -0
- data/simplecov-1.0.0/lib/simplecov_json_formatter.rb +4 -0
- data/simplecov-1.0.0/schemas/coverage-v1.0.schema.json +306 -0
- data/simplecov-1.0.0/schemas/coverage.schema.json +306 -0
- data/ultra-pro-pkg.gemspec +12 -0
- metadata +156 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module CLI
|
|
7
|
+
# Loads a project's `.simplecov` config file purely to read
|
|
8
|
+
# `coverage_dir` from it, with `SimpleCov.start` and the at_exit
|
|
9
|
+
# hook installer neutered so the load doesn't trigger coverage
|
|
10
|
+
# tracking. Used by the CLI to default `--input` / `--report`
|
|
11
|
+
# paths to whatever the project's dotfile declares, without making
|
|
12
|
+
# every read-only subcommand pay for actually starting Coverage.
|
|
13
|
+
module Dotfile
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def coverage_dir
|
|
17
|
+
dotfile = find
|
|
18
|
+
return "coverage" unless dotfile
|
|
19
|
+
|
|
20
|
+
with_simplecov_loaded { read_from(dotfile) }
|
|
21
|
+
rescue LoadError, StandardError => e
|
|
22
|
+
# simplecov:disable — defensive fallback for a bad dotfile (parse
|
|
23
|
+
# error, EACCES, etc.); never fires in the project's own dogfood run
|
|
24
|
+
warn "simplecov: failed to read coverage_dir from #{dotfile}: #{e.class}: #{e.message}"
|
|
25
|
+
"coverage"
|
|
26
|
+
# simplecov:enable
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Load the dotfile, snapshot+restore `SimpleCov.coverage_dir` so we
|
|
30
|
+
# don't quietly clobber it in a host process that's already
|
|
31
|
+
# configured (e.g. when the CLI is exercised inline by simplecov's
|
|
32
|
+
# own spec suite). The snapshot is intentionally narrow: a dotfile
|
|
33
|
+
# can still mutate other SimpleCov configuration (filters, groups,
|
|
34
|
+
# formatters, command_name, ...) via `SimpleCov.configure` or
|
|
35
|
+
# `SimpleCov.start { ... }` blocks. The CLI normally runs as a
|
|
36
|
+
# top-level process where that's harmless; callers driving it from
|
|
37
|
+
# inside a Ruby host that cares about isolation should arrange that
|
|
38
|
+
# themselves.
|
|
39
|
+
def read_from(dotfile)
|
|
40
|
+
snapshot = SimpleCov.instance_variable_get(:@coverage_dir)
|
|
41
|
+
load_with_start_neutered(dotfile)
|
|
42
|
+
dir = SimpleCov.coverage_dir
|
|
43
|
+
SimpleCov.instance_variable_set(:@coverage_dir, snapshot)
|
|
44
|
+
dir
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def find
|
|
48
|
+
dir = Pathname.new(Dir.pwd)
|
|
49
|
+
loop do
|
|
50
|
+
candidate = dir.join(".simplecov")
|
|
51
|
+
return candidate.to_s if candidate.exist?
|
|
52
|
+
break if dir.root?
|
|
53
|
+
|
|
54
|
+
dir = dir.parent
|
|
55
|
+
end
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def with_simplecov_loaded
|
|
60
|
+
previous_no_defaults = ENV.fetch("SIMPLECOV_NO_DEFAULTS", nil)
|
|
61
|
+
previous_cli = ENV.fetch("SIMPLECOV_CLI", nil)
|
|
62
|
+
ENV["SIMPLECOV_NO_DEFAULTS"] = "1"
|
|
63
|
+
# SIMPLECOV_CLI lets a project's `.simplecov` opt some config into
|
|
64
|
+
# CLI-only behavior — e.g. simplecov itself sets `coverage_dir`
|
|
65
|
+
# to the dogfood path here but skips that for descendants.
|
|
66
|
+
ENV["SIMPLECOV_CLI"] = "1"
|
|
67
|
+
require "simplecov"
|
|
68
|
+
yield
|
|
69
|
+
ensure
|
|
70
|
+
ENV["SIMPLECOV_NO_DEFAULTS"] = previous_no_defaults
|
|
71
|
+
ENV["SIMPLECOV_CLI"] = previous_cli
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Load `path` with `SimpleCov.start` and the at_exit installer
|
|
75
|
+
# turned into no-ops, so a project whose dotfile calls
|
|
76
|
+
# `SimpleCov.start` doesn't trigger Coverage just because we asked
|
|
77
|
+
# for `coverage_dir`. Config inside any `SimpleCov.start { ... }`
|
|
78
|
+
# block still runs.
|
|
79
|
+
def load_with_start_neutered(path)
|
|
80
|
+
klass = SimpleCov.singleton_class
|
|
81
|
+
names = %i[start_tracking install_at_exit_hook]
|
|
82
|
+
stash = names.to_h { |name| [name, klass.instance_method(name)] }
|
|
83
|
+
# define_method over an existing method emits a "method redefined"
|
|
84
|
+
# warning under $VERBOSE; the override and restore are intentional.
|
|
85
|
+
silence_verbose { names.each { |name| klass.define_method(name) { nil } } }
|
|
86
|
+
load path
|
|
87
|
+
ensure
|
|
88
|
+
silence_verbose { stash.each { |name, method| klass.define_method(name, method) } }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def silence_verbose
|
|
92
|
+
previous = $VERBOSE
|
|
93
|
+
$VERBOSE = nil
|
|
94
|
+
yield
|
|
95
|
+
ensure
|
|
96
|
+
$VERBOSE = previous
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
module SimpleCov
|
|
7
|
+
module CLI
|
|
8
|
+
# `simplecov merge <files...>` — wrap SimpleCov::ResultMerger so a
|
|
9
|
+
# CI matrix that produces one .resultset.json per worker can stitch
|
|
10
|
+
# them together from the shell instead of dropping a Rake task into
|
|
11
|
+
# every project. Requires the full simplecov library to be on the
|
|
12
|
+
# load path; lazy-required so the read-only subcommands above don't
|
|
13
|
+
# pay for ResultMerger (and its Coverage runtime guard).
|
|
14
|
+
module Merge
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def run(args, stdout:, stderr:, **)
|
|
18
|
+
opts = parse(args)
|
|
19
|
+
return error(stderr, "missing input files") if opts[:files].empty?
|
|
20
|
+
return 1 unless valid_inputs?(opts[:files], stderr)
|
|
21
|
+
|
|
22
|
+
require "simplecov"
|
|
23
|
+
result = SimpleCov::ResultMerger.merge_results(*opts[:files], ignore_timeout: !opts[:honor_timeout])
|
|
24
|
+
return error(stderr, "no mergeable results in input files") unless result
|
|
25
|
+
|
|
26
|
+
commit(opts, result, stdout)
|
|
27
|
+
0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def commit(opts, result, stdout)
|
|
31
|
+
verb = opts[:dry_run] ? "would write" : "wrote"
|
|
32
|
+
write(opts[:output], result) unless opts[:dry_run]
|
|
33
|
+
stdout.puts("simplecov merge: #{verb} #{opts[:output]}") unless opts[:quiet]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def valid_inputs?(files, stderr)
|
|
37
|
+
parsed = parse_inputs(files, stderr) or return false
|
|
38
|
+
|
|
39
|
+
warn_about_duplicate_command_names(parsed, stderr)
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parse(args)
|
|
44
|
+
opts = {output: SimpleCov::CLI.default_resultset, honor_timeout: false, dry_run: false, quiet: false}
|
|
45
|
+
files =
|
|
46
|
+
OptionParser.new do |o|
|
|
47
|
+
o.on("--output PATH") { |v| opts[:output] = v }
|
|
48
|
+
o.on("--honor-timeout") { opts[:honor_timeout] = true }
|
|
49
|
+
o.on("--dry-run") { opts[:dry_run] = true }
|
|
50
|
+
o.on("-q", "--quiet") { opts[:quiet] = true }
|
|
51
|
+
end.parse(args)
|
|
52
|
+
opts.merge(files: files)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Validate every input file up-front and return a {path => parsed}
|
|
56
|
+
# hash. Surfacing per-file errors here turns ResultMerger's
|
|
57
|
+
# generic "no mergeable results" into a message that points at
|
|
58
|
+
# the specific input causing the failure.
|
|
59
|
+
def parse_inputs(files, stderr)
|
|
60
|
+
files.each_with_object({}) do |path, memo|
|
|
61
|
+
data = parse_input(path, stderr) or return nil
|
|
62
|
+
|
|
63
|
+
memo[path] = data
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def parse_input(path, stderr)
|
|
68
|
+
return parse_input_error(stderr, path, "not found") unless File.exist?(path)
|
|
69
|
+
|
|
70
|
+
data = JSON.parse(File.read(path))
|
|
71
|
+
return data if data.is_a?(Hash) && !data.empty?
|
|
72
|
+
|
|
73
|
+
parse_input_error(stderr, path, "has no resultset entries")
|
|
74
|
+
rescue JSON::ParserError => e
|
|
75
|
+
parse_input_error(stderr, path, "isn't valid JSON (#{e.message})")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parse_input_error(stderr, path, reason)
|
|
79
|
+
stderr.puts("simplecov merge: input file #{path.inspect} #{reason}")
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# When two input files share a command_name, ResultMerger folds
|
|
84
|
+
# them together with last-write-wins on the timestamp — easy to
|
|
85
|
+
# mistake for "no merge happened." Surface the overlap so the
|
|
86
|
+
# operator can rename the workers or accept the merge knowingly.
|
|
87
|
+
def warn_about_duplicate_command_names(parsed, stderr)
|
|
88
|
+
files_per_command = parsed.each_with_object({}) do |(path, data), memo|
|
|
89
|
+
data.each_key { |command_name| (memo[command_name] ||= []) << path }
|
|
90
|
+
end
|
|
91
|
+
files_per_command.each do |command_name, paths|
|
|
92
|
+
next if paths.size < 2
|
|
93
|
+
|
|
94
|
+
stderr.puts(duplicate_warning(command_name, paths))
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def duplicate_warning(command_name, paths)
|
|
99
|
+
"simplecov merge: warning — command_name #{command_name.inspect} " \
|
|
100
|
+
"appears in #{paths.size} input files (#{paths.join(', ')}); " \
|
|
101
|
+
"entries will be merged"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def write(path, result)
|
|
105
|
+
require "fileutils"
|
|
106
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
107
|
+
File.write(path, JSON.pretty_generate(result.to_hash))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def error(stderr, message)
|
|
111
|
+
stderr.puts("simplecov merge: #{message}")
|
|
112
|
+
1
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module CLI
|
|
7
|
+
# `simplecov open [--report PATH]` — open the HTML report in the
|
|
8
|
+
# platform's default browser. Tiny QoL wrapper around `xdg-open` /
|
|
9
|
+
# `open` / `start` so users don't have to type a file:// URL.
|
|
10
|
+
module Open
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def run(args, stderr:, **)
|
|
14
|
+
path = parse(args)
|
|
15
|
+
return error(stderr, "#{path} not found") unless File.exist?(path)
|
|
16
|
+
|
|
17
|
+
opener = browser_opener
|
|
18
|
+
return error(stderr, "no known opener for #{RbConfig::CONFIG['host_os']}") unless opener
|
|
19
|
+
|
|
20
|
+
system(*opener, path) ? 0 : 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def error(stderr, message)
|
|
24
|
+
stderr.puts("simplecov open: #{message}")
|
|
25
|
+
1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def parse(args)
|
|
29
|
+
path = SimpleCov::CLI.default_report
|
|
30
|
+
OptionParser.new do |o|
|
|
31
|
+
o.on("--report PATH") { |v| path = v }
|
|
32
|
+
end.parse(args)
|
|
33
|
+
path
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the argv for the platform's "open this file" command, or
|
|
37
|
+
# nil if the host OS isn't recognized. On Windows, `start` is a
|
|
38
|
+
# cmd.exe builtin (not an executable), so route through `cmd /c`;
|
|
39
|
+
# the empty string is the window-title positional `start` takes
|
|
40
|
+
# before the path so a quoted path isn't mis-parsed as the title.
|
|
41
|
+
def browser_opener
|
|
42
|
+
case RbConfig::CONFIG["host_os"]
|
|
43
|
+
when /darwin/ then ["open"]
|
|
44
|
+
when /mswin|mingw|cygwin/ then ["cmd", "/c", "start", ""]
|
|
45
|
+
when /linux|bsd|solaris/ then ["xdg-open"]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
module SimpleCov
|
|
7
|
+
module CLI
|
|
8
|
+
# `simplecov report [--input PATH]` — pretty-print the overall
|
|
9
|
+
# totals row plus per-group totals from a JSONFormatter
|
|
10
|
+
# coverage.json. Same numbers as the HTML report's totals row, for
|
|
11
|
+
# contexts where opening a browser isn't an option (CI logs, ssh
|
|
12
|
+
# sessions, terminal-only workflows).
|
|
13
|
+
module Report
|
|
14
|
+
SECTIONS = [%w[Line lines], %w[Branch branches], %w[Method methods]].freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def run(args, stdout:, stderr:)
|
|
19
|
+
opts = parse(args)
|
|
20
|
+
return 1 unless (data = load_data(opts[:input], stderr))
|
|
21
|
+
|
|
22
|
+
if opts[:json]
|
|
23
|
+
emit_json(stdout, data)
|
|
24
|
+
else
|
|
25
|
+
emit_text(stdout, data, SimpleCov::CLI.color_enabled?(opts, stdout))
|
|
26
|
+
end
|
|
27
|
+
0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse(args)
|
|
31
|
+
opts = {input: SimpleCov::CLI.default_input, json: false, no_color: false}
|
|
32
|
+
OptionParser.new do |o|
|
|
33
|
+
o.on("--input PATH") { |v| opts[:input] = v }
|
|
34
|
+
o.on("--json") { opts[:json] = true }
|
|
35
|
+
o.on("--no-color") { opts[:no_color] = true }
|
|
36
|
+
end.parse(args)
|
|
37
|
+
opts
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def load_data(input, stderr)
|
|
41
|
+
return JSON.parse(File.read(input)) if File.exist?(input)
|
|
42
|
+
|
|
43
|
+
stderr.puts("simplecov report: #{input} not found")
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def emit_text(stdout, data, color)
|
|
48
|
+
emit_totals(stdout, "All Files", data.fetch("total", {}), color)
|
|
49
|
+
data.fetch("groups", {}).each { |name, group| emit_totals(stdout, name, group, color) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def emit_totals(stdout, label, totals, color)
|
|
53
|
+
stdout.puts(label)
|
|
54
|
+
SECTIONS.each { |display, key| emit_section(stdout, display, totals[key], color) }
|
|
55
|
+
stdout.puts
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def emit_section(stdout, display, section, color)
|
|
59
|
+
return unless section.is_a?(Hash) && section["total"].to_i.positive?
|
|
60
|
+
|
|
61
|
+
stdout.puts(format(" %<label>-7s %<pct>s (%<covered>d / %<total>d)",
|
|
62
|
+
label: "#{display}:",
|
|
63
|
+
pct: SimpleCov::Color.colorize_percent(section["percent"].to_f, enabled: color),
|
|
64
|
+
covered: section["covered"] || 0,
|
|
65
|
+
total: section["total"] || 0))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def emit_json(stdout, data)
|
|
69
|
+
payload = {"All Files" => collect_section(data.fetch("total", {}))}
|
|
70
|
+
data.fetch("groups", {}).each { |name, group| payload[name] = collect_section(group) }
|
|
71
|
+
stdout.puts(JSON.pretty_generate(payload))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def collect_section(totals)
|
|
75
|
+
SECTIONS.each_with_object({}) do |(_, key), out|
|
|
76
|
+
section = totals[key]
|
|
77
|
+
next unless section.is_a?(Hash) && section["total"].to_i.positive?
|
|
78
|
+
|
|
79
|
+
out[key] = {"percent" => section["percent"], "covered" => section["covered"], "total" => section["total"]}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module CLI
|
|
5
|
+
# `simplecov run <command...>` — exec the given command with
|
|
6
|
+
# simplecov auto-loaded so a coverage report drops into the
|
|
7
|
+
# project's coverage/ directory at the end. Useful for projects
|
|
8
|
+
# without a test_helper that already calls SimpleCov.start (e.g.
|
|
9
|
+
# plain `bundle exec rake test` on an unconfigured library).
|
|
10
|
+
module Run
|
|
11
|
+
AUTOSTART = File.expand_path("../autostart", __dir__)
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def run(args, stderr:, **)
|
|
16
|
+
cmd = args.first == "--" ? args.drop(1) : args
|
|
17
|
+
if cmd.empty?
|
|
18
|
+
stderr.puts("simplecov run: missing command")
|
|
19
|
+
return 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Kernel.exec(rubyopt_env, *cmd)
|
|
23
|
+
rescue Errno::ENOENT => e
|
|
24
|
+
stderr.puts("simplecov run: #{e.message}")
|
|
25
|
+
127
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def rubyopt_env
|
|
29
|
+
existing = ENV["RUBYOPT"].to_s.strip
|
|
30
|
+
injection = "-r#{AUTOSTART}"
|
|
31
|
+
merged = existing.empty? ? injection : "#{existing} #{injection}"
|
|
32
|
+
ENV.to_hash.merge("RUBYOPT" => merged)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module CLI
|
|
7
|
+
# `simplecov serve [--port N] [--host HOST]` — serve the coverage
|
|
8
|
+
# report over HTTP. A 30-line static file server backed by stdlib
|
|
9
|
+
# `socket`, so there's no extra dependency just for "view a local
|
|
10
|
+
# report on a CI box where `file://` doesn't work."
|
|
11
|
+
module Serve
|
|
12
|
+
MIME = {
|
|
13
|
+
".html" => "text/html; charset=utf-8",
|
|
14
|
+
".htm" => "text/html; charset=utf-8",
|
|
15
|
+
".css" => "text/css",
|
|
16
|
+
".js" => "application/javascript",
|
|
17
|
+
".json" => "application/json",
|
|
18
|
+
".svg" => "image/svg+xml",
|
|
19
|
+
".png" => "image/png",
|
|
20
|
+
".gif" => "image/gif",
|
|
21
|
+
".jpg" => "image/jpeg",
|
|
22
|
+
".jpeg" => "image/jpeg",
|
|
23
|
+
".ico" => "image/x-icon",
|
|
24
|
+
".txt" => "text/plain; charset=utf-8"
|
|
25
|
+
}.freeze
|
|
26
|
+
STATUS_TEXT = {
|
|
27
|
+
200 => "OK", 400 => "Bad Request", 403 => "Forbidden",
|
|
28
|
+
404 => "Not Found", 405 => "Method Not Allowed"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def run(args, stdout:, stderr:, **)
|
|
34
|
+
opts = parse(args)
|
|
35
|
+
dir = SimpleCov::CLI.coverage_dir
|
|
36
|
+
return error(stderr, "#{dir} doesn't exist; run your test suite first") unless File.directory?(dir)
|
|
37
|
+
|
|
38
|
+
require "socket"
|
|
39
|
+
server = TCPServer.new(opts[:host], opts[:port])
|
|
40
|
+
announce(stdout, server, dir)
|
|
41
|
+
serve_loop(server, dir, stdout)
|
|
42
|
+
0
|
|
43
|
+
ensure
|
|
44
|
+
server&.close
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parse(args)
|
|
48
|
+
opts = {port: 0, host: "127.0.0.1"}
|
|
49
|
+
OptionParser.new do |o|
|
|
50
|
+
o.on("--port N", Integer) { |v| opts[:port] = v }
|
|
51
|
+
o.on("--host HOST") { |v| opts[:host] = v }
|
|
52
|
+
end.parse(args)
|
|
53
|
+
opts
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def announce(stdout, server, dir)
|
|
57
|
+
port = server.addr[1]
|
|
58
|
+
host = server.addr[3]
|
|
59
|
+
stdout.puts("simplecov serve: serving #{dir} at http://#{host}:#{port}/")
|
|
60
|
+
stdout.puts("Press Ctrl-C to stop.")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def serve_loop(server, dir, stdout)
|
|
64
|
+
loop { handle_connection(server.accept, dir) }
|
|
65
|
+
rescue Interrupt
|
|
66
|
+
stdout.puts("\nsimplecov serve: stopping")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Reads one HTTP request line, drains headers, serves the file or
|
|
70
|
+
# writes a status response. Wide rescue so a misbehaving client
|
|
71
|
+
# can't crash the server.
|
|
72
|
+
def handle_connection(client, root)
|
|
73
|
+
method, path = client.readline.split
|
|
74
|
+
drain_headers(client)
|
|
75
|
+
return respond(client, 405) unless method == "GET"
|
|
76
|
+
|
|
77
|
+
file = resolve(path, root)
|
|
78
|
+
return respond(client, file == :forbidden ? 403 : 404) unless file.is_a?(String)
|
|
79
|
+
|
|
80
|
+
respond(client, 200, File.binread(file), MIME[File.extname(file).downcase])
|
|
81
|
+
rescue StandardError
|
|
82
|
+
# Misbehaving clients (truncated requests, connection resets,
|
|
83
|
+
# invalid encoding) shouldn't take the whole server down.
|
|
84
|
+
nil
|
|
85
|
+
ensure
|
|
86
|
+
# simplecov:disable — `client` is the parameter, never nil here;
|
|
87
|
+
# the `&.` is purely defensive in case of future refactors
|
|
88
|
+
client&.close
|
|
89
|
+
# simplecov:enable
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def drain_headers(client)
|
|
93
|
+
loop { break if client.readline.strip.empty? }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns the absolute path of the file to serve, :forbidden for
|
|
97
|
+
# a traversal attempt (including symlinks that escape root), or
|
|
98
|
+
# nil for "not found".
|
|
99
|
+
def resolve(request_path, root)
|
|
100
|
+
path = request_path.split("?", 2).first.to_s.sub(%r{^/}, "")
|
|
101
|
+
absolute_root = File.realpath(root)
|
|
102
|
+
candidate = File.expand_path(path.empty? ? "index.html" : path, absolute_root)
|
|
103
|
+
# Reject `..` traversal and absolute-path attempts before
|
|
104
|
+
# touching disk so they're 403, not 404.
|
|
105
|
+
return :forbidden unless inside?(candidate, absolute_root)
|
|
106
|
+
|
|
107
|
+
candidate = File.join(candidate, "index.html") if File.directory?(candidate)
|
|
108
|
+
return nil unless File.file?(candidate)
|
|
109
|
+
|
|
110
|
+
# Resolve symlinks last and re-check: a file inside root could
|
|
111
|
+
# be a symlink pointing outside (e.g. /etc/passwd).
|
|
112
|
+
real = File.realpath(candidate)
|
|
113
|
+
inside?(real, absolute_root) ? real : :forbidden
|
|
114
|
+
rescue Errno::ENOENT
|
|
115
|
+
# simplecov:disable — TOCTOU: candidate vanished between
|
|
116
|
+
# File.file? and File.realpath. Treat as "not found".
|
|
117
|
+
nil
|
|
118
|
+
# simplecov:enable
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def inside?(path, root)
|
|
122
|
+
path == root || path.start_with?(root + File::SEPARATOR)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def respond(client, status, body = "", content_type = "text/plain")
|
|
126
|
+
client.write("HTTP/1.1 #{status} #{STATUS_TEXT[status] || 'Error'}\r\n",
|
|
127
|
+
"Content-Type: #{content_type || 'application/octet-stream'}\r\n",
|
|
128
|
+
"Content-Length: #{body.bytesize}\r\n",
|
|
129
|
+
"Connection: close\r\n\r\n")
|
|
130
|
+
client.write(body)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def error(stderr, message)
|
|
134
|
+
stderr.puts("simplecov serve: #{message}")
|
|
135
|
+
1
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require_relative "report"
|
|
6
|
+
|
|
7
|
+
module SimpleCov
|
|
8
|
+
module CLI
|
|
9
|
+
# `simplecov uncovered [--threshold N] [--top N] [--criterion C]` — list
|
|
10
|
+
# the lowest-coverage files (by the chosen criterion, ascending), so a
|
|
11
|
+
# developer can answer "where should I add tests next?" without
|
|
12
|
+
# opening a browser. Reads coverage.json directly.
|
|
13
|
+
module Uncovered
|
|
14
|
+
DEFAULT_TOP = 10
|
|
15
|
+
|
|
16
|
+
# The coverage.json fields backing each criterion.
|
|
17
|
+
CRITERION_KEYS = {
|
|
18
|
+
line: {percent: "lines_covered_percent", covered: "covered_lines", total: "total_lines"},
|
|
19
|
+
branch: {percent: "branches_covered_percent", covered: "covered_branches", total: "total_branches"},
|
|
20
|
+
method: {percent: "methods_covered_percent", covered: "covered_methods", total: "total_methods"}
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def run(args, stdout:, stderr:, **)
|
|
26
|
+
opts = parse(args)
|
|
27
|
+
keys = CRITERION_KEYS[opts[:criterion]]
|
|
28
|
+
return unknown_criterion(opts[:criterion], stderr) unless keys
|
|
29
|
+
|
|
30
|
+
report(opts, keys, stdout, stderr)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def report(opts, keys, stdout, stderr)
|
|
34
|
+
return 1 unless (data = Report.load_data(opts[:input], stderr))
|
|
35
|
+
|
|
36
|
+
files = rank(data.fetch("coverage", {}), opts[:threshold], keys).first(opts[:top])
|
|
37
|
+
return stdout.puts(empty_message(opts[:json])) || 0 if files.empty?
|
|
38
|
+
|
|
39
|
+
emit(stdout, files, opts)
|
|
40
|
+
0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def unknown_criterion(criterion, stderr)
|
|
44
|
+
stderr.puts("simplecov uncovered: unknown --criterion #{criterion.inspect} (expected line, branch, or method)")
|
|
45
|
+
1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def emit(stdout, files, opts)
|
|
49
|
+
opts[:json] ? emit_json(stdout, files) : emit_text(stdout, files, SimpleCov::CLI.color_enabled?(opts, stdout))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse(args)
|
|
53
|
+
opts = {input: SimpleCov::CLI.default_input, threshold: 100.0, top: DEFAULT_TOP, criterion: :line, no_color: false}
|
|
54
|
+
build_parser(opts).parse(args)
|
|
55
|
+
opts
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Option parsing with per-flag coercions is inherently ABC-heavy; the
|
|
59
|
+
# metric is noise here.
|
|
60
|
+
def build_parser(opts) # rubocop:disable Metrics/AbcSize
|
|
61
|
+
OptionParser.new do |o|
|
|
62
|
+
o.on("--input PATH") { |v| opts[:input] = v }
|
|
63
|
+
o.on("--threshold N", Float) { |v| opts[:threshold] = v }
|
|
64
|
+
o.on("--top N", Integer) { |v| opts[:top] = v }
|
|
65
|
+
o.on("--criterion C") { |v| opts[:criterion] = v.to_sym }
|
|
66
|
+
o.on("--json") { opts[:json] = true }
|
|
67
|
+
o.on("--no-color") { opts[:no_color] = true }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def emit_text(stdout, files, color)
|
|
72
|
+
files.each { |fname, pct, covered, total| stdout.puts(format_row(fname, pct, covered, total, color)) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def emit_json(stdout, files)
|
|
76
|
+
rows = files.map do |fname, pct, covered, total|
|
|
77
|
+
{"file" => fname, "percent" => pct, "covered" => covered, "total" => total}
|
|
78
|
+
end
|
|
79
|
+
stdout.puts(JSON.pretty_generate(rows))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def empty_message(json)
|
|
83
|
+
json ? "[]" : "simplecov uncovered: nothing to report"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def rank(coverage_hash, threshold, keys)
|
|
87
|
+
rows = coverage_hash.filter_map { |fname, payload| row_for(fname, payload, threshold, keys) }
|
|
88
|
+
rows.sort_by { |_fname, pct, _c, _t| pct }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def row_for(fname, payload, threshold, keys)
|
|
92
|
+
return unless payload.is_a?(Hash) && payload[keys[:total]].to_i.positive?
|
|
93
|
+
|
|
94
|
+
pct = payload[keys[:percent]].to_f
|
|
95
|
+
return if pct >= threshold
|
|
96
|
+
|
|
97
|
+
[fname, pct, payload[keys[:covered]].to_i, payload[keys[:total]].to_i]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def format_row(fname, pct, covered, total, color)
|
|
101
|
+
format("%<pct>s %<covered>d/%<total>d %<fname>s",
|
|
102
|
+
pct: SimpleCov::Color.colorize_percent(pct, format("%6.2f%%", pct), enabled: color),
|
|
103
|
+
covered: covered, total: total, fname: fname)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|