simplecov 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/simplecov/cli/affected/runner.rb +38 -0
- data/lib/simplecov/cli/affected.rb +4 -5
- data/lib/simplecov/cli/annotations.rb +166 -0
- data/lib/simplecov/cli/clean.rb +2 -1
- data/lib/simplecov/cli/coverage_file.rb +2 -1
- data/lib/simplecov/cli/patch/output.rb +16 -0
- data/lib/simplecov/cli/patch.rb +8 -2
- data/lib/simplecov/cli/real_path.rb +41 -0
- data/lib/simplecov/cli/serve/static_file_handler.rb +5 -3
- 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/cli.rb +1 -0
- 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 +5 -7
- data/lib/simplecov/static_coverage_extractor.rb +27 -23
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov/view_coverage/template_compiler.rb +16 -7
- data/man/simplecov.1 +8 -3
- data/sig/simplecov.rbs +5 -0
- metadata +13 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c96bd7c97b97593cd5f90f20315a755ddbb9e25bd2b43ca54cc19fdf6c0540ac
|
|
4
|
+
data.tar.gz: 23b1b6392bfcf9119b2a5c575adf54568c47ef2c5838e11cbb77bffd72d61666
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3fb49f89386b147aa212d3b8814fb4afef021f394a2231c1e0649b5c35ea4e4ed14ba96cb9b8deacf1d6825629f56c55f900e3decaea287bff81f8a05d12b83f
|
|
7
|
+
data.tar.gz: a57ae0348b43869e6084866da885132c118acf88d83d967992d78aec222f8b6fab1ff6ab6572d3cc1ecdb2e701650aac9837ba45e963e64312ca8d3cdaaad1bd
|
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,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module CLI
|
|
7
|
+
module Affected
|
|
8
|
+
# Starts the runner at the repository root, since the selection's paths
|
|
9
|
+
# are relative to it, and answers the runner's exit status.
|
|
10
|
+
module SpawnedRunner
|
|
11
|
+
extend self
|
|
12
|
+
|
|
13
|
+
def call(command, root)
|
|
14
|
+
_, status = Process.wait2(spawn(*command, chdir: root))
|
|
15
|
+
status.exitstatus || 1
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# JRuby on Windows answers Process.wait2 with a nil status and ignores
|
|
20
|
+
# the chdir option Kernel#system takes, so there the runner starts
|
|
21
|
+
# inside Dir.chdir, and Kernel#system reports its status.
|
|
22
|
+
module SystemRunner
|
|
23
|
+
extend self
|
|
24
|
+
|
|
25
|
+
def call(command, root)
|
|
26
|
+
ran = Dir.chdir(root) { system(*command) }
|
|
27
|
+
raise Errno::ENOENT, command.first if ran.nil?
|
|
28
|
+
|
|
29
|
+
$CHILD_STATUS.exitstatus || 1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# simplecov:disable branch — fixed by the running engine and OS
|
|
34
|
+
RUNNER = (RUBY_ENGINE.eql?("jruby") && Gem.win_platform?) ? SystemRunner : SpawnedRunner
|
|
35
|
+
# simplecov:enable branch
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -5,6 +5,7 @@ require "optparse"
|
|
|
5
5
|
require_relative "command_helpers"
|
|
6
6
|
require_relative "tests"
|
|
7
7
|
require_relative "affected/changed_files"
|
|
8
|
+
require_relative "affected/runner"
|
|
8
9
|
require_relative "affected/selection"
|
|
9
10
|
|
|
10
11
|
module SimpleCov
|
|
@@ -133,12 +134,10 @@ module SimpleCov
|
|
|
133
134
|
run_command(opts.fetch(:run) + selection.fetch(:tests), opts.fetch(:root), stderr)
|
|
134
135
|
end
|
|
135
136
|
|
|
136
|
-
# The
|
|
137
|
-
#
|
|
138
|
-
# every engine's exception message carries it.
|
|
137
|
+
# The command is named explicitly in the failure because not every
|
|
138
|
+
# engine's exception message carries it.
|
|
139
139
|
def run_command(command, root, stderr)
|
|
140
|
-
|
|
141
|
-
status.exitstatus || 1
|
|
140
|
+
RUNNER.call(command, root)
|
|
142
141
|
rescue SystemCallError => e
|
|
143
142
|
stderr.puts("simplecov affected: cannot run #{command.first.inspect} (#{e})")
|
|
144
143
|
127
|
|
@@ -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
|
data/lib/simplecov/cli/clean.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "optparse"
|
|
5
5
|
require_relative "command_helpers"
|
|
6
|
+
require_relative "real_path"
|
|
6
7
|
|
|
7
8
|
module SimpleCov
|
|
8
9
|
module CLI
|
|
@@ -68,7 +69,7 @@ module SimpleCov
|
|
|
68
69
|
end
|
|
69
70
|
|
|
70
71
|
def canonical_path(path)
|
|
71
|
-
|
|
72
|
+
REAL_PATHS.realpath(path)
|
|
72
73
|
rescue SystemCallError
|
|
73
74
|
File.expand_path(path)
|
|
74
75
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../coverage_json"
|
|
4
|
+
require_relative "real_path"
|
|
4
5
|
|
|
5
6
|
module SimpleCov
|
|
6
7
|
module CLI
|
|
@@ -55,7 +56,7 @@ module SimpleCov
|
|
|
55
56
|
|
|
56
57
|
# A key whose file no longer exists keeps its literal spelling.
|
|
57
58
|
def normalize(key)
|
|
58
|
-
|
|
59
|
+
REAL_PATHS.realdirpath(key)
|
|
59
60
|
rescue SystemCallError
|
|
60
61
|
key
|
|
61
62
|
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
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module CLI
|
|
5
|
+
# The JDK's answer to File.realpath and File.realdirpath, for JRuby. On
|
|
6
|
+
# Windows JRuby's own versions follow no symlinks, and realdirpath accepts
|
|
7
|
+
# a directory that does not exist, so the CLI's containment and identity
|
|
8
|
+
# checks would compare paths that were never resolved. Path#toRealPath has
|
|
9
|
+
# neither problem on any OS. The path is expanded first because the JDK
|
|
10
|
+
# resolves a relative path against the JVM's working directory, which
|
|
11
|
+
# Dir.chdir does not move.
|
|
12
|
+
#
|
|
13
|
+
# simplecov:disable — JRuby-only; this suite's coverage is measured on CRuby
|
|
14
|
+
module JDKRealPath
|
|
15
|
+
extend self
|
|
16
|
+
|
|
17
|
+
# mutant:disable — JRuby-only, unreachable from the engine mutant runs on
|
|
18
|
+
def realpath(path)
|
|
19
|
+
expanded = File.expand_path(path)
|
|
20
|
+
java.nio.file.Paths.get(expanded).toRealPath.toString.tr("\\", "/") # steep:ignore NoMethod
|
|
21
|
+
rescue java.nio.file.NoSuchFileException, java.nio.file.NotDirectoryException # steep:ignore NoMethod
|
|
22
|
+
raise Errno::ENOENT, expanded
|
|
23
|
+
rescue java.nio.file.AccessDeniedException # steep:ignore NoMethod
|
|
24
|
+
raise Errno::EACCES, expanded
|
|
25
|
+
rescue java.nio.file.InvalidPathException # steep:ignore NoMethod
|
|
26
|
+
raise Errno::EINVAL, expanded
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# mutant:disable — JRuby-only, unreachable from the engine mutant runs on
|
|
30
|
+
def realdirpath(path)
|
|
31
|
+
expanded = File.expand_path(path)
|
|
32
|
+
return realpath(expanded) if File.exist?(expanded)
|
|
33
|
+
|
|
34
|
+
File.join(realpath(File.dirname(expanded)), File.basename(expanded))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
# simplecov:enable
|
|
38
|
+
|
|
39
|
+
REAL_PATHS = RUBY_ENGINE.eql?("jruby") ? JDKRealPath : File # simplecov:disable branch — fixed by the running engine
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../real_path"
|
|
4
|
+
|
|
3
5
|
module SimpleCov
|
|
4
6
|
module CLI
|
|
5
7
|
module Serve
|
|
@@ -93,7 +95,7 @@ module SimpleCov
|
|
|
93
95
|
# added, it must happen BEFORE the `inside?` check.
|
|
94
96
|
def resolve(request_path, root)
|
|
95
97
|
path = request_path.split("?").first.to_s.delete_prefix("/")
|
|
96
|
-
absolute_root =
|
|
98
|
+
absolute_root = REAL_PATHS.realpath(root)
|
|
97
99
|
candidate = File.expand_path(path, absolute_root)
|
|
98
100
|
# Rejected before touching disk, so traversal and absolute-path attempts
|
|
99
101
|
# are 403, not 404.
|
|
@@ -104,10 +106,10 @@ module SimpleCov
|
|
|
104
106
|
|
|
105
107
|
# Symlinks are resolved last and re-checked: a file inside root could be
|
|
106
108
|
# a symlink pointing outside.
|
|
107
|
-
real =
|
|
109
|
+
real = REAL_PATHS.realpath(candidate)
|
|
108
110
|
inside?(real, absolute_root) ? real : :forbidden
|
|
109
111
|
rescue Errno::ENOENT
|
|
110
|
-
# TOCTOU: candidate vanished between File.file? and
|
|
112
|
+
# TOCTOU: candidate vanished between File.file? and resolving it.
|
|
111
113
|
nil
|
|
112
114
|
end
|
|
113
115
|
|
|
@@ -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:
|
data/lib/simplecov/cli.rb
CHANGED
|
@@ -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
|