simplecov 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +148 -5
- data/exe/simplecov +0 -3
- data/lib/minitest/simplecov_plugin.rb +12 -9
- data/lib/simplecov/atomic_file.rb +16 -19
- data/lib/simplecov/autostart.rb +5 -7
- data/lib/simplecov/baseline/parser.rb +58 -0
- data/lib/simplecov/baseline.rb +170 -0
- data/lib/simplecov/cli/affected/changed_files.rb +57 -0
- data/lib/simplecov/cli/affected/selection.rb +117 -0
- data/lib/simplecov/cli/affected.rb +148 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +23 -24
- data/lib/simplecov/cli/command_helpers.rb +55 -19
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +25 -28
- data/lib/simplecov/cli/coverage_file.rb +58 -14
- data/lib/simplecov/cli/dead_code/output.rb +100 -0
- data/lib/simplecov/cli/dead_code.rb +139 -0
- data/lib/simplecov/cli/diff/output.rb +45 -0
- data/lib/simplecov/cli/diff.rb +35 -67
- data/lib/simplecov/cli/dotfile.rb +83 -38
- data/lib/simplecov/cli/git.rb +41 -0
- data/lib/simplecov/cli/history/output.rb +135 -0
- data/lib/simplecov/cli/history.rb +67 -0
- data/lib/simplecov/cli/merge.rb +34 -39
- data/lib/simplecov/cli/open.rb +12 -15
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +135 -0
- data/lib/simplecov/cli/patch.rb +169 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +18 -24
- data/lib/simplecov/cli/run.rb +8 -9
- data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
- data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
- data/lib/simplecov/cli/serve.rb +27 -24
- data/lib/simplecov/cli/show/annotator.rb +87 -0
- data/lib/simplecov/cli/show/sweep.rb +36 -0
- data/lib/simplecov/cli/show.rb +133 -0
- data/lib/simplecov/cli/status/facts.rb +87 -0
- data/lib/simplecov/cli/status.rb +93 -0
- data/lib/simplecov/cli/tests/redundancy.rb +78 -0
- data/lib/simplecov/cli/tests.rb +148 -0
- data/lib/simplecov/cli/uncovered/misses.rb +37 -0
- data/lib/simplecov/cli/uncovered.rb +70 -32
- data/lib/simplecov/cli/usage.rb +152 -0
- data/lib/simplecov/cli/watch/live_report.rb +73 -0
- data/lib/simplecov/cli/watch/narrator.rb +55 -0
- data/lib/simplecov/cli/watch/poller.rb +45 -0
- data/lib/simplecov/cli/watch/session.rb +134 -0
- data/lib/simplecov/cli/watch/test_plan.rb +43 -0
- data/lib/simplecov/cli/watch.rb +85 -0
- data/lib/simplecov/cli.rb +43 -92
- data/lib/simplecov/color.rb +19 -37
- data/lib/simplecov/combine/branches_combiner.rb +7 -31
- data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
- data/lib/simplecov/combine/identity_interner.rb +10 -13
- data/lib/simplecov/combine/interned_counts.rb +7 -9
- data/lib/simplecov/combine/lines_combiner.rb +20 -44
- data/lib/simplecov/combine/methods_combiner.rb +9 -35
- data/lib/simplecov/combine/results_combiner.rb +4 -21
- data/lib/simplecov/combine.rb +0 -6
- data/lib/simplecov/command_guesser.rb +20 -34
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +91 -79
- data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +6 -10
- data/lib/simplecov/configuration/filters.rb +40 -73
- data/lib/simplecov/configuration/formatting.rb +74 -83
- data/lib/simplecov/configuration/groups.rb +19 -13
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +31 -22
- data/lib/simplecov/configuration/merging.rb +94 -62
- data/lib/simplecov/configuration/missed_caps.rb +51 -0
- data/lib/simplecov/configuration/production.rb +23 -0
- data/lib/simplecov/configuration/test_tracking.rb +48 -0
- data/lib/simplecov/configuration/thresholds.rb +33 -62
- data/lib/simplecov/configuration/view_coverage.rb +38 -0
- data/lib/simplecov/configuration.rb +70 -76
- data/lib/simplecov/configuration_error.rb +5 -0
- data/lib/simplecov/context_map/union.rb +81 -0
- data/lib/simplecov/context_map.rb +187 -0
- data/lib/simplecov/coverage_json.rb +2 -5
- data/lib/simplecov/coverage_statistics.rb +14 -24
- data/lib/simplecov/coverage_violations.rb +157 -64
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +4 -23
- data/lib/simplecov/deprecation.rb +45 -27
- data/lib/simplecov/directive.rb +46 -62
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +17 -11
- data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
- data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
- data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
- data/lib/simplecov/exit_codes.rb +16 -10
- data/lib/simplecov/exit_handling.rb +94 -59
- data/lib/simplecov/file_list.rb +20 -51
- data/lib/simplecov/filter.rb +43 -66
- data/lib/simplecov/formatter/base.rb +37 -55
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
- data/lib/simplecov/formatter/html_formatter/public/index.html +15 -9
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
- data/lib/simplecov/formatter/html_formatter.rb +24 -41
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
- data/lib/simplecov/formatter/json_formatter.rb +7 -13
- data/lib/simplecov/formatter/multi_formatter.rb +5 -11
- data/lib/simplecov/formatter/simple_formatter.rb +2 -8
- data/lib/simplecov/formatter.rb +14 -10
- data/lib/simplecov/group_names.rb +13 -12
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +6 -9
- data/lib/simplecov/lines_classifier.rb +31 -16
- data/lib/simplecov/load_global_config.rb +7 -7
- data/lib/simplecov/parallel_adapters/base.rb +13 -41
- data/lib/simplecov/parallel_adapters/generic.rb +7 -19
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
- data/lib/simplecov/parallel_adapters.rb +24 -55
- data/lib/simplecov/parallel_coordination.rb +39 -49
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +67 -126
- data/lib/simplecov/process.rb +14 -32
- data/lib/simplecov/production/error.rb +11 -0
- data/lib/simplecov/production/file_sink.rb +130 -0
- data/lib/simplecov/production.rb +249 -0
- data/lib/simplecov/profiles/rails.rb +1 -12
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +9 -19
- data/lib/simplecov/profiles.rb +11 -33
- data/lib/simplecov/report_deferral.rb +11 -15
- data/lib/simplecov/report_stamp.rb +6 -9
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +7 -12
- data/lib/simplecov/result.rb +68 -117
- data/lib/simplecov/result_adapter.rb +56 -68
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
- data/lib/simplecov/result_merger/resultset_file.rb +20 -27
- data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
- data/lib/simplecov/result_merger/resultset_store.rb +23 -14
- data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
- data/lib/simplecov/result_merger.rb +59 -58
- data/lib/simplecov/result_processing.rb +74 -119
- data/lib/simplecov/run_identity.rb +16 -18
- data/lib/simplecov/simulate_coverage.rb +34 -50
- data/lib/simplecov/source_file/branch.rb +8 -33
- data/lib/simplecov/source_file/branch_builder.rb +28 -47
- data/lib/simplecov/source_file/builder_context.rb +6 -13
- data/lib/simplecov/source_file/line.rb +13 -33
- data/lib/simplecov/source_file/line_builder.rb +9 -15
- data/lib/simplecov/source_file/method.rb +3 -7
- data/lib/simplecov/source_file/method_builder.rb +6 -15
- data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
- data/lib/simplecov/source_file/skip_chunks.rb +19 -28
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +10 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
- data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
- data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
- data/lib/simplecov/static_coverage_extractor.rb +46 -62
- data/lib/simplecov/test_tracker/accessors.rb +34 -0
- data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
- data/lib/simplecov/test_tracker/delta.rb +69 -0
- data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
- data/lib/simplecov/test_tracker.rb +162 -0
- data/lib/simplecov/unloaded_file_injector.rb +18 -31
- data/lib/simplecov/useless_results_remover.rb +7 -6
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
- data/lib/simplecov/view_coverage.rb +55 -0
- data/lib/simplecov-html.rb +1 -0
- data/lib/simplecov.rb +57 -113
- data/lib/simplecov_json_formatter.rb +1 -0
- data/man/simplecov.1 +349 -0
- data/sig/simplecov.rbs +523 -595
- metadata +72 -5
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module CLI
|
|
5
|
+
module Ratchet
|
|
6
|
+
module Output
|
|
7
|
+
extend CommandHelpers
|
|
8
|
+
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
def emit(stdout, opts, outcome, generated)
|
|
12
|
+
return stdout.puts(JSON.generate(json_summary(opts, outcome, generated))) if opts.fetch(:json)
|
|
13
|
+
|
|
14
|
+
verb = opts.fetch(:dry_run) ? "would write" : "wrote"
|
|
15
|
+
stdout.puts("simplecov ratchet: #{verb} #{opts.fetch(:baseline)} (#{change_summary(outcome, generated)})")
|
|
16
|
+
report_regressed(stdout, outcome.regressed)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def change_summary(outcome, generated)
|
|
20
|
+
files = outcome.baseline.entries.size
|
|
21
|
+
return "#{files} #{one?(files) ? "file" : "files"}" if generated
|
|
22
|
+
|
|
23
|
+
"#{outcome.tightened.size} tightened, #{outcome.pruned.size} pruned, #{outcome.unchanged.size} unchanged"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def report_regressed(stdout, regressed)
|
|
27
|
+
return if regressed.empty?
|
|
28
|
+
|
|
29
|
+
noun = one?(regressed.size) ? "1 file below its floor" : "#{regressed.size} files below their floors"
|
|
30
|
+
stdout.puts("simplecov ratchet: #{noun}, entries kept unchanged")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def json_summary(opts, outcome, generated)
|
|
34
|
+
{
|
|
35
|
+
written: !opts.fetch(:dry_run), path: opts.fetch(:baseline), generated: generated,
|
|
36
|
+
files: outcome.baseline.entries.size,
|
|
37
|
+
tightened: outcome.tightened, pruned: outcome.pruned,
|
|
38
|
+
regressed: outcome.regressed, unchanged: outcome.unchanged
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../atomic_file"
|
|
4
|
+
require_relative "command_helpers"
|
|
5
|
+
require_relative "coverage_file"
|
|
6
|
+
require_relative "ratchet/output"
|
|
7
|
+
|
|
8
|
+
module SimpleCov
|
|
9
|
+
module CLI
|
|
10
|
+
# `simplecov ratchet`: rewrite the checked-in per-file coverage baseline
|
|
11
|
+
# (#1268) from the current report. Floors only ever tighten, entries for
|
|
12
|
+
# files the report no longer carries are pruned, and files without an entry
|
|
13
|
+
# are never given one, so new code stays answerable to the global thresholds
|
|
14
|
+
# rather than to a floor cut at whatever it launched with. `--init`
|
|
15
|
+
# regenerates the whole file from the current state.
|
|
16
|
+
#
|
|
17
|
+
# The baseline path follows the project's `.simplecov` `baseline_file`
|
|
18
|
+
# setting when one is present, the same way the read-only commands follow
|
|
19
|
+
# `coverage_dir`.
|
|
20
|
+
module Ratchet
|
|
21
|
+
extend CommandHelpers
|
|
22
|
+
|
|
23
|
+
extend self
|
|
24
|
+
|
|
25
|
+
def run(args, stdout:, stderr:, **)
|
|
26
|
+
require "simplecov"
|
|
27
|
+
opts = parse(args, stderr) or return 1
|
|
28
|
+
coverage = CoverageFile.load_coverage(opts.fetch(:input), command: "ratchet", stderr: stderr) or return 1
|
|
29
|
+
|
|
30
|
+
outcome, generated = compute(opts, current_floors(coverage))
|
|
31
|
+
AtomicFile.write(opts.fetch(:baseline), outcome.baseline.to_yaml) unless opts.fetch(:dry_run)
|
|
32
|
+
Output.emit(stdout, opts, outcome, generated)
|
|
33
|
+
0
|
|
34
|
+
rescue ConfigurationError => e
|
|
35
|
+
error(stderr, e)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse(args, stderr)
|
|
39
|
+
opts, rest = parse_common(args, init: false, dry_run: false) do |parser, options|
|
|
40
|
+
parser.on("--baseline PATH") { |v| options[:baseline] = v }
|
|
41
|
+
parser.on("--init") { options[:init] = true }
|
|
42
|
+
parser.on("--dry-run") { options[:dry_run] = true }
|
|
43
|
+
end
|
|
44
|
+
return error_nil(stderr, "unexpected argument #{rest.first.inspect}") unless rest.empty?
|
|
45
|
+
|
|
46
|
+
opts[:baseline] ||= Dotfile.baseline_file
|
|
47
|
+
opts
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Only criteria the report carries for a file appear, so a line-only report
|
|
51
|
+
# leaves existing branch floors alone.
|
|
52
|
+
def current_floors(coverage)
|
|
53
|
+
initial = {} #: Baseline::current_floors
|
|
54
|
+
coverage.each_with_object(initial) do |(path, entry), floors|
|
|
55
|
+
criteria = CoverageFile::CRITERIA.filter_map do |criterion, keys|
|
|
56
|
+
percent = entry[keys.fetch(:percent)]
|
|
57
|
+
missed = entry[keys.fetch(:missed)]
|
|
58
|
+
[criterion, {percent: SimpleCov.round_coverage(percent), missed: missed}] if usable?(percent, missed)
|
|
59
|
+
end.to_h
|
|
60
|
+
floors[path] = criteria unless criteria.empty?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# A report row from another tool may be missing counts; only a complete pair
|
|
65
|
+
# can become a floor.
|
|
66
|
+
def usable?(percent, missed)
|
|
67
|
+
percent.is_a?(Numeric) && missed.instance_of?(Integer)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def compute(opts, current)
|
|
71
|
+
existing = Baseline.read_if_exists(opts.fetch(:baseline)) unless opts.fetch(:init)
|
|
72
|
+
return [existing.ratchet(current), false] if existing
|
|
73
|
+
|
|
74
|
+
none = [] #: Array[String]
|
|
75
|
+
outcome = Baseline::Outcome.new(
|
|
76
|
+
baseline: Baseline.generate(current),
|
|
77
|
+
tightened: none, pruned: none, regressed: none, unchanged: none
|
|
78
|
+
)
|
|
79
|
+
[outcome, true]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/simplecov/cli/report.rb
CHANGED
|
@@ -6,46 +6,40 @@ require_relative "command_helpers"
|
|
|
6
6
|
|
|
7
7
|
module SimpleCov
|
|
8
8
|
module CLI
|
|
9
|
-
# `simplecov report [--input PATH]` — pretty-print the overall
|
|
10
|
-
# totals row plus per-group totals from a JSONFormatter
|
|
11
|
-
# coverage.json. Same numbers as the HTML report's totals row, for
|
|
12
|
-
# contexts where opening a browser isn't an option (CI logs, ssh
|
|
13
|
-
# sessions, terminal-only workflows).
|
|
14
9
|
module Report
|
|
15
10
|
extend CommandHelpers
|
|
16
11
|
|
|
17
12
|
TOTAL_LABEL = "All Files"
|
|
18
|
-
SECTIONS = [%w[Line lines], %w[Branch branches], %w[Method methods]].freeze
|
|
13
|
+
SECTIONS = [%w[Line lines], %w[Branch branches], %w[Method methods]].freeze #: Array[[String, String]]
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
extend self
|
|
21
16
|
|
|
22
17
|
def run(args, stdout:, stderr:)
|
|
23
18
|
opts, = parse_common(args)
|
|
24
|
-
return 1 unless (data = load_data(opts
|
|
19
|
+
return 1 unless (data = load_data(opts.fetch(:input), stderr))
|
|
25
20
|
|
|
26
|
-
if opts
|
|
21
|
+
if opts.fetch(:json)
|
|
27
22
|
emit_json(stdout, data)
|
|
28
23
|
else
|
|
29
|
-
emit_text(stdout, data,
|
|
24
|
+
emit_text(stdout, data, CLI.color_enabled?(opts, stdout))
|
|
30
25
|
end
|
|
31
26
|
0
|
|
32
27
|
end
|
|
33
28
|
|
|
34
|
-
# Valid JSON isn't enough: a wrong-typed "total" or "groups"
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# sibling commands print.
|
|
29
|
+
# Valid JSON isn't enough: a wrong-typed "total" or "groups" used to escape
|
|
30
|
+
# here and crash the emitters with a backtrace instead of the one-line error
|
|
31
|
+
# the sibling commands print.
|
|
38
32
|
def load_data(input, stderr)
|
|
39
33
|
data = CoverageFile.load_document(input, command: "report", stderr: stderr)
|
|
40
34
|
return unless data
|
|
41
35
|
|
|
42
36
|
none = {} #: Hash[String, untyped]
|
|
43
|
-
unless data.fetch("total", none).
|
|
37
|
+
unless data.fetch("total", none).instance_of?(Hash)
|
|
44
38
|
return CoverageFile.report_invalid(stderr, "report", input, '"total" must be an object')
|
|
45
39
|
end
|
|
46
40
|
|
|
47
41
|
groups = data.fetch("groups", none)
|
|
48
|
-
return data if groups.
|
|
42
|
+
return data if groups.instance_of?(Hash) && groups.each_value.all?(Hash)
|
|
49
43
|
|
|
50
44
|
CoverageFile.report_invalid(stderr, "report", input, '"groups" must be an object of objects')
|
|
51
45
|
end
|
|
@@ -54,7 +48,7 @@ module SimpleCov
|
|
|
54
48
|
none = {} #: Hash[String, untyped]
|
|
55
49
|
emit_totals(stdout, TOTAL_LABEL, data.fetch("total", none), color)
|
|
56
50
|
data.fetch("groups", none).each do |name, group|
|
|
57
|
-
label = name
|
|
51
|
+
label = name.eql?(TOTAL_LABEL) ? "#{name} (group)" : name
|
|
58
52
|
emit_totals(stdout, label, group, color)
|
|
59
53
|
end
|
|
60
54
|
end
|
|
@@ -66,12 +60,12 @@ module SimpleCov
|
|
|
66
60
|
end
|
|
67
61
|
|
|
68
62
|
def emit_section(stdout, display, section, color)
|
|
69
|
-
return unless section.
|
|
63
|
+
return unless section.instance_of?(Hash) && section["total"].to_i.positive?
|
|
70
64
|
|
|
71
65
|
stdout.puts(stats_row(display,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
Color.colorize_percent(section["percent"].to_f, enabled: color),
|
|
67
|
+
section["covered"],
|
|
68
|
+
section.fetch("total")))
|
|
75
69
|
end
|
|
76
70
|
|
|
77
71
|
def emit_json(stdout, data)
|
|
@@ -85,10 +79,10 @@ module SimpleCov
|
|
|
85
79
|
sections = {} #: Hash[String, untyped]
|
|
86
80
|
SECTIONS.each_with_object(sections) do |(_, key), out|
|
|
87
81
|
section = totals[key]
|
|
88
|
-
next unless section.
|
|
82
|
+
next unless section.instance_of?(Hash) && section["total"].to_i.positive?
|
|
89
83
|
|
|
90
|
-
out[key
|
|
91
|
-
"percent" => section["percent"], "covered" => section["covered"], "total" => section
|
|
84
|
+
out[key] = {
|
|
85
|
+
"percent" => section["percent"], "covered" => section["covered"], "total" => section.fetch("total")
|
|
92
86
|
}
|
|
93
87
|
end
|
|
94
88
|
end
|
data/lib/simplecov/cli/run.rb
CHANGED
|
@@ -2,29 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
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
5
|
module Run
|
|
11
6
|
AUTOSTART = File.expand_path("../autostart", __dir__.to_s)
|
|
12
7
|
|
|
13
|
-
|
|
8
|
+
extend self
|
|
14
9
|
|
|
15
10
|
def run(args, stderr:, **)
|
|
16
|
-
cmd = args.first
|
|
11
|
+
cmd = args.first.eql?("--") ? args.drop(1) : args
|
|
17
12
|
if cmd.empty?
|
|
18
13
|
stderr.puts("simplecov run: missing command")
|
|
19
14
|
return 1
|
|
20
15
|
end
|
|
21
16
|
|
|
22
|
-
|
|
17
|
+
exec_command(rubyopt_env, cmd)
|
|
23
18
|
rescue Errno::ENOENT => e
|
|
24
|
-
stderr.puts("simplecov run: #{e
|
|
19
|
+
stderr.puts("simplecov run: #{e}")
|
|
25
20
|
127
|
|
26
21
|
end
|
|
27
22
|
|
|
23
|
+
def exec_command(env, cmd)
|
|
24
|
+
Kernel.exec(env, *cmd)
|
|
25
|
+
end
|
|
26
|
+
|
|
28
27
|
def rubyopt_env
|
|
29
28
|
existing = ENV["RUBYOPT"].to_s.strip
|
|
30
29
|
injection = "-r#{AUTOSTART}"
|
|
@@ -5,9 +5,6 @@ require_relative "../../coverage_json"
|
|
|
5
5
|
module SimpleCov
|
|
6
6
|
module CLI
|
|
7
7
|
module Serve
|
|
8
|
-
# Ensures the coverage directory has a renderable entry point before
|
|
9
|
-
# the HTTP server binds. Existing self-contained reports take priority
|
|
10
|
-
# over their optional JSON sidecar.
|
|
11
8
|
module ReportPreparer
|
|
12
9
|
def self.call(dir)
|
|
13
10
|
index_path = File.join(dir, "index.html")
|
|
@@ -17,11 +14,15 @@ module SimpleCov
|
|
|
17
14
|
|
|
18
15
|
return "#{dir} has no index.html or coverage.json; run your test suite first" unless File.file?(json_path)
|
|
19
16
|
|
|
17
|
+
build_index(json_path, dir)
|
|
18
|
+
rescue CoverageJSON::Error, SystemCallError => e
|
|
19
|
+
"cannot build index.html from #{json_path}: #{e}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.build_index(json_path, dir)
|
|
20
23
|
require_relative "../../formatter/html_formatter"
|
|
21
|
-
|
|
24
|
+
Formatter::HTMLFormatter.new.format_from_json(json_path, dir)
|
|
22
25
|
nil
|
|
23
|
-
rescue SimpleCov::CoverageJSON::Error, SystemCallError => e
|
|
24
|
-
"cannot build index.html from #{json_path}: #{e.message}"
|
|
25
26
|
end
|
|
26
27
|
end
|
|
27
28
|
end
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module CLI
|
|
5
5
|
module Serve
|
|
6
|
-
# The HTTP mechanics behind `simplecov serve`: reads one request
|
|
7
|
-
#
|
|
8
|
-
# traversal-safe path resolution. Kept apart from the CLI wiring
|
|
9
|
-
#
|
|
6
|
+
# The HTTP mechanics behind `simplecov serve`: reads one request per
|
|
7
|
+
# connection and answers it from the report directory, with
|
|
8
|
+
# traversal-safe path resolution. Kept apart from the CLI wiring in
|
|
9
|
+
# serve.rb.
|
|
10
10
|
module StaticFileHandler
|
|
11
11
|
MIME = {
|
|
12
12
|
".html" => "text/html; charset=utf-8", ".htm" => "text/html; charset=utf-8",
|
|
@@ -24,96 +24,102 @@ module SimpleCov
|
|
|
24
24
|
STATUS_TEXT = {200 => "OK", 400 => "Bad Request", 403 => "Forbidden",
|
|
25
25
|
404 => "Not Found", 405 => "Method Not Allowed"}.freeze
|
|
26
26
|
|
|
27
|
-
# Seconds a connection may sit idle mid-request before its reads
|
|
28
|
-
#
|
|
27
|
+
# Seconds a connection may sit idle mid-request before its reads raise
|
|
28
|
+
# IO::TimeoutError and the connection is dropped.
|
|
29
29
|
READ_TIMEOUT = 5
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# Reads one HTTP request line, drains headers, serves the file or
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
#
|
|
43
|
-
#
|
|
31
|
+
extend self
|
|
32
|
+
|
|
33
|
+
# Reads one HTTP request line, drains headers, serves the file or writes a
|
|
34
|
+
# status response. Wide rescue so a misbehaving client can't crash the
|
|
35
|
+
# server.
|
|
36
|
+
#
|
|
37
|
+
# `routes` maps exact request paths (query string excluded) to callables
|
|
38
|
+
# that take over the connection, the seam `simplecov watch` mounts its
|
|
39
|
+
# /events stream on. A route may hold the socket for as long as it
|
|
40
|
+
# likes; the ensure below closes it when the route returns.
|
|
41
|
+
def handle_connection(client, root, routes = {})
|
|
42
|
+
# JRuby doesn't implement IO#timeout=. Without the guard the
|
|
43
|
+
# NoMethodError lands in the wide rescue below and every connection
|
|
44
|
+
# closes with an empty response. An idle connection then pins its
|
|
45
|
+
# thread instead of timing out, which only leaks a thread in an
|
|
46
|
+
# interactive dev server.
|
|
44
47
|
client.timeout = READ_TIMEOUT if client.respond_to?(:timeout=)
|
|
45
|
-
# simplecov:enable branch
|
|
46
48
|
method, path = client.readline.split
|
|
47
49
|
drain_headers(client)
|
|
48
|
-
# A request line without both tokens used to raise on
|
|
49
|
-
#
|
|
50
|
-
#
|
|
50
|
+
# A request line without both tokens used to raise on `path.split` inside
|
|
51
|
+
# the wide rescue, closing the connection with an empty response
|
|
52
|
+
# instead of the 400 below.
|
|
51
53
|
return respond(client, 400) if path.nil?
|
|
52
|
-
return respond(client, 405) unless method == "GET"
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
rescue
|
|
56
|
-
# Misbehaving clients (truncated requests, connection resets,
|
|
57
|
-
#
|
|
55
|
+
dispatch(client, method, path, root, routes)
|
|
56
|
+
rescue
|
|
57
|
+
# Misbehaving clients (truncated requests, connection resets, invalid
|
|
58
|
+
# encoding) shouldn't take the whole server down.
|
|
58
59
|
nil
|
|
59
60
|
ensure
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
client.close
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def dispatch(client, method, path, root, routes)
|
|
65
|
+
return respond(client, 405) unless method.eql?("GET")
|
|
66
|
+
|
|
67
|
+
route = routes[path.split("?").first]
|
|
68
|
+
return route.call(client) if route
|
|
69
|
+
|
|
70
|
+
serve_file(client, path, root)
|
|
64
71
|
end
|
|
65
72
|
|
|
66
73
|
def serve_file(client, path, root)
|
|
67
74
|
file = resolve(path, root)
|
|
68
|
-
|
|
75
|
+
# `resolve` answers a path, nothing for a file that is not there, or the
|
|
76
|
+
# refusal itself.
|
|
77
|
+
return respond(client, file ? 403 : 404) unless file.instance_of?(String)
|
|
69
78
|
|
|
70
79
|
respond(client, 200, File.binread(file), MIME[File.extname(file).downcase])
|
|
71
80
|
end
|
|
72
81
|
|
|
73
82
|
def drain_headers(client)
|
|
74
|
-
loop { break if client.readline.
|
|
83
|
+
loop { break if client.readline.rstrip.empty? }
|
|
75
84
|
end
|
|
76
85
|
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
86
|
+
# Answers the absolute path of the file to serve, :forbidden for a
|
|
87
|
+
# traversal attempt (symlinks that escape root included), or nil for
|
|
88
|
+
# "not found".
|
|
80
89
|
#
|
|
81
|
-
# The request path is deliberately NOT percent-decoded: filenames
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
# check below.
|
|
90
|
+
# The request path is deliberately NOT percent-decoded: filenames needing
|
|
91
|
+
# escapes don't occur in generated reports, and keeping `%2e%2e%2f` as
|
|
92
|
+
# literal bytes is part of the traversal defense. If decoding is ever
|
|
93
|
+
# added, it must happen BEFORE the `inside?` check.
|
|
86
94
|
def resolve(request_path, root)
|
|
87
|
-
path = request_path.split("?"
|
|
95
|
+
path = request_path.split("?").first.to_s.delete_prefix("/")
|
|
88
96
|
absolute_root = File.realpath(root)
|
|
89
|
-
candidate = File.expand_path(path
|
|
90
|
-
#
|
|
91
|
-
#
|
|
97
|
+
candidate = File.expand_path(path, absolute_root)
|
|
98
|
+
# Rejected before touching disk, so traversal and absolute-path attempts
|
|
99
|
+
# are 403, not 404.
|
|
92
100
|
return :forbidden unless inside?(candidate, absolute_root)
|
|
93
101
|
|
|
94
102
|
candidate = File.join(candidate, "index.html") if File.directory?(candidate)
|
|
95
103
|
return nil unless File.file?(candidate)
|
|
96
104
|
|
|
97
|
-
#
|
|
98
|
-
#
|
|
105
|
+
# Symlinks are resolved last and re-checked: a file inside root could be
|
|
106
|
+
# a symlink pointing outside.
|
|
99
107
|
real = File.realpath(candidate)
|
|
100
108
|
inside?(real, absolute_root) ? real : :forbidden
|
|
101
109
|
rescue Errno::ENOENT
|
|
102
|
-
#
|
|
103
|
-
# File.file? and File.realpath. Treat as "not found".
|
|
110
|
+
# TOCTOU: candidate vanished between File.file? and File.realpath.
|
|
104
111
|
nil
|
|
105
|
-
# simplecov:enable
|
|
106
112
|
end
|
|
107
113
|
|
|
108
114
|
def inside?(path, root)
|
|
109
|
-
path
|
|
115
|
+
path.eql?(root) || path.start_with?(root + File::SEPARATOR)
|
|
110
116
|
end
|
|
111
117
|
|
|
112
118
|
def respond(client, status, body = "", content_type = "text/plain")
|
|
113
|
-
client.write("HTTP/1.1 #{status} #{STATUS_TEXT[status] ||
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
client.write("HTTP/1.1 #{status} #{STATUS_TEXT[status] || "Error"}\r\n",
|
|
120
|
+
"Content-Type: #{content_type || "application/octet-stream"}\r\n",
|
|
121
|
+
"Content-Length: #{body.bytesize}\r\n",
|
|
122
|
+
"Connection: close\r\n\r\n")
|
|
117
123
|
client.write(body)
|
|
118
124
|
end
|
|
119
125
|
end
|
data/lib/simplecov/cli/serve.rb
CHANGED
|
@@ -7,24 +7,22 @@ require_relative "serve/static_file_handler"
|
|
|
7
7
|
|
|
8
8
|
module SimpleCov
|
|
9
9
|
module CLI
|
|
10
|
-
# `simplecov serve
|
|
11
|
-
#
|
|
12
|
-
# `
|
|
13
|
-
#
|
|
14
|
-
# the CLI wiring (options, binding, the accept loop); the HTTP
|
|
15
|
-
# mechanics live in StaticFileHandler.
|
|
10
|
+
# `simplecov serve`: a small static file server backed by stdlib `socket`, so
|
|
11
|
+
# there is no extra dependency just for viewing a local report on a box where
|
|
12
|
+
# `file://` doesn't work. This module is the CLI wiring; the HTTP mechanics
|
|
13
|
+
# live in StaticFileHandler.
|
|
16
14
|
module Serve
|
|
17
15
|
extend CommandHelpers
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
extend self
|
|
20
18
|
|
|
21
19
|
def run(args, stdout:, stderr:, **)
|
|
22
20
|
opts = parse(args)
|
|
23
|
-
dir =
|
|
21
|
+
dir = CLI.coverage_dir
|
|
24
22
|
message = ReportPreparer.call(dir)
|
|
25
23
|
return error(stderr, message) if message
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
require_socket
|
|
28
26
|
with_server(opts, stderr) do |server|
|
|
29
27
|
announce(stdout, server, dir)
|
|
30
28
|
serve_loop(server, dir, stdout)
|
|
@@ -32,14 +30,20 @@ module SimpleCov
|
|
|
32
30
|
end
|
|
33
31
|
end
|
|
34
32
|
|
|
35
|
-
#
|
|
36
|
-
#
|
|
33
|
+
# Required here rather than at load time so the other subcommands do not pay
|
|
34
|
+
# for sockets.
|
|
35
|
+
def require_socket
|
|
36
|
+
require "socket"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Answers 1 when the socket can't be bound: port already taken, privileged
|
|
40
|
+
# port, unresolvable host. The receiver cast works around an rbs stdlib gap,
|
|
41
|
+
# where TCPSocket's explicit `self.new` shadows TCPServer#initialize's
|
|
42
|
+
# (host, port) form.
|
|
37
43
|
def with_server(opts, stderr)
|
|
38
|
-
|
|
39
|
-
# explicit `self.new` shadows TCPServer#initialize's (host, port) form.
|
|
40
|
-
server = (_ = TCPServer).new(opts[:host], opts[:port]) #: TCPServer
|
|
44
|
+
server = (_ = TCPServer).new(opts.fetch(:host), opts.fetch(:port)) #: TCPServer
|
|
41
45
|
rescue SystemCallError, SocketError => e
|
|
42
|
-
error(stderr, "cannot bind to #{opts
|
|
46
|
+
error(stderr, "cannot bind to #{opts.fetch(:host)}:#{opts.fetch(:port)} (#{e})")
|
|
43
47
|
else
|
|
44
48
|
begin
|
|
45
49
|
yield server
|
|
@@ -50,29 +54,28 @@ module SimpleCov
|
|
|
50
54
|
|
|
51
55
|
def parse(args)
|
|
52
56
|
opts = {port: 0, host: "127.0.0.1"} #: Hash[Symbol, untyped]
|
|
53
|
-
|
|
57
|
+
build_parser do |o|
|
|
54
58
|
o.on("--port N", Integer) { |v| opts[:port] = v }
|
|
55
|
-
o.on("--host HOST")
|
|
59
|
+
o.on("--host HOST") { |v| opts[:host] = v }
|
|
56
60
|
end.parse(args)
|
|
57
61
|
opts
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
def announce(stdout, server, dir)
|
|
61
|
-
port = server.addr
|
|
62
|
-
host = url_host(server.addr
|
|
65
|
+
port = server.addr.fetch(1)
|
|
66
|
+
host = url_host(server.addr.fetch(3))
|
|
63
67
|
stdout.puts("simplecov serve: serving #{dir} at http://#{host}:#{port}/")
|
|
64
68
|
stdout.puts("Press Ctrl-C to stop.")
|
|
65
69
|
end
|
|
66
70
|
|
|
67
|
-
# IPv6 literals need brackets in a URL
|
|
68
|
-
# http://[::1]:PORT/, not the invalid http://::1:PORT
|
|
71
|
+
# IPv6 literals need brackets in a URL: `--host ::1` must announce
|
|
72
|
+
# http://[::1]:PORT/, not the invalid http://::1:PORT/.
|
|
69
73
|
def url_host(host)
|
|
70
74
|
host.include?(":") ? "[#{host}]" : host
|
|
71
75
|
end
|
|
72
76
|
|
|
73
|
-
# One thread per connection: browsers open speculative sockets
|
|
74
|
-
#
|
|
75
|
-
# request (the report's assets included) behind them.
|
|
77
|
+
# One thread per connection: browsers open speculative sockets that send no
|
|
78
|
+
# bytes, and a serial loop would stall every real request behind them.
|
|
76
79
|
def serve_loop(server, dir, stdout)
|
|
77
80
|
loop do
|
|
78
81
|
Thread.new(server.accept) { |client| StaticFileHandler.handle_connection(client, dir) }
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module CLI
|
|
5
|
+
module Show
|
|
6
|
+
# Renders one file's annotated source: a right-aligned line-number gutter,
|
|
7
|
+
# the hit count beside it, and a caret line naming each miss under the line
|
|
8
|
+
# it happened on.
|
|
9
|
+
module Annotator
|
|
10
|
+
extend self
|
|
11
|
+
|
|
12
|
+
def call(source, entry, stdout, color:)
|
|
13
|
+
markers = markers_for(entry)
|
|
14
|
+
widths = {number: source.size.to_s.length, count: count_width(entry)}
|
|
15
|
+
source.each_with_index do |line, index|
|
|
16
|
+
rendered = row(index + 1, entry.fetch("lines").at(index), line, widths, color)
|
|
17
|
+
emit(stdout, rendered, markers[index + 1], widths, color)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def emit(stdout, rendered, labels, widths, color)
|
|
22
|
+
stdout.puts(rendered)
|
|
23
|
+
return if labels.empty?
|
|
24
|
+
|
|
25
|
+
gutter = " " * (widths.fetch(:number) + widths.fetch(:count) + 4)
|
|
26
|
+
stdout.puts(gutter + paint("^ #{labels.join(", ")}", :red, color))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def missed_lines(entry)
|
|
30
|
+
entry.fetch("lines").each_with_index.filter_map do |hit, index|
|
|
31
|
+
index + 1 if hit.instance_of?(Integer) && hit.zero?
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def markers_for(entry)
|
|
36
|
+
markers = Hash.new { |hash, line| hash[line] = [] } #: Hash[Integer, Array[String]]
|
|
37
|
+
missed_lines(entry).each { |line| markers[line] << "missed" }
|
|
38
|
+
each_missed(entry["branches"]) { |line| markers[line] << "branch missed" }
|
|
39
|
+
each_missed(entry["methods"]) { |line| markers[line] << "method missed" }
|
|
40
|
+
markers
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def each_missed(items)
|
|
44
|
+
return unless items.instance_of?(Array)
|
|
45
|
+
|
|
46
|
+
items.each do |item|
|
|
47
|
+
line = missed_line_of(item)
|
|
48
|
+
yield line if line
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The reported line of a zero-hit item, nil for a covered or malformed one,
|
|
53
|
+
# the same tolerance the patch subcommand applies to branch entries.
|
|
54
|
+
def missed_line_of(item)
|
|
55
|
+
return nil unless item.instance_of?(Hash)
|
|
56
|
+
|
|
57
|
+
hits = item["coverage"]
|
|
58
|
+
return nil unless hits.instance_of?(Integer) && hits.zero?
|
|
59
|
+
|
|
60
|
+
line = item["report_line"] || item["start_line"]
|
|
61
|
+
line if line.instance_of?(Integer)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def count_width(entry)
|
|
65
|
+
entry.fetch("lines").grep(Integer) { |hit| hit.to_s.length }.max || 1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def row(number, hit, line, widths, color)
|
|
69
|
+
format("%#{widths.fetch(:number)}d %s %s", number, count(hit, widths, color), line).rstrip
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Padded to width before it is painted, so escape codes can't skew the
|
|
73
|
+
# column, and blank for a line carrying no count.
|
|
74
|
+
def count(hit, widths, color)
|
|
75
|
+
padded = (hit.instance_of?(Integer) ? hit.to_s : "").rjust(widths.fetch(:count))
|
|
76
|
+
return padded unless hit.instance_of?(Integer)
|
|
77
|
+
|
|
78
|
+
paint(padded, hit.zero? ? :red : :green, color)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def paint(text, tint, color)
|
|
82
|
+
Color.colorize(text, tint, enabled: color)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|