simplecov 0.22.0 → 1.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/LICENSE +1 -1
- data/README.md +173 -875
- data/exe/simplecov +8 -0
- data/lib/minitest/simplecov_plugin.rb +18 -7
- data/lib/simplecov/atomic_file.rb +67 -0
- data/lib/simplecov/autostart.rb +9 -0
- 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/annotations.rb +166 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +86 -0
- data/lib/simplecov/cli/command_helpers.rb +91 -0
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +83 -0
- data/lib/simplecov/cli/coverage_file.rb +109 -0
- 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 +115 -0
- data/lib/simplecov/cli/dotfile.rb +156 -0
- 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 +115 -0
- data/lib/simplecov/cli/open.rb +45 -0
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +151 -0
- data/lib/simplecov/cli/patch.rb +175 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +91 -0
- data/lib/simplecov/cli/run.rb +35 -0
- data/lib/simplecov/cli/serve/report_preparer.rb +30 -0
- data/lib/simplecov/cli/serve/static_file_handler.rb +128 -0
- data/lib/simplecov/cli/serve.rb +88 -0
- 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 +33 -0
- data/lib/simplecov/cli/uncovered.rb +127 -0
- data/lib/simplecov/cli/usage.rb +153 -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 +114 -0
- data/lib/simplecov/color.rb +65 -0
- data/lib/simplecov/combine/branches_combiner.rb +44 -21
- data/lib/simplecov/combine/coverage_accumulator.rb +175 -0
- data/lib/simplecov/combine/identity_interner.rb +27 -0
- data/lib/simplecov/combine/interned_counts.rb +28 -0
- data/lib/simplecov/combine/lines_combiner.rb +35 -29
- data/lib/simplecov/combine/methods_combiner.rb +48 -0
- data/lib/simplecov/combine/results_combiner.rb +9 -49
- data/lib/simplecov/combine.rb +0 -24
- data/lib/simplecov/command_guesser.rb +99 -42
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +184 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +143 -0
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +37 -0
- data/lib/simplecov/configuration/filters.rb +131 -0
- data/lib/simplecov/configuration/formatting.rb +129 -0
- data/lib/simplecov/configuration/groups.rb +48 -0
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +72 -0
- data/lib/simplecov/configuration/merging.rb +197 -0
- 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 +150 -0
- data/lib/simplecov/configuration/view_coverage.rb +37 -0
- data/lib/simplecov/configuration.rb +100 -443
- 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 +21 -0
- data/lib/simplecov/coverage_statistics.rb +24 -28
- data/lib/simplecov/coverage_violations.rb +254 -0
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +20 -28
- data/lib/simplecov/deprecation.rb +63 -0
- data/lib/simplecov/directive/erb.rb +44 -0
- data/lib/simplecov/directive/haml.rb +33 -0
- data/lib/simplecov/directive/indented_template.rb +37 -0
- data/lib/simplecov/directive/slim.rb +35 -0
- data/lib/simplecov/directive/template.rb +29 -0
- data/lib/simplecov/directive.rb +152 -0
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +39 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +24 -5
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +17 -67
- 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 +27 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +20 -38
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +28 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -34
- data/lib/simplecov/exit_codes.rb +26 -0
- data/lib/simplecov/exit_handling.rb +163 -0
- data/lib/simplecov/file_list.rb +63 -48
- data/lib/simplecov/filter.rb +98 -42
- data/lib/simplecov/formatter/base.rb +100 -0
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +95 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +93 -0
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +153 -0
- data/lib/simplecov/formatter/html_formatter.rb +88 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +122 -0
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +132 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +97 -0
- data/lib/simplecov/formatter/json_formatter.rb +40 -0
- data/lib/simplecov/formatter/multi_formatter.rb +8 -10
- data/lib/simplecov/formatter/simple_formatter.rb +12 -15
- data/lib/simplecov/formatter.rb +21 -1
- data/lib/simplecov/group_names.rb +33 -0
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +16 -5
- data/lib/simplecov/lines_classifier.rb +67 -19
- data/lib/simplecov/load_global_config.rb +12 -4
- data/lib/simplecov/parallel_adapters/base.rb +50 -0
- data/lib/simplecov/parallel_adapters/generic.rb +33 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +72 -0
- data/lib/simplecov/parallel_adapters.rb +54 -0
- data/lib/simplecov/parallel_coordination.rb +124 -0
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +171 -0
- data/lib/simplecov/process.rb +34 -12
- 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/bundler_filter.rb +1 -1
- data/lib/simplecov/profiles/hidden_filter.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +13 -10
- data/lib/simplecov/profiles/root_filter.rb +2 -5
- data/lib/simplecov/profiles/strict.rb +22 -0
- data/lib/simplecov/profiles/test_frameworks.rb +1 -4
- data/lib/simplecov/profiles.rb +26 -19
- data/lib/simplecov/report_deferral.rb +45 -0
- data/lib/simplecov/report_stamp.rb +25 -0
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +48 -0
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +46 -0
- data/lib/simplecov/result.rb +123 -47
- data/lib/simplecov/result_adapter.rb +130 -11
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +24 -0
- data/lib/simplecov/result_merger/resultset_file.rb +67 -0
- data/lib/simplecov/result_merger/resultset_run_identity.rb +65 -0
- data/lib/simplecov/result_merger/resultset_store.rb +62 -0
- data/lib/simplecov/result_merger/unloaded_files.rb +95 -0
- data/lib/simplecov/result_merger.rb +120 -124
- data/lib/simplecov/result_processing.rb +188 -0
- data/lib/simplecov/run_identity.rb +75 -0
- data/lib/simplecov/simulate_coverage.rb +72 -17
- data/lib/simplecov/source_file/branch.rb +8 -35
- data/lib/simplecov/source_file/branch_builder.rb +95 -0
- data/lib/simplecov/source_file/builder_context.rb +28 -0
- data/lib/simplecov/source_file/line.rb +17 -32
- data/lib/simplecov/source_file/line_builder.rb +37 -0
- data/lib/simplecov/source_file/method.rb +54 -0
- data/lib/simplecov/source_file/method_builder.rb +51 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +103 -0
- data/lib/simplecov/source_file/skip_chunks.rb +72 -0
- data/lib/simplecov/source_file/source_loader.rb +89 -0
- data/lib/simplecov/source_file/statistics.rb +76 -0
- data/lib/simplecov/source_file.rb +64 -260
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +155 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +191 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +45 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +46 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +61 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +163 -0
- data/lib/simplecov/static_coverage_extractor.rb +78 -0
- 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 +62 -0
- data/lib/simplecov/useless_results_remover.rb +17 -7
- 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 +5 -0
- data/lib/simplecov.rb +143 -394
- data/lib/simplecov_json_formatter.rb +5 -0
- data/man/simplecov.1 +354 -0
- data/sig/simplecov.rbs +1710 -0
- metadata +183 -61
- data/CHANGELOG.md +0 -191
- data/doc/alternate-formatters.md +0 -71
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -24
- data/lib/simplecov/default_formatter.rb +0 -20
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "configuration_error"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
# Emits legacy-API deprecation warnings, deduplicated by the source location
|
|
7
|
+
# that triggered them. A deprecated method called in a loop, or a config
|
|
8
|
+
# block re-evaluated once per parallel worker, otherwise repeats the same
|
|
9
|
+
# notice until stderr is unreadable. Keying on the caller location collapses
|
|
10
|
+
# those repeats while still warning separately about each distinct call site
|
|
11
|
+
# the user needs to fix (#1204).
|
|
12
|
+
module Deprecation
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
MODES = %i[warn raise].freeze
|
|
16
|
+
|
|
17
|
+
# The mode lives here rather than on the configuration singleton so that
|
|
18
|
+
# `warn` depends on nothing but itself. `SimpleCov.deprecations` is the
|
|
19
|
+
# front door that writes it, and the CLI, which loads none of the
|
|
20
|
+
# configuration surface, can still warn.
|
|
21
|
+
def mode
|
|
22
|
+
@mode ||= :warn
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def mode=(mode)
|
|
26
|
+
raise ConfigurationError, "deprecations takes :warn or :raise, got #{mode.inspect}" unless MODES.include?(mode)
|
|
27
|
+
|
|
28
|
+
@mode = mode
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# `message` is the notice without the `[DEPRECATION]` tag or location
|
|
32
|
+
# prefix, both of which are added here. `deprecations :raise` turns every
|
|
33
|
+
# deprecated API into an error before the dedup: an error is not a notice to
|
|
34
|
+
# collapse, and a CI guard must fail on the second offender as surely as on
|
|
35
|
+
# the first. A missing backtrace falls back to keying on the message, so it
|
|
36
|
+
# never silently swallows every notice.
|
|
37
|
+
def warn(message, location: caller_location)
|
|
38
|
+
raise ConfigurationError, message if mode.equal?(:raise)
|
|
39
|
+
|
|
40
|
+
return unless emitted.add?(location || message)
|
|
41
|
+
|
|
42
|
+
Kernel.warn "#{"#{location}: " if location}[DEPRECATION] #{message}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Every shipped call site is a one-level alias such as `track_files`, so the
|
|
46
|
+
# frame three up from here is the user's own code. `Array(...)` coerces a
|
|
47
|
+
# missing backtrace to `[]` so `.first` yields nil rather than raising.
|
|
48
|
+
def caller_location
|
|
49
|
+
Array(Kernel.caller(3..3)).first
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Parallel workers are separate processes with their own set, so each warns
|
|
53
|
+
# at most once.
|
|
54
|
+
def emitted
|
|
55
|
+
@emitted ||= Set.new
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def reset!
|
|
59
|
+
@emitted = Set.new
|
|
60
|
+
@mode = :warn
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class Directive
|
|
5
|
+
# The Ruby an ERB template holds, at the template's own line numbers, so
|
|
6
|
+
# directive comments in a template are found the way they are in a `.rb`
|
|
7
|
+
# file. Lexing the template itself as Ruby is not an option: `%>` opens a
|
|
8
|
+
# percent literal that swallows everything up to the next `>`, so comments
|
|
9
|
+
# after the first tag are never tokenized.
|
|
10
|
+
#
|
|
11
|
+
# Text outside the tags is blanked and the tag delimiters become spaces, so
|
|
12
|
+
# every token keeps its line and column. A comment tag becomes a Ruby
|
|
13
|
+
# comment, which makes `<%# simplecov:disable %>` the template's own form of
|
|
14
|
+
# the directive.
|
|
15
|
+
module Erb
|
|
16
|
+
SEGMENT = /
|
|
17
|
+
(?<escaped><%%)
|
|
18
|
+
| <%(?<kind>\#|==?|-)?(?<body>.*?)(?<close>-?%>)
|
|
19
|
+
| (?<text>[^<]+|<)
|
|
20
|
+
/mx
|
|
21
|
+
|
|
22
|
+
def self.ruby_lines(lines)
|
|
23
|
+
source = lines.map { |line| line.end_with?("\n") ? line : "#{line}\n" }.join
|
|
24
|
+
source.gsub(SEGMENT) { convert(Regexp.last_match) }.lines
|
|
25
|
+
rescue ArgumentError, EncodingError
|
|
26
|
+
lines
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.convert(match)
|
|
30
|
+
body = match[:body]
|
|
31
|
+
return blank(match[0]) if body.nil?
|
|
32
|
+
return " #" + body.gsub("\n", "\n#") + blank(match[:close]) if match[:kind].eql?("#")
|
|
33
|
+
|
|
34
|
+
blank("<%" + match[:kind].to_s) + body + blank(match[:close]).sub(" ", ";")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.blank(text)
|
|
38
|
+
Template.blank(text)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private_class_method :convert, :blank
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "indented_template"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
class Directive
|
|
7
|
+
# The Ruby a Haml template holds. Script lines (`-`, `=`, and their `!`,
|
|
8
|
+
# `&`, and `~` variants) and the script a tag outputs keep their Ruby with
|
|
9
|
+
# the markers blanked, a `-#` comment becomes a Ruby comment, a `:ruby`
|
|
10
|
+
# filter's lines are Ruby as written, and everything else is blanked.
|
|
11
|
+
module Haml
|
|
12
|
+
include IndentedTemplate
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
SCRIPT = /\A[!&]?[-=~]/
|
|
16
|
+
TAG_SCRIPT = /\A[%.#][\w:.#-]*(?:\{[^{}]*\}|\([^()]*\)|\[[^\[\]]*\])*[<>]*[!&]?[=~]/
|
|
17
|
+
|
|
18
|
+
def convert(indent, rest)
|
|
19
|
+
if rest.start_with?("-#")
|
|
20
|
+
["#{indent} ##{rest[2..]}", COMMENT]
|
|
21
|
+
elsif rest.match?(/\A:ruby[ \t]*\n\z/)
|
|
22
|
+
[Template.blank(indent + rest), RUBY]
|
|
23
|
+
elsif rest.start_with?(":")
|
|
24
|
+
[Template.blank(indent + rest), TEXT]
|
|
25
|
+
elsif (marker = rest[SCRIPT] || rest[TAG_SCRIPT])
|
|
26
|
+
[indent + Template.blank(marker) + rest[marker.length..]]
|
|
27
|
+
else
|
|
28
|
+
[Template.blank(indent + rest)]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class Directive
|
|
5
|
+
# The driver the line-oriented template languages share. Each line is
|
|
6
|
+
# converted on its own unless the line above it opened a block, which the
|
|
7
|
+
# language decides by answering a continuation alongside the converted
|
|
8
|
+
# text: `COMMENT` for a comment marker, whose deeper-indented lines
|
|
9
|
+
# continue the comment, `RUBY` for an embedded Ruby block, whose lines are
|
|
10
|
+
# Ruby as written, and `TEXT` for any other embedded block, whose lines are
|
|
11
|
+
# blanked. A blank line neither ends a block nor belongs to it.
|
|
12
|
+
module IndentedTemplate
|
|
13
|
+
COMMENT = ->(line) { line.start_with?("\n") ? line : "##{line[1..]}" }
|
|
14
|
+
RUBY = ->(line) { line }
|
|
15
|
+
TEXT = ->(line) { Template.blank(line) }
|
|
16
|
+
|
|
17
|
+
def ruby_lines(lines)
|
|
18
|
+
open = nil #: untyped
|
|
19
|
+
lines.map do |line|
|
|
20
|
+
line = "#{line}\n" unless line.end_with?("\n")
|
|
21
|
+
indent = line[/\A[ \t]*/].length
|
|
22
|
+
rest = line[indent..]
|
|
23
|
+
if open && (rest.eql?("\n") || indent > open.last)
|
|
24
|
+
open.first.call(line)
|
|
25
|
+
else
|
|
26
|
+
converted = convert(line[0, indent], rest)
|
|
27
|
+
continuation = converted.at(1)
|
|
28
|
+
open = continuation && [continuation, indent]
|
|
29
|
+
converted.fetch(0)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
rescue ArgumentError, EncodingError
|
|
33
|
+
lines
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "indented_template"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
class Directive
|
|
7
|
+
# The Ruby a Slim template holds. Control and output lines (`-`, `=`, `==`,
|
|
8
|
+
# and their whitespace variants) and the output a bare tag carries keep
|
|
9
|
+
# their Ruby with the markers blanked, a `/` comment becomes a Ruby
|
|
10
|
+
# comment, a `ruby:` block's lines are Ruby as written, and everything else
|
|
11
|
+
# is blanked. A tag with attributes is blanked whole, since `=` inside them
|
|
12
|
+
# is not the output marker.
|
|
13
|
+
module Slim
|
|
14
|
+
include IndentedTemplate
|
|
15
|
+
extend self
|
|
16
|
+
|
|
17
|
+
CODE = /\A(?:-|={1,2}[<>']*)(?=\s)/
|
|
18
|
+
TAG_CODE = /\A[\w.#-]+\s*={1,2}[<>']*(?=\s)/
|
|
19
|
+
|
|
20
|
+
def convert(indent, rest)
|
|
21
|
+
if rest.start_with?("/")
|
|
22
|
+
["#{indent}##{rest[1..]}", COMMENT]
|
|
23
|
+
elsif rest.match?(/\Aruby:[ \t]*\n\z/)
|
|
24
|
+
[Template.blank(indent + rest), RUBY]
|
|
25
|
+
elsif rest.match?(/\A\w+:[ \t]*\n\z/)
|
|
26
|
+
[Template.blank(indent + rest), TEXT]
|
|
27
|
+
elsif (marker = rest[CODE] || rest[TAG_CODE])
|
|
28
|
+
[indent + Template.blank(marker) + rest[marker.length..]]
|
|
29
|
+
else
|
|
30
|
+
[Template.blank(indent + rest)]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "erb"
|
|
4
|
+
require_relative "haml"
|
|
5
|
+
require_relative "slim"
|
|
6
|
+
|
|
7
|
+
module SimpleCov
|
|
8
|
+
class Directive
|
|
9
|
+
# Hands a template's lines to the extractor for its language, so the
|
|
10
|
+
# directive scan sees only the Ruby a template holds, at the template's own
|
|
11
|
+
# line numbers. A file in no template language is Ruby already.
|
|
12
|
+
module Template
|
|
13
|
+
EXTRACTORS = {".erb" => Erb, ".haml" => Haml, ".slim" => Slim}.freeze
|
|
14
|
+
|
|
15
|
+
def self.template?(filename)
|
|
16
|
+
EXTRACTORS.key?(File.extname(filename))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.ruby_lines(filename, lines)
|
|
20
|
+
extractor = EXTRACTORS[File.extname(filename)]
|
|
21
|
+
extractor ? extractor.ruby_lines(lines) : lines
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.blank(text)
|
|
25
|
+
text.gsub(/[^\n]/, " ")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ripper"
|
|
4
|
+
require_relative "directive/template"
|
|
5
|
+
|
|
6
|
+
module SimpleCov
|
|
7
|
+
# Parses `# simplecov:disable` / `# simplecov:enable` directive comments, in
|
|
8
|
+
# both the block form (the directive is the entire comment on its own line,
|
|
9
|
+
# opening a region that runs until the matching enable) and the inline form
|
|
10
|
+
# (the directive trails real code, affecting that line alone).
|
|
11
|
+
#
|
|
12
|
+
# Categories are `:line`, `:branch`, and `:method`, combinable with commas.
|
|
13
|
+
# Omitting categories targets all three. Any text after the directive is a
|
|
14
|
+
# free-form reason and is discarded, so an unrecognised category name
|
|
15
|
+
# silently falls into the reason bucket and disables everything: a
|
|
16
|
+
# deliberate over-disable, so the typo is visible in the report rather than
|
|
17
|
+
# silently disabling nothing.
|
|
18
|
+
#
|
|
19
|
+
# Comment extraction goes through `Ripper.lex` so directive markers inside
|
|
20
|
+
# string literals or heredocs are correctly ignored.
|
|
21
|
+
class Directive
|
|
22
|
+
CATEGORIES = %i[line branch method].freeze
|
|
23
|
+
|
|
24
|
+
CATEGORY_PATTERN = "(?:#{CATEGORIES.join("|")})".freeze
|
|
25
|
+
CATEGORIES_PATTERN = "(?:#{CATEGORY_PATTERN}(?:\\s*,\\s*#{CATEGORY_PATTERN})*)".freeze
|
|
26
|
+
PATTERN = /
|
|
27
|
+
\#\s*simplecov\s*:\s*
|
|
28
|
+
(?<mode>disable|enable)\b
|
|
29
|
+
(?:\s+(?<categories>#{CATEGORIES_PATTERN})\b)?
|
|
30
|
+
.*?
|
|
31
|
+
\s*\z
|
|
32
|
+
/x
|
|
33
|
+
|
|
34
|
+
attr_reader :line_number, :mode, :categories
|
|
35
|
+
|
|
36
|
+
# The disabled line ranges per category. An unclosed `disable` block
|
|
37
|
+
# extends to the end of the file.
|
|
38
|
+
def self.disabled_ranges(lines)
|
|
39
|
+
ranges = CATEGORIES.to_h do |category|
|
|
40
|
+
empty = [] # : Array[Range[Integer]]
|
|
41
|
+
[category, empty]
|
|
42
|
+
end
|
|
43
|
+
open_starts = {} # : Hash[Symbol, Integer]
|
|
44
|
+
|
|
45
|
+
directives_in(lines).each { |directive| directive.apply(ranges, open_starts) }
|
|
46
|
+
open_starts.each { |category, start| ranges.fetch(category) << (start..lines.size) }
|
|
47
|
+
|
|
48
|
+
ranges
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Every directive in the file, in source order. Comments inside string
|
|
52
|
+
# literals or heredocs are skipped because Ripper.lex doesn't tag them as
|
|
53
|
+
# :on_comment tokens.
|
|
54
|
+
def self.directives_in(lines)
|
|
55
|
+
return [] unless source_might_contain_directive?(lines)
|
|
56
|
+
|
|
57
|
+
comments_in(lines).filter_map do |line_number, column, text|
|
|
58
|
+
parse_comment(lines, line_number, column, text)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Cheap pre-check, so files that obviously can't contain a directive are
|
|
63
|
+
# never tokenized.
|
|
64
|
+
def self.source_might_contain_directive?(lines)
|
|
65
|
+
lines.any? do |line|
|
|
66
|
+
line.include?("simplecov")
|
|
67
|
+
rescue ArgumentError, EncodingError
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.parse_comment(lines, line_number, column, text)
|
|
73
|
+
match = PATTERN.match(text)
|
|
74
|
+
return nil unless match
|
|
75
|
+
|
|
76
|
+
new(
|
|
77
|
+
line_number: line_number,
|
|
78
|
+
# The pattern admits exactly two modes, so asking whether this one is
|
|
79
|
+
# `disable` names the other by elimination. Comparing the captured text
|
|
80
|
+
# also keeps the nilable capture out of `to_sym`.
|
|
81
|
+
mode: match[:mode].eql?("disable") ? :disable : :enable,
|
|
82
|
+
categories: parse_categories(match[:categories]),
|
|
83
|
+
inline: inline?(lines, line_number, column + match.begin(0))
|
|
84
|
+
)
|
|
85
|
+
rescue ArgumentError, EncodingError
|
|
86
|
+
# The comment text contains an invalid byte sequence, say.
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The bare form targets every category, and answers the frozen constant
|
|
91
|
+
# itself: a directive's categories are only ever iterated.
|
|
92
|
+
def self.parse_categories(text)
|
|
93
|
+
return CATEGORIES if text.nil?
|
|
94
|
+
|
|
95
|
+
text.split(/\s*,\s*/).map(&:to_sym)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# `column` is the byte column of the directive's `#` in the source line,
|
|
99
|
+
# adjusted for any prefix that may precede it within the comment token.
|
|
100
|
+
def self.inline?(lines, line_number, column)
|
|
101
|
+
line = lines.at(line_number - 1).to_s
|
|
102
|
+
# Indentation is all a non-inline directive may have before it, so
|
|
103
|
+
# stripping the prefix from the left decides the question. Doing it from
|
|
104
|
+
# the right instead would raise on a line whose trailing bytes are
|
|
105
|
+
# invalid, which is a property of the code after the directive.
|
|
106
|
+
!line.byteslice(0, column).to_s.lstrip.empty?
|
|
107
|
+
rescue ArgumentError, EncodingError
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.comments_in(lines)
|
|
112
|
+
source = lines.map { |line| line.end_with?("\n") ? line : "#{line}\n" }.join
|
|
113
|
+
Ripper.lex(source).filter_map do |(line_number, column), type, text|
|
|
114
|
+
[line_number, column, text] if type.equal?(:on_comment)
|
|
115
|
+
end
|
|
116
|
+
rescue ArgumentError, EncodingError
|
|
117
|
+
[]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private_class_method :directives_in, :source_might_contain_directive?,
|
|
121
|
+
:parse_comment, :parse_categories, :inline?, :comments_in
|
|
122
|
+
|
|
123
|
+
def initialize(line_number:, mode:, categories:, inline:)
|
|
124
|
+
@line_number = line_number
|
|
125
|
+
@mode = mode
|
|
126
|
+
@categories = categories
|
|
127
|
+
@inline = inline
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def disabled?
|
|
131
|
+
mode.equal?(:disable)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def inline?
|
|
135
|
+
@inline
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Inline directives mark just their line; block disables open a region;
|
|
139
|
+
# block enables close one. Re-opening an already-open block is a no-op.
|
|
140
|
+
def apply(ranges, open_starts)
|
|
141
|
+
categories.each do |category|
|
|
142
|
+
if inline?
|
|
143
|
+
ranges.fetch(category) << (line_number..line_number) if disabled?
|
|
144
|
+
elsif disabled?
|
|
145
|
+
open_starts[category] ||= line_number
|
|
146
|
+
elsif (start = open_starts.delete(category))
|
|
147
|
+
ranges.fetch(category) << (start..line_number)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ExitCodes
|
|
5
|
+
# Fails when a file listed in the checked-in baseline drops below its own
|
|
6
|
+
# floor on both axes: a lower percent than the floor records and more misses
|
|
7
|
+
# than it allows. The second condition is the dampener that keeps an edit
|
|
8
|
+
# with no coverage change from failing the run; a percent-only floor is
|
|
9
|
+
# decided by the percent alone.
|
|
10
|
+
class BaselineCheck < Check
|
|
11
|
+
def initialize(result, baseline)
|
|
12
|
+
super(result, nil)
|
|
13
|
+
@baseline = baseline
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def exit_code
|
|
17
|
+
MINIMUM_COVERAGE
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def compute_violations
|
|
23
|
+
CoverageViolations.baseline(result, @baseline)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def violation_lines(violation)
|
|
27
|
+
message = format(
|
|
28
|
+
"%<criterion>s coverage (%<actual>s) dropped below its baseline floor (%<expected>s%%) in %<filename>s",
|
|
29
|
+
criterion: violation.fetch(:criterion).capitalize,
|
|
30
|
+
actual: Color.colorize_percent(violation.fetch(:actual)),
|
|
31
|
+
expected: violation.fetch(:expected),
|
|
32
|
+
filename: violation.fetch(:project_filename)
|
|
33
|
+
)
|
|
34
|
+
["#{message}#{missed_clause(violation)}."]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def missed_clause(violation)
|
|
38
|
+
allowed = violation.fetch(:allowed_missed)
|
|
39
|
+
return unless allowed
|
|
40
|
+
|
|
41
|
+
units = UNITS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion)))
|
|
42
|
+
" (#{violation.fetch(:actual_missed)} uncovered #{units}, #{allowed} allowed)"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ExitCodes
|
|
5
|
+
# Shared skeleton for the threshold checks. Subclasses supply `exit_code`,
|
|
6
|
+
# `compute_violations` (memoized here, since `failing?` and `report_lines`
|
|
7
|
+
# both consult it), and `violation_lines`.
|
|
8
|
+
class Check
|
|
9
|
+
def initialize(result, thresholds)
|
|
10
|
+
@result = result
|
|
11
|
+
@thresholds = thresholds
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def failing?
|
|
15
|
+
violations.any?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def violations
|
|
19
|
+
@violations ||= compute_violations
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Concatenated rather than flat-mapped: a check answering a bare string
|
|
23
|
+
# instead of its list of lines is an error this raises, not a spelling the
|
|
24
|
+
# flattening would forgive.
|
|
25
|
+
def report_lines
|
|
26
|
+
lines = [] #: Array[String]
|
|
27
|
+
violations.each_with_object(lines) { |violation, into| into.concat(violation_lines(violation)) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def report
|
|
31
|
+
report_lines.each { |line| ExitCodes.print_error(line) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
attr_reader :result, :thresholds
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -3,27 +3,46 @@
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module ExitCodes
|
|
5
5
|
module ExitCodeHandling
|
|
6
|
-
|
|
6
|
+
extend self
|
|
7
7
|
|
|
8
8
|
def call(result, coverage_limits:)
|
|
9
9
|
checks = coverage_checks(result, coverage_limits)
|
|
10
10
|
|
|
11
11
|
failing_check = checks.find(&:failing?)
|
|
12
12
|
if failing_check
|
|
13
|
-
failing_check.report
|
|
13
|
+
failing_check.report if SimpleCov.print_errors
|
|
14
14
|
failing_check.exit_code
|
|
15
15
|
else
|
|
16
|
-
|
|
16
|
+
SUCCESS
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def coverage_checks(result, coverage_limits)
|
|
21
21
|
[
|
|
22
22
|
MinimumOverallCoverageCheck.new(result, coverage_limits.minimum_coverage),
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
minimum_by_file_check(result, coverage_limits),
|
|
24
|
+
BaselineCheck.new(result, coverage_limits.baseline),
|
|
25
|
+
MinimumCoverageByGroupCheck.new(result, coverage_limits.minimum_coverage_by_group),
|
|
26
|
+
MaximumOverallCoverageCheck.new(result, coverage_limits.maximum_coverage),
|
|
27
|
+
MaximumCoverageDropCheck.new(result, coverage_limits.maximum_coverage_drop),
|
|
28
|
+
MaximumMissedCheck.new(result, coverage_limits.maximum_missed),
|
|
29
|
+
maximum_missed_per_file_check(result, coverage_limits)
|
|
25
30
|
]
|
|
26
31
|
end
|
|
32
|
+
|
|
33
|
+
def minimum_by_file_check(result, coverage_limits)
|
|
34
|
+
MinimumCoverageByFileCheck.new(
|
|
35
|
+
result, coverage_limits.minimum_coverage_by_file, coverage_limits.minimum_coverage_by_file_overrides,
|
|
36
|
+
baseline: coverage_limits.baseline
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def maximum_missed_per_file_check(result, coverage_limits)
|
|
41
|
+
MaximumMissedPerFileCheck.new(
|
|
42
|
+
result, coverage_limits.maximum_missed_per_file, coverage_limits.maximum_missed_per_file_overrides,
|
|
43
|
+
baseline: coverage_limits.baseline
|
|
44
|
+
)
|
|
45
|
+
end
|
|
27
46
|
end
|
|
28
47
|
end
|
|
29
48
|
end
|
|
@@ -2,81 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module ExitCodes
|
|
5
|
-
class MaximumCoverageDropCheck
|
|
6
|
-
def initialize(result, maximum_coverage_drop)
|
|
7
|
-
@result = result
|
|
8
|
-
@maximum_coverage_drop = maximum_coverage_drop
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def failing?
|
|
12
|
-
return false unless maximum_coverage_drop && last_run
|
|
13
|
-
|
|
14
|
-
coverage_drop_violations.any?
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def report
|
|
18
|
-
coverage_drop_violations.each do |violation|
|
|
19
|
-
$stderr.printf(
|
|
20
|
-
"%<criterion>s coverage has dropped by %<drop_percent>.2f%% since the last time (maximum allowed: %<max_drop>.2f%%).\n",
|
|
21
|
-
criterion: violation[:criterion].capitalize,
|
|
22
|
-
drop_percent: SimpleCov.round_coverage(violation[:drop_percent]),
|
|
23
|
-
max_drop: violation[:max_drop]
|
|
24
|
-
)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
5
|
+
class MaximumCoverageDropCheck < Check
|
|
28
6
|
def exit_code
|
|
29
|
-
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
attr_reader :result, :maximum_coverage_drop
|
|
35
|
-
|
|
36
|
-
def last_run
|
|
37
|
-
return @last_run if defined?(@last_run)
|
|
38
|
-
|
|
39
|
-
@last_run = SimpleCov::LastRun.read
|
|
7
|
+
MAXIMUM_COVERAGE_DROP
|
|
40
8
|
end
|
|
41
9
|
|
|
42
|
-
|
|
43
|
-
@coverage_drop_violations ||=
|
|
44
|
-
compute_coverage_drop_data.select do |achieved|
|
|
45
|
-
achieved.fetch(:max_drop) < achieved.fetch(:drop_percent)
|
|
46
|
-
end
|
|
47
|
-
end
|
|
10
|
+
private
|
|
48
11
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
{
|
|
52
|
-
criterion: criterion,
|
|
53
|
-
max_drop: percent,
|
|
54
|
-
drop_percent: drop_percent(criterion)
|
|
55
|
-
}
|
|
56
|
-
end
|
|
12
|
+
def compute_violations
|
|
13
|
+
CoverageViolations.maximum_drop(result, thresholds)
|
|
57
14
|
end
|
|
58
15
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
SimpleCov.round_coverage(
|
|
64
|
-
result.coverage_statistics.fetch(criterion).percent
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
# floats, I tell ya.
|
|
68
|
-
# irb(main):001:0* 80.01 - 80.0
|
|
69
|
-
# => 0.010000000000005116
|
|
70
|
-
drop.floor(MAX_DROP_ACCURACY)
|
|
16
|
+
# The drop percent is a delta, not a coverage level, so it has no natural
|
|
17
|
+
# green/yellow/red mapping and the whole line goes red.
|
|
18
|
+
def violation_lines(violation)
|
|
19
|
+
[Color.colorize(message_for(violation), :red)]
|
|
71
20
|
end
|
|
72
21
|
|
|
73
|
-
def
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
22
|
+
def message_for(violation)
|
|
23
|
+
format(
|
|
24
|
+
"%<criterion>s coverage has dropped by %<drop_percent>.2f%% since the last time " \
|
|
25
|
+
"(maximum allowed: %<max_drop>.2f%%).",
|
|
26
|
+
criterion: violation.fetch(:criterion).capitalize,
|
|
27
|
+
drop_percent: violation.fetch(:actual),
|
|
28
|
+
max_drop: violation.fetch(:maximum)
|
|
29
|
+
)
|
|
80
30
|
end
|
|
81
31
|
end
|
|
82
32
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ExitCodes
|
|
5
|
+
class MaximumMissedCheck < Check
|
|
6
|
+
def exit_code
|
|
7
|
+
MINIMUM_COVERAGE
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def compute_violations
|
|
13
|
+
CoverageViolations.maximum_missed(result, thresholds)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def violation_lines(violation)
|
|
17
|
+
[format(
|
|
18
|
+
"Missed %<units>s (%<actual>d) exceed the configured maximum_missed (%<maximum>d).",
|
|
19
|
+
units: UNITS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion))),
|
|
20
|
+
actual: violation.fetch(:actual),
|
|
21
|
+
maximum: violation.fetch(:maximum)
|
|
22
|
+
)]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|