raider_reporter 0.9.0 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dda0a98d0886eefbfd851be91d57c881e74abee367956b0272fc9b7a464045cf
4
- data.tar.gz: 8012de5d41e890b7dae144ab5f208c20ed2ffa94736645a4308901f6abe24174
3
+ metadata.gz: 291269e17dbda3cec36ff4822af7ba9b16b85f5f1d231eed8b96fb9b89f9e338
4
+ data.tar.gz: 16e1c480d70fc174b25f7b91fcd5f9b242b04f1e7b751172e796050a0c405a39
5
5
  SHA512:
6
- metadata.gz: fa44978dfb6d93f9bbb316fe36d837b3ecb1ae668c7292b7f7e6ac345646b4c9099dd30375d8881e466f8f21eeb4ae6ae01d53e76652f975ffeaaa83b263939c
7
- data.tar.gz: 6a43fa45d7a90997e64e95b9f5e40e978c398343695ce74950809f9cadbafd7420edce162c4560d01dd67892861b61180c5c7938afc2cf28a6191f1d1e18a54b
6
+ metadata.gz: 6bc649c77eaa3a22110dd19b9a5eb28f1e60aab2d6a2877985a8254c85ad647f58d83427e298f79d48a5e420893afc12c06c54a05526a0b0488ca43c015a6510
7
+ data.tar.gz: 1590461029cc94956c3e28fffa9003252d8de1f064211dbcc9d3dcdfe0cbe11c4f9125fafb459978f77f082419b80c36a4702d0a7917ba480d4ed6a740a5f8fb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 — 2026-10-26
4
+
5
+ - 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
6
+ - Videos get their own size cap, `config.max_video_bytes` (default 50 MB); the 5 MB `max_attachment_bytes` still applies to everything else. A video over its cap is skipped with a warning
7
+ - Uploads keep non-video attachments (screenshots, logs) first; videos then fill whatever room is left in the upload budget. A video that does not fit is skipped and later, smaller ones are still tried
8
+ - The Cucumber formatter works on Cucumber 12, which removed `config.strict`: it no longer logs `undefined method 'strict'` and keeps its failure counts (Cucumber 10 and 11 behave as before)
9
+ - No breaking changes
10
+
3
11
  ## 0.9.0 — 2026-10-24
4
12
 
5
13
  - `raider-reporter gate` waits for the Raider quality gate verdict of the CI build (`GET /api/v1/builds/gate`, every 10 seconds, up to `RAIDER_GATE_TIMEOUT` seconds, default 600, 1–3600) and exits 0 when the gate passed or the project has no gate rules, 1 when it failed (naming the failed rules), and 2 when it gets no verdict (timeout, Raider unreachable, token rejected, build not found, any other error). 429, 5xx and network errors are retried within the time limit
data/README.md CHANGED
@@ -30,7 +30,7 @@ Run `bundle exec rspec` as usual. (For Cucumber, see [Cucumber](#cucumber).) Res
30
30
 
31
31
  ## Cucumber
32
32
 
33
- Requirements: Cucumber 9, 10 or 11 in your own Gemfile (`gem "cucumber"`). It is not a dependency of this gem; requiring `raider_reporter/cucumber` without Cucumber raises a `LoadError` that says so. Cucumber 9.2.1 on Ruby 4 also needs `gem "logger"` in your Gemfile, because Cucumber requires it without declaring it.
33
+ Requirements: Cucumber 9, 10, 11 or 12 (12 needs Ruby 3.3+) in your own Gemfile (`gem "cucumber"`). It is not a dependency of this gem; requiring `raider_reporter/cucumber` without Cucumber raises a `LoadError` that says so. Cucumber 9.2.1 on Ruby 4 also needs `gem "logger"` in your Gemfile, because Cucumber requires it without declaring it.
34
34
 
35
35
  ### Plugin mode (recommended)
36
36
 
@@ -120,7 +120,11 @@ 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 5 MB.
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`), or 50 MB for videos (`video/*`, `max_video_bytes`).
124
+
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
+
127
+ When a run is uploaded, screenshots and other non-video attachments are sent first; videos then fill the room left in the upload budget. A video that does not fit is skipped and later, smaller videos are still tried. The local `raider-results/attachments/` folder keeps every attachment that passed its size cap.
124
128
 
125
129
  ## Configuration
126
130
 
@@ -139,6 +143,9 @@ end
139
143
  | `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
144
  | `html_report` | `true` | Write `index.html`, the local HTML report, into `output_dir` after each run. See [Local report](#local-report). |
141
145
  | `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
+ | `max_attachment_bytes` | `5 MB` | Largest non-video attachment `attach` accepts. |
147
+ | `max_video_bytes` | `50 MB` | Largest `video/*` attachment `attach` accepts. |
148
+ | `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). |
142
149
  | `environment` | `{}` | Free-form key/values stored with the run (browser, suite name, and so on). Values are JSON values: strings, booleans or numbers. |
143
150
 
144
151
  ## Local output
@@ -8,9 +8,11 @@ require "set"
8
8
  module RaiderReporter
9
9
  # Accumulates one suite run. Framework formatters call it; writers read the Run it returns.
10
10
  class Collector
11
- def initialize(max_attachment_bytes:, warn:, clock: -> { Time.now.utc }, uuid: -> { SecureRandom.uuid },
11
+ def initialize(max_attachment_bytes:, warn:, max_video_bytes: Configuration::DEFAULT_MAX_VIDEO_BYTES,
12
+ clock: -> { Time.now.utc }, uuid: -> { SecureRandom.uuid },
12
13
  quarantine: -> { Quarantine::EMPTY })
13
14
  @max_attachment_bytes = max_attachment_bytes
15
+ @max_video_bytes = max_video_bytes
14
16
  @warn = warn
15
17
  @clock = clock
16
18
  @uuid = uuid
@@ -118,8 +120,9 @@ module RaiderReporter
118
120
  @warn.call("attachment #{path} no longer exists; skipped")
119
121
  return false
120
122
  end
121
- if File.size(path) > @max_attachment_bytes
122
- @warn.call("attachment #{path} is larger than #{@max_attachment_bytes} bytes; skipped")
123
+ limit = attachment.content_type.to_s.start_with?("video/") ? @max_video_bytes : @max_attachment_bytes
124
+ if File.size(path) > limit
125
+ @warn.call("attachment #{path} is larger than #{limit} bytes; skipped")
123
126
  return false
124
127
  end
125
128
  true
@@ -5,9 +5,10 @@ require "raider_reporter/azure_env"
5
5
  module RaiderReporter
6
6
  class Configuration
7
7
  TRUTHY = %w[1 true].freeze
8
+ DEFAULT_MAX_VIDEO_BYTES = 50 * 1024 * 1024
8
9
 
9
10
  attr_accessor :token, :endpoint, :output_dir, :clean_output_dir, :html_report, :environment, :max_attachment_bytes,
10
- :quarantine, :exec_summary_path
11
+ :max_video_bytes, :quarantine, :exec_summary_path
11
12
 
12
13
  def initialize(env = ENV)
13
14
  @token = AzureEnv.value(env, "RAIDER_TOKEN")
@@ -17,6 +18,7 @@ module RaiderReporter
17
18
  @html_report = true
18
19
  @environment = {}
19
20
  @max_attachment_bytes = 5 * 1024 * 1024
21
+ @max_video_bytes = DEFAULT_MAX_VIDEO_BYTES
20
22
  @quarantine = TRUTHY.include?(AzureEnv.value(env, "RAIDER_QUARANTINE").to_s.downcase)
21
23
  @exec_summary_path = AzureEnv.value(env, "RAIDER_EXEC_SUMMARY")
22
24
  end
@@ -160,8 +160,8 @@ module RaiderReporter
160
160
  return if @outcomes_broken
161
161
 
162
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)
163
+ failed = !result_ok?(result)
164
+ flaky = !failed && @failed_before[key] && strict_flaky?
165
165
  @failed_before[key] ||= failed
166
166
  @outcomes[key] = failed || flaky ? true : false
167
167
  rescue StandardError => e
@@ -169,6 +169,23 @@ module RaiderReporter
169
169
  RaiderReporter.warn("#{e.class}: #{e.message}")
170
170
  end
171
171
 
172
+ # Cucumber 10 and 11 carry a strict configuration (config.strict) that result.ok?
173
+ # takes as a keyword. Cucumber 12 removed it: result.ok? takes nothing, pending and
174
+ # undefined always fail, and a flaky scenario never does. Detect, never compare versions.
175
+ def strict_configuration
176
+ @config.strict if @config.respond_to?(:strict)
177
+ end
178
+
179
+ def result_ok?(result)
180
+ strict = strict_configuration
181
+ strict ? result.ok?(strict: strict) : result.ok?
182
+ end
183
+
184
+ def strict_flaky?
185
+ strict = strict_configuration
186
+ strict ? strict.strict?(:flaky) : false
187
+ end
188
+
172
189
  # Scenarios that make cucumber's run fail; one error outside tests when the run
173
190
  # stopped before every scenario finished (--fail-fast) (spec §4.4).
174
191
  def record_framework_counts
@@ -5,7 +5,7 @@ begin
5
5
  rescue LoadError => e
6
6
  raise unless e.path == "cucumber"
7
7
 
8
- raise LoadError, "raider_reporter/cucumber needs the cucumber gem (>= 9, < 12) in your Gemfile"
8
+ raise LoadError, "raider_reporter/cucumber needs the cucumber gem (>= 9, < 13) in your Gemfile"
9
9
  end
10
10
  require "raider_reporter"
11
11
  require_relative "cucumber/formatter"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RaiderReporter
4
- VERSION = "0.9.0"
4
+ VERSION = "0.10.0"
5
5
  end
@@ -224,8 +224,11 @@ module RaiderReporter
224
224
  href = safe_href(attachment["path"])
225
225
  next unless href
226
226
 
227
- if attachment["content_type"].to_s.start_with?("image/")
227
+ content_type = attachment["content_type"].to_s
228
+ if content_type.start_with?("image/")
228
229
  %(<a href="#{href}"><img loading="lazy" src="#{href}" alt="#{h(attachment['name'])}"></a>)
230
+ elsif content_type.start_with?("video/")
231
+ %(<video controls preload="metadata" src="#{href}"></video><a href="#{href}" download>#{h(attachment['name'])}</a>)
229
232
  else
230
233
  %(<a href="#{href}">#{h(attachment['name'])}</a>)
231
234
  end
@@ -280,6 +283,7 @@ module RaiderReporter
280
283
  .r p{margin:2px 0;overflow-wrap:anywhere}.n{font-weight:500}.x{color:var(--ruby);font-weight:600}
281
284
  pre{margin:4px 0;padding:8px;background:var(--card);border-radius:6px;overflow-x:auto;white-space:pre-wrap;font-size:12px}
282
285
  .a img{max-width:100%;max-height:240px;border:1px solid var(--bdr);border-radius:4px;margin-top:4px}
286
+ .a video{max-width:100%;max-height:360px;display:block}
283
287
  a{color:var(--ruby)}.empty{padding:24px 0;color:var(--mute)}
284
288
  CSS
285
289
 
@@ -48,30 +48,59 @@ module RaiderReporter
48
48
 
49
49
  private
50
50
 
51
- # Keeps the leading attachments that fit the byte and count budget and
52
- # rebuilds the payload without the rest, so part names stay contiguous.
53
- # The local run.json is written from the original payload and keeps all.
51
+ # Keeps the attachments that fit the byte and count budget, non-video
52
+ # attachments first and videos in whatever room is left, and rebuilds the
53
+ # payload without the rest, so part names stay contiguous. Survivors keep
54
+ # their original order. The local run.json is written from the original
55
+ # payload and keeps all.
54
56
  def within_budget(payload)
55
57
  entries = payload.attachment_entries
56
- bytes = 0
57
- keep = entries.index do |entry|
58
- bytes += File.size?(entry[:attachment].source_path).to_i
59
- bytes > MAX_UPLOAD_ATTACHMENT_BYTES
60
- end || entries.size
61
- keep = [keep, MAX_UPLOAD_ATTACHMENTS].min
62
- return payload if keep == entries.size
63
-
64
- dropped = entries.size - keep
58
+ keep = budgeted(entries)
59
+ return payload if keep.size == entries.size
60
+
61
+ dropped = entries.size - keep.size
65
62
  @warn.call("not uploading #{dropped} #{dropped == 1 ? 'attachment' : 'attachments'}: over the upload " \
66
63
  "budget of #{MAX_UPLOAD_ATTACHMENT_BYTES / (1024 * 1024)} MB / " \
67
64
  "#{MAX_UPLOAD_ATTACHMENTS} files; they remain in #{@config.output_dir}")
68
- seen = 0
69
65
  run = payload.run
70
66
  Payload.new(run.with(results: run.results.map do |result|
71
- result.with(attachments: result.attachments.select { (seen += 1) <= keep })
67
+ result.with(attachments: result.attachments.select { |attachment| keep.key?(attachment) })
72
68
  end))
73
69
  end
74
70
 
71
+ # Identity-keyed set of the attachments to upload. Non-videos come first
72
+ # and stop at the first byte overflow; videos then fill the room left,
73
+ # a video that does not fit is skipped and later ones are still tried.
74
+ def budgeted(entries)
75
+ videos, others = entries.partition { |entry| video?(entry[:attachment]) }
76
+ keep = {}.compare_by_identity
77
+ bytes = 0
78
+ others.each do |entry|
79
+ bytes += size_of(entry)
80
+ break if bytes > MAX_UPLOAD_ATTACHMENT_BYTES || keep.size >= MAX_UPLOAD_ATTACHMENTS
81
+
82
+ keep[entry[:attachment]] = true
83
+ end
84
+ videos.each do |entry|
85
+ break if keep.size >= MAX_UPLOAD_ATTACHMENTS
86
+
87
+ size = size_of(entry)
88
+ next if bytes + size > MAX_UPLOAD_ATTACHMENT_BYTES
89
+
90
+ bytes += size
91
+ keep[entry[:attachment]] = true
92
+ end
93
+ keep
94
+ end
95
+
96
+ def video?(attachment)
97
+ attachment.content_type.to_s.start_with?("video/")
98
+ end
99
+
100
+ def size_of(entry)
101
+ File.size?(entry[:attachment].source_path).to_i
102
+ end
103
+
75
104
  def add_field(body, boundary, name, value)
76
105
  body << "--#{boundary}\r\nContent-Disposition: form-data; name=\"#{name}\"\r\n\r\n".b
77
106
  body << value.to_s.b << "\r\n".b
@@ -50,6 +50,7 @@ module RaiderReporter
50
50
  @project_root = Dir.pwd
51
51
  @collector = Collector.new(
52
52
  max_attachment_bytes: configuration.max_attachment_bytes,
53
+ max_video_bytes: configuration.max_video_bytes,
53
54
  warn: ->(message) { warn(message) },
54
55
  quarantine: -> { quarantined_keys }
55
56
  )
@@ -70,14 +71,15 @@ module RaiderReporter
70
71
  return false
71
72
  end
72
73
 
73
- limit = configuration.max_attachment_bytes
74
+ content_type = ContentTypes.resolve(type, path)
75
+ limit = attachment_limit(content_type)
74
76
  if File.size(path.to_s) > limit
75
77
  warn("attach(#{name.inspect}): file #{path} is larger than #{limit} bytes; ignored")
76
78
  return false
77
79
  end
78
80
 
79
81
  collector.attach(Attachment.new(name: name.to_s, source_path: collector.stage(path.to_s),
80
- content_type: ContentTypes.resolve(type, path)))
82
+ content_type: content_type))
81
83
  rescue StandardError => e
82
84
  warn("attach(#{name.inspect}) failed: #{e.class}: #{e.message}")
83
85
  false
@@ -119,6 +121,10 @@ module RaiderReporter
119
121
 
120
122
  private
121
123
 
124
+ def attachment_limit(content_type)
125
+ content_type.start_with?("video/") ? configuration.max_video_bytes : configuration.max_attachment_bytes
126
+ end
127
+
122
128
  # First, so a later reporting problem cannot lose it. A failure here only means
123
129
  # no summary, and exec then keeps the command's exit code.
124
130
  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.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Augustin Gottlieb
@@ -74,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  requirements: []
77
- rubygems_version: 4.0.6
77
+ rubygems_version: 4.0.20
78
78
  specification_version: 4
79
79
  summary: RSpec and Cucumber formatters that write Raider reports locally and upload
80
80
  them to Raider