raider_reporter 0.7.0 → 0.8.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/CHANGELOG.md +8 -0
- data/README.md +23 -4
- data/lib/raider_reporter/cli.rb +20 -4
- data/lib/raider_reporter/collector.rb +32 -1
- data/lib/raider_reporter/configuration.rb +11 -1
- data/lib/raider_reporter/cucumber/formatter.rb +43 -0
- data/lib/raider_reporter/exec.rb +101 -0
- data/lib/raider_reporter/exec_summary.rb +46 -0
- data/lib/raider_reporter/models.rb +2 -2
- data/lib/raider_reporter/payload.rb +6 -2
- data/lib/raider_reporter/quarantine.rb +79 -0
- data/lib/raider_reporter/rspec/formatter.rb +24 -4
- data/lib/raider_reporter/version.rb +1 -1
- data/lib/raider_reporter/writers/html.rb +9 -4
- data/lib/raider_reporter.rb +25 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 300255531548132ae2fedcd7f10e8e9832925a204f5790ed8accb1d272ab64c6
|
|
4
|
+
data.tar.gz: ae60aacce4ecd140c8d771b92e8d83b704ec945a3339917d79fb7aeb50d49da9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0213a3b50dbeeedb77ac4e335afc632bf6a25ec815e875cabd4f54bed79fb4164b23770968f38911503055bdb6bc941460f379f08c7e34238f4c379740352f8e
|
|
7
|
+
data.tar.gz: 6c2c8759eee7dccfb81f93095d4a5c522b6029680ba999ab56073a91a76f5b7e7524da659c9350ea22f8185e1d9c1f294a75ee53aea4ceced898c36b699e0a8d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 — 2026-10-24
|
|
4
|
+
|
|
5
|
+
- Quarantine: with `RAIDER_QUARANTINE=1` (or `config.quarantine = true`) and a token and endpoint, the formatters fetch the project's quarantine list once per run (`GET /api/v1/quarantine`, 3-second timeout) and report matching failures as `quarantined`, keeping their message, backtrace and attachments. Fail-open: any fetch problem prints `[raider_reporter] quarantine list unavailable (<reason>); no tests quarantined` and nothing is quarantined
|
|
6
|
+
- `raider-reporter exec -- <command…>` runs the suite (no shell) and exits 0 when the command exited 1 and every failure the framework counted was quarantined, with no errors outside tests; it prints `raider-reporter: N quarantined failure(s) did not fail the build`. Otherwise it exits with the command's code (128 + signal when killed, 127 when the command cannot start, 2 without a command)
|
|
7
|
+
- Runs with a quarantined result are written as schema 2; every other run stays schema 1, so older Raider servers keep accepting them
|
|
8
|
+
- The HTML report shows quarantined results in their own section and color; `raider-reporter comment` reads `N quarantined` from the headline
|
|
9
|
+
- RSpec and Cucumber exit codes are unchanged without `exec`
|
|
10
|
+
|
|
3
11
|
## 0.7.0 — 2026-10-18
|
|
4
12
|
|
|
5
13
|
- Add a local HTML report: every run now also writes `raider-results/index.html`, one self-contained page (inline CSS and JS, no network requests) built from every `run*.json` in the folder, so shards from one machine share a report. Failures come first with their message, exception class, backtrace and screenshots; a filter box narrows by test name or file. Open it in a browser after a run, or publish it as a CI artifact
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# raider_reporter
|
|
2
2
|
|
|
3
|
-
Raider-schema test reporting for RSpec and Cucumber. `raider_reporter` is an RSpec formatter and a Cucumber reporter that records every example's result, failure details, screenshots and videos, writes them to a local `run.json` (schema v1), and can optionally upload the run to a Raider server. It replaces Allure for Ruby Raider projects.
|
|
3
|
+
Raider-schema test reporting for RSpec and Cucumber. `raider_reporter` is an RSpec formatter and a Cucumber reporter that records every example's result, failure details, screenshots and videos, writes them to a local `run.json` (schema v1, or v2 when a run has a quarantined result), and can optionally upload the run to a Raider server. It replaces Allure for Ruby Raider projects.
|
|
4
4
|
|
|
5
5
|
It has no runtime dependencies and requires Ruby 3.2 or newer.
|
|
6
6
|
|
|
@@ -66,7 +66,7 @@ cucumber --format pretty --format RaiderReporter::Cucumber::Formatter --out raid
|
|
|
66
66
|
- `identifier` is `"<file_path>:<line>"`: the scenario line, or the Examples row line for outline rows.
|
|
67
67
|
- `tags` are the feature, scenario and Examples tags, merged, sorted, unique, without `@`.
|
|
68
68
|
|
|
69
|
-
Statuses: passed, failed and ambiguous (reported as failed), pending and undefined (reported as pending), skipped.
|
|
69
|
+
Statuses: passed, failed and ambiguous (reported as failed), pending and undefined (reported as pending), skipped. Failed (and ambiguous) scenarios on the quarantine list are reported as quarantined.
|
|
70
70
|
|
|
71
71
|
### Failures
|
|
72
72
|
|
|
@@ -138,13 +138,14 @@ end
|
|
|
138
138
|
| `output_dir` | `"raider-results"` | Directory for local output. |
|
|
139
139
|
| `clean_output_dir` | `true` | Remove the previous `run.json` and `attachments/` directory (`run-<n>.json` and `attachments-<n>/` when sharded) inside `output_dir` before writing. Nothing else in `output_dir` is touched, except a raider_reporter-written `index.html`. |
|
|
140
140
|
| `html_report` | `true` | Write `index.html`, the local HTML report, into `output_dir` after each run. See [Local report](#local-report). |
|
|
141
|
+
| `quarantine` | `true` when `ENV["RAIDER_QUARANTINE"]` is `1` or `true` | Fetch the quarantine list at run start and report matching failures as `quarantined`. Needs `token` and `endpoint`. See [Quarantine](#quarantine). |
|
|
141
142
|
| `environment` | `{}` | Free-form key/values stored with the run (browser, suite name, and so on). Values are JSON values: strings, booleans or numbers. |
|
|
142
143
|
|
|
143
144
|
## Local output
|
|
144
145
|
|
|
145
146
|
```
|
|
146
147
|
raider-results/
|
|
147
|
-
run.json # the run in schema v1
|
|
148
|
+
run.json # the run in schema v1 (v2 with quarantined results)
|
|
148
149
|
attachments/ # copies of attached screenshots and videos
|
|
149
150
|
index.html # the local report, see below
|
|
150
151
|
```
|
|
@@ -179,6 +180,24 @@ Reporting is fail-open: no error inside the gem, network failure, timeout or non
|
|
|
179
180
|
|
|
180
181
|
The hosted Raider service is not public yet; uploads go to a Raider server whose address you provide in `RAIDER_ENDPOINT`.
|
|
181
182
|
|
|
183
|
+
## Quarantine
|
|
184
|
+
|
|
185
|
+
A test quarantined in Raider (with a reason and an expiry) still runs and is still reported, but its failure is reported as `quarantined`, and a CI build stays green when quarantined tests are its only failures.
|
|
186
|
+
|
|
187
|
+
1. Turn it on in CI with `RAIDER_QUARANTINE=1` (or `config.quarantine = true`), next to `RAIDER_TOKEN` and `RAIDER_ENDPOINT`.
|
|
188
|
+
2. Run the suite through `raider-reporter exec`:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
bundle exec raider-reporter exec -- bundle exec rspec
|
|
192
|
+
bundle exec raider-reporter exec -- bundle exec cucumber
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
When the run starts, the formatter fetches the project's quarantine list once (`GET /api/v1/quarantine`, 3-second timeout). On any error it prints `[raider_reporter] quarantine list unavailable (<reason>); no tests quarantined` and nothing is quarantined. Matching failures keep their message, backtrace and screenshots; the run is written as schema 2, and `run.json`, the upload and the HTML report show `quarantined`.
|
|
196
|
+
|
|
197
|
+
`exec` runs the command without a shell and exits with its status, with one exception: when the command exits 1, every failure the framework counted is quarantined, and nothing went wrong outside tests, it prints `raider-reporter: N quarantined failure(s) did not fail the build` to stderr and exits 0. "Outside tests" means RSpec errors outside examples, a run stopped early (`--fail-fast`, an interrupt, a load error), or a Cucumber run that did not finish every scenario. RSpec `--fail-fast` counts even when the last example triggers it, and interrupts count too, so the build stays red. Under Cucumber `--strict`, a flaky scenario (failed, then passed on retry) counts as a real failure and keeps the build red. A command killed by a signal exits 128 + the signal number; a command that cannot be started exits 127. Several processes under one `exec` (for example `parallel_rspec`) never turn red into green: their summaries conflict and the command's exit code is used.
|
|
198
|
+
|
|
199
|
+
Without `exec`, RSpec and Cucumber exit codes are unchanged; quarantine only changes how failures are reported.
|
|
200
|
+
|
|
182
201
|
## Pull request comments
|
|
183
202
|
|
|
184
203
|
On pull requests, `raider-reporter comment` posts one comment with the build's results (and sets a `raider/tests` commit status on the PR head commit). Run it in a job that runs after your test jobs, so every shard has uploaded. Use the workflow's `GITHUB_TOKEN` (or a GitHub App token): a personal access token posts as a user, so the step adds a new comment on every run instead of updating the existing one.
|
|
@@ -319,7 +338,7 @@ Tip: run `rm -rf raider-results` before a parallel run. Each process only cleans
|
|
|
319
338
|
|
|
320
339
|
## Schema
|
|
321
340
|
|
|
322
|
-
`run.json` follows schema v1, documented in [docs/schema-v1.md](docs/schema-v1.md). Statuses are
|
|
341
|
+
`run.json` follows schema v1, documented in [docs/schema-v1.md](docs/schema-v1.md). Statuses are `passed`, `failed`, `pending` and `skipped`, plus `quarantined` in schema 2 (written only when a run has a quarantined result). In both RSpec and Cucumber, a failure may carry `app_frame` (when a project frame is found): the first backtrace line inside your project, for example `spec/pages/login_page.rb:42:in 'LoginPage#submit'`, which Raider uses to group failures with the same error.
|
|
323
342
|
|
|
324
343
|
## Contributing
|
|
325
344
|
|
data/lib/raider_reporter/cli.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "raider_reporter"
|
|
4
|
+
require "raider_reporter/exec"
|
|
4
5
|
|
|
5
6
|
module RaiderReporter
|
|
7
|
+
# `raider-reporter exec -- <command…>` runs the test suite and decides the build's exit code (see Exec).
|
|
6
8
|
# `raider-reporter comment` posts the build summary as a PR comment and sets a commit status
|
|
7
9
|
# (fail-open: always exits 0, notices go to stderr). `raider-reporter report [dir]` rebuilds the
|
|
8
10
|
# local HTML report (exits 1 when there are no run files or the report cannot be written).
|
|
@@ -24,6 +26,8 @@ module RaiderReporter
|
|
|
24
26
|
}.freeze
|
|
25
27
|
DEFAULT_API_URL = "https://api.github.com"
|
|
26
28
|
USAGE = <<~TEXT
|
|
29
|
+
#{Exec::USAGE.chomp}
|
|
30
|
+
|
|
27
31
|
Usage: raider-reporter report [dir]
|
|
28
32
|
|
|
29
33
|
Rebuilds <dir>/index.html (default raider-results) from every run*.json in it. Prints its path.
|
|
@@ -48,6 +52,20 @@ module RaiderReporter
|
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
def run
|
|
55
|
+
return run_exec if @argv.first == "exec"
|
|
56
|
+
|
|
57
|
+
run_reporting
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# exec decides the build's exit code; Exec owns its own failure handling.
|
|
63
|
+
def run_exec
|
|
64
|
+
command = @argv[1] == "--" ? @argv.drop(2) : []
|
|
65
|
+
Exec.new(command, err: @err).run
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def run_reporting
|
|
51
69
|
return report if @argv.first == "report" && !@argv.include?("--help") && !@argv.include?("-h")
|
|
52
70
|
|
|
53
71
|
command = @argv.first
|
|
@@ -62,8 +80,6 @@ module RaiderReporter
|
|
|
62
80
|
0
|
|
63
81
|
end
|
|
64
82
|
|
|
65
|
-
private
|
|
66
|
-
|
|
67
83
|
def report
|
|
68
84
|
dir = File.expand_path(@argv[1] || "raider-results")
|
|
69
85
|
path = Writers::Html.new.write(dir)
|
|
@@ -181,7 +197,7 @@ module RaiderReporter
|
|
|
181
197
|
url.empty? ? DEFAULT_API_URL : url
|
|
182
198
|
end
|
|
183
199
|
|
|
184
|
-
# "**Raider: failed** - 2 failed,
|
|
200
|
+
# "**Raider: failed** - 2 failed, 1 quarantined, 148 passed (1m 12s)" -> "2 failed, 1 quarantined, 148 passed".
|
|
185
201
|
# Falls back to a fixed text per state when the Markdown carries no headline counts.
|
|
186
202
|
def description(summary)
|
|
187
203
|
counts = headline_counts(summary.markdown)
|
|
@@ -193,7 +209,7 @@ module RaiderReporter
|
|
|
193
209
|
line = markdown.to_s.lines.find { |l| l.include?("Raider:") }
|
|
194
210
|
return nil unless line
|
|
195
211
|
|
|
196
|
-
counts = line.scan(/\b\d+ (?:failed|passed|skipped|pending)\b/)
|
|
212
|
+
counts = line.scan(/\b\d+ (?:failed|quarantined|passed|skipped|pending)\b/)
|
|
197
213
|
counts.empty? ? nil : counts.join(", ")
|
|
198
214
|
end
|
|
199
215
|
|
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "tmpdir"
|
|
5
5
|
require "fileutils"
|
|
6
|
+
require "set"
|
|
6
7
|
|
|
7
8
|
module RaiderReporter
|
|
8
9
|
# Accumulates one suite run. Framework formatters call it; writers read the Run it returns.
|
|
9
10
|
class Collector
|
|
10
|
-
def initialize(max_attachment_bytes:, warn:, clock: -> { Time.now.utc }, uuid: -> { SecureRandom.uuid }
|
|
11
|
+
def initialize(max_attachment_bytes:, warn:, clock: -> { Time.now.utc }, uuid: -> { SecureRandom.uuid },
|
|
12
|
+
quarantine: -> { Quarantine::EMPTY })
|
|
11
13
|
@max_attachment_bytes = max_attachment_bytes
|
|
12
14
|
@warn = warn
|
|
13
15
|
@clock = clock
|
|
14
16
|
@uuid = uuid
|
|
17
|
+
@quarantine = quarantine
|
|
18
|
+
@quarantined = nil
|
|
15
19
|
@results = []
|
|
16
20
|
@pending_attachments = nil
|
|
17
21
|
@staged = 0
|
|
@@ -20,6 +24,26 @@ module RaiderReporter
|
|
|
20
24
|
def start_run
|
|
21
25
|
@started_at = @clock.call
|
|
22
26
|
@results = []
|
|
27
|
+
@quarantined = load_quarantine
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# True when the test's key is in the list loaded at run start.
|
|
31
|
+
def quarantined?(file_path, full_name)
|
|
32
|
+
return false if @quarantined.nil? || @quarantined.empty?
|
|
33
|
+
|
|
34
|
+
@quarantined.include?(RaiderReporter.test_key(file_path, full_name))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :framework_counts
|
|
38
|
+
|
|
39
|
+
# What the framework itself counted (spec §4.4); nil until it reports, and
|
|
40
|
+
# then no exec summary is written.
|
|
41
|
+
def record_framework_counts(failures:, errors_outside_tests:)
|
|
42
|
+
@framework_counts = { failures: Integer(failures), errors_outside_tests: Integer(errors_outside_tests) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def quarantined_count
|
|
46
|
+
@results.count { |result| result.status == "quarantined" }
|
|
23
47
|
end
|
|
24
48
|
|
|
25
49
|
def start_test
|
|
@@ -81,6 +105,13 @@ module RaiderReporter
|
|
|
81
105
|
|
|
82
106
|
private
|
|
83
107
|
|
|
108
|
+
def load_quarantine
|
|
109
|
+
@quarantine.call
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
@warn.call(Quarantine.warning("#{e.class}: #{e.message}"))
|
|
112
|
+
Quarantine::EMPTY
|
|
113
|
+
end
|
|
114
|
+
|
|
84
115
|
def keep?(attachment)
|
|
85
116
|
path = attachment.source_path
|
|
86
117
|
unless File.file?(path)
|
|
@@ -4,7 +4,10 @@ require "raider_reporter/azure_env"
|
|
|
4
4
|
|
|
5
5
|
module RaiderReporter
|
|
6
6
|
class Configuration
|
|
7
|
-
|
|
7
|
+
TRUTHY = %w[1 true].freeze
|
|
8
|
+
|
|
9
|
+
attr_accessor :token, :endpoint, :output_dir, :clean_output_dir, :html_report, :environment, :max_attachment_bytes,
|
|
10
|
+
:quarantine, :exec_summary_path
|
|
8
11
|
|
|
9
12
|
def initialize(env = ENV)
|
|
10
13
|
@token = AzureEnv.value(env, "RAIDER_TOKEN")
|
|
@@ -14,12 +17,19 @@ module RaiderReporter
|
|
|
14
17
|
@html_report = true
|
|
15
18
|
@environment = {}
|
|
16
19
|
@max_attachment_bytes = 5 * 1024 * 1024
|
|
20
|
+
@quarantine = TRUTHY.include?(AzureEnv.value(env, "RAIDER_QUARANTINE").to_s.downcase)
|
|
21
|
+
@exec_summary_path = AzureEnv.value(env, "RAIDER_EXEC_SUMMARY")
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
def upload?
|
|
20
25
|
present?(token) && present?(endpoint)
|
|
21
26
|
end
|
|
22
27
|
|
|
28
|
+
# Quarantine needs the server: on, with both token and endpoint (spec §4.1).
|
|
29
|
+
def quarantine?
|
|
30
|
+
quarantine == true && upload?
|
|
31
|
+
end
|
|
32
|
+
|
|
23
33
|
private
|
|
24
34
|
|
|
25
35
|
def present?(value)
|
|
@@ -84,6 +84,10 @@ module RaiderReporter
|
|
|
84
84
|
@buffer = {}
|
|
85
85
|
@attempts = Hash.new(0)
|
|
86
86
|
@attempt = nil
|
|
87
|
+
@outcomes = {}
|
|
88
|
+
@failed_before = {}
|
|
89
|
+
@outcomes_broken = false
|
|
90
|
+
@expected_cases = nil
|
|
87
91
|
guard { setup(config) if self.class.claim(config) }
|
|
88
92
|
end
|
|
89
93
|
|
|
@@ -95,6 +99,8 @@ module RaiderReporter
|
|
|
95
99
|
@collector = RaiderReporter.start_collector!
|
|
96
100
|
RaiderReporter.dry_run = config.dry_run?
|
|
97
101
|
config.on_event(:test_run_started) { |_event| guard { @collector.start_run } }
|
|
102
|
+
config.on_event(:test_run_started) { |event| guard { expect_cases(event.test_cases) } }
|
|
103
|
+
config.on_event(:test_case_finished) { |event| count_outcome(event.test_case, event.result) }
|
|
98
104
|
config.on_event(:test_case_started) { |event| guard { case_started(event.test_case) } }
|
|
99
105
|
config.on_event(:test_step_finished) { |event| guard { step_finished(event.test_step, event.result) } }
|
|
100
106
|
config.on_event(:test_case_finished) { |event| guard { case_finished(event.test_case, event.result) } }
|
|
@@ -124,6 +130,7 @@ module RaiderReporter
|
|
|
124
130
|
# Always buffer: a lookup problem may lose detail, never the scenario.
|
|
125
131
|
failure = status == "failed" ? safely(nil) { failure_for(problem, result, test_case) } : nil
|
|
126
132
|
fields = identity_for(test_case)
|
|
133
|
+
status = "quarantined" if status == "failed" && quarantined?(fields)
|
|
127
134
|
key = "#{test_case.location}|#{test_case.name}"
|
|
128
135
|
@attempts[key] += 1
|
|
129
136
|
# The newest attempt replaces the older one, taking its attachments with it.
|
|
@@ -135,11 +142,42 @@ module RaiderReporter
|
|
|
135
142
|
def run_finished
|
|
136
143
|
@buffer.each_value { |result| @collector.add_result(result) }
|
|
137
144
|
@buffer.clear
|
|
145
|
+
safely(nil) { record_framework_counts }
|
|
138
146
|
RaiderReporter.finish!($stderr)
|
|
139
147
|
ensure
|
|
140
148
|
self.class.release(@config)
|
|
141
149
|
end
|
|
142
150
|
|
|
151
|
+
def expect_cases(test_cases)
|
|
152
|
+
@expected_cases = test_cases.map { |test_case| "#{test_case.location}|#{test_case.name}" }.uniq.size
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Its own handler, registered before case_finished: if counting ever fails, no
|
|
156
|
+
# summary is written and exec keeps cucumber's exit code. The newest attempt wins,
|
|
157
|
+
# except that under strict flaky (--strict, --strict-flaky) a scenario that failed
|
|
158
|
+
# on an earlier attempt and then passed still fails cucumber's run.
|
|
159
|
+
def count_outcome(test_case, result)
|
|
160
|
+
return if @outcomes_broken
|
|
161
|
+
|
|
162
|
+
key = "#{test_case.location}|#{test_case.name}"
|
|
163
|
+
failed = !result.ok?(strict: @config.strict)
|
|
164
|
+
flaky = !failed && @failed_before[key] && @config.strict.strict?(:flaky)
|
|
165
|
+
@failed_before[key] ||= failed
|
|
166
|
+
@outcomes[key] = failed || flaky ? true : false
|
|
167
|
+
rescue StandardError => e
|
|
168
|
+
@outcomes_broken = true
|
|
169
|
+
RaiderReporter.warn("#{e.class}: #{e.message}")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Scenarios that make cucumber's run fail; one error outside tests when the run
|
|
173
|
+
# stopped before every scenario finished (--fail-fast) (spec §4.4).
|
|
174
|
+
def record_framework_counts
|
|
175
|
+
return if @outcomes_broken || @expected_cases.nil?
|
|
176
|
+
|
|
177
|
+
aborted = @outcomes.size < @expected_cases ? 1 : 0
|
|
178
|
+
@collector.record_framework_counts(failures: @outcomes.values.count(true), errors_outside_tests: aborted)
|
|
179
|
+
end
|
|
180
|
+
|
|
143
181
|
def identity_for(test_case)
|
|
144
182
|
file_path = relative_path(test_case.location.file)
|
|
145
183
|
line_number = test_case.location.lines.max
|
|
@@ -152,6 +190,11 @@ module RaiderReporter
|
|
|
152
190
|
}
|
|
153
191
|
end
|
|
154
192
|
|
|
193
|
+
# A lookup problem must never turn a real failure into a quarantined one.
|
|
194
|
+
def quarantined?(fields)
|
|
195
|
+
safely(false) { @collector.quarantined?(fields[:file_path], fields[:full_name]) }
|
|
196
|
+
end
|
|
197
|
+
|
|
155
198
|
def full_name(test_case)
|
|
156
199
|
safely(test_case.name) do
|
|
157
200
|
feature = @ast.gherkin_document(test_case.location.file)&.feature
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "raider_reporter/exec_summary"
|
|
5
|
+
|
|
6
|
+
module RaiderReporter
|
|
7
|
+
# `raider-reporter exec -- <command…>` (spec §4.5). Runs the command without a
|
|
8
|
+
# shell, streams its output, and keeps its exit code, except that exit 1 becomes 0
|
|
9
|
+
# when the summary says every counted failure was quarantined and nothing else
|
|
10
|
+
# went wrong. Anything uncertain keeps the command's code.
|
|
11
|
+
class Exec
|
|
12
|
+
USAGE = <<~TEXT
|
|
13
|
+
Usage: raider-reporter exec -- <command> [args...]
|
|
14
|
+
|
|
15
|
+
Runs the command (no shell) and exits with its status. When the command exits 1, every failure it
|
|
16
|
+
counted was quarantined in Raider and nothing else went wrong, exits 0 instead and says so on stderr.
|
|
17
|
+
Quarantine needs RAIDER_QUARANTINE=1 with RAIDER_TOKEN and RAIDER_ENDPOINT.
|
|
18
|
+
TEXT
|
|
19
|
+
USAGE_ERROR = 2
|
|
20
|
+
NOT_STARTED = 127
|
|
21
|
+
# A terminal Ctrl-C already reaches the command through the process group.
|
|
22
|
+
FORWARDED = %w[TERM HUP].freeze
|
|
23
|
+
IGNORED = %w[INT].freeze
|
|
24
|
+
|
|
25
|
+
def self.quarantined_only?(summary)
|
|
26
|
+
return false unless summary
|
|
27
|
+
|
|
28
|
+
failures = summary["framework_failures"]
|
|
29
|
+
failures.positive? && failures == summary["quarantined"] && summary["errors_outside_tests"].zero?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(command, err: $stderr)
|
|
33
|
+
@command = command
|
|
34
|
+
@err = err
|
|
35
|
+
@pid = nil
|
|
36
|
+
@code = nil
|
|
37
|
+
@pending = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def run
|
|
41
|
+
if @command.empty?
|
|
42
|
+
@err.puts(USAGE)
|
|
43
|
+
return USAGE_ERROR
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
Dir.mktmpdir("raider-exec") { |dir| run_with(File.join(dir, "summary.json")) }
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
@err.puts("[raider_reporter] exec failed: #{e.class}: #{e.message}")
|
|
49
|
+
@code || 1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def run_with(summary_path)
|
|
55
|
+
previous = install_traps
|
|
56
|
+
@pid = start(summary_path)
|
|
57
|
+
return NOT_STARTED unless @pid
|
|
58
|
+
|
|
59
|
+
forward(@pending) if @pending
|
|
60
|
+
|
|
61
|
+
@code = wait
|
|
62
|
+
summary = ExecSummary.read(summary_path)
|
|
63
|
+
return @code unless @code == 1 && self.class.quarantined_only?(summary)
|
|
64
|
+
|
|
65
|
+
@err.puts("raider-reporter: #{summary['quarantined']} quarantined failure(s) did not fail the build")
|
|
66
|
+
0
|
|
67
|
+
ensure
|
|
68
|
+
previous&.each { |signal, handler| trap(signal, handler || "DEFAULT") }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def start(summary_path)
|
|
72
|
+
program, *args = @command
|
|
73
|
+
Process.spawn({ ExecSummary::ENV_NAME => summary_path }, [program, program], *args)
|
|
74
|
+
rescue SystemCallError => e
|
|
75
|
+
@err.puts("[raider_reporter] exec: cannot run #{program}: #{e.message}")
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def wait
|
|
80
|
+
_, status = Process.wait2(@pid)
|
|
81
|
+
status.signaled? ? 128 + status.termsig : status.exitstatus
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def forward(signal)
|
|
85
|
+
Process.kill(signal, @pid)
|
|
86
|
+
rescue SystemCallError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Installed before spawning; a signal that arrives before the child exists is kept in
|
|
91
|
+
# @pending and forwarded right after spawn. A Ruby handler (not "IGNORE") is reset to
|
|
92
|
+
# the default in the child by exec(2).
|
|
93
|
+
def install_traps
|
|
94
|
+
forwarded = FORWARDED.to_h do |signal|
|
|
95
|
+
handler = trap(signal) { @pid ? forward(signal) : @pending = signal }
|
|
96
|
+
[signal, handler]
|
|
97
|
+
end
|
|
98
|
+
forwarded.merge(IGNORED.to_h { |signal| [signal, trap(signal) {}] })
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RaiderReporter
|
|
6
|
+
# The file `raider-reporter exec` reads to decide its exit code (spec §4.4). One
|
|
7
|
+
# process writes it. A second writer (several processes under one exec) turns it
|
|
8
|
+
# into a conflict, which exec treats as no summary, so the command's code wins.
|
|
9
|
+
module ExecSummary
|
|
10
|
+
ENV_NAME = "RAIDER_EXEC_SUMMARY"
|
|
11
|
+
KEYS = %w[framework_failures quarantined errors_outside_tests].freeze
|
|
12
|
+
CONFLICT = { "conflict" => true }.freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def write(path, framework_failures:, quarantined:, errors_outside_tests:, pid: Process.pid)
|
|
17
|
+
summary = { "framework_failures" => framework_failures, "quarantined" => quarantined,
|
|
18
|
+
"errors_outside_tests" => errors_outside_tests, "pid" => pid }
|
|
19
|
+
File.open(path, File::RDWR | File::CREAT, 0o600) do |file|
|
|
20
|
+
file.flock(File::LOCK_EX)
|
|
21
|
+
existing = file.read
|
|
22
|
+
same_writer = existing.empty? || parse(existing)&.fetch("pid", nil) == pid
|
|
23
|
+
file.rewind
|
|
24
|
+
file.truncate(0)
|
|
25
|
+
file.write(JSON.generate(same_writer ? summary : CONFLICT))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def read(path)
|
|
30
|
+
data = parse(File.read(path))
|
|
31
|
+
return nil if data.nil? || data.key?("conflict")
|
|
32
|
+
return nil unless KEYS.all? { |key| data[key].is_a?(Integer) && !data[key].negative? }
|
|
33
|
+
|
|
34
|
+
data.slice(*KEYS)
|
|
35
|
+
rescue SystemCallError, IOError
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parse(text)
|
|
40
|
+
data = JSON.parse(text)
|
|
41
|
+
data.is_a?(Hash) ? data : nil
|
|
42
|
+
rescue JSON::ParserError
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RaiderReporter
|
|
4
|
-
SCHEMA_VERSION =
|
|
5
|
-
STATUSES = %w[passed failed pending skipped].freeze
|
|
4
|
+
SCHEMA_VERSION = 2
|
|
5
|
+
STATUSES = %w[passed failed pending skipped quarantined].freeze
|
|
6
6
|
|
|
7
7
|
Failure = Data.define(:message, :exception_class, :backtrace, :app_frame) do
|
|
8
8
|
def initialize(message:, exception_class:, backtrace:, app_frame: nil)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RaiderReporter
|
|
4
|
-
# Serializes a Run
|
|
4
|
+
# Serializes a Run: schema 1, or schema 2 when a result is quarantined. Local mode points attachments at copied
|
|
5
5
|
# files; upload mode points them at multipart part names.
|
|
6
6
|
class Payload
|
|
7
7
|
MODES = %i[local upload].freeze
|
|
@@ -17,7 +17,7 @@ module RaiderReporter
|
|
|
17
17
|
|
|
18
18
|
entries = attachment_entries.each
|
|
19
19
|
{
|
|
20
|
-
"schema_version" =>
|
|
20
|
+
"schema_version" => schema_version,
|
|
21
21
|
"client_run_id" => @run.client_run_id,
|
|
22
22
|
"run" => run_hash,
|
|
23
23
|
"results" => @run.results.map do |result|
|
|
@@ -34,6 +34,10 @@ module RaiderReporter
|
|
|
34
34
|
|
|
35
35
|
def run_file = shard_index ? "run-#{shard_index}.json" : "run.json"
|
|
36
36
|
|
|
37
|
+
# Version 2 only adds the quarantined status; older servers keep accepting
|
|
38
|
+
# every run that has none (spec §4.3).
|
|
39
|
+
def schema_version = @run.results.any? { |result| result.status == "quarantined" } ? SCHEMA_VERSION : 1
|
|
40
|
+
|
|
37
41
|
def attachment_entries
|
|
38
42
|
@attachment_entries ||= @run.results.flat_map(&:attachments).each_with_index.map do |attachment, index|
|
|
39
43
|
{
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "set"
|
|
7
|
+
require "timeout"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
module RaiderReporter
|
|
11
|
+
NUL = "\u0000"
|
|
12
|
+
NUL_REPLACEMENT = "\u{FFFD}"
|
|
13
|
+
|
|
14
|
+
# The server's test identity: raider_saas TestCase.key_for over the payload's
|
|
15
|
+
# file_path and full_name, after ingest replaced NUL in each of them.
|
|
16
|
+
def self.test_key(file_path, full_name)
|
|
17
|
+
parts = [file_path, full_name].map { |part| part.to_s.tr(NUL, NUL_REPLACEMENT) }
|
|
18
|
+
Digest::SHA256.hexdigest(parts.join(NUL))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Fetches the project's quarantine list (spec §4.2). Fail-open: any problem
|
|
22
|
+
# means one warning and an empty set, never an exception.
|
|
23
|
+
module Quarantine
|
|
24
|
+
PATH = "/api/v1/quarantine"
|
|
25
|
+
TIMEOUT = 3
|
|
26
|
+
EMPTY = Set.new.freeze
|
|
27
|
+
|
|
28
|
+
class Unavailable < StandardError; end
|
|
29
|
+
|
|
30
|
+
def self.load(config, warn:)
|
|
31
|
+
return EMPTY unless config.quarantine?
|
|
32
|
+
|
|
33
|
+
Set.new(fetch(config)).freeze
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
reason = e.is_a?(Unavailable) ? e.message : "#{e.class}: #{e.message}"
|
|
36
|
+
warn.call(warning(reason, token: config.token))
|
|
37
|
+
EMPTY
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The one place the fail-open warning text is built (used by load and the Collector).
|
|
41
|
+
def self.warning(reason, token: nil)
|
|
42
|
+
"quarantine list unavailable (#{redact(reason, token)}); no tests quarantined"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.fetch(config)
|
|
46
|
+
uri = URI.parse("#{config.endpoint.to_s.strip.chomp('/')}#{PATH}")
|
|
47
|
+
raise Unavailable, "RAIDER_ENDPOINT must start with http:// or https://" unless uri.is_a?(URI::HTTP)
|
|
48
|
+
|
|
49
|
+
request = Net::HTTP::Get.new(uri)
|
|
50
|
+
request["Authorization"] = "Bearer #{config.token}"
|
|
51
|
+
request["Accept"] = "application/json"
|
|
52
|
+
request["User-Agent"] = "raider_reporter/#{VERSION}"
|
|
53
|
+
# The per-step timeouts do not bound a server that trickles bytes; the deadline does.
|
|
54
|
+
response = Timeout.timeout(TIMEOUT) do
|
|
55
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
56
|
+
open_timeout: TIMEOUT, read_timeout: TIMEOUT, max_retries: 0) { |http| http.request(request) }
|
|
57
|
+
end
|
|
58
|
+
raise Unavailable, "HTTP #{response.code}" unless response.code == "200"
|
|
59
|
+
|
|
60
|
+
keys(response.body)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.keys(body)
|
|
64
|
+
data = JSON.parse(body.to_s)
|
|
65
|
+
list = data.is_a?(Hash) ? data["quarantined"] : nil
|
|
66
|
+
raise Unavailable, "unexpected response" unless list.is_a?(Array)
|
|
67
|
+
|
|
68
|
+
list.filter_map { |entry| entry["test_key"] if entry.is_a?(Hash) && entry["test_key"].is_a?(String) }
|
|
69
|
+
rescue JSON::ParserError
|
|
70
|
+
raise Unavailable, "invalid JSON"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.redact(text, token)
|
|
74
|
+
token.to_s.empty? ? text : text.gsub(token.to_s, "[REDACTED]")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private_class_method :fetch, :keys, :redact
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -11,7 +11,7 @@ module RaiderReporter
|
|
|
11
11
|
class Formatter
|
|
12
12
|
::RSpec::Core::Formatters.register self,
|
|
13
13
|
:start, :example_started, :example_passed,
|
|
14
|
-
:example_failed, :example_pending, :close
|
|
14
|
+
:example_failed, :example_pending, :dump_summary, :close
|
|
15
15
|
|
|
16
16
|
NON_TAG_KEYS = %i[pending skip].freeze
|
|
17
17
|
MAX_MESSAGE = Text::MAX_MESSAGE
|
|
@@ -45,6 +45,13 @@ module RaiderReporter
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
def dump_summary(notification)
|
|
49
|
+
guard do
|
|
50
|
+
@collector.record_framework_counts(failures: notification.failure_count,
|
|
51
|
+
errors_outside_tests: errors_outside_tests(notification))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
48
55
|
def close(_notification)
|
|
49
56
|
guard { RaiderReporter.finish! }
|
|
50
57
|
end
|
|
@@ -54,19 +61,32 @@ module RaiderReporter
|
|
|
54
61
|
def record(example, status)
|
|
55
62
|
metadata = example.metadata
|
|
56
63
|
execution = example.execution_result
|
|
64
|
+
file_path = metadata[:file_path].to_s.delete_prefix("./")
|
|
65
|
+
full_name = utf8(example.full_description)
|
|
66
|
+
failure = status == "failed" ? failure_for(execution.exception) : nil
|
|
67
|
+
# The key is computed from the payload's own file_path and full_name (spec §4.2).
|
|
68
|
+
status = "quarantined" if status == "failed" && @collector.quarantined?(file_path, full_name)
|
|
57
69
|
@collector.finish_test(
|
|
58
70
|
identifier: example.id,
|
|
59
|
-
full_name:
|
|
60
|
-
file_path:
|
|
71
|
+
full_name: full_name,
|
|
72
|
+
file_path: file_path,
|
|
61
73
|
line_number: metadata[:line_number],
|
|
62
74
|
status: status,
|
|
63
75
|
duration_ms: ((execution.run_time || 0) * 1000).round,
|
|
64
76
|
attempts: metadata.fetch(:retry_attempts, 0).to_i + 1,
|
|
65
77
|
tags: metadata.select { |key, value| value == true && !NON_TAG_KEYS.include?(key) }.keys.map(&:to_s).sort,
|
|
66
|
-
failure:
|
|
78
|
+
failure: failure
|
|
67
79
|
)
|
|
68
80
|
end
|
|
69
81
|
|
|
82
|
+
# Errors RSpec reported outside examples, plus one when it stopped early
|
|
83
|
+
# (--fail-fast, an interrupt, a load error): then not every example ran.
|
|
84
|
+
def errors_outside_tests(notification)
|
|
85
|
+
world = ::RSpec.world
|
|
86
|
+
quitting = world.wants_to_quit || (world.respond_to?(:rspec_is_quitting) && world.rspec_is_quitting)
|
|
87
|
+
notification.errors_outside_of_examples_count + (quitting ? 1 : 0)
|
|
88
|
+
end
|
|
89
|
+
|
|
70
90
|
def failure_for(exception)
|
|
71
91
|
return nil unless exception
|
|
72
92
|
|
|
@@ -16,7 +16,7 @@ module RaiderReporter
|
|
|
16
16
|
MARKER = '<meta name="generator" content="raider_reporter">'
|
|
17
17
|
ROW_LIMIT = 2_000
|
|
18
18
|
RUN_FILE = /\Arun(-[^\/]+)?\.json\z/
|
|
19
|
-
SECTIONS = [%w[failed Failed], %w[passed Passed], %w[pending Pending], %w[skipped Skipped]].freeze
|
|
19
|
+
SECTIONS = [%w[failed Failed], %w[quarantined Quarantined], %w[passed Passed], %w[pending Pending], %w[skipped Skipped]].freeze
|
|
20
20
|
PROVIDERS = { "github_actions" => "GitHub Actions", "azure_pipelines" => "Azure Pipelines" }.freeze
|
|
21
21
|
|
|
22
22
|
def self.ours?(path)
|
|
@@ -140,7 +140,11 @@ module RaiderReporter
|
|
|
140
140
|
flaky = counts["flaky"]
|
|
141
141
|
decided = passed + counts["failed"]
|
|
142
142
|
rate = decided.zero? ? "—" : "#{(passed * 100.0 / decided).round}%"
|
|
143
|
-
parts = SECTIONS.
|
|
143
|
+
parts = SECTIONS.filter_map do |status, _|
|
|
144
|
+
next if status == "quarantined" && counts[status].zero?
|
|
145
|
+
|
|
146
|
+
%(<span class="b #{status}">#{counts[status]} #{status}</span>)
|
|
147
|
+
end
|
|
144
148
|
parts << %(<span class="b flaky">#{flaky} flaky</span>) if flaky.positive?
|
|
145
149
|
"#{parts.join(' ')} <span class=\"rate\">Pass rate #{rate}</span>"
|
|
146
150
|
end
|
|
@@ -257,8 +261,8 @@ module RaiderReporter
|
|
|
257
261
|
def h(value) = ERB::Util.html_escape(value.to_s)
|
|
258
262
|
|
|
259
263
|
CSS = <<~CSS.gsub(/\s*\n\s*/, "")
|
|
260
|
-
:root{--bg:#fff;--fg:#1f2328;--mute:#656d76;--bdr:#d0d7de;--card:#f6f8fa;--ruby:#C14420;--ok:#1a7f37;--warn:#9a6700}
|
|
261
|
-
@media (prefers-color-scheme:dark){:root{--bg:#0d1117;--fg:#e6edf3;--mute:#8d96a0;--bdr:#30363d;--card:#161b22;--ok:#3fb950;--warn:#d29922}}
|
|
264
|
+
:root{--bg:#fff;--fg:#1f2328;--mute:#656d76;--bdr:#d0d7de;--card:#f6f8fa;--ruby:#C14420;--ok:#1a7f37;--warn:#9a6700;--q:#6B4FA0}
|
|
265
|
+
@media (prefers-color-scheme:dark){:root{--bg:#0d1117;--fg:#e6edf3;--mute:#8d96a0;--bdr:#30363d;--card:#161b22;--ok:#3fb950;--warn:#d29922;--q:#a78bfa}}
|
|
262
266
|
*{box-sizing:border-box}
|
|
263
267
|
body{margin:0;padding:16px;background:var(--bg);color:var(--fg);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
|
264
268
|
header,main,footer{max-width:1100px;margin:0 auto}
|
|
@@ -266,6 +270,7 @@ module RaiderReporter
|
|
|
266
270
|
.meta,.l,footer,.more{color:var(--mute);font-size:12px}
|
|
267
271
|
.b{display:inline-block;padding:0 6px;border-radius:9px;font-size:11px;font-weight:600;border:1px solid var(--bdr)}
|
|
268
272
|
.b.failed{background:var(--ruby);color:#fff;border-color:var(--ruby)}
|
|
273
|
+
.b.quarantined{color:var(--q);border-color:var(--q)}
|
|
269
274
|
.b.passed{color:var(--ok)}.b.pending,.b.skipped,.b.flaky{color:var(--warn)}
|
|
270
275
|
.rate{font-weight:600;margin-left:6px}
|
|
271
276
|
#q{width:100%;margin:12px 0;padding:8px;border:1px solid var(--bdr);border-radius:6px;background:var(--card);color:var(--fg)}
|
data/lib/raider_reporter.rb
CHANGED
|
@@ -8,6 +8,8 @@ require_relative "raider_reporter/models"
|
|
|
8
8
|
require_relative "raider_reporter/app_frame"
|
|
9
9
|
require_relative "raider_reporter/payload"
|
|
10
10
|
require_relative "raider_reporter/configuration"
|
|
11
|
+
require_relative "raider_reporter/quarantine"
|
|
12
|
+
require_relative "raider_reporter/exec_summary"
|
|
11
13
|
require_relative "raider_reporter/content_types"
|
|
12
14
|
require_relative "raider_reporter/collector"
|
|
13
15
|
require_relative "raider_reporter/writers/local"
|
|
@@ -40,6 +42,7 @@ module RaiderReporter
|
|
|
40
42
|
@configuration = nil
|
|
41
43
|
@collector = nil
|
|
42
44
|
@project_root = nil
|
|
45
|
+
@quarantined_keys = nil
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
def start_collector!
|
|
@@ -47,10 +50,16 @@ module RaiderReporter
|
|
|
47
50
|
@project_root = Dir.pwd
|
|
48
51
|
@collector = Collector.new(
|
|
49
52
|
max_attachment_bytes: configuration.max_attachment_bytes,
|
|
50
|
-
warn: ->(message) { warn(message) }
|
|
53
|
+
warn: ->(message) { warn(message) },
|
|
54
|
+
quarantine: -> { quarantined_keys }
|
|
51
55
|
)
|
|
52
56
|
end
|
|
53
57
|
|
|
58
|
+
# Fetched once per process, at the first run start (spec §4.2).
|
|
59
|
+
def quarantined_keys
|
|
60
|
+
@quarantined_keys ||= Quarantine.load(configuration, warn: ->(message) { warn(message) })
|
|
61
|
+
end
|
|
62
|
+
|
|
54
63
|
def attach(name, path, type: nil)
|
|
55
64
|
unless collector&.in_test?
|
|
56
65
|
warn("attach(#{name.inspect}) called outside a running example; ignored")
|
|
@@ -82,6 +91,8 @@ module RaiderReporter
|
|
|
82
91
|
def finish!(output = $stderr)
|
|
83
92
|
return nil if dry_run?
|
|
84
93
|
|
|
94
|
+
write_exec_summary
|
|
95
|
+
|
|
85
96
|
detector = CiDetector.new
|
|
86
97
|
ci = detector.ci
|
|
87
98
|
shard = ShardDetector.new(job: ci&.fetch("job", nil)).shard
|
|
@@ -108,6 +119,19 @@ module RaiderReporter
|
|
|
108
119
|
|
|
109
120
|
private
|
|
110
121
|
|
|
122
|
+
# First, so a later reporting problem cannot lose it. A failure here only means
|
|
123
|
+
# no summary, and exec then keeps the command's exit code.
|
|
124
|
+
def write_exec_summary
|
|
125
|
+
path = configuration.exec_summary_path
|
|
126
|
+
counts = collector&.framework_counts
|
|
127
|
+
return if path.nil? || counts.nil?
|
|
128
|
+
|
|
129
|
+
ExecSummary.write(path, framework_failures: counts[:failures], quarantined: collector.quarantined_count,
|
|
130
|
+
errors_outside_tests: counts[:errors_outside_tests])
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
warn("could not write the exec summary: #{e.class}: #{e.message}")
|
|
133
|
+
end
|
|
134
|
+
|
|
111
135
|
def write_html_report
|
|
112
136
|
return unless configuration.html_report
|
|
113
137
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: raider_reporter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Augustin Gottlieb
|
|
@@ -36,9 +36,12 @@ files:
|
|
|
36
36
|
- lib/raider_reporter/content_types.rb
|
|
37
37
|
- lib/raider_reporter/cucumber.rb
|
|
38
38
|
- lib/raider_reporter/cucumber/formatter.rb
|
|
39
|
+
- lib/raider_reporter/exec.rb
|
|
40
|
+
- lib/raider_reporter/exec_summary.rb
|
|
39
41
|
- lib/raider_reporter/github_event.rb
|
|
40
42
|
- lib/raider_reporter/models.rb
|
|
41
43
|
- lib/raider_reporter/payload.rb
|
|
44
|
+
- lib/raider_reporter/quarantine.rb
|
|
42
45
|
- lib/raider_reporter/rspec.rb
|
|
43
46
|
- lib/raider_reporter/rspec/formatter.rb
|
|
44
47
|
- lib/raider_reporter/shard_detector.rb
|
|
@@ -68,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
68
71
|
- !ruby/object:Gem::Version
|
|
69
72
|
version: '0'
|
|
70
73
|
requirements: []
|
|
71
|
-
rubygems_version: 4.0.
|
|
74
|
+
rubygems_version: 4.0.21
|
|
72
75
|
specification_version: 4
|
|
73
76
|
summary: RSpec and Cucumber formatters that write Raider reports locally and upload
|
|
74
77
|
them to Raider
|