raider_reporter 0.1.0 → 0.2.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 +11 -0
- data/README.md +61 -3
- data/lib/raider_reporter/ci_detector.rb +5 -1
- data/lib/raider_reporter/collector.rb +2 -2
- data/lib/raider_reporter/models.rb +1 -1
- data/lib/raider_reporter/payload.rb +11 -4
- data/lib/raider_reporter/shard_detector.rb +49 -0
- data/lib/raider_reporter/version.rb +1 -1
- data/lib/raider_reporter/writers/local.rb +6 -6
- data/lib/raider_reporter/writers/uploader.rb +6 -4
- data/lib/raider_reporter.rb +6 -2
- 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: 7c1a8baed9855a5cba10100e57b9052184f6f748b7f015d630541d52905031c6
|
|
4
|
+
data.tar.gz: cc40652b897de2d3832a3d5da58d8bdb6f8e3a6873b11da927b68fd5a0478e24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e16586146bc9d569b5cd827c5533e6aae6cdef3bec2bc4ae3d6390c230a193ad56be6cc8fd01d11e90e933522a7f2f2ae54613450acb5a95daa609364634af1e
|
|
7
|
+
data.tar.gz: 134a1d1de73ff5b93c00740996ffea7cc8cde10f7be727bbb2ab8320b3158b710ac2df413d3e68ce334bff3fc7b3d40cd5cf2150fcc4cb6cd21d9b0e79521554
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 — 2026-10-11
|
|
4
|
+
|
|
5
|
+
- Report the CI build key, attempt and job so runs of one build are grouped (GitHub Actions)
|
|
6
|
+
- Detect shards from `RAIDER_SHARD_INDEX`/`RAIDER_SHARD_TOTAL` or `parallel_tests` (`TEST_ENV_NUMBER`, `PARALLEL_TEST_GROUPS`) and report them per run; a matrix leg running parallel_tests is composed into unique shards
|
|
7
|
+
- Write parallel-safe local output (`run-<n>.json`, `attachments-<n>/`) when sharded; fixes parallel_tests processes overwriting each other's results
|
|
8
|
+
- Print the Raider build URL after upload, falling back to the run URL
|
|
9
|
+
|
|
10
|
+
## 0.1.1 — 2026-10-11
|
|
11
|
+
|
|
12
|
+
- Send run timestamps with millisecond precision, so durations under a second are no longer reported as 0
|
|
13
|
+
|
|
3
14
|
## 0.1.0 — 2026-10-11
|
|
4
15
|
|
|
5
16
|
- RSpec formatter writing Raider schema v1 to `raider-results/run.json`
|
data/README.md
CHANGED
|
@@ -63,7 +63,7 @@ end
|
|
|
63
63
|
| `token` | `ENV["RAIDER_TOKEN"]` | API token used for uploads. |
|
|
64
64
|
| `endpoint` | `ENV["RAIDER_ENDPOINT"]` | Base URL of the Raider server to upload to. |
|
|
65
65
|
| `output_dir` | `"raider-results"` | Directory for local output. |
|
|
66
|
-
| `clean_output_dir` | `true` | Remove the previous `run.json` and `attachments/` directory inside `output_dir` before writing. Nothing else in `output_dir` is touched. |
|
|
66
|
+
| `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. |
|
|
67
67
|
| `environment` | `{}` | Free-form key/values stored with the run (browser, suite name, and so on). Values are JSON values: strings, booleans or numbers. |
|
|
68
68
|
|
|
69
69
|
## Local output
|
|
@@ -74,7 +74,7 @@ raider-results/
|
|
|
74
74
|
attachments/ # copies of attached screenshots and videos
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
Add `raider-results/` to your `.gitignore`.
|
|
77
|
+
Add `raider-results/` to your `.gitignore`. When a run is sharded, the files are named per shard; see [Parallel runs](#parallel-runs).
|
|
78
78
|
|
|
79
79
|
## Uploading to Raider
|
|
80
80
|
|
|
@@ -84,7 +84,7 @@ If both a token and an endpoint are set, the run (plus attachments) is uploaded
|
|
|
84
84
|
RAIDER_TOKEN=... RAIDER_ENDPOINT=https://your-raider-server bundle exec rspec
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
On success the formatter prints `Raider report: <url
|
|
87
|
+
On success the formatter prints `Raider report: <url>`, 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.
|
|
88
88
|
|
|
89
89
|
An upload carries at most 45 MB of attachments and at most 1000 attachment files. Attachments past that budget are left out of the upload (in order, with one warning) but stay in the local `raider-results/`.
|
|
90
90
|
|
|
@@ -92,6 +92,64 @@ Reporting is fail-open: no error inside the gem, network failure, timeout or non
|
|
|
92
92
|
|
|
93
93
|
The hosted Raider service is not public yet; uploads go to a Raider server whose address you provide in `RAIDER_ENDPOINT`.
|
|
94
94
|
|
|
95
|
+
## Parallel runs
|
|
96
|
+
|
|
97
|
+
When a suite is split across several processes or CI jobs, each one uploads its own run. Raider groups runs of the same CI build, so the shards show up together as one build. Each run records its shard (`index`, `total` and a `label`) and, on GitHub Actions, a build key made from the repository, run id and run attempt. A re-run attempt of a workflow is a separate build.
|
|
98
|
+
|
|
99
|
+
Shard detection, in order of precedence:
|
|
100
|
+
|
|
101
|
+
1. `RAIDER_SHARD_INDEX` (1 or more) and, optionally, `RAIDER_SHARD_TOTAL`.
|
|
102
|
+
2. `parallel_tests` / `parallel_rspec`: `TEST_ENV_NUMBER` (empty means shard 1) and `PARALLEL_TEST_GROUPS` for the total.
|
|
103
|
+
3. Otherwise the run is not sharded.
|
|
104
|
+
|
|
105
|
+
A GitHub Actions matrix needs only the two variables:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
jobs:
|
|
109
|
+
test:
|
|
110
|
+
runs-on: ubuntu-latest
|
|
111
|
+
strategy:
|
|
112
|
+
matrix:
|
|
113
|
+
shard: [1, 2, 3, 4]
|
|
114
|
+
steps:
|
|
115
|
+
- uses: actions/checkout@v4
|
|
116
|
+
- uses: ruby/setup-ruby@v1
|
|
117
|
+
with:
|
|
118
|
+
bundler-cache: true
|
|
119
|
+
- run: bundle exec rspec # split the spec files by shard however you like
|
|
120
|
+
env:
|
|
121
|
+
RAIDER_TOKEN: ${{ secrets.RAIDER_TOKEN }}
|
|
122
|
+
RAIDER_ENDPOINT: ${{ vars.RAIDER_ENDPOINT }}
|
|
123
|
+
RAIDER_SHARD_INDEX: ${{ matrix.shard }}
|
|
124
|
+
RAIDER_SHARD_TOTAL: 4
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`parallel_rspec` works with no configuration, because it sets `TEST_ENV_NUMBER` and `PARALLEL_TEST_GROUPS` for each process.
|
|
128
|
+
|
|
129
|
+
When a run is sharded, local output is written per shard so that processes sharing one `raider-results/` do not overwrite each other. Cleaning only removes the current shard's own files:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
raider-results/
|
|
133
|
+
run-1.json
|
|
134
|
+
attachments-1/
|
|
135
|
+
run-2.json
|
|
136
|
+
attachments-2/
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Unsharded runs still write `run.json` and `attachments/`. Tools that read local results should load `run*.json`.
|
|
140
|
+
|
|
141
|
+
### Matrix and parallel_tests together
|
|
142
|
+
|
|
143
|
+
A GitHub matrix leg can also run `parallel_rspec -n 2`. When `RAIDER_SHARD_INDEX` and `TEST_ENV_NUMBER` are both present and `PARALLEL_TEST_GROUPS` is a positive integer `g`, the two are composed so every process gets a unique shard:
|
|
144
|
+
|
|
145
|
+
- `index = (RAIDER_SHARD_INDEX - 1) * g + parallel_index`
|
|
146
|
+
- `total = RAIDER_SHARD_TOTAL * g`
|
|
147
|
+
- `label = "<job>#<RAIDER_SHARD_INDEX>.<parallel_index>"`
|
|
148
|
+
|
|
149
|
+
For example, matrix leg 2 of 2 with `parallel_rspec -n 2` yields shards 3 and 4 of 4, labelled `#2.1` and `#2.2`. `RAIDER_SHARD_TOTAL` must match the matrix size. Without it, `total` is null. If `PARALLEL_TEST_GROUPS` is missing or invalid, only `RAIDER_SHARD_INDEX` is used. Every matrix leg must use the same parallel_tests process count (`-n`) and set `RAIDER_SHARD_TOTAL`; mixing different `-n` values, or composed and plain legs, can repeat shard numbers.
|
|
150
|
+
|
|
151
|
+
Tip: run `rm -rf raider-results` before a parallel run. Each process only cleans its own files, so files from an earlier run with more processes stay behind.
|
|
152
|
+
|
|
95
153
|
## Examples
|
|
96
154
|
|
|
97
155
|
- `examples/rspec_basic` is a runnable RSpec project with a passing, a failing (with an attachment) and a skipped example. Run it with `cd examples/rspec_basic && bundle install && bundle exec rspec`; the failure is deliberate.
|
|
@@ -16,7 +16,11 @@ module RaiderReporter
|
|
|
16
16
|
build_url = if @env["GITHUB_SERVER_URL"] && @env["GITHUB_REPOSITORY"] && run_id
|
|
17
17
|
"#{@env['GITHUB_SERVER_URL']}/#{@env['GITHUB_REPOSITORY']}/actions/runs/#{run_id}"
|
|
18
18
|
end
|
|
19
|
-
|
|
19
|
+
attempt = @env["GITHUB_RUN_ATTEMPT"].to_s.empty? ? "1" : @env["GITHUB_RUN_ATTEMPT"]
|
|
20
|
+
repo = @env["GITHUB_REPOSITORY"]
|
|
21
|
+
build_key = ("github_actions:#{repo}:#{run_id}:#{attempt}" if repo && run_id)
|
|
22
|
+
{ "provider" => "github_actions", "build_url" => build_url, "build_id" => run_id,
|
|
23
|
+
"attempt" => attempt, "job" => @env["GITHUB_JOB"], "build_key" => build_key }.compact
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
def git
|
|
@@ -61,10 +61,10 @@ module RaiderReporter
|
|
|
61
61
|
result
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
def finish_run(ci:, git:, environment:)
|
|
64
|
+
def finish_run(ci:, git:, environment:, shard: nil)
|
|
65
65
|
Run.new(
|
|
66
66
|
client_run_id: @uuid.call, started_at: @started_at, finished_at: @clock.call,
|
|
67
|
-
ci: ci, git: git, environment: environment,
|
|
67
|
+
ci: ci, git: git, environment: environment, shard: shard,
|
|
68
68
|
results: @results.map { |r| r.with(attachments: r.attachments.select { |a| keep?(a) }) }
|
|
69
69
|
)
|
|
70
70
|
end
|
|
@@ -10,5 +10,5 @@ module RaiderReporter
|
|
|
10
10
|
:identifier, :full_name, :file_path, :line_number, :status,
|
|
11
11
|
:duration_ms, :attempts, :tags, :failure, :attachments
|
|
12
12
|
)
|
|
13
|
-
Run = Data.define(:client_run_id, :started_at, :finished_at, :ci, :git, :environment, :results)
|
|
13
|
+
Run = Data.define(:client_run_id, :started_at, :finished_at, :ci, :git, :environment, :results, :shard)
|
|
14
14
|
end
|
|
@@ -28,12 +28,18 @@ module RaiderReporter
|
|
|
28
28
|
}
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def shard_index = @run.shard&.fetch("index")
|
|
32
|
+
|
|
33
|
+
def attachments_dir = shard_index ? "attachments-#{shard_index}" : "attachments"
|
|
34
|
+
|
|
35
|
+
def run_file = shard_index ? "run-#{shard_index}.json" : "run.json"
|
|
36
|
+
|
|
31
37
|
def attachment_entries
|
|
32
38
|
@attachment_entries ||= @run.results.flat_map(&:attachments).each_with_index.map do |attachment, index|
|
|
33
39
|
{
|
|
34
40
|
attachment: attachment,
|
|
35
41
|
part: "att_#{index}",
|
|
36
|
-
path: "
|
|
42
|
+
path: "#{attachments_dir}/#{index}-#{File.basename(attachment.source_path)}"
|
|
37
43
|
}
|
|
38
44
|
end
|
|
39
45
|
end
|
|
@@ -42,11 +48,12 @@ module RaiderReporter
|
|
|
42
48
|
|
|
43
49
|
def run_hash
|
|
44
50
|
{
|
|
45
|
-
"started_at" => @run.started_at&.utc&.iso8601,
|
|
46
|
-
"finished_at" => @run.finished_at&.utc&.iso8601,
|
|
51
|
+
"started_at" => @run.started_at&.utc&.iso8601(3),
|
|
52
|
+
"finished_at" => @run.finished_at&.utc&.iso8601(3),
|
|
47
53
|
"ci" => @run.ci,
|
|
48
54
|
"git" => @run.git,
|
|
49
|
-
"environment" => @run.environment
|
|
55
|
+
"environment" => @run.environment,
|
|
56
|
+
"shard" => @run.shard
|
|
50
57
|
}.compact
|
|
51
58
|
end
|
|
52
59
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RaiderReporter
|
|
4
|
+
# Identifies which slice of a parallel suite this process is.
|
|
5
|
+
class ShardDetector
|
|
6
|
+
def initialize(env: ENV, job: nil)
|
|
7
|
+
@env = env
|
|
8
|
+
@job = job.to_s.strip.empty? ? nil : job.to_s
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def shard
|
|
12
|
+
index, total, suffix = composed || explicit || parallel_tests
|
|
13
|
+
return nil unless index
|
|
14
|
+
|
|
15
|
+
{ "index" => index, "total" => total, "label" => "#{@job || "local"}##{suffix || index}" }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# A matrix leg that also runs parallel_tests: every process gets a unique index.
|
|
21
|
+
def composed
|
|
22
|
+
explicit_index, explicit_total = explicit
|
|
23
|
+
parallel_index, = parallel_tests
|
|
24
|
+
groups = positive(@env["PARALLEL_TEST_GROUPS"])
|
|
25
|
+
return nil unless explicit_index && parallel_index && groups
|
|
26
|
+
|
|
27
|
+
[(explicit_index - 1) * groups + parallel_index,
|
|
28
|
+
explicit_total && explicit_total * groups,
|
|
29
|
+
"#{explicit_index}.#{parallel_index}"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def explicit
|
|
33
|
+
index = positive(@env["RAIDER_SHARD_INDEX"])
|
|
34
|
+
[index, positive(@env["RAIDER_SHARD_TOTAL"])] if index
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parallel_tests
|
|
38
|
+
return nil unless @env.key?("TEST_ENV_NUMBER")
|
|
39
|
+
|
|
40
|
+
raw = @env["TEST_ENV_NUMBER"].to_s
|
|
41
|
+
index = raw.empty? ? 1 : positive(raw)
|
|
42
|
+
[index, positive(@env["PARALLEL_TEST_GROUPS"])] if index
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def positive(value)
|
|
46
|
+
Integer(value.to_s, exception: false)&.then { |n| n.positive? ? n : nil }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -12,12 +12,12 @@ module RaiderReporter
|
|
|
12
12
|
|
|
13
13
|
def write(payload)
|
|
14
14
|
dir = File.expand_path(@config.output_dir)
|
|
15
|
-
clean(dir) if @config.clean_output_dir
|
|
16
|
-
FileUtils.mkdir_p(File.join(dir,
|
|
15
|
+
clean(dir, payload) if @config.clean_output_dir
|
|
16
|
+
FileUtils.mkdir_p(File.join(dir, payload.attachments_dir))
|
|
17
17
|
payload.attachment_entries.each do |entry|
|
|
18
18
|
FileUtils.cp(entry[:attachment].source_path, File.join(dir, entry[:path]))
|
|
19
19
|
end
|
|
20
|
-
run_json = File.join(dir,
|
|
20
|
+
run_json = File.join(dir, payload.run_file)
|
|
21
21
|
File.write(run_json, JSON.pretty_generate(payload.to_h(mode: :local)))
|
|
22
22
|
run_json
|
|
23
23
|
end
|
|
@@ -25,9 +25,9 @@ module RaiderReporter
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
27
|
# Remove only what this writer creates, so output_dir "." or "spec" is safe.
|
|
28
|
-
def clean(dir)
|
|
29
|
-
FileUtils.rm_f(File.join(dir,
|
|
30
|
-
FileUtils.rm_rf(File.join(dir,
|
|
28
|
+
def clean(dir, payload)
|
|
29
|
+
FileUtils.rm_f(File.join(dir, payload.run_file))
|
|
30
|
+
FileUtils.rm_rf(File.join(dir, payload.attachments_dir))
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
end
|
|
@@ -8,7 +8,7 @@ require "securerandom"
|
|
|
8
8
|
module RaiderReporter
|
|
9
9
|
module Writers
|
|
10
10
|
class Uploader
|
|
11
|
-
Result = Data.define(:ok, :url, :run_id, :error)
|
|
11
|
+
Result = Data.define(:ok, :url, :run_id, :build_url, :error)
|
|
12
12
|
# Keeps uploads under the server's 50 MB request limit and multipart part limit.
|
|
13
13
|
MAX_UPLOAD_ATTACHMENT_BYTES = 45 * 1024 * 1024
|
|
14
14
|
MAX_UPLOAD_ATTACHMENTS = 1000
|
|
@@ -42,7 +42,7 @@ module RaiderReporter
|
|
|
42
42
|
end
|
|
43
43
|
to_result(response)
|
|
44
44
|
rescue StandardError => e
|
|
45
|
-
Result.new(ok: false, url: nil, run_id: nil, error: "#{e.class}: #{e.message}")
|
|
45
|
+
Result.new(ok: false, url: nil, run_id: nil, build_url: nil, error: "#{e.class}: #{e.message}")
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
private
|
|
@@ -93,9 +93,11 @@ module RaiderReporter
|
|
|
93
93
|
def to_result(response)
|
|
94
94
|
if response.is_a?(Net::HTTPSuccess)
|
|
95
95
|
body = parse_body(response.body)
|
|
96
|
-
|
|
96
|
+
build = body["build"].is_a?(Hash) ? body["build"] : {}
|
|
97
|
+
Result.new(ok: true, url: present(body["url"]), run_id: present(body["run_id"]),
|
|
98
|
+
build_url: present(build["url"]), error: nil)
|
|
97
99
|
else
|
|
98
|
-
Result.new(ok: false, url: nil, run_id: nil, error: "HTTP #{response.code}: #{response.body.to_s[0, 200]}")
|
|
100
|
+
Result.new(ok: false, url: nil, run_id: nil, build_url: nil, error: "HTTP #{response.code}: #{response.body.to_s[0, 200]}")
|
|
99
101
|
end
|
|
100
102
|
end
|
|
101
103
|
|
data/lib/raider_reporter.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "raider_reporter/content_types"
|
|
|
10
10
|
require_relative "raider_reporter/collector"
|
|
11
11
|
require_relative "raider_reporter/writers/local"
|
|
12
12
|
require_relative "raider_reporter/ci_detector"
|
|
13
|
+
require_relative "raider_reporter/shard_detector"
|
|
13
14
|
require_relative "raider_reporter/writers/uploader"
|
|
14
15
|
|
|
15
16
|
module RaiderReporter
|
|
@@ -69,7 +70,9 @@ module RaiderReporter
|
|
|
69
70
|
return nil if dry_run?
|
|
70
71
|
|
|
71
72
|
detector = CiDetector.new
|
|
72
|
-
|
|
73
|
+
ci = detector.ci
|
|
74
|
+
shard = ShardDetector.new(job: ci&.fetch("job", nil)).shard
|
|
75
|
+
run = collector.finish_run(ci: ci, git: detector.git, environment: default_environment, shard: shard)
|
|
73
76
|
payload = Payload.new(run)
|
|
74
77
|
begin
|
|
75
78
|
Writers::Local.new(configuration).write(payload)
|
|
@@ -92,7 +95,8 @@ module RaiderReporter
|
|
|
92
95
|
private
|
|
93
96
|
|
|
94
97
|
def success_line(result)
|
|
95
|
-
|
|
98
|
+
link = result.build_url || result.url
|
|
99
|
+
return "Raider report: #{link}" if link
|
|
96
100
|
return "Raider report uploaded (run #{result.run_id})" if result.run_id
|
|
97
101
|
|
|
98
102
|
"Raider report uploaded"
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Augustin Gottlieb
|
|
@@ -29,6 +29,7 @@ files:
|
|
|
29
29
|
- lib/raider_reporter/payload.rb
|
|
30
30
|
- lib/raider_reporter/rspec.rb
|
|
31
31
|
- lib/raider_reporter/rspec/formatter.rb
|
|
32
|
+
- lib/raider_reporter/shard_detector.rb
|
|
32
33
|
- lib/raider_reporter/version.rb
|
|
33
34
|
- lib/raider_reporter/writers/local.rb
|
|
34
35
|
- lib/raider_reporter/writers/uploader.rb
|