simplecov 1.2.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/README.md +1 -1
- data/lib/simplecov/cli/annotations.rb +166 -0
- data/lib/simplecov/cli/patch/output.rb +16 -0
- data/lib/simplecov/cli/patch.rb +8 -2
- data/lib/simplecov/cli/uncovered/misses.rb +5 -9
- data/lib/simplecov/cli/uncovered.rb +6 -20
- data/lib/simplecov/cli/usage.rb +2 -1
- data/lib/simplecov/configuration/eval_coverage.rb +1 -1
- data/lib/simplecov/configuration/view_coverage.rb +2 -3
- data/lib/simplecov/coverage_violations.rb +4 -2
- 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 +1 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +12 -12
- data/lib/simplecov/production.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +4 -4
- data/lib/simplecov/profiles/strict.rb +3 -3
- data/lib/simplecov/result.rb +6 -0
- data/lib/simplecov/result_processing.rb +20 -5
- data/lib/simplecov/simulate_coverage.rb +2 -2
- data/lib/simplecov/source_file/builder_context.rb +7 -0
- data/lib/simplecov/source_file/skip_chunks.rb +11 -2
- data/lib/simplecov/source_file/statistics.rb +16 -6
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +9 -42
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +1 -5
- data/lib/simplecov/static_coverage_extractor/visitor.rb +4 -7
- data/lib/simplecov/static_coverage_extractor.rb +6 -22
- data/lib/simplecov/version.rb +1 -1
- data/man/simplecov.1 +8 -3
- data/sig/simplecov.rbs +5 -0
- metadata +10 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d476eb77902a3475da8b97f4af5d61fc3ff9fd0829221447b78b82581ce7ddff
|
|
4
|
+
data.tar.gz: 5ad815e1bdc6c010579d550fbea11aaa860c2c92f2f4f77cca3824a611f1b38e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c3de21fa0caf77d6bb4154094be10b9bca3c30155b27b8a4679791bea6221dd09c9cd88eeb0ca3dc0e4959646459cf10d21ec8db234a1ad0f06197bdaa1a0e3
|
|
7
|
+
data.tar.gz: 6264dd029949453bdaf3a94ad893b647f40424eccbae4d0e9485be26e28d6c5ff92bdcff0a4e0f7acbb95b9afa5710df4ef6bca1230c888cbe4f99122ce63cb0
|
data/README.md
CHANGED
|
@@ -212,7 +212,7 @@ Both commands are documented in [the CLI docs](docs/CLI.md).
|
|
|
212
212
|
|
|
213
213
|
View templates execute real logic, and now they can be part of the report.
|
|
214
214
|
`cover_views` brings ERB, Haml, and Slim templates in, measured through eval
|
|
215
|
-
coverage
|
|
215
|
+
coverage:
|
|
216
216
|
|
|
217
217
|
```ruby
|
|
218
218
|
SimpleCov.start 'rails' do
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module SimpleCov
|
|
7
|
+
module CLI
|
|
8
|
+
# The `--annotate KIND` output shared by `uncovered` and `patch`: each command
|
|
9
|
+
# reduces its answer to diagnostics (a path, a contiguous missed line range,
|
|
10
|
+
# and the criterion that missed), and one emitter per CI host renders them in
|
|
11
|
+
# that host's native inline-annotation channel, so a gap surfaces on the diff
|
|
12
|
+
# with no upload step and no extra gem.
|
|
13
|
+
module Annotations
|
|
14
|
+
KINDS = %w[github gitlab rdjson azure teamcity buildkite].freeze
|
|
15
|
+
|
|
16
|
+
MESSAGES = {
|
|
17
|
+
line: "Not covered by tests",
|
|
18
|
+
branch: "Branch not covered by tests",
|
|
19
|
+
method: "Method not covered by tests"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
DESCRIPTIONS = {
|
|
23
|
+
line: "Lines the tests never executed",
|
|
24
|
+
branch: "Branches the tests never took",
|
|
25
|
+
method: "Methods the tests never called"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
SOURCE = {"name" => "simplecov", "url" => "https://github.com/simplecov-ruby/simplecov"}.freeze
|
|
29
|
+
|
|
30
|
+
# TeamCity's service message grammar and Azure's logging command grammar
|
|
31
|
+
# each reserve a few characters in attribute values.
|
|
32
|
+
TEAMCITY_ESCAPES = {"|" => "||", "'" => "|'", "[" => "|[", "]" => "|]", "\n" => "|n", "\r" => "|r"}.freeze
|
|
33
|
+
AZURE_ESCAPES = {"%" => "%AZP25", ";" => "%3B", "]" => "%5D", "\n" => "%0A", "\r" => "%0D"}.freeze
|
|
34
|
+
|
|
35
|
+
extend self
|
|
36
|
+
|
|
37
|
+
def issue(opts)
|
|
38
|
+
kind = opts.fetch(:annotate)
|
|
39
|
+
return nil unless kind
|
|
40
|
+
return "unknown --annotate #{kind.inspect} (expected #{expected_kinds})" unless KINDS.include?(kind)
|
|
41
|
+
|
|
42
|
+
"cannot combine --annotate with --json" if opts.fetch(:json)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def expected_kinds
|
|
46
|
+
*rest, last = KINDS
|
|
47
|
+
"#{rest.join(", ")}, or #{last}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def diagnostics(path, missed, criterion)
|
|
51
|
+
missed.slice_when { |previous, current| current > previous + 1 }.map do |run|
|
|
52
|
+
{path: path, first: run.first, last: run.last, criterion: criterion, message: MESSAGES.fetch(criterion)}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def emit(stdout, kind, diagnostics)
|
|
57
|
+
case kind
|
|
58
|
+
when "github" then github(stdout, diagnostics)
|
|
59
|
+
when "gitlab" then stdout.puts(JSON.pretty_generate(gitlab(diagnostics)))
|
|
60
|
+
when "rdjson" then stdout.puts(JSON.pretty_generate(rdjson(diagnostics)))
|
|
61
|
+
when "azure" then azure(stdout, diagnostics)
|
|
62
|
+
when "teamcity" then teamcity(stdout, diagnostics)
|
|
63
|
+
else buildkite(stdout, diagnostics)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def github(stdout, diagnostics)
|
|
68
|
+
diagnostics.each do |diagnostic|
|
|
69
|
+
stdout.puts("::warning file=#{diagnostic.fetch(:path)},line=#{diagnostic.fetch(:first)}," \
|
|
70
|
+
"endLine=#{diagnostic.fetch(:last)}::#{diagnostic.fetch(:message)}")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# GitLab's Code Quality report, a subset of the Code Climate spec, read from
|
|
75
|
+
# a `codequality` artifact for merge request annotations.
|
|
76
|
+
def gitlab(diagnostics)
|
|
77
|
+
diagnostics.map do |diagnostic|
|
|
78
|
+
{
|
|
79
|
+
"description" => diagnostic.fetch(:message),
|
|
80
|
+
"check_name" => "coverage",
|
|
81
|
+
"fingerprint" => fingerprint(diagnostic),
|
|
82
|
+
"severity" => "minor",
|
|
83
|
+
"location" => {
|
|
84
|
+
"path" => diagnostic.fetch(:path),
|
|
85
|
+
"lines" => {"begin" => diagnostic.fetch(:first), "end" => diagnostic.fetch(:last)}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def fingerprint(diagnostic)
|
|
92
|
+
Digest::SHA256.hexdigest("#{diagnostic.fetch(:path)}:#{diagnostic.fetch(:first)}-#{diagnostic.fetch(:last)}:" \
|
|
93
|
+
"#{diagnostic.fetch(:criterion)}")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# reviewdog's Diagnostic Format, which reviewdog posts to whichever host it
|
|
97
|
+
# runs under.
|
|
98
|
+
def rdjson(diagnostics)
|
|
99
|
+
{"source" => SOURCE, "severity" => "WARNING", "diagnostics" => diagnostics.map { |d| rdjson_diagnostic(d) }}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def rdjson_diagnostic(diagnostic)
|
|
103
|
+
{
|
|
104
|
+
"message" => diagnostic.fetch(:message),
|
|
105
|
+
"location" => {
|
|
106
|
+
"path" => diagnostic.fetch(:path),
|
|
107
|
+
"range" => {"start" => {"line" => diagnostic.fetch(:first)}, "end" => {"line" => diagnostic.fetch(:last)}}
|
|
108
|
+
},
|
|
109
|
+
"severity" => "WARNING",
|
|
110
|
+
"code" => {"value" => diagnostic.fetch(:criterion).to_s}
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Azure Pipelines logging commands carry one line number, so a range is
|
|
115
|
+
# spelled out in the message.
|
|
116
|
+
def azure(stdout, diagnostics)
|
|
117
|
+
diagnostics.each do |diagnostic|
|
|
118
|
+
stdout.puts("##vso[task.logissue type=warning;sourcepath=#{escape(diagnostic.fetch(:path), AZURE_ESCAPES)};" \
|
|
119
|
+
"linenumber=#{diagnostic.fetch(:first)}]#{described(diagnostic)}")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# TeamCity's code inspection service messages: each criterion is one
|
|
124
|
+
# inspection type, declared once before its first inspection.
|
|
125
|
+
def teamcity(stdout, diagnostics)
|
|
126
|
+
diagnostics.group_by { |diagnostic| diagnostic.fetch(:criterion) }.each do |criterion, group|
|
|
127
|
+
id = "simplecov.#{criterion}"
|
|
128
|
+
stdout.puts(service_message("inspectionType", id: id, name: MESSAGES.fetch(criterion),
|
|
129
|
+
description: DESCRIPTIONS.fetch(criterion), category: "Code coverage"))
|
|
130
|
+
group.each do |diagnostic|
|
|
131
|
+
stdout.puts(service_message("inspection", typeId: id, message: described(diagnostic),
|
|
132
|
+
file: diagnostic.fetch(:path), line: diagnostic.fetch(:first), SEVERITY: "WARNING"))
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def service_message(name, attributes)
|
|
138
|
+
body = attributes.map { |key, value| "#{key}='#{escape(value.to_s, TEAMCITY_ESCAPES)}'" }.join(" ")
|
|
139
|
+
"##teamcity[#{name} #{body}]"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Buildkite has no per-line channel; its annotations are Markdown, so this
|
|
143
|
+
# is the body for `buildkite-agent annotate`.
|
|
144
|
+
def buildkite(stdout, diagnostics)
|
|
145
|
+
sections = diagnostics.group_by { |diagnostic| diagnostic.fetch(:message) }.map do |message, group|
|
|
146
|
+
(["#### #{message}"] + group.map { |diagnostic| "- `#{diagnostic.fetch(:path)}:#{range(diagnostic)}`" }).join("\n")
|
|
147
|
+
end
|
|
148
|
+
stdout.puts(sections.join("\n\n")) unless sections.empty?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def described(diagnostic)
|
|
152
|
+
return diagnostic.fetch(:message) if diagnostic.fetch(:first).equal?(diagnostic.fetch(:last))
|
|
153
|
+
|
|
154
|
+
"#{diagnostic.fetch(:message)} (lines #{range(diagnostic)})"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def range(diagnostic)
|
|
158
|
+
[diagnostic.fetch(:first), diagnostic.fetch(:last)].uniq.join("-")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def escape(text, escapes)
|
|
162
|
+
text.gsub(Regexp.union(escapes.keys), escapes)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "../annotations"
|
|
4
5
|
|
|
5
6
|
module SimpleCov
|
|
6
7
|
module CLI
|
|
@@ -8,8 +9,23 @@ module SimpleCov
|
|
|
8
9
|
module Output
|
|
9
10
|
OPTIONAL_CRITERIA = %i[branch method].freeze
|
|
10
11
|
|
|
12
|
+
CRITERIA = %i[line branch method].freeze
|
|
13
|
+
|
|
11
14
|
extend self
|
|
12
15
|
|
|
16
|
+
# One diagnostic per contiguous missed range per measured criterion, in the
|
|
17
|
+
# CI host's own annotation form. Totals and the empty-change notice stay
|
|
18
|
+
# off stdout: the exit status under --minimum is the verdict.
|
|
19
|
+
def annotate(stdout, rows, kind)
|
|
20
|
+
diagnostics = rows.flat_map do |row|
|
|
21
|
+
CRITERIA.flat_map do |criterion|
|
|
22
|
+
stats = row.fetch(criterion)
|
|
23
|
+
stats ? Annotations.diagnostics(row.fetch(:file), stats.fetch(:missing), criterion) : []
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
Annotations.emit(stdout, kind, diagnostics)
|
|
27
|
+
end
|
|
28
|
+
|
|
13
29
|
def emit(stdout, rows, opts)
|
|
14
30
|
if opts.fetch(:json)
|
|
15
31
|
stdout.puts(JSON.pretty_generate(json_rows(rows)))
|
data/lib/simplecov/cli/patch.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "annotations"
|
|
3
4
|
require_relative "command_helpers"
|
|
4
5
|
require_relative "patch/changed_lines"
|
|
5
6
|
require_relative "patch/output"
|
|
@@ -35,20 +36,25 @@ module SimpleCov
|
|
|
35
36
|
return 1 unless diffed
|
|
36
37
|
|
|
37
38
|
rows = compute_rows(opts.fetch(:coverage), diffed, stderr)
|
|
38
|
-
|
|
39
|
+
kind = opts.fetch(:annotate)
|
|
40
|
+
kind ? Output.annotate(stdout, rows, kind) : Output.emit(stdout, rows, opts)
|
|
39
41
|
Output.gate(rows, opts.fetch(:minimum))
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def parse(args, stderr)
|
|
43
45
|
# No `base:` default: the run fills it in from the repository when the
|
|
44
46
|
# option is left out.
|
|
45
|
-
opts, rest = parse_common(args, find_renames: false, minimum: nil) do |parser, options|
|
|
47
|
+
opts, rest = parse_common(args, find_renames: false, minimum: nil, annotate: nil) do |parser, options|
|
|
46
48
|
parser.on("--base REF") { |v| options[:base] = v }
|
|
47
49
|
parser.on("--minimum N", Float) { |v| options[:minimum] = v }
|
|
48
50
|
parser.on("--find-renames") { options[:find_renames] = true }
|
|
51
|
+
parser.on("--annotate KIND") { |v| options[:annotate] = v }
|
|
49
52
|
end
|
|
50
53
|
return unless positional_ok?(rest, stderr)
|
|
51
54
|
|
|
55
|
+
issue = Annotations.issue(opts)
|
|
56
|
+
return error_nil(stderr, issue) if issue
|
|
57
|
+
|
|
52
58
|
opts[:coverage] = CoverageFile.load_coverage(opts.fetch(:input), command: "patch", stderr: stderr) or return nil
|
|
53
59
|
opts
|
|
54
60
|
end
|
|
@@ -20,16 +20,12 @@ module SimpleCov
|
|
|
20
20
|
missed.uniq.sort
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
files.each do |fname, _pct, _covered, _total, missed|
|
|
28
|
-
path = fname.delete_prefix("#{File.expand_path(SimpleCov.root)}/")
|
|
29
|
-
missed.slice_when { |previous, current| current > previous + 1 }.each do |run|
|
|
30
|
-
stdout.puts("::warning file=#{path},line=#{run.first},endLine=#{run.last}::Not covered by tests")
|
|
31
|
-
end
|
|
23
|
+
def annotate(stdout, files, criterion, kind)
|
|
24
|
+
root = "#{File.expand_path(SimpleCov.root)}/"
|
|
25
|
+
diagnostics = files.flat_map do |fname, _pct, _covered, _total, missed|
|
|
26
|
+
Annotations.diagnostics(fname.delete_prefix(root), missed, criterion)
|
|
32
27
|
end
|
|
28
|
+
Annotations.emit(stdout, kind, diagnostics)
|
|
33
29
|
end
|
|
34
30
|
end
|
|
35
31
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "optparse"
|
|
5
|
+
require_relative "annotations"
|
|
5
6
|
require_relative "command_helpers"
|
|
6
7
|
require_relative "patch/output"
|
|
7
8
|
require_relative "show/annotator"
|
|
@@ -21,7 +22,7 @@ module SimpleCov
|
|
|
21
22
|
|
|
22
23
|
def run(args, stdout:, stderr:, **)
|
|
23
24
|
opts = parse(args)
|
|
24
|
-
issue =
|
|
25
|
+
issue = Annotations.issue(opts)
|
|
25
26
|
return error(stderr, issue) if issue
|
|
26
27
|
|
|
27
28
|
keys = CoverageFile::CRITERIA[opts.fetch(:criterion)]
|
|
@@ -31,28 +32,11 @@ module SimpleCov
|
|
|
31
32
|
report(opts, keys, stdout, stderr)
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
def precheck(opts)
|
|
35
|
-
return nil unless opts.fetch(:annotate)
|
|
36
|
-
unless opts.fetch(:annotate).eql?("github")
|
|
37
|
-
return "unknown --annotate #{opts.fetch(:annotate).inspect} (only github is supported)"
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
"cannot combine --annotate with --json" if opts.fetch(:json)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
35
|
def report(opts, keys, stdout, stderr)
|
|
44
36
|
coverage = CoverageFile.load_coverage(opts.fetch(:input), command: "uncovered", stderr: stderr)
|
|
45
37
|
return 1 unless coverage
|
|
46
38
|
|
|
47
|
-
|
|
48
|
-
return empty(opts, stdout) if files.empty?
|
|
49
|
-
|
|
50
|
-
emit(stdout, files, opts)
|
|
51
|
-
0
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def empty(opts, stdout)
|
|
55
|
-
stdout.puts(empty_message(opts.fetch(:json))) unless opts.fetch(:annotate)
|
|
39
|
+
emit(stdout, rank(coverage, opts, keys).first(opts.fetch(:top)), opts)
|
|
56
40
|
0
|
|
57
41
|
end
|
|
58
42
|
|
|
@@ -62,7 +46,9 @@ module SimpleCov
|
|
|
62
46
|
end
|
|
63
47
|
|
|
64
48
|
def emit(stdout, files, opts)
|
|
65
|
-
|
|
49
|
+
kind = opts.fetch(:annotate)
|
|
50
|
+
return Misses.annotate(stdout, files, opts.fetch(:criterion), kind) if kind
|
|
51
|
+
return stdout.puts(empty_message(opts.fetch(:json))) if files.empty?
|
|
66
52
|
|
|
67
53
|
opts.fetch(:json) ? emit_json(stdout, files) : emit_text(stdout, files, CLI.color_enabled?(opts, stdout))
|
|
68
54
|
end
|
data/lib/simplecov/cli/usage.rb
CHANGED
|
@@ -89,7 +89,7 @@ module SimpleCov
|
|
|
89
89
|
--top N Show at most N files (default: 10)
|
|
90
90
|
--criterion C line, branch, or method (default: line)
|
|
91
91
|
--missing Append the missed line ranges to each row
|
|
92
|
-
--annotate
|
|
92
|
+
--annotate KIND Emit CI annotations instead of rows: github, gitlab, rdjson, azure, teamcity, or buildkite
|
|
93
93
|
--json Emit results as a JSON array (for CI)
|
|
94
94
|
|
|
95
95
|
tests options:
|
|
@@ -116,6 +116,7 @@ module SimpleCov
|
|
|
116
116
|
--base REF Diff against the merge-base of REF for the touched lines (default: origin's HEAD, else main, or in CI the PR's target branch)
|
|
117
117
|
--minimum N Exit non-zero when patch coverage on any measured criterion is below N%
|
|
118
118
|
--find-renames Follow a renamed file instead of counting the moved file as all-new
|
|
119
|
+
--annotate KIND Emit CI annotations instead of rows: github, gitlab, rdjson, azure, teamcity, or buildkite
|
|
119
120
|
--json Emit results as a JSON array (for CI)
|
|
120
121
|
|
|
121
122
|
dead-code options:
|
|
@@ -11,9 +11,8 @@ module SimpleCov
|
|
|
11
11
|
# them as empty.
|
|
12
12
|
#
|
|
13
13
|
# Templates the suite renders are measured through eval coverage, which this
|
|
14
|
-
# enables
|
|
15
|
-
#
|
|
16
|
-
# missing.
|
|
14
|
+
# enables. Templates it never renders are compiled at the end of the run so
|
|
15
|
+
# they appear at 0% instead of going missing.
|
|
17
16
|
def cover_views(*globs)
|
|
18
17
|
globs = globs.flatten.compact
|
|
19
18
|
globs = DEFAULT_VIEW_GLOBS.dup if globs.empty?
|
|
@@ -159,10 +159,12 @@ module SimpleCov
|
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
# The misconfiguration notice is enforcement output, not a Ruby warning: it
|
|
162
|
-
# must survive `-W0` and `Warning.warn` hooks, and honor `print_errors`.
|
|
162
|
+
# must survive `-W0` and `Warning.warn` hooks, and honor `print_errors`. A
|
|
163
|
+
# configured group that matched no files is absent from `result.groups`
|
|
164
|
+
# too, but has nothing to gate rather than nothing to look up.
|
|
163
165
|
def lookup_group(result, group_name)
|
|
164
166
|
group = result.groups[group_name]
|
|
165
|
-
if group.nil? && SimpleCov.print_errors
|
|
167
|
+
if group.nil? && SimpleCov.print_errors && !result.configured_group?(group_name)
|
|
166
168
|
ExitCodes.print_error "minimum_coverage_by_group: no group named '#{group_name}' exists. " \
|
|
167
169
|
"Available groups: #{result.groups.keys.join(", ")}"
|
|
168
170
|
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
|