simplecov 1.1.1 → 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/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/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 +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 +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 +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 +33 -0
- data/lib/simplecov/cli/uncovered.rb +58 -34
- 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 +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 +7 -11
- 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 +37 -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 +160 -65
- 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/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 +47 -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 +4 -15
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +11 -21
- 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 +74 -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 +92 -122
- 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 +13 -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 +29 -29
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +20 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +55 -139
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +9 -26
- 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 +45 -65
- data/lib/simplecov/static_coverage_extractor.rb +43 -75
- 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 +354 -0
- data/sig/simplecov.rbs +528 -595
- metadata +79 -6
data/lib/simplecov/defaults.rb
CHANGED
|
@@ -4,46 +4,27 @@ require "English"
|
|
|
4
4
|
require "pathname"
|
|
5
5
|
require_relative "formatter/html_formatter"
|
|
6
6
|
|
|
7
|
-
# Default configuration. Profiles autoload on first reference via
|
|
8
|
-
# `SimpleCov.profiles.fetch_proc`; the unused ones (e.g. "rails")
|
|
9
|
-
# never get required unless a user opts in.
|
|
10
7
|
SimpleCov.configure do
|
|
11
8
|
formatter SimpleCov::Formatter::HTMLFormatter
|
|
12
9
|
|
|
13
10
|
load_profile "bundler_filter"
|
|
14
11
|
load_profile "hidden_filter"
|
|
15
|
-
# Exclude files outside of SimpleCov.root. Mirrors the early prune done
|
|
16
|
-
# by SimpleCov::UselessResultsRemover so the user-facing filter chain
|
|
17
|
-
# honors the same boundary; both share the regex.
|
|
18
12
|
load_profile "root_filter"
|
|
19
|
-
# Exclude test framework directories (`test/`, `spec/`, `features/`,
|
|
20
|
-
# `autotest/`). The test suite runs 100% of the test files themselves,
|
|
21
|
-
# which inflates totals and obscures the application coverage that
|
|
22
|
-
# actually matters. Drop with `remove_filter %r{\A(test|features|spec|autotest)/}`
|
|
23
|
-
# if you want test files counted (e.g. to surface dead helpers).
|
|
24
13
|
load_profile "test_frameworks"
|
|
25
14
|
end
|
|
26
15
|
|
|
27
|
-
#
|
|
28
|
-
# The program name is kept separately as well: the join
|
|
29
|
-
# path containing a space, and the guesser reads the executable from it.
|
|
30
|
-
SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(
|
|
16
|
+
# Stashed as early as possible, before rake or test/unit tamper with ARGV
|
|
17
|
+
# (#110). The program name is kept separately as well: the join is lossy for
|
|
18
|
+
# a path containing a space, and the guesser reads the executable from it.
|
|
19
|
+
SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(" ")}"
|
|
31
20
|
SimpleCov::CommandGuesser.original_program_name = $PROGRAM_NAME
|
|
32
21
|
|
|
33
|
-
# Autoload config from ~/.simplecov if present
|
|
34
22
|
require_relative "load_global_config"
|
|
35
23
|
|
|
36
|
-
# Autoload config from .simplecov if present
|
|
37
|
-
# Recurse upwards until we find .simplecov or reach the root directory
|
|
38
|
-
|
|
39
24
|
config_path = Pathname.new(SimpleCov.root)
|
|
40
25
|
loop do
|
|
41
26
|
filename = config_path.join(".simplecov")
|
|
42
27
|
if filename.exist?
|
|
43
|
-
# `.simplecov` is a configuration file; SimpleCov.start calls inside
|
|
44
|
-
# it draw a deprecation warning, and tracking still begins for
|
|
45
|
-
# backward compatibility. See `SimpleCov.with_dot_simplecov_autoload`
|
|
46
|
-
# and issue #581.
|
|
47
28
|
SimpleCov.with_dot_simplecov_autoload do
|
|
48
29
|
load filename.to_s
|
|
49
30
|
rescue LoadError, StandardError => e
|
|
@@ -1,45 +1,63 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "configuration_error"
|
|
4
|
+
|
|
3
5
|
module SimpleCov
|
|
4
|
-
# Emits legacy-API deprecation warnings, deduplicated by the source
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# to fix. See issue #1204.
|
|
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).
|
|
11
12
|
module Deprecation
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
# `
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
|
|
29
40
|
return unless emitted.add?(location || message)
|
|
30
41
|
|
|
31
42
|
Kernel.warn "#{"#{location}: " if location}[DEPRECATION] #{message}"
|
|
32
43
|
end
|
|
33
44
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
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.
|
|
36
54
|
def emitted
|
|
37
55
|
@emitted ||= Set.new
|
|
38
56
|
end
|
|
39
57
|
|
|
40
|
-
# @api private — reset emitted state between tests.
|
|
41
58
|
def reset!
|
|
42
59
|
@emitted = Set.new
|
|
60
|
+
@mode = :warn
|
|
43
61
|
end
|
|
44
62
|
end
|
|
45
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
|
data/lib/simplecov/directive.rb
CHANGED
|
@@ -1,44 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "ripper"
|
|
4
|
+
require_relative "directive/template"
|
|
4
5
|
|
|
5
6
|
module SimpleCov
|
|
6
|
-
# Parses `# simplecov:disable` / `# simplecov:enable` directive comments
|
|
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).
|
|
7
11
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# ...
|
|
15
|
-
# # simplecov:enable line
|
|
16
|
-
#
|
|
17
|
-
# Inline form (the directive trails real code on the same line) only affects
|
|
18
|
-
# that single line and does not need to be re-enabled:
|
|
19
|
-
#
|
|
20
|
-
# raise "absurd" # simplecov:disable
|
|
21
|
-
#
|
|
22
|
-
# Categories are `:line`, `:branch`, and `:method`. They may be combined
|
|
23
|
-
# with commas. Omitting categories targets all three.
|
|
24
|
-
#
|
|
25
|
-
# Any text after the directive (and the optional category list) is treated
|
|
26
|
-
# as a free-form reason and discarded:
|
|
27
|
-
#
|
|
28
|
-
# # simplecov:disable line not worth testing this glue
|
|
29
|
-
#
|
|
30
|
-
# As a consequence, an unrecognised category name silently falls into the
|
|
31
|
-
# reason bucket. `# simplecov:disable cyclomatic` is parsed as the bare
|
|
32
|
-
# form (disable everything) with reason "cyclomatic" — a deliberate
|
|
33
|
-
# over-disable so the typo is visible in the report rather than silently
|
|
34
|
-
# disabling nothing.
|
|
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.
|
|
35
18
|
#
|
|
36
19
|
# Comment extraction goes through `Ripper.lex` so directive markers inside
|
|
37
20
|
# string literals or heredocs are correctly ignored.
|
|
38
21
|
class Directive
|
|
39
22
|
CATEGORIES = %i[line branch method].freeze
|
|
40
23
|
|
|
41
|
-
CATEGORY_PATTERN = "(?:#{CATEGORIES.join(
|
|
24
|
+
CATEGORY_PATTERN = "(?:#{CATEGORIES.join("|")})".freeze
|
|
42
25
|
CATEGORIES_PATTERN = "(?:#{CATEGORY_PATTERN}(?:\\s*,\\s*#{CATEGORY_PATTERN})*)".freeze
|
|
43
26
|
PATTERN = /
|
|
44
27
|
\#\s*simplecov\s*:\s*
|
|
@@ -50,11 +33,9 @@ module SimpleCov
|
|
|
50
33
|
|
|
51
34
|
attr_reader :line_number, :mode, :categories
|
|
52
35
|
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
def self.disabled_ranges(src_lines)
|
|
57
|
-
lines = src_lines.to_a
|
|
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)
|
|
58
39
|
ranges = CATEGORIES.to_h do |category|
|
|
59
40
|
empty = [] # : Array[Range[Integer]]
|
|
60
41
|
[category, empty]
|
|
@@ -62,14 +43,14 @@ module SimpleCov
|
|
|
62
43
|
open_starts = {} # : Hash[Symbol, Integer]
|
|
63
44
|
|
|
64
45
|
directives_in(lines).each { |directive| directive.apply(ranges, open_starts) }
|
|
65
|
-
open_starts.each { |category, start| ranges
|
|
46
|
+
open_starts.each { |category, start| ranges.fetch(category) << (start..lines.size) }
|
|
66
47
|
|
|
67
48
|
ranges
|
|
68
49
|
end
|
|
69
50
|
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
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.
|
|
73
54
|
def self.directives_in(lines)
|
|
74
55
|
return [] unless source_might_contain_directive?(lines)
|
|
75
56
|
|
|
@@ -78,13 +59,13 @@ module SimpleCov
|
|
|
78
59
|
end
|
|
79
60
|
end
|
|
80
61
|
|
|
81
|
-
# Cheap pre-check so
|
|
82
|
-
#
|
|
62
|
+
# Cheap pre-check, so files that obviously can't contain a directive are
|
|
63
|
+
# never tokenized.
|
|
83
64
|
def self.source_might_contain_directive?(lines)
|
|
84
65
|
lines.any? do |line|
|
|
85
66
|
line.include?("simplecov")
|
|
86
67
|
rescue ArgumentError, EncodingError
|
|
87
|
-
false
|
|
68
|
+
false
|
|
88
69
|
end
|
|
89
70
|
end
|
|
90
71
|
|
|
@@ -94,72 +75,76 @@ module SimpleCov
|
|
|
94
75
|
|
|
95
76
|
new(
|
|
96
77
|
line_number: line_number,
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
|
|
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,
|
|
100
82
|
categories: parse_categories(match[:categories]),
|
|
101
83
|
inline: inline?(lines, line_number, column + match.begin(0))
|
|
102
84
|
)
|
|
103
85
|
rescue ArgumentError, EncodingError
|
|
104
|
-
#
|
|
86
|
+
# The comment text contains an invalid byte sequence, say.
|
|
105
87
|
nil
|
|
106
88
|
end
|
|
107
89
|
|
|
90
|
+
# The bare form targets every category, and answers the frozen constant
|
|
91
|
+
# itself: a directive's categories are only ever iterated.
|
|
108
92
|
def self.parse_categories(text)
|
|
109
|
-
return CATEGORIES
|
|
93
|
+
return CATEGORIES if text.nil?
|
|
110
94
|
|
|
111
95
|
text.split(/\s*,\s*/).map(&:to_sym)
|
|
112
96
|
end
|
|
113
97
|
|
|
114
|
-
# Whether the directive sits after non-whitespace content on its line.
|
|
115
98
|
# `column` is the byte column of the directive's `#` in the source line,
|
|
116
|
-
# adjusted for any prefix that may precede it within the comment token
|
|
117
|
-
# (e.g., `# prefix # simplecov:disable line`).
|
|
99
|
+
# adjusted for any prefix that may precede it within the comment token.
|
|
118
100
|
def self.inline?(lines, line_number, column)
|
|
119
|
-
line = lines
|
|
120
|
-
|
|
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?
|
|
121
107
|
rescue ArgumentError, EncodingError
|
|
122
|
-
false
|
|
108
|
+
false
|
|
123
109
|
end
|
|
124
110
|
|
|
125
111
|
def self.comments_in(lines)
|
|
126
112
|
source = lines.map { |line| line.end_with?("\n") ? line : "#{line}\n" }.join
|
|
127
113
|
Ripper.lex(source).filter_map do |(line_number, column), type, text|
|
|
128
|
-
[line_number, column, text] if type
|
|
114
|
+
[line_number, column, text] if type.equal?(:on_comment)
|
|
129
115
|
end
|
|
130
116
|
rescue ArgumentError, EncodingError
|
|
131
|
-
[]
|
|
117
|
+
[]
|
|
132
118
|
end
|
|
133
119
|
|
|
134
120
|
private_class_method :directives_in, :source_might_contain_directive?,
|
|
135
|
-
|
|
121
|
+
:parse_comment, :parse_categories, :inline?, :comments_in
|
|
136
122
|
|
|
137
123
|
def initialize(line_number:, mode:, categories:, inline:)
|
|
138
124
|
@line_number = line_number
|
|
139
|
-
@mode
|
|
140
|
-
@categories
|
|
141
|
-
@inline
|
|
125
|
+
@mode = mode
|
|
126
|
+
@categories = categories
|
|
127
|
+
@inline = inline
|
|
142
128
|
end
|
|
143
129
|
|
|
144
130
|
def disabled?
|
|
145
|
-
mode
|
|
131
|
+
mode.equal?(:disable)
|
|
146
132
|
end
|
|
147
133
|
|
|
148
134
|
def inline?
|
|
149
135
|
@inline
|
|
150
136
|
end
|
|
151
137
|
|
|
152
|
-
# Apply this directive's effect to the in-flight per-category state.
|
|
153
138
|
# Inline directives mark just their line; block disables open a region;
|
|
154
139
|
# block enables close one. Re-opening an already-open block is a no-op.
|
|
155
140
|
def apply(ranges, open_starts)
|
|
156
141
|
categories.each do |category|
|
|
157
142
|
if inline?
|
|
158
|
-
ranges
|
|
143
|
+
ranges.fetch(category) << (line_number..line_number) if disabled?
|
|
159
144
|
elsif disabled?
|
|
160
145
|
open_starts[category] ||= line_number
|
|
161
146
|
elsif (start = open_starts.delete(category))
|
|
162
|
-
ranges
|
|
147
|
+
ranges.fetch(category) << (start..line_number)
|
|
163
148
|
end
|
|
164
149
|
end
|
|
165
150
|
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
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module ExitCodes
|
|
5
|
-
# Shared skeleton for the threshold checks
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# `compute_violations` (memoized here — `failing?` and `report` both
|
|
9
|
-
# consult it), and `report_violation`.
|
|
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`.
|
|
10
8
|
class Check
|
|
11
9
|
def initialize(result, thresholds)
|
|
12
10
|
@result = result
|
|
@@ -17,17 +15,25 @@ module SimpleCov
|
|
|
17
15
|
violations.any?
|
|
18
16
|
end
|
|
19
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
|
+
|
|
20
30
|
def report
|
|
21
|
-
|
|
31
|
+
report_lines.each { |line| ExitCodes.print_error(line) }
|
|
22
32
|
end
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
private
|
|
25
35
|
|
|
26
36
|
attr_reader :result, :thresholds
|
|
27
|
-
|
|
28
|
-
def violations
|
|
29
|
-
@violations ||= compute_violations
|
|
30
|
-
end
|
|
31
37
|
end
|
|
32
38
|
end
|
|
33
39
|
end
|