specbandit 0.11.0 → 0.13.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/lib/specbandit/cli.rb +21 -3
- data/lib/specbandit/configuration.rb +7 -1
- data/lib/specbandit/rspec_adapter.rb +2 -2
- data/lib/specbandit/version.rb +1 -1
- data/lib/specbandit/worker.rb +110 -105
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 95622117125136b9368557368697eb0b9663983289dc7a5c53692a230ac4d7ef
|
|
4
|
+
data.tar.gz: 5d74e976511d86b4c8da066cc782fc358af1c69e89ae02edcc3935b077ad473e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19a2cff1aab50ad0412e515a2081b85dcd8f94addf96232bfb2be3694b02980410df1a8788d3451907a985b26fc6cfecb1dd7d45750ba108b63f84975ea36996
|
|
7
|
+
data.tar.gz: 9af9ad631d684be5485a43620b76dcda02e7e223e50b6ecab47def52ee24fa4ab296dff43e5eea11c03acaea5146178da9cced4740ebb1036b41edef38ba0852
|
data/lib/specbandit/cli.rb
CHANGED
|
@@ -123,10 +123,22 @@ module Specbandit
|
|
|
123
123
|
Specbandit.configuration.key_rerun_ttl = v
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
opts.on('--key-failed KEY', 'Redis key to record failed test files for later review') do |v|
|
|
127
|
+
Specbandit.configuration.key_failed = v
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
opts.on('--key-failed-ttl SECONDS', Integer, 'TTL for failed key in seconds (default: 604800 / 1 week)') do |v|
|
|
131
|
+
Specbandit.configuration.key_failed_ttl = v
|
|
132
|
+
end
|
|
133
|
+
|
|
126
134
|
opts.on('--rerun', 'Signal this is a re-run (fail hard if rerun key is empty)') do
|
|
127
135
|
Specbandit.configuration.rerun = true
|
|
128
136
|
end
|
|
129
137
|
|
|
138
|
+
opts.on('--report FILE', 'Write JSON report with run statistics to FILE') do |v|
|
|
139
|
+
Specbandit.configuration.report = v
|
|
140
|
+
end
|
|
141
|
+
|
|
130
142
|
opts.on('--verbose', 'Show per-batch file list and full command output (default: quiet)') do
|
|
131
143
|
Specbandit.configuration.verbose = true
|
|
132
144
|
end
|
|
@@ -207,9 +219,12 @@ module Specbandit
|
|
|
207
219
|
--rspec-opts OPTS Extra options forwarded to RSpec (for rspec adapter)
|
|
208
220
|
--batch-size N Files per batch (default: 5, or set SPECBANDIT_BATCH_SIZE)
|
|
209
221
|
--redis-url URL Redis URL (default: redis://localhost:6379)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
222
|
+
--key-rerun KEY Per-runner rerun key for re-run support
|
|
223
|
+
--key-rerun-ttl N TTL for rerun key (default: 604800 / 1 week)
|
|
224
|
+
--key-failed KEY Redis key to record failed test files
|
|
225
|
+
--key-failed-ttl N TTL for failed key (default: 604800 / 1 week)
|
|
226
|
+
--rerun Signal this is a re-run (fail hard if rerun key is empty)
|
|
227
|
+
--report FILE Write JSON report to FILE after run
|
|
213
228
|
--verbose Show per-batch file list and full command output
|
|
214
229
|
|
|
215
230
|
Arguments after -- are forwarded to the adapter (rspec opts, command opts, etc.).
|
|
@@ -226,8 +241,11 @@ module Specbandit
|
|
|
226
241
|
SPECBANDIT_RSPEC_OPTS RSpec options (rspec adapter)
|
|
227
242
|
SPECBANDIT_KEY_RERUN Per-runner rerun key
|
|
228
243
|
SPECBANDIT_KEY_RERUN_TTL Rerun key TTL in seconds (default: 604800)
|
|
244
|
+
SPECBANDIT_KEY_FAILED Redis key for failed test files
|
|
245
|
+
SPECBANDIT_KEY_FAILED_TTL Failed key TTL in seconds (default: 604800)
|
|
229
246
|
SPECBANDIT_RERUN Signal re-run mode (1/true/yes)
|
|
230
247
|
SPECBANDIT_VERBOSE Enable verbose output (1/true/yes)
|
|
248
|
+
SPECBANDIT_REPORT Path to write JSON report file
|
|
231
249
|
|
|
232
250
|
File input priority for push:
|
|
233
251
|
1. stdin (piped) echo "spec/a_spec.rb" | specbandit push --key KEY
|
|
@@ -4,12 +4,14 @@ module Specbandit
|
|
|
4
4
|
class Configuration
|
|
5
5
|
attr_accessor :redis_url, :batch_size, :key, :rspec_opts, :key_ttl,
|
|
6
6
|
:key_rerun, :key_rerun_ttl, :rerun, :verbose,
|
|
7
|
-
:adapter, :command, :command_opts
|
|
7
|
+
:adapter, :command, :command_opts,
|
|
8
|
+
:key_failed, :key_failed_ttl, :report
|
|
8
9
|
|
|
9
10
|
DEFAULT_REDIS_URL = 'redis://localhost:6379'
|
|
10
11
|
DEFAULT_BATCH_SIZE = 5
|
|
11
12
|
DEFAULT_KEY_TTL = 21_600 # 6 hours in seconds
|
|
12
13
|
DEFAULT_KEY_RERUN_TTL = 604_800 # 1 week in seconds
|
|
14
|
+
DEFAULT_KEY_FAILED_TTL = 604_800 # 1 week in seconds
|
|
13
15
|
DEFAULT_ADAPTER = 'cli'
|
|
14
16
|
|
|
15
17
|
def initialize
|
|
@@ -25,6 +27,9 @@ module Specbandit
|
|
|
25
27
|
@adapter = ENV.fetch('SPECBANDIT_ADAPTER', DEFAULT_ADAPTER)
|
|
26
28
|
@command = ENV.fetch('SPECBANDIT_COMMAND', nil)
|
|
27
29
|
@command_opts = parse_space_separated(ENV.fetch('SPECBANDIT_COMMAND_OPTS', nil))
|
|
30
|
+
@key_failed = ENV.fetch('SPECBANDIT_KEY_FAILED', nil)
|
|
31
|
+
@key_failed_ttl = Integer(ENV.fetch('SPECBANDIT_KEY_FAILED_TTL', DEFAULT_KEY_FAILED_TTL))
|
|
32
|
+
@report = ENV.fetch('SPECBANDIT_REPORT', nil)
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
def validate!
|
|
@@ -32,6 +37,7 @@ module Specbandit
|
|
|
32
37
|
raise Error, 'batch_size must be a positive integer' unless batch_size.positive?
|
|
33
38
|
raise Error, 'key_ttl must be a positive integer' unless key_ttl.positive?
|
|
34
39
|
raise Error, 'key_rerun_ttl must be a positive integer' unless key_rerun_ttl.positive?
|
|
40
|
+
raise Error, 'key_failed_ttl must be a positive integer' unless key_failed_ttl.positive?
|
|
35
41
|
raise Error, '--rerun requires --key-rerun to be set' if rerun && (key_rerun.nil? || key_rerun.empty?)
|
|
36
42
|
end
|
|
37
43
|
|
|
@@ -65,10 +65,10 @@ module Specbandit
|
|
|
65
65
|
ensure
|
|
66
66
|
# Print RSpec output through our output stream
|
|
67
67
|
rspec_output = out&.string
|
|
68
|
-
output.print(rspec_output) if
|
|
68
|
+
output.print(rspec_output) if rspec_output && !rspec_output.empty?
|
|
69
69
|
|
|
70
70
|
rspec_err = err&.string
|
|
71
|
-
output.print(rspec_err) if
|
|
71
|
+
output.print(rspec_err) if rspec_err && !rspec_err.empty?
|
|
72
72
|
|
|
73
73
|
# Don't unlink the tempfile here — the Worker needs to read it.
|
|
74
74
|
# The Worker is responsible for cleanup after accumulation.
|
data/lib/specbandit/version.rb
CHANGED
data/lib/specbandit/worker.rb
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'stringio'
|
|
4
3
|
require 'json'
|
|
5
4
|
|
|
6
5
|
module Specbandit
|
|
7
6
|
class Worker
|
|
8
|
-
attr_reader :queue, :key, :batch_size, :adapter, :key_rerun, :key_rerun_ttl, :
|
|
7
|
+
attr_reader :queue, :key, :batch_size, :adapter, :key_rerun, :key_rerun_ttl, :key_failed, :key_failed_ttl, :rerun,
|
|
8
|
+
:output, :verbose, :report
|
|
9
9
|
|
|
10
10
|
def initialize(
|
|
11
11
|
key: Specbandit.configuration.key,
|
|
@@ -13,8 +13,11 @@ module Specbandit
|
|
|
13
13
|
adapter: nil,
|
|
14
14
|
key_rerun: Specbandit.configuration.key_rerun,
|
|
15
15
|
key_rerun_ttl: Specbandit.configuration.key_rerun_ttl,
|
|
16
|
+
key_failed: Specbandit.configuration.key_failed,
|
|
17
|
+
key_failed_ttl: Specbandit.configuration.key_failed_ttl,
|
|
16
18
|
rerun: Specbandit.configuration.rerun,
|
|
17
19
|
verbose: Specbandit.configuration.verbose,
|
|
20
|
+
report: Specbandit.configuration.report,
|
|
18
21
|
queue: nil,
|
|
19
22
|
output: $stdout,
|
|
20
23
|
# Legacy parameter for backward compatibility.
|
|
@@ -25,12 +28,16 @@ module Specbandit
|
|
|
25
28
|
@batch_size = batch_size
|
|
26
29
|
@key_rerun = key_rerun
|
|
27
30
|
@key_rerun_ttl = key_rerun_ttl
|
|
31
|
+
@key_failed = key_failed
|
|
32
|
+
@key_failed_ttl = key_failed_ttl
|
|
28
33
|
@rerun = rerun
|
|
29
34
|
@verbose = verbose
|
|
35
|
+
@report = report
|
|
30
36
|
@queue = queue || RedisQueue.new
|
|
31
37
|
@output = output
|
|
32
38
|
@batch_results = []
|
|
33
39
|
@accumulated_examples = []
|
|
40
|
+
@accumulated_failed_files = []
|
|
34
41
|
@accumulated_summary = { duration: 0.0, example_count: 0, failure_count: 0, pending_count: 0,
|
|
35
42
|
errors_outside_of_examples_count: 0 }
|
|
36
43
|
|
|
@@ -47,6 +54,7 @@ module Specbandit
|
|
|
47
54
|
#
|
|
48
55
|
# Returns 0 if all batches passed (or nothing to do), 1 if any batch failed.
|
|
49
56
|
def run
|
|
57
|
+
@run_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
50
58
|
adapter.setup
|
|
51
59
|
|
|
52
60
|
exit_code = if key_rerun
|
|
@@ -67,8 +75,7 @@ module Specbandit
|
|
|
67
75
|
|
|
68
76
|
print_summary if @batch_results.any?
|
|
69
77
|
merge_json_results
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
write_report
|
|
72
79
|
exit_code
|
|
73
80
|
ensure
|
|
74
81
|
adapter.teardown
|
|
@@ -80,29 +87,32 @@ module Specbandit
|
|
|
80
87
|
# Used when re-running a failed CI job -- the rerun key already
|
|
81
88
|
# contains the exact files this runner executed previously.
|
|
82
89
|
def run_replay(files)
|
|
83
|
-
output.puts "[specbandit] Replay mode: found #{files.size} files in rerun key '#{key_rerun}'."
|
|
84
|
-
output.puts '[specbandit] Running previously recorded files (not touching shared queue).'
|
|
90
|
+
output.puts "[specbandit] Replay mode: found #{files.size} files in rerun key '#{key_rerun}'." if verbose
|
|
91
|
+
output.puts '[specbandit] Running previously recorded files (not touching shared queue).' if verbose
|
|
85
92
|
|
|
86
93
|
failed = false
|
|
87
94
|
batch_num = 0
|
|
88
95
|
|
|
89
96
|
files.each_slice(batch_size) do |batch|
|
|
90
97
|
batch_num += 1
|
|
91
|
-
output.puts "[specbandit] Batch ##{batch_num}: running #{batch.size} files"
|
|
98
|
+
output.puts "[specbandit] Batch ##{batch_num}: running #{batch.size} files" if verbose
|
|
92
99
|
batch.each { |f| output.puts " #{f}" } if verbose
|
|
93
100
|
|
|
94
101
|
result = adapter.run_batch(batch, batch_num)
|
|
102
|
+
record_failed_files(batch, result)
|
|
95
103
|
process_batch_result(result)
|
|
96
104
|
|
|
97
105
|
if result.exit_code != 0
|
|
98
|
-
output.puts "[specbandit] Batch ##{batch_num} FAILED (exit code: #{result.exit_code})"
|
|
106
|
+
output.puts "[specbandit] Batch ##{batch_num} FAILED (exit code: #{result.exit_code})" if verbose
|
|
99
107
|
failed = true
|
|
100
|
-
|
|
108
|
+
elsif verbose
|
|
101
109
|
output.puts "[specbandit] Batch ##{batch_num} passed."
|
|
102
110
|
end
|
|
103
111
|
end
|
|
104
112
|
|
|
105
|
-
|
|
113
|
+
if verbose
|
|
114
|
+
output.puts "[specbandit] Replay finished: #{batch_num} batches. #{failed ? 'SOME FAILED' : 'All passed.'}"
|
|
115
|
+
end
|
|
106
116
|
failed ? 1 : 0
|
|
107
117
|
end
|
|
108
118
|
|
|
@@ -111,8 +121,8 @@ module Specbandit
|
|
|
111
121
|
# rerun key so this runner can replay them on a re-run.
|
|
112
122
|
def run_steal(record:)
|
|
113
123
|
mode_label = record ? 'Record' : 'Steal'
|
|
114
|
-
output.puts "[specbandit] #{mode_label} mode: stealing batches from '#{key}'."
|
|
115
|
-
output.puts "[specbandit] Recording stolen files to rerun key '#{key_rerun}'." if record
|
|
124
|
+
output.puts "[specbandit] #{mode_label} mode: stealing batches from '#{key}'." if verbose
|
|
125
|
+
output.puts "[specbandit] Recording stolen files to rerun key '#{key_rerun}'." if verbose && record
|
|
116
126
|
|
|
117
127
|
failed = false
|
|
118
128
|
batch_num = 0
|
|
@@ -121,7 +131,7 @@ module Specbandit
|
|
|
121
131
|
files = queue.steal(key, batch_size)
|
|
122
132
|
|
|
123
133
|
if files.empty?
|
|
124
|
-
output.puts '[specbandit] Queue exhausted. No more files to run.'
|
|
134
|
+
output.puts '[specbandit] Queue exhausted. No more files to run.' if verbose
|
|
125
135
|
break
|
|
126
136
|
end
|
|
127
137
|
|
|
@@ -129,34 +139,74 @@ module Specbandit
|
|
|
129
139
|
queue.push(key_rerun, files, ttl: key_rerun_ttl) if record
|
|
130
140
|
|
|
131
141
|
batch_num += 1
|
|
132
|
-
output.puts "[specbandit] Batch ##{batch_num}: running #{files.size} files"
|
|
142
|
+
output.puts "[specbandit] Batch ##{batch_num}: running #{files.size} files" if verbose
|
|
133
143
|
files.each { |f| output.puts " #{f}" } if verbose
|
|
134
144
|
|
|
135
145
|
result = adapter.run_batch(files, batch_num)
|
|
146
|
+
record_failed_files(files, result)
|
|
136
147
|
process_batch_result(result)
|
|
137
148
|
|
|
138
149
|
if result.exit_code != 0
|
|
139
|
-
output.puts "[specbandit] Batch ##{batch_num} FAILED (exit code: #{result.exit_code})"
|
|
150
|
+
output.puts "[specbandit] Batch ##{batch_num} FAILED (exit code: #{result.exit_code})" if verbose
|
|
140
151
|
failed = true
|
|
141
|
-
|
|
152
|
+
elsif verbose
|
|
142
153
|
output.puts "[specbandit] Batch ##{batch_num} passed."
|
|
143
154
|
end
|
|
144
155
|
end
|
|
145
156
|
|
|
146
157
|
if batch_num.zero?
|
|
147
|
-
output.puts '[specbandit] Nothing to do (queue was empty).'
|
|
148
|
-
|
|
158
|
+
output.puts '[specbandit] Nothing to do (queue was empty).' if verbose
|
|
159
|
+
elsif verbose
|
|
149
160
|
output.puts "[specbandit] Finished #{batch_num} batches. #{failed ? 'SOME FAILED' : 'All passed.'}"
|
|
150
161
|
end
|
|
151
162
|
|
|
152
163
|
failed ? 1 : 0
|
|
153
164
|
end
|
|
154
165
|
|
|
166
|
+
# Record failed files to the failed key in Redis for later review.
|
|
167
|
+
# Called after each batch; only pushes when key_failed is configured
|
|
168
|
+
# and the batch had a non-zero exit code.
|
|
169
|
+
#
|
|
170
|
+
# For RSpec batches with JSON output, only the individual failed file
|
|
171
|
+
# paths are recorded (not the entire batch). For CLI adapter batches
|
|
172
|
+
# (no per-file granularity), the whole batch is recorded as fallback.
|
|
173
|
+
def record_failed_files(files, result)
|
|
174
|
+
return unless key_failed
|
|
175
|
+
return if result.exit_code.zero?
|
|
176
|
+
|
|
177
|
+
failed_files = extract_failed_files(result) || files
|
|
178
|
+
return if failed_files.empty?
|
|
179
|
+
|
|
180
|
+
queue.push(key_failed, failed_files, ttl: key_failed_ttl)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Extract individual failed file paths from an RspecBatchResult's JSON output.
|
|
184
|
+
# Returns nil when per-file data is not available (CLI adapter).
|
|
185
|
+
def extract_failed_files(result)
|
|
186
|
+
return nil unless result.is_a?(RspecBatchResult) && result.json_path && File.exist?(result.json_path)
|
|
187
|
+
|
|
188
|
+
data = JSON.parse(File.read(result.json_path))
|
|
189
|
+
failed = data.fetch('examples', [])
|
|
190
|
+
.select { |e| e['status'] == 'failed' }
|
|
191
|
+
.filter_map { |e| e['file_path'] }
|
|
192
|
+
.uniq
|
|
193
|
+
|
|
194
|
+
failed.empty? ? nil : failed
|
|
195
|
+
rescue JSON::ParserError
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
|
|
155
199
|
# Process a BatchResult: store it, and for RSpec batches,
|
|
156
200
|
# read the JSON output for rich reporting.
|
|
157
201
|
def process_batch_result(result)
|
|
158
202
|
@batch_results << result
|
|
159
203
|
|
|
204
|
+
# Accumulate failed files for the report while JSON data is still available.
|
|
205
|
+
if result.exit_code != 0
|
|
206
|
+
per_file = extract_failed_files(result)
|
|
207
|
+
@accumulated_failed_files.concat(per_file || result.files)
|
|
208
|
+
end
|
|
209
|
+
|
|
160
210
|
# If the adapter returned an RspecBatchResult with a json_path,
|
|
161
211
|
# accumulate the structured results for rich reporting.
|
|
162
212
|
return unless result.is_a?(RspecBatchResult) && result.json_path
|
|
@@ -243,6 +293,48 @@ module Specbandit
|
|
|
243
293
|
File.write(path, JSON.pretty_generate(merged))
|
|
244
294
|
end
|
|
245
295
|
|
|
296
|
+
# Write a JSON report file with run statistics when --report is set.
|
|
297
|
+
def write_report
|
|
298
|
+
return unless report
|
|
299
|
+
return if @batch_results.empty?
|
|
300
|
+
|
|
301
|
+
wall_time = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @run_start_time).round(2)
|
|
302
|
+
durations = batch_durations
|
|
303
|
+
failed_batches_count = @batch_results.count { |r| r.exit_code != 0 }
|
|
304
|
+
passed_batches_count = @batch_results.count { |r| r.exit_code == 0 }
|
|
305
|
+
|
|
306
|
+
data = {
|
|
307
|
+
specbandit_version: Specbandit::VERSION,
|
|
308
|
+
summary: {
|
|
309
|
+
total_files: @batch_results.sum { |r| r.files.size },
|
|
310
|
+
total_batches: @batch_results.size,
|
|
311
|
+
passed_batches: passed_batches_count,
|
|
312
|
+
failed_batches: failed_batches_count,
|
|
313
|
+
passed: failed_batches_count == 0
|
|
314
|
+
},
|
|
315
|
+
failed_files: @accumulated_failed_files.uniq,
|
|
316
|
+
total_wall_time: wall_time,
|
|
317
|
+
batch_timings: {
|
|
318
|
+
count: durations.size,
|
|
319
|
+
min: format('%.2f', durations.min || 0),
|
|
320
|
+
avg: format('%.2f', durations.empty? ? 0 : durations.sum / durations.size),
|
|
321
|
+
max: format('%.2f', durations.max || 0),
|
|
322
|
+
all: durations.map { |d| d.round(2) }
|
|
323
|
+
},
|
|
324
|
+
batches: @batch_results.map do |r|
|
|
325
|
+
{
|
|
326
|
+
batch_num: r.batch_num,
|
|
327
|
+
files: r.files,
|
|
328
|
+
exit_code: r.exit_code,
|
|
329
|
+
duration: r.duration.round(2),
|
|
330
|
+
passed: r.exit_code == 0
|
|
331
|
+
}
|
|
332
|
+
end
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
File.write(report, JSON.pretty_generate(data))
|
|
336
|
+
end
|
|
337
|
+
|
|
246
338
|
# Print a unified summary to the output stream after all batches.
|
|
247
339
|
def print_summary
|
|
248
340
|
output.puts ''
|
|
@@ -310,92 +402,5 @@ module Specbandit
|
|
|
310
402
|
parts << "#{@accumulated_summary[:pending_count]} pending" if @accumulated_summary[:pending_count] > 0
|
|
311
403
|
parts.join(', ')
|
|
312
404
|
end
|
|
313
|
-
|
|
314
|
-
# Write a markdown summary to $GITHUB_STEP_SUMMARY for GitHub Actions.
|
|
315
|
-
def write_github_step_summary
|
|
316
|
-
path = ENV['GITHUB_STEP_SUMMARY']
|
|
317
|
-
return unless path
|
|
318
|
-
|
|
319
|
-
md = StringIO.new
|
|
320
|
-
|
|
321
|
-
if has_rspec_results?
|
|
322
|
-
write_rspec_github_summary(md)
|
|
323
|
-
else
|
|
324
|
-
write_generic_github_summary(md)
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
File.open(path, 'a') { |f| f.write(md.string) }
|
|
328
|
-
rescue StandardError
|
|
329
|
-
# Never fail the build because of summary writing
|
|
330
|
-
nil
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
def write_rspec_github_summary(md)
|
|
334
|
-
md.puts '### Specbandit Results'
|
|
335
|
-
md.puts ''
|
|
336
|
-
md.puts '| Metric | Value |'
|
|
337
|
-
md.puts '|--------|-------|'
|
|
338
|
-
md.puts "| Batches | #{batch_durations.size} |"
|
|
339
|
-
md.puts "| Examples | #{@accumulated_summary[:example_count]} |"
|
|
340
|
-
md.puts "| Failures | #{@accumulated_summary[:failure_count]} |"
|
|
341
|
-
md.puts "| Pending | #{@accumulated_summary[:pending_count]} |"
|
|
342
|
-
|
|
343
|
-
md.puts format('| Batch time (min) | %.1fs |', batch_durations.min || 0)
|
|
344
|
-
md.puts format('| Batch time (avg) | %.1fs |',
|
|
345
|
-
batch_durations.empty? ? 0 : batch_durations.sum / batch_durations.size)
|
|
346
|
-
md.puts format('| Batch time (max) | %.1fs |', batch_durations.max || 0)
|
|
347
|
-
md.puts ''
|
|
348
|
-
|
|
349
|
-
failed_examples = @accumulated_examples.select { |e| e['status'] == 'failed' }
|
|
350
|
-
return unless failed_examples.any?
|
|
351
|
-
|
|
352
|
-
md.puts "<details><summary>#{failed_examples.size} failed specs</summary>"
|
|
353
|
-
md.puts ''
|
|
354
|
-
md.puts '| Location | Description | Error |'
|
|
355
|
-
md.puts '|----------|-------------|-------|'
|
|
356
|
-
failed_examples.each do |ex|
|
|
357
|
-
location = ex['file_path'] || 'unknown'
|
|
358
|
-
line = ex['line_number']
|
|
359
|
-
location = "#{location}:#{line}" if line
|
|
360
|
-
desc = (ex['full_description'] || ex['description'] || '').gsub('|', '\\|')
|
|
361
|
-
message = (ex.dig('exception', 'message') || '').gsub('|', '\\|').gsub("\n", ' ')
|
|
362
|
-
message = "#{message[0, 100]}..." if message.length > 100
|
|
363
|
-
md.puts "| `#{location}` | #{desc} | #{message} |"
|
|
364
|
-
end
|
|
365
|
-
md.puts ''
|
|
366
|
-
md.puts '</details>'
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
def write_generic_github_summary(md)
|
|
370
|
-
total_files = @batch_results.sum { |r| r.files.size }
|
|
371
|
-
failed_batch_results = @batch_results.select { |r| r.exit_code != 0 }
|
|
372
|
-
|
|
373
|
-
md.puts '### Specbandit Results'
|
|
374
|
-
md.puts ''
|
|
375
|
-
md.puts '| Metric | Value |'
|
|
376
|
-
md.puts '|--------|-------|'
|
|
377
|
-
md.puts "| Batches | #{batch_durations.size} |"
|
|
378
|
-
md.puts "| Files | #{total_files} |"
|
|
379
|
-
md.puts "| Failed batches | #{failed_batch_results.size} |"
|
|
380
|
-
|
|
381
|
-
md.puts format('| Batch time (min) | %.1fs |', batch_durations.min || 0)
|
|
382
|
-
md.puts format('| Batch time (avg) | %.1fs |',
|
|
383
|
-
batch_durations.empty? ? 0 : batch_durations.sum / batch_durations.size)
|
|
384
|
-
md.puts format('| Batch time (max) | %.1fs |', batch_durations.max || 0)
|
|
385
|
-
md.puts ''
|
|
386
|
-
|
|
387
|
-
return unless failed_batch_results.any?
|
|
388
|
-
|
|
389
|
-
md.puts "<details><summary>#{failed_batch_results.size} failed batches</summary>"
|
|
390
|
-
md.puts ''
|
|
391
|
-
md.puts '| Batch | Exit Code | Files |'
|
|
392
|
-
md.puts '|-------|-----------|-------|'
|
|
393
|
-
failed_batch_results.each do |r|
|
|
394
|
-
files_str = r.files.map { |f| "`#{f}`" }.join(', ')
|
|
395
|
-
md.puts "| ##{r.batch_num} | #{r.exit_code} | #{files_str} |"
|
|
396
|
-
end
|
|
397
|
-
md.puts ''
|
|
398
|
-
md.puts '</details>'
|
|
399
|
-
end
|
|
400
405
|
end
|
|
401
406
|
end
|