raider_reporter 0.10.0 → 0.12.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 +15 -0
- data/README.md +16 -9
- data/lib/raider_reporter/app_frame.rb +37 -12
- data/lib/raider_reporter/collector.rb +11 -1
- data/lib/raider_reporter/configuration.rb +12 -1
- data/lib/raider_reporter/content_types.rb +10 -2
- data/lib/raider_reporter/cucumber/formatter.rb +33 -5
- data/lib/raider_reporter/event_stream.rb +78 -0
- data/lib/raider_reporter/models.rb +6 -1
- data/lib/raider_reporter/payload.rb +4 -1
- data/lib/raider_reporter/quarantine.rb +1 -1
- data/lib/raider_reporter/rspec/formatter.rb +46 -12
- data/lib/raider_reporter/version.rb +1 -1
- data/lib/raider_reporter/writers/html.rb +7 -0
- data/lib/raider_reporter/writers/uploader.rb +11 -11
- data/lib/raider_reporter.rb +11 -8
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5caf918bfe636da31412f0d9befd2122cdb81c1c207ec594b5e60f11a9aa930
|
|
4
|
+
data.tar.gz: 96b844a743d018f47b54cbb80c3552ab2a76e2be6b247095099efa2457f07a3a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c030e57458d0a854dd9d52fef8086301b52d62cfa181e689c5a30f88cc6b6feb108510c767183d3abb3cac7ed0deeb1037e0ed7b9b4a80357e95c1ca805bf06
|
|
7
|
+
data.tar.gz: '0382b224926f9a6595f66355102afd825ced75a0e107bdf65c37c8cb7af8d5a93cfef66ef8a5f7952bb63351c9c9f16c226afd28ca5c46112daf29240da29343'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0 — 2026-11-01
|
|
4
|
+
|
|
5
|
+
- Raider traces: the content type `application/vnd.raider.trace+zip` (`type: :trace`) gets its own size cap, `config.max_trace_bytes` (default 25 MB). The 5 MB `max_attachment_bytes` and the 50 MB `max_video_bytes` still apply to everything else. A trace over its cap is skipped with a warning
|
|
6
|
+
- `RaiderReporter.attach(name, path, type:, attempt:)` takes an optional `attempt`, written to the attachment's entry in `run.json` and in the upload when given
|
|
7
|
+
- Uploads send screenshots and other files first, then traces, then videos, in the room left in the budget. A trace that does not fit is skipped and later, smaller ones are still tried
|
|
8
|
+
- The local HTML report shows a trace as a download link ("Trace (open in Raider Desktop)", with the attempt when known)
|
|
9
|
+
- `.zip` files resolve to `application/zip`
|
|
10
|
+
- No breaking changes
|
|
11
|
+
|
|
12
|
+
## 0.11.0 — 2026-10-29
|
|
13
|
+
|
|
14
|
+
- Live events: when `RAIDER_EVENTS_DIR` is set, the RSpec formatter and the Cucumber formatter/plugin append NDJSON lines (`plan`, `started`, `finished`, `done`, each with `"v":1`) to `<dir>/worker-<n>.ndjson`, where `n` is the shard index (1 without a shard). Cucumber writes `started` and `finished` once per attempt under `--retry`. The Raider desktop app tails them during a run. The files are created owner-only (0600) and a symlinked file is refused. With the variable unset, nothing changes. A write error switches the stream off after one warning and never fails the suite
|
|
15
|
+
- Backtrace frames inside the project are written project-relative (`spec/login_spec.rb:12:in ...`); gem, stdlib and `vendor/` frames are unchanged
|
|
16
|
+
- No breaking changes
|
|
17
|
+
|
|
3
18
|
## 0.10.0 — 2026-10-26
|
|
4
19
|
|
|
5
20
|
- Video attachments play inline in the HTML report: `video/*` attachments render as a `<video controls>` player (metadata preloaded only) with a download link, instead of a plain link. Unsafe paths are still dropped
|
data/README.md
CHANGED
|
@@ -101,7 +101,7 @@ Cucumber's `attach` is captured only when given the path of an existing file. In
|
|
|
101
101
|
|
|
102
102
|
`parallel_tests` works with no configuration for Cucumber too: each process writes `run-<n>.json`. See [Parallel runs](#parallel-runs).
|
|
103
103
|
|
|
104
|
-
## Attaching screenshots and
|
|
104
|
+
## Attaching screenshots, videos and traces
|
|
105
105
|
|
|
106
106
|
Call `RaiderReporter.attach` while an example is running, for example from an `after` hook:
|
|
107
107
|
|
|
@@ -120,11 +120,17 @@ end
|
|
|
120
120
|
|
|
121
121
|
`attach` copies the file right away, so the attachment keeps the bytes the file had at that moment even if the same path is overwritten by a later example. Using a path per example (as above) still makes the files on disk easier to find.
|
|
122
122
|
|
|
123
|
-
`attach` returns `true` on success. It returns `false` and prints a `[raider_reporter]` warning, never raising, if it is called outside a running example (for instance when the formatter is not loaded), if the file does not exist, or if the file is larger than its size cap: 5 MB (`max_attachment_bytes`),
|
|
123
|
+
`attach` returns `true` on success. It returns `false` and prints a `[raider_reporter]` warning, never raising, if it is called outside a running example (for instance when the formatter is not loaded), if the file does not exist, or if the file is larger than its size cap: 5 MB (`max_attachment_bytes`), 50 MB for videos (`video/*`, `max_video_bytes`), or 25 MB for traces (`max_trace_bytes`).
|
|
124
124
|
|
|
125
125
|
Videos (`.webm`, `.mp4`) are attached the same way, for example `RaiderReporter.attach("video", path, type: :webm)`. In the local HTML report they play inline, with a download link.
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
Raider traces (`trace.zip`, content type `application/vnd.raider.trace+zip`, written by ruby_raider projects for failing tests) are attached with `RaiderReporter.attach("trace", path, type: :trace, attempt: 2)`. `attempt` is optional on any attachment. When given, it is written to the attachment's entry in `run.json` and in the upload, so a viewer can tell the traces of different retries apart. In the local HTML report a trace is a download link; open it in Raider Desktop. The format is described in the [trace viewer design](https://github.com/RaiderHQ/raider_planning/blob/main/docs/superpowers/specs/2026-11-01-trace-viewer-design.md).
|
|
128
|
+
|
|
129
|
+
When a run is uploaded, screenshots and other attachments are sent first, then traces, then videos, in the room left in the upload budget. A trace or video that does not fit is skipped and later, smaller ones are still tried. The local `raider-results/attachments/` folder keeps every attachment that passed its size cap.
|
|
130
|
+
|
|
131
|
+
## Live events
|
|
132
|
+
|
|
133
|
+
Set `RAIDER_EVENTS_DIR` and the RSpec formatter, or the Cucumber formatter/plugin, appends one JSON line per event to `<dir>/worker-<n>.ndjson` (`n` is the shard index, or 1): `plan` (the examples that will run), `started`, `finished` (status, duration, attempts; Cucumber writes one `started`/`finished` pair per `--retry` attempt) and `done`. Raider desktop tails these files for live results. Unset, nothing is written; a write error turns the stream off with one warning.
|
|
128
134
|
|
|
129
135
|
## Configuration
|
|
130
136
|
|
|
@@ -138,13 +144,14 @@ end
|
|
|
138
144
|
| Option | Default | Description |
|
|
139
145
|
| --- | --- | --- |
|
|
140
146
|
| `token` | `ENV["RAIDER_TOKEN"]` | API token used for uploads. |
|
|
141
|
-
| `endpoint` | `ENV["RAIDER_ENDPOINT"]` | Base URL of
|
|
147
|
+
| `endpoint` | `ENV["RAIDER_ENDPOINT"]` | Base URL of your Raider Hideout server (what its setup page or bin/rails hideout:setup prints as RAIDER_ENDPOINT). |
|
|
142
148
|
| `output_dir` | `"raider-results"` | Directory for local output. |
|
|
143
149
|
| `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`. |
|
|
144
150
|
| `html_report` | `true` | Write `index.html`, the local HTML report, into `output_dir` after each run. See [Local report](#local-report). |
|
|
145
151
|
| `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). |
|
|
146
152
|
| `max_attachment_bytes` | `5 MB` | Largest non-video attachment `attach` accepts. |
|
|
147
153
|
| `max_video_bytes` | `50 MB` | Largest `video/*` attachment `attach` accepts. |
|
|
154
|
+
| `max_trace_bytes` | `25 MB` | Largest Raider trace (`application/vnd.raider.trace+zip`) `attach` accepts. |
|
|
148
155
|
| `exec_summary_path` | `ENV["RAIDER_EXEC_SUMMARY"]` | File where the run writes its failure counts (framework failures, quarantined, errors outside tests) for `raider-reporter exec` to read when it decides the exit code. `exec` sets it for the command it runs; leave it unset otherwise. Nothing is written when it is unset. See [Quarantine](#quarantine). |
|
|
149
156
|
| `environment` | `{}` | Free-form key/values stored with the run (browser, suite name, and so on). Values are JSON values: strings, booleans or numbers. |
|
|
150
157
|
|
|
@@ -176,7 +183,7 @@ Set `config.html_report = false` to turn it off. An `index.html` that raider_rep
|
|
|
176
183
|
If both a token and an endpoint are set, the run (plus attachments) is uploaded when the suite finishes:
|
|
177
184
|
|
|
178
185
|
```
|
|
179
|
-
RAIDER_TOKEN=... RAIDER_ENDPOINT=https://
|
|
186
|
+
RAIDER_TOKEN=... RAIDER_ENDPOINT=https://hideout.example.com bundle exec rspec
|
|
180
187
|
```
|
|
181
188
|
|
|
182
189
|
On success the formatter prints `Raider report: <url>` to stderr (so tools that parse RSpec's stdout, such as `--format json`, stay clean), using the build URL when the server returns one and the run URL otherwise (or `Raider report uploaded (run <id>)` if the response has no URL). If either value is missing, nothing is uploaded and the local files are still written.
|
|
@@ -185,7 +192,7 @@ An upload carries at most 45 MB of attachments and at most 1000 attachment files
|
|
|
185
192
|
|
|
186
193
|
Reporting is fail-open: no error inside the gem, network failure, timeout or non-2xx response will raise or change RSpec's exit status. Each connect/write/read step of the upload times out after 30 seconds. Problems are printed as warnings prefixed with `[raider_reporter]`. An endpoint without `http://` or `https://` (for example `localhost:3000`) produces a warning and nothing is uploaded; a trailing slash is fine.
|
|
187
194
|
|
|
188
|
-
|
|
195
|
+
Raider Hideout is self-hosted: run your own server and set `RAIDER_ENDPOINT` to its URL. There is no hosted service.
|
|
189
196
|
|
|
190
197
|
## Quarantine
|
|
191
198
|
|
|
@@ -230,10 +237,10 @@ On pull requests, `raider-reporter comment` posts one comment with the build's r
|
|
|
230
237
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
231
238
|
```
|
|
232
239
|
|
|
233
|
-
- The comment is rendered by the Raider server (`GET /api/v1/builds/summary`); the gem only fetches the Markdown and posts it. The same comment is updated on every run instead of adding a new one.
|
|
240
|
+
- The comment is rendered by the Raider Hideout server (`GET /api/v1/builds/summary`); the gem only fetches the Markdown and posts it. The same comment is updated on every run instead of adding a new one.
|
|
234
241
|
- Pull requests from forks are skipped: GitHub gives their workflows a read-only token, so the command prints a notice and exits 0.
|
|
235
242
|
- The command never fails your CI: it always exits 0 and prints notices to stderr, prefixed `[raider_reporter]`. It needs `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, `GITHUB_EVENT_PATH`, `RAIDER_TOKEN` and `RAIDER_ENDPOINT`; if one is missing, or the event is not a pull request, it does nothing.
|
|
236
|
-
- It requires a Raider server that supports the summary endpoint and the `git.pr` field. Upgrade the server before using it.
|
|
243
|
+
- It requires a Raider Hideout server that supports the summary endpoint and the `git.pr` field. Upgrade the server before using it.
|
|
237
244
|
|
|
238
245
|
## Quality gate
|
|
239
246
|
|
|
@@ -313,7 +320,7 @@ jobs:
|
|
|
313
320
|
- **Artifact names:** pipeline artifacts cannot be overwritten within a run, so the name includes the job position and attempt. A fixed name would fail when you re-run failed jobs or use `strategy: parallel`.
|
|
314
321
|
- **Azure Repos only:** the PR comment works for repositories hosted in Azure Repos. A GitHub-hosted repository built by Azure Pipelines still uploads results, but `comment` prints `skipped: not an Azure Repos pull request`.
|
|
315
322
|
- **Parallel jobs:** `strategy: parallel: N` shards are detected automatically and grouped into one build, like GitHub matrix jobs.
|
|
316
|
-
- The command never fails your pipeline: it always exits 0 and prints notices to stderr, prefixed `[raider_reporter]`. It needs `SYSTEM_ACCESSTOKEN`, `RAIDER_TOKEN` and `RAIDER_ENDPOINT`, and requires a Raider server that supports the summary endpoint.
|
|
323
|
+
- The command never fails your pipeline: it always exits 0 and prints notices to stderr, prefixed `[raider_reporter]`. It needs `SYSTEM_ACCESSTOKEN`, `RAIDER_TOKEN` and `RAIDER_ENDPOINT`, and requires a Raider Hideout server that supports the summary endpoint.
|
|
317
324
|
|
|
318
325
|
## Parallel runs
|
|
319
326
|
|
|
@@ -17,23 +17,48 @@ module RaiderReporter
|
|
|
17
17
|
excluded = excluded_prefixes
|
|
18
18
|
Array(backtrace).each do |line|
|
|
19
19
|
line = line.to_s
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
relative = relative_path(line, base, excluded)
|
|
21
|
+
return Text.utf8("#{relative}#{line[line[/\A(.+?):\d+/, 1].length..]}")[0, MAX_LENGTH] if relative
|
|
22
|
+
end
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Rewrites every backtrace line that lies in project code to
|
|
27
|
+
# "relative/path.rb:42:in '...'"; all other lines are returned unchanged.
|
|
28
|
+
def self.relativize(backtrace, root:)
|
|
29
|
+
return [] if backtrace.nil? || backtrace.empty?
|
|
30
|
+
return backtrace if root.nil?
|
|
31
|
+
|
|
32
|
+
base = "#{File.expand_path(root.to_s).chomp("/")}/"
|
|
33
|
+
excluded = excluded_prefixes
|
|
34
|
+
Array(backtrace).map do |line|
|
|
35
|
+
text = line.to_s.scrub
|
|
36
|
+
relative = relative_path(text, base, excluded)
|
|
37
|
+
relative ? "#{relative}#{text[text[/\A(.+?):\d+/, 1].length..]}" : line
|
|
38
|
+
rescue StandardError
|
|
39
|
+
line
|
|
40
|
+
end
|
|
41
|
+
end
|
|
22
42
|
|
|
23
|
-
|
|
24
|
-
|
|
43
|
+
# The project-relative path of a backtrace line's file, or nil when the
|
|
44
|
+
# line is not in project code.
|
|
45
|
+
def self.relative_path(line, base, excluded)
|
|
46
|
+
path = line[/\A(.+?):\d+/, 1]
|
|
47
|
+
return nil if path.nil? || path.start_with?("<internal:", "(", "-")
|
|
25
48
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
next if excluded.any? { |prefix| absolute.start_with?(prefix) }
|
|
49
|
+
absolute = File.expand_path(path, base)
|
|
50
|
+
return nil unless File.file?(absolute)
|
|
29
51
|
|
|
30
|
-
|
|
31
|
-
|
|
52
|
+
absolute = inside_root(absolute, base)
|
|
53
|
+
return nil unless absolute
|
|
54
|
+
return nil if excluded.any? { |prefix| absolute.start_with?(prefix) }
|
|
32
55
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
56
|
+
relative = absolute.delete_prefix(base)
|
|
57
|
+
return nil if relative.start_with?("vendor/", ".bundle/") || relative.include?("/.bundle/")
|
|
58
|
+
|
|
59
|
+
relative
|
|
36
60
|
end
|
|
61
|
+
private_class_method :relative_path
|
|
37
62
|
|
|
38
63
|
# Returns the path (rewritten under base when only the real paths match,
|
|
39
64
|
# e.g. a symlinked tmpdir) or nil when it lies outside the root.
|
|
@@ -4,15 +4,18 @@ require "securerandom"
|
|
|
4
4
|
require "tmpdir"
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require "set"
|
|
7
|
+
require_relative "content_types"
|
|
7
8
|
|
|
8
9
|
module RaiderReporter
|
|
9
10
|
# Accumulates one suite run. Framework formatters call it; writers read the Run it returns.
|
|
10
11
|
class Collector
|
|
11
12
|
def initialize(max_attachment_bytes:, warn:, max_video_bytes: Configuration::DEFAULT_MAX_VIDEO_BYTES,
|
|
13
|
+
max_trace_bytes: Configuration::DEFAULT_MAX_TRACE_BYTES,
|
|
12
14
|
clock: -> { Time.now.utc }, uuid: -> { SecureRandom.uuid },
|
|
13
15
|
quarantine: -> { Quarantine::EMPTY })
|
|
14
16
|
@max_attachment_bytes = max_attachment_bytes
|
|
15
17
|
@max_video_bytes = max_video_bytes
|
|
18
|
+
@max_trace_bytes = max_trace_bytes
|
|
16
19
|
@warn = warn
|
|
17
20
|
@clock = clock
|
|
18
21
|
@uuid = uuid
|
|
@@ -120,12 +123,19 @@ module RaiderReporter
|
|
|
120
123
|
@warn.call("attachment #{path} no longer exists; skipped")
|
|
121
124
|
return false
|
|
122
125
|
end
|
|
123
|
-
limit = attachment.content_type
|
|
126
|
+
limit = limit_for(attachment.content_type)
|
|
124
127
|
if File.size(path) > limit
|
|
125
128
|
@warn.call("attachment #{path} is larger than #{limit} bytes; skipped")
|
|
126
129
|
return false
|
|
127
130
|
end
|
|
128
131
|
true
|
|
129
132
|
end
|
|
133
|
+
|
|
134
|
+
def limit_for(content_type)
|
|
135
|
+
return @max_trace_bytes if ContentTypes.trace?(content_type)
|
|
136
|
+
return @max_video_bytes if ContentTypes.video?(content_type)
|
|
137
|
+
|
|
138
|
+
@max_attachment_bytes
|
|
139
|
+
end
|
|
130
140
|
end
|
|
131
141
|
end
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "raider_reporter/azure_env"
|
|
4
|
+
require "raider_reporter/content_types"
|
|
4
5
|
|
|
5
6
|
module RaiderReporter
|
|
6
7
|
class Configuration
|
|
7
8
|
TRUTHY = %w[1 true].freeze
|
|
8
9
|
DEFAULT_MAX_VIDEO_BYTES = 50 * 1024 * 1024
|
|
10
|
+
DEFAULT_MAX_TRACE_BYTES = 25 * 1024 * 1024
|
|
9
11
|
|
|
10
12
|
attr_accessor :token, :endpoint, :output_dir, :clean_output_dir, :html_report, :environment, :max_attachment_bytes,
|
|
11
|
-
:max_video_bytes, :quarantine, :exec_summary_path
|
|
13
|
+
:max_video_bytes, :max_trace_bytes, :quarantine, :exec_summary_path
|
|
12
14
|
|
|
13
15
|
def initialize(env = ENV)
|
|
14
16
|
@token = AzureEnv.value(env, "RAIDER_TOKEN")
|
|
@@ -19,10 +21,19 @@ module RaiderReporter
|
|
|
19
21
|
@environment = {}
|
|
20
22
|
@max_attachment_bytes = 5 * 1024 * 1024
|
|
21
23
|
@max_video_bytes = DEFAULT_MAX_VIDEO_BYTES
|
|
24
|
+
@max_trace_bytes = DEFAULT_MAX_TRACE_BYTES
|
|
22
25
|
@quarantine = TRUTHY.include?(AzureEnv.value(env, "RAIDER_QUARANTINE").to_s.downcase)
|
|
23
26
|
@exec_summary_path = AzureEnv.value(env, "RAIDER_EXEC_SUMMARY")
|
|
24
27
|
end
|
|
25
28
|
|
|
29
|
+
# The size cap for one attachment of this content type.
|
|
30
|
+
def attachment_limit(content_type)
|
|
31
|
+
return max_trace_bytes if ContentTypes.trace?(content_type)
|
|
32
|
+
return max_video_bytes if ContentTypes.video?(content_type)
|
|
33
|
+
|
|
34
|
+
max_attachment_bytes
|
|
35
|
+
end
|
|
36
|
+
|
|
26
37
|
def upload?
|
|
27
38
|
present?(token) && present?(endpoint)
|
|
28
39
|
end
|
|
@@ -2,18 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
module RaiderReporter
|
|
4
4
|
module ContentTypes
|
|
5
|
+
TRACE = "application/vnd.raider.trace+zip"
|
|
6
|
+
|
|
5
7
|
BY_EXTENSION = {
|
|
6
8
|
".png" => "image/png", ".jpg" => "image/jpeg", ".jpeg" => "image/jpeg", ".gif" => "image/gif",
|
|
7
9
|
".webp" => "image/webp", ".mp4" => "video/mp4", ".webm" => "video/webm", ".mov" => "video/quicktime",
|
|
8
|
-
".txt" => "text/plain", ".log" => "text/plain", ".html" => "text/html", ".json" => "application/json"
|
|
10
|
+
".txt" => "text/plain", ".log" => "text/plain", ".html" => "text/html", ".json" => "application/json",
|
|
11
|
+
".zip" => "application/zip"
|
|
9
12
|
}.freeze
|
|
10
13
|
BY_SYMBOL = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", mp4: "video/mp4",
|
|
11
|
-
webm: "video/webm", text: "text/plain", html: "text/html", json: "application/json"
|
|
14
|
+
webm: "video/webm", text: "text/plain", html: "text/html", json: "application/json",
|
|
15
|
+
trace: TRACE }.freeze
|
|
12
16
|
|
|
13
17
|
MIME = %r{\A[\w.+-]+/[\w.+-]+\z}
|
|
14
18
|
|
|
15
19
|
module_function
|
|
16
20
|
|
|
21
|
+
def trace?(content_type) = content_type.to_s == TRACE
|
|
22
|
+
|
|
23
|
+
def video?(content_type) = content_type.to_s.start_with?("video/")
|
|
24
|
+
|
|
17
25
|
def resolve(type, path)
|
|
18
26
|
if type.is_a?(String)
|
|
19
27
|
return type if type.match?(MIME)
|
|
@@ -100,6 +100,7 @@ module RaiderReporter
|
|
|
100
100
|
RaiderReporter.dry_run = config.dry_run?
|
|
101
101
|
config.on_event(:test_run_started) { |_event| guard { @collector.start_run } }
|
|
102
102
|
config.on_event(:test_run_started) { |event| guard { expect_cases(event.test_cases) } }
|
|
103
|
+
config.on_event(:test_run_started) { |event| guard { RaiderReporter.event_stream&.plan(event.test_cases.map { |tc| event_test(tc) }) } }
|
|
103
104
|
config.on_event(:test_case_finished) { |event| count_outcome(event.test_case, event.result) }
|
|
104
105
|
config.on_event(:test_case_started) { |event| guard { case_started(event.test_case) } }
|
|
105
106
|
config.on_event(:test_step_finished) { |event| guard { step_finished(event.test_step, event.result) } }
|
|
@@ -113,6 +114,7 @@ module RaiderReporter
|
|
|
113
114
|
def case_started(test_case)
|
|
114
115
|
@collector.start_test
|
|
115
116
|
@attempt = { test_case: test_case }
|
|
117
|
+
RaiderReporter.event_stream&.started(event_test(test_case))
|
|
116
118
|
end
|
|
117
119
|
|
|
118
120
|
# The first non-passed step explains why the scenario did not pass.
|
|
@@ -133,10 +135,16 @@ module RaiderReporter
|
|
|
133
135
|
status = "quarantined" if status == "failed" && quarantined?(fields)
|
|
134
136
|
key = "#{test_case.location}|#{test_case.name}"
|
|
135
137
|
@attempts[key] += 1
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
begin
|
|
139
|
+
# The newest attempt replaces the older one, taking its attachments with it.
|
|
140
|
+
@buffer[key] = @collector.build_test(
|
|
141
|
+
**fields, status: status, duration_ms: duration_ms(result), attempts: @attempts[key], failure: failure
|
|
142
|
+
)
|
|
143
|
+
ensure
|
|
144
|
+
# Even if building the result failed: the desktop app must still see the attempt end
|
|
145
|
+
RaiderReporter.event_stream&.finished(id: fields[:identifier], status: status,
|
|
146
|
+
duration_ms: duration_ms(result), attempts: @attempts[key])
|
|
147
|
+
end
|
|
140
148
|
end
|
|
141
149
|
|
|
142
150
|
def run_finished
|
|
@@ -145,9 +153,23 @@ module RaiderReporter
|
|
|
145
153
|
safely(nil) { record_framework_counts }
|
|
146
154
|
RaiderReporter.finish!($stderr)
|
|
147
155
|
ensure
|
|
156
|
+
# Even if finish! raised, so the desktop app sees the stream end
|
|
157
|
+
safely(nil) { finish_events }
|
|
148
158
|
self.class.release(@config)
|
|
149
159
|
end
|
|
150
160
|
|
|
161
|
+
def finish_events
|
|
162
|
+
stream = RaiderReporter.event_stream
|
|
163
|
+
stream&.done
|
|
164
|
+
stream&.close
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def event_test(test_case)
|
|
168
|
+
fields = identity_for(test_case)
|
|
169
|
+
{ id: fields[:identifier], file: fields[:file_path], line: fields[:line_number],
|
|
170
|
+
description: Text.utf8(test_case.name), full_name: fields[:full_name] }
|
|
171
|
+
end
|
|
172
|
+
|
|
151
173
|
def expect_cases(test_cases)
|
|
152
174
|
@expected_cases = test_cases.map { |test_case| "#{test_case.location}|#{test_case.name}" }.uniq.size
|
|
153
175
|
end
|
|
@@ -259,11 +281,17 @@ module RaiderReporter
|
|
|
259
281
|
Failure.new(
|
|
260
282
|
message: Text.utf8("#{step_label(step_info, test_case)}\n\n#{exception.message}")[0, Text::MAX_MESSAGE],
|
|
261
283
|
exception_class: Text.utf8(exception.class.name),
|
|
262
|
-
backtrace: (exception.backtrace || []).first(Text::MAX_BACKTRACE).map { |line| Text.utf8(line) },
|
|
284
|
+
backtrace: relativize(exception.backtrace || []).first(Text::MAX_BACKTRACE).map { |line| Text.utf8(line) },
|
|
263
285
|
app_frame: app_frame_for(exception)
|
|
264
286
|
)
|
|
265
287
|
end
|
|
266
288
|
|
|
289
|
+
def relativize(backtrace)
|
|
290
|
+
AppFrame.relativize(backtrace, root: RaiderReporter.project_root || Dir.pwd)
|
|
291
|
+
rescue StandardError
|
|
292
|
+
backtrace
|
|
293
|
+
end
|
|
294
|
+
|
|
267
295
|
def app_frame_for(exception)
|
|
268
296
|
AppFrame.from(exception.backtrace, root: RaiderReporter.project_root || Dir.pwd)
|
|
269
297
|
rescue StandardError
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "raider_reporter/text"
|
|
6
|
+
require "raider_reporter/shard_detector"
|
|
7
|
+
|
|
8
|
+
module RaiderReporter
|
|
9
|
+
# Live NDJSON events for the Raider desktop app, one file per worker. On only
|
|
10
|
+
# when RAIDER_EVENTS_DIR is set. A write error switches it off after one warning.
|
|
11
|
+
class EventStream
|
|
12
|
+
VERSION = 1
|
|
13
|
+
# Owner-only, and never through a symlink someone planted in the events dir.
|
|
14
|
+
OPEN_FLAGS = File::WRONLY | File::APPEND | File::CREAT | (defined?(File::NOFOLLOW) ? File::NOFOLLOW : 0)
|
|
15
|
+
|
|
16
|
+
def self.open(warn:, env: ENV, shard: ShardDetector.new.shard)
|
|
17
|
+
dir = env["RAIDER_EVENTS_DIR"].to_s
|
|
18
|
+
return nil if dir.empty?
|
|
19
|
+
|
|
20
|
+
FileUtils.mkdir_p(dir)
|
|
21
|
+
file = File.open(File.join(dir, "worker-#{shard ? shard["index"] : 1}.ndjson"), OPEN_FLAGS, 0o600)
|
|
22
|
+
file.sync = true
|
|
23
|
+
new(file, warn: warn)
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
warn.call("events stream disabled: #{e.class}: #{e.message}")
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(file, warn:)
|
|
30
|
+
@file = file
|
|
31
|
+
@warn = warn
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def plan(tests)
|
|
35
|
+
emit("plan", tests: tests.map { |test| test_fields(test) })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def started(test)
|
|
39
|
+
emit("started", **test_fields(test))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def finished(id:, status:, duration_ms:, attempts:)
|
|
43
|
+
emit("finished", id: cap(id), status: status, duration_ms: duration_ms, attempts: attempts)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def done
|
|
47
|
+
emit("done")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def close
|
|
51
|
+
@file&.close
|
|
52
|
+
rescue StandardError
|
|
53
|
+
nil
|
|
54
|
+
ensure
|
|
55
|
+
@file = nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def test_fields(test)
|
|
61
|
+
{ id: cap(test[:id]), file: cap(test[:file]), line: test[:line],
|
|
62
|
+
description: cap(test[:description]), full_name: cap(test[:full_name]) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def cap(value)
|
|
66
|
+
Text.utf8(value)[0, Text::MAX_MESSAGE]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def emit(type, **fields)
|
|
70
|
+
return unless @file
|
|
71
|
+
|
|
72
|
+
@file.write("#{JSON.generate({ v: VERSION, type: type }.merge(fields))}\n")
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
@warn.call("events stream disabled: #{e.class}: #{e.message}")
|
|
75
|
+
close
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -9,7 +9,12 @@ module RaiderReporter
|
|
|
9
9
|
super
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
|
-
|
|
12
|
+
# attempt: which try of the test produced it, when the caller knows (traces); nil otherwise.
|
|
13
|
+
Attachment = Data.define(:name, :source_path, :content_type, :attempt) do
|
|
14
|
+
def initialize(name:, source_path:, content_type:, attempt: nil)
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
13
18
|
TestResult = Data.define(
|
|
14
19
|
:identifier, :full_name, :file_path, :line_number, :status,
|
|
15
20
|
:duration_ms, :attempts, :tags, :failure, :attachments
|
|
@@ -88,7 +88,10 @@ module RaiderReporter
|
|
|
88
88
|
|
|
89
89
|
def attachment_hash(entry, mode)
|
|
90
90
|
ref = mode == :local ? { "path" => entry[:path] } : { "part" => entry[:part] }
|
|
91
|
-
|
|
91
|
+
attachment = entry[:attachment]
|
|
92
|
+
hash = { "name" => attachment.name, "content_type" => attachment.content_type }.merge(ref)
|
|
93
|
+
hash["attempt"] = attachment.attempt if attachment.attempt
|
|
94
|
+
hash
|
|
92
95
|
end
|
|
93
96
|
end
|
|
94
97
|
end
|
|
@@ -11,7 +11,7 @@ module RaiderReporter
|
|
|
11
11
|
NUL = "\u0000"
|
|
12
12
|
NUL_REPLACEMENT = "\u{FFFD}"
|
|
13
13
|
|
|
14
|
-
# The server's test identity:
|
|
14
|
+
# The server's test identity: Raider Hideout's TestCase.key_for over the payload's
|
|
15
15
|
# file_path and full_name, after ingest replaced NUL in each of them.
|
|
16
16
|
def self.test_key(file_path, full_name)
|
|
17
17
|
parts = [file_path, full_name].map { |part| part.to_s.tr(NUL, NUL_REPLACEMENT) }
|
|
@@ -24,10 +24,12 @@ module RaiderReporter
|
|
|
24
24
|
|
|
25
25
|
def start(_notification)
|
|
26
26
|
guard { @collector.start_run }
|
|
27
|
+
guard { RaiderReporter.event_stream&.plan(planned_tests) }
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
def example_started(
|
|
30
|
+
def example_started(notification)
|
|
30
31
|
guard { @collector.start_test }
|
|
32
|
+
guard { RaiderReporter.event_stream&.started(event_test(notification.example)) }
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def example_passed(notification)
|
|
@@ -53,6 +55,10 @@ module RaiderReporter
|
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
def close(_notification)
|
|
58
|
+
guard do
|
|
59
|
+
RaiderReporter.event_stream&.done
|
|
60
|
+
RaiderReporter.event_stream&.close
|
|
61
|
+
end
|
|
56
62
|
guard { RaiderReporter.finish! }
|
|
57
63
|
end
|
|
58
64
|
|
|
@@ -66,17 +72,38 @@ module RaiderReporter
|
|
|
66
72
|
failure = status == "failed" ? failure_for(execution.exception) : nil
|
|
67
73
|
# The key is computed from the payload's own file_path and full_name (spec §4.2).
|
|
68
74
|
status = "quarantined" if status == "failed" && @collector.quarantined?(file_path, full_name)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
attempts = metadata.fetch(:retry_attempts, 0).to_i + 1
|
|
76
|
+
duration_ms = ((execution.run_time || 0) * 1000).round
|
|
77
|
+
begin
|
|
78
|
+
@collector.finish_test(
|
|
79
|
+
identifier: example.id,
|
|
80
|
+
full_name: full_name,
|
|
81
|
+
file_path: file_path,
|
|
82
|
+
line_number: metadata[:line_number],
|
|
83
|
+
status: status,
|
|
84
|
+
duration_ms: duration_ms,
|
|
85
|
+
attempts: attempts,
|
|
86
|
+
tags: metadata.select { |key, value| value == true && !NON_TAG_KEYS.include?(key) }.keys.map(&:to_s).sort,
|
|
87
|
+
failure: failure
|
|
88
|
+
)
|
|
89
|
+
ensure
|
|
90
|
+
# Even if recording failed: the desktop app must still see the example end
|
|
91
|
+
RaiderReporter.event_stream&.finished(id: example.id, status: status, duration_ms: duration_ms,
|
|
92
|
+
attempts: attempts)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The examples this run will execute (after tag/line filters), in run order.
|
|
97
|
+
def planned_tests
|
|
98
|
+
world = ::RSpec.world
|
|
99
|
+
world.ordered_example_groups.flat_map(&:descendants)
|
|
100
|
+
.flat_map { |group| world.filtered_examples[group] }.map { |example| event_test(example) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def event_test(example)
|
|
104
|
+
{ id: example.id, file: example.metadata[:file_path].to_s.delete_prefix("./"),
|
|
105
|
+
line: example.metadata[:line_number], description: example.description,
|
|
106
|
+
full_name: example.full_description }
|
|
80
107
|
end
|
|
81
108
|
|
|
82
109
|
# Errors RSpec reported outside examples, plus one when it stopped early
|
|
@@ -91,6 +118,7 @@ module RaiderReporter
|
|
|
91
118
|
return nil unless exception
|
|
92
119
|
|
|
93
120
|
backtrace = ::RSpec.configuration.backtrace_formatter.format_backtrace(exception.backtrace || [])
|
|
121
|
+
backtrace = relativize(backtrace)
|
|
94
122
|
Failure.new(
|
|
95
123
|
message: utf8(exception.message)[0, MAX_MESSAGE],
|
|
96
124
|
exception_class: utf8(exception.class.name),
|
|
@@ -108,6 +136,12 @@ module RaiderReporter
|
|
|
108
136
|
exception.all_exceptions.first&.backtrace
|
|
109
137
|
end
|
|
110
138
|
|
|
139
|
+
def relativize(backtrace)
|
|
140
|
+
AppFrame.relativize(backtrace, root: RaiderReporter.project_root || Dir.pwd)
|
|
141
|
+
rescue StandardError
|
|
142
|
+
backtrace
|
|
143
|
+
end
|
|
144
|
+
|
|
111
145
|
def app_frame_for(exception)
|
|
112
146
|
AppFrame.from(raw_backtrace(exception), root: RaiderReporter.project_root || Dir.pwd)
|
|
113
147
|
rescue StandardError
|
|
@@ -4,6 +4,7 @@ require "erb"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "json"
|
|
6
6
|
require "time"
|
|
7
|
+
require_relative "../content_types"
|
|
7
8
|
|
|
8
9
|
module RaiderReporter
|
|
9
10
|
module Writers
|
|
@@ -229,6 +230,8 @@ module RaiderReporter
|
|
|
229
230
|
%(<a href="#{href}"><img loading="lazy" src="#{href}" alt="#{h(attachment['name'])}"></a>)
|
|
230
231
|
elsif content_type.start_with?("video/")
|
|
231
232
|
%(<video controls preload="metadata" src="#{href}"></video><a href="#{href}" download>#{h(attachment['name'])}</a>)
|
|
233
|
+
elsif ContentTypes.trace?(content_type)
|
|
234
|
+
%(<a href="#{href}" download>#{h(trace_label(attachment['attempt']))}</a>)
|
|
232
235
|
else
|
|
233
236
|
%(<a href="#{href}">#{h(attachment['name'])}</a>)
|
|
234
237
|
end
|
|
@@ -236,6 +239,10 @@ module RaiderReporter
|
|
|
236
239
|
items.empty? ? "" : %(<div class="a">#{items.join(' ')}</div>)
|
|
237
240
|
end
|
|
238
241
|
|
|
242
|
+
def trace_label(attempt)
|
|
243
|
+
attempt.is_a?(Integer) ? "Trace, attempt #{attempt} (open in Raider Desktop)" : "Trace (open in Raider Desktop)"
|
|
244
|
+
end
|
|
245
|
+
|
|
239
246
|
# Relative paths inside the results folder only, each segment URL-encoded.
|
|
240
247
|
def safe_href(path)
|
|
241
248
|
path = path.to_s
|
|
@@ -4,6 +4,7 @@ require "net/http"
|
|
|
4
4
|
require "uri"
|
|
5
5
|
require "json"
|
|
6
6
|
require "securerandom"
|
|
7
|
+
require_relative "../content_types"
|
|
7
8
|
|
|
8
9
|
module RaiderReporter
|
|
9
10
|
module Writers
|
|
@@ -48,8 +49,9 @@ module RaiderReporter
|
|
|
48
49
|
|
|
49
50
|
private
|
|
50
51
|
|
|
51
|
-
# Keeps the attachments that fit the byte and count budget,
|
|
52
|
-
#
|
|
52
|
+
# Keeps the attachments that fit the byte and count budget: screenshots,
|
|
53
|
+
# logs and other files first, then traces, then videos in whatever room
|
|
54
|
+
# is left. It rebuilds the
|
|
53
55
|
# payload without the rest, so part names stay contiguous. Survivors keep
|
|
54
56
|
# their original order. The local run.json is written from the original
|
|
55
57
|
# payload and keeps all.
|
|
@@ -68,11 +70,13 @@ module RaiderReporter
|
|
|
68
70
|
end))
|
|
69
71
|
end
|
|
70
72
|
|
|
71
|
-
# Identity-keyed set of the attachments to upload.
|
|
72
|
-
# and stop at the first byte overflow;
|
|
73
|
-
#
|
|
73
|
+
# Identity-keyed set of the attachments to upload. Other files come first
|
|
74
|
+
# and stop at the first byte overflow; traces and then videos fill the
|
|
75
|
+
# room left, and one that does not fit is skipped while later ones are
|
|
76
|
+
# still tried.
|
|
74
77
|
def budgeted(entries)
|
|
75
|
-
|
|
78
|
+
traces, rest = entries.partition { |entry| ContentTypes.trace?(entry[:attachment].content_type) }
|
|
79
|
+
videos, others = rest.partition { |entry| ContentTypes.video?(entry[:attachment].content_type) }
|
|
76
80
|
keep = {}.compare_by_identity
|
|
77
81
|
bytes = 0
|
|
78
82
|
others.each do |entry|
|
|
@@ -81,7 +85,7 @@ module RaiderReporter
|
|
|
81
85
|
|
|
82
86
|
keep[entry[:attachment]] = true
|
|
83
87
|
end
|
|
84
|
-
videos.each do |entry|
|
|
88
|
+
(traces + videos).each do |entry|
|
|
85
89
|
break if keep.size >= MAX_UPLOAD_ATTACHMENTS
|
|
86
90
|
|
|
87
91
|
size = size_of(entry)
|
|
@@ -93,10 +97,6 @@ module RaiderReporter
|
|
|
93
97
|
keep
|
|
94
98
|
end
|
|
95
99
|
|
|
96
|
-
def video?(attachment)
|
|
97
|
-
attachment.content_type.to_s.start_with?("video/")
|
|
98
|
-
end
|
|
99
|
-
|
|
100
100
|
def size_of(entry)
|
|
101
101
|
File.size?(entry[:attachment].source_path).to_i
|
|
102
102
|
end
|
data/lib/raider_reporter.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative "raider_reporter/writers/local"
|
|
|
16
16
|
require_relative "raider_reporter/writers/html"
|
|
17
17
|
require_relative "raider_reporter/ci_detector"
|
|
18
18
|
require_relative "raider_reporter/shard_detector"
|
|
19
|
+
require_relative "raider_reporter/event_stream"
|
|
19
20
|
require_relative "raider_reporter/writers/uploader"
|
|
20
21
|
require_relative "raider_reporter/comment/summary_client"
|
|
21
22
|
require_relative "raider_reporter/comment/github_client"
|
|
@@ -23,7 +24,7 @@ require_relative "raider_reporter/comment/azure_client"
|
|
|
23
24
|
|
|
24
25
|
module RaiderReporter
|
|
25
26
|
class << self
|
|
26
|
-
attr_reader :collector, :project_root
|
|
27
|
+
attr_reader :collector, :project_root, :event_stream
|
|
27
28
|
|
|
28
29
|
def configuration
|
|
29
30
|
@configuration ||= Configuration.new
|
|
@@ -39,6 +40,8 @@ module RaiderReporter
|
|
|
39
40
|
def reset!
|
|
40
41
|
@dry_run = false
|
|
41
42
|
@collector&.cleanup
|
|
43
|
+
@event_stream&.close
|
|
44
|
+
@event_stream = nil
|
|
42
45
|
@configuration = nil
|
|
43
46
|
@collector = nil
|
|
44
47
|
@project_root = nil
|
|
@@ -48,9 +51,12 @@ module RaiderReporter
|
|
|
48
51
|
def start_collector!
|
|
49
52
|
@dry_run = false
|
|
50
53
|
@project_root = Dir.pwd
|
|
54
|
+
@event_stream&.close
|
|
55
|
+
@event_stream = EventStream.open(warn: ->(message) { warn(message) })
|
|
51
56
|
@collector = Collector.new(
|
|
52
57
|
max_attachment_bytes: configuration.max_attachment_bytes,
|
|
53
58
|
max_video_bytes: configuration.max_video_bytes,
|
|
59
|
+
max_trace_bytes: configuration.max_trace_bytes,
|
|
54
60
|
warn: ->(message) { warn(message) },
|
|
55
61
|
quarantine: -> { quarantined_keys }
|
|
56
62
|
)
|
|
@@ -61,7 +67,8 @@ module RaiderReporter
|
|
|
61
67
|
@quarantined_keys ||= Quarantine.load(configuration, warn: ->(message) { warn(message) })
|
|
62
68
|
end
|
|
63
69
|
|
|
64
|
-
|
|
70
|
+
# attempt: the try of the test that produced the file, for attachments kept per attempt (traces).
|
|
71
|
+
def attach(name, path, type: nil, attempt: nil)
|
|
65
72
|
unless collector&.in_test?
|
|
66
73
|
warn("attach(#{name.inspect}) called outside a running example; ignored")
|
|
67
74
|
return false
|
|
@@ -72,14 +79,14 @@ module RaiderReporter
|
|
|
72
79
|
end
|
|
73
80
|
|
|
74
81
|
content_type = ContentTypes.resolve(type, path)
|
|
75
|
-
limit = attachment_limit(content_type)
|
|
82
|
+
limit = configuration.attachment_limit(content_type)
|
|
76
83
|
if File.size(path.to_s) > limit
|
|
77
84
|
warn("attach(#{name.inspect}): file #{path} is larger than #{limit} bytes; ignored")
|
|
78
85
|
return false
|
|
79
86
|
end
|
|
80
87
|
|
|
81
88
|
collector.attach(Attachment.new(name: name.to_s, source_path: collector.stage(path.to_s),
|
|
82
|
-
content_type: content_type))
|
|
89
|
+
content_type: content_type, attempt: attempt))
|
|
83
90
|
rescue StandardError => e
|
|
84
91
|
warn("attach(#{name.inspect}) failed: #{e.class}: #{e.message}")
|
|
85
92
|
false
|
|
@@ -121,10 +128,6 @@ module RaiderReporter
|
|
|
121
128
|
|
|
122
129
|
private
|
|
123
130
|
|
|
124
|
-
def attachment_limit(content_type)
|
|
125
|
-
content_type.start_with?("video/") ? configuration.max_video_bytes : configuration.max_attachment_bytes
|
|
126
|
-
end
|
|
127
|
-
|
|
128
131
|
# First, so a later reporting problem cannot lose it. A failure here only means
|
|
129
132
|
# no summary, and exec then keeps the command's exit code.
|
|
130
133
|
def write_exec_summary
|
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.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Augustin Gottlieb
|
|
@@ -36,6 +36,7 @@ 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/event_stream.rb
|
|
39
40
|
- lib/raider_reporter/exec.rb
|
|
40
41
|
- lib/raider_reporter/exec_summary.rb
|
|
41
42
|
- lib/raider_reporter/gate.rb
|