Module: SimpleCov::CLI::Annotations
Overview
The --annotate KIND output shared by uncovered and patch: each command
reduces its answer to diagnostics (a path, a contiguous missed line range,
and the criterion that missed), and one emitter per CI host renders them in
that host's native inline-annotation channel, so a gap surfaces on the diff
with no upload step and no extra gem.
Constant Summary collapse
- KINDS =
%w[github gitlab rdjson azure teamcity buildkite].freeze
- MESSAGES =
{ line: "Not covered by tests", branch: "Branch not covered by tests", method: "Method not covered by tests" }.freeze
- DESCRIPTIONS =
{ line: "Lines the tests never executed", branch: "Branches the tests never took", method: "Methods the tests never called" }.freeze
- SOURCE =
{"name" => "simplecov", "url" => "https://github.com/simplecov-ruby/simplecov"}.freeze
- TEAMCITY_ESCAPES =
TeamCity's service message grammar and Azure's logging command grammar each reserve a few characters in attribute values.
{"|" => "||", "'" => "|'", "[" => "|[", "]" => "|]", "\n" => "|n", "\r" => "|r"}.freeze
- AZURE_ESCAPES =
{"%" => "%AZP25", ";" => "%3B", "]" => "%5D", "\n" => "%0A", "\r" => "%0D"}.freeze
Instance Method Summary collapse
-
#azure(stdout, diagnostics) ⇒ Object
Azure Pipelines logging commands carry one line number, so a range is spelled out in the message.
-
#buildkite(stdout, diagnostics) ⇒ Object
Buildkite has no per-line channel; its annotations are Markdown, so this is the body for
buildkite-agent annotate. - #described(diagnostic) ⇒ Object
- #diagnostics(path, missed, criterion) ⇒ Object
- #emit(stdout, kind, diagnostics) ⇒ Object
- #escape(text, escapes) ⇒ Object
- #expected_kinds ⇒ Object
- #fingerprint(diagnostic) ⇒ Object
- #github(stdout, diagnostics) ⇒ Object
-
#gitlab(diagnostics) ⇒ Object
GitLab's Code Quality report, a subset of the Code Climate spec, read from a
codequalityartifact for merge request annotations. - #issue(opts) ⇒ Object
- #range(diagnostic) ⇒ Object
-
#rdjson(diagnostics) ⇒ Object
reviewdog's Diagnostic Format, which reviewdog posts to whichever host it runs under.
- #rdjson_diagnostic(diagnostic) ⇒ Object
- #service_message(name, attributes) ⇒ Object
-
#teamcity(stdout, diagnostics) ⇒ Object
TeamCity's code inspection service messages: each criterion is one inspection type, declared once before its first inspection.
Instance Method Details
#azure(stdout, diagnostics) ⇒ Object
Azure Pipelines logging commands carry one line number, so a range is spelled out in the message.
116 117 118 119 120 121 |
# File 'lib/simplecov/cli/annotations.rb', line 116 def azure(stdout, diagnostics) diagnostics.each do |diagnostic| stdout.puts("##vso[task.logissue type=warning;sourcepath=#{escape(diagnostic.fetch(:path), AZURE_ESCAPES)};" \ "linenumber=#{diagnostic.fetch(:first)}]#{described(diagnostic)}") end end |
#buildkite(stdout, diagnostics) ⇒ Object
Buildkite has no per-line channel; its annotations are Markdown, so this
is the body for buildkite-agent annotate.
144 145 146 147 148 149 |
# File 'lib/simplecov/cli/annotations.rb', line 144 def buildkite(stdout, diagnostics) sections = diagnostics.group_by { |diagnostic| diagnostic.fetch(:message) }.map do |, group| (["#### #{message}"] + group.map { |diagnostic| "- `#{diagnostic.fetch(:path)}:#{range(diagnostic)}`" }).join("\n") end stdout.puts(sections.join("\n\n")) unless sections.empty? end |
#described(diagnostic) ⇒ Object
151 152 153 154 155 |
# File 'lib/simplecov/cli/annotations.rb', line 151 def described(diagnostic) return diagnostic.fetch(:message) if diagnostic.fetch(:first).equal?(diagnostic.fetch(:last)) "#{diagnostic.fetch(:message)} (lines #{range(diagnostic)})" end |
#diagnostics(path, missed, criterion) ⇒ Object
50 51 52 53 54 |
# File 'lib/simplecov/cli/annotations.rb', line 50 def diagnostics(path, missed, criterion) missed.slice_when { |previous, current| current > previous + 1 }.map do |run| {path: path, first: run.first, last: run.last, criterion: criterion, message: MESSAGES.fetch(criterion)} end end |
#emit(stdout, kind, diagnostics) ⇒ Object
56 57 58 59 60 61 62 63 64 65 |
# File 'lib/simplecov/cli/annotations.rb', line 56 def emit(stdout, kind, diagnostics) case kind when "github" then github(stdout, diagnostics) when "gitlab" then stdout.puts(JSON.pretty_generate(gitlab(diagnostics))) when "rdjson" then stdout.puts(JSON.pretty_generate(rdjson(diagnostics))) when "azure" then azure(stdout, diagnostics) when "teamcity" then teamcity(stdout, diagnostics) else buildkite(stdout, diagnostics) end end |
#escape(text, escapes) ⇒ Object
161 162 163 |
# File 'lib/simplecov/cli/annotations.rb', line 161 def escape(text, escapes) text.gsub(Regexp.union(escapes.keys), escapes) end |
#expected_kinds ⇒ Object
45 46 47 48 |
# File 'lib/simplecov/cli/annotations.rb', line 45 def expected_kinds *rest, last = KINDS "#{rest.join(", ")}, or #{last}" end |
#fingerprint(diagnostic) ⇒ Object
91 92 93 94 |
# File 'lib/simplecov/cli/annotations.rb', line 91 def fingerprint(diagnostic) Digest::SHA256.hexdigest("#{diagnostic.fetch(:path)}:#{diagnostic.fetch(:first)}-#{diagnostic.fetch(:last)}:" \ "#{diagnostic.fetch(:criterion)}") end |
#github(stdout, diagnostics) ⇒ Object
67 68 69 70 71 72 |
# File 'lib/simplecov/cli/annotations.rb', line 67 def github(stdout, diagnostics) diagnostics.each do |diagnostic| stdout.puts("::warning file=#{diagnostic.fetch(:path)},line=#{diagnostic.fetch(:first)}," \ "endLine=#{diagnostic.fetch(:last)}::#{diagnostic.fetch(:message)}") end end |
#gitlab(diagnostics) ⇒ Object
GitLab's Code Quality report, a subset of the Code Climate spec, read from
a codequality artifact for merge request annotations.
76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/simplecov/cli/annotations.rb', line 76 def gitlab(diagnostics) diagnostics.map do |diagnostic| { "description" => diagnostic.fetch(:message), "check_name" => "coverage", "fingerprint" => fingerprint(diagnostic), "severity" => "minor", "location" => { "path" => diagnostic.fetch(:path), "lines" => {"begin" => diagnostic.fetch(:first), "end" => diagnostic.fetch(:last)} } } end end |
#issue(opts) ⇒ Object
37 38 39 40 41 42 43 |
# File 'lib/simplecov/cli/annotations.rb', line 37 def issue(opts) kind = opts.fetch(:annotate) return nil unless kind return "unknown --annotate #{kind.inspect} (expected #{expected_kinds})" unless KINDS.include?(kind) "cannot combine --annotate with --json" if opts.fetch(:json) end |
#range(diagnostic) ⇒ Object
157 158 159 |
# File 'lib/simplecov/cli/annotations.rb', line 157 def range(diagnostic) [diagnostic.fetch(:first), diagnostic.fetch(:last)].uniq.join("-") end |
#rdjson(diagnostics) ⇒ Object
reviewdog's Diagnostic Format, which reviewdog posts to whichever host it runs under.
98 99 100 |
# File 'lib/simplecov/cli/annotations.rb', line 98 def rdjson(diagnostics) {"source" => SOURCE, "severity" => "WARNING", "diagnostics" => diagnostics.map { |d| rdjson_diagnostic(d) }} end |
#rdjson_diagnostic(diagnostic) ⇒ Object
102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/simplecov/cli/annotations.rb', line 102 def rdjson_diagnostic(diagnostic) { "message" => diagnostic.fetch(:message), "location" => { "path" => diagnostic.fetch(:path), "range" => {"start" => {"line" => diagnostic.fetch(:first)}, "end" => {"line" => diagnostic.fetch(:last)}} }, "severity" => "WARNING", "code" => {"value" => diagnostic.fetch(:criterion).to_s} } end |
#service_message(name, attributes) ⇒ Object
137 138 139 140 |
# File 'lib/simplecov/cli/annotations.rb', line 137 def (name, attributes) body = attributes.map { |key, value| "#{key}='#{escape(value.to_s, TEAMCITY_ESCAPES)}'" }.join(" ") "##teamcity[#{name} #{body}]" end |
#teamcity(stdout, diagnostics) ⇒ Object
TeamCity's code inspection service messages: each criterion is one inspection type, declared once before its first inspection.
125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/simplecov/cli/annotations.rb', line 125 def teamcity(stdout, diagnostics) diagnostics.group_by { |diagnostic| diagnostic.fetch(:criterion) }.each do |criterion, group| id = "simplecov.#{criterion}" stdout.puts(("inspectionType", id: id, name: MESSAGES.fetch(criterion), description: DESCRIPTIONS.fetch(criterion), category: "Code coverage")) group.each do |diagnostic| stdout.puts(("inspection", typeId: id, message: described(diagnostic), file: diagnostic.fetch(:path), line: diagnostic.fetch(:first), SEVERITY: "WARNING")) end end end |