specbandit 0.12.0 → 0.13.1
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/README.md +2 -0
- data/lib/specbandit/cli.rb +21 -3
- data/lib/specbandit/configuration.rb +7 -1
- data/lib/specbandit/version.rb +1 -1
- data/lib/specbandit/worker.rb +132 -16
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 14a1d9a07eb3ab1b19c59c9e054aec4fb32e5da1ac3f33841feb1fb068085648
|
|
4
|
+
data.tar.gz: 756a15f2a9c7f9cce3db7fa06e499235fd18ec332a51d6db1900ee1a9a2d1fb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6bc49db9d7bd80540b17d19b15fb759a2ab0c1c867026f3234610074323237cf4cd5ee8d7e590c472b324dbc152b0db83d35fa05bbf443175efad1f5fc658c1c
|
|
7
|
+
data.tar.gz: '09cdd5433154bc6a35d13006beccdcba288d8538fc79585e79b8939edf1a0c2e1149d98d21cbdc8ee4eb7f756640d00575b4ebb3791217f5359c511a8ddea6a0'
|
data/README.md
CHANGED
|
@@ -318,6 +318,8 @@ Specbandit detects the mode automatically based on the state of `--key-rerun`:
|
|
|
318
318
|
| Yes | Empty | Yes | **Fail** | Exit 1 with error. Prevents silent false pass on stale re-runs. |
|
|
319
319
|
| No | -- | Yes | **Error** | Validation error: `--rerun` requires `--key-rerun`. |
|
|
320
320
|
|
|
321
|
+
> **Empty key names count as "not provided".** A `--key-rerun` whose value is an empty string -- e.g. `--key-rerun "$VAR"` where `$VAR` is unset in CI -- is treated exactly like `--key-rerun` being absent (the **No** rows above), not as a configured-but-empty key. Combined with `--rerun`, that means an empty/unset name fails hard (exit 1) rather than silently steal-and-pass. The same applies to `--key-failed`: an empty/unset name is treated as "not configured" and no failed files are recorded.
|
|
322
|
+
|
|
321
323
|
#### The `--rerun` safety flag
|
|
322
324
|
|
|
323
325
|
Without `--rerun`, specbandit cannot distinguish a first run from a re-run when the rerun key is empty (e.g., TTL expired or Redis was flushed). In that case it silently falls back to Record mode, which may find an empty shared queue and exit 0 with zero tests -- a **silent false pass**.
|
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
|
|
data/lib/specbandit/version.rb
CHANGED
data/lib/specbandit/worker.rb
CHANGED
|
@@ -4,7 +4,8 @@ require 'json'
|
|
|
4
4
|
|
|
5
5
|
module Specbandit
|
|
6
6
|
class Worker
|
|
7
|
-
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
|
|
8
9
|
|
|
9
10
|
def initialize(
|
|
10
11
|
key: Specbandit.configuration.key,
|
|
@@ -12,8 +13,11 @@ module Specbandit
|
|
|
12
13
|
adapter: nil,
|
|
13
14
|
key_rerun: Specbandit.configuration.key_rerun,
|
|
14
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,
|
|
15
18
|
rerun: Specbandit.configuration.rerun,
|
|
16
19
|
verbose: Specbandit.configuration.verbose,
|
|
20
|
+
report: Specbandit.configuration.report,
|
|
17
21
|
queue: nil,
|
|
18
22
|
output: $stdout,
|
|
19
23
|
# Legacy parameter for backward compatibility.
|
|
@@ -24,12 +28,16 @@ module Specbandit
|
|
|
24
28
|
@batch_size = batch_size
|
|
25
29
|
@key_rerun = key_rerun
|
|
26
30
|
@key_rerun_ttl = key_rerun_ttl
|
|
31
|
+
@key_failed = key_failed
|
|
32
|
+
@key_failed_ttl = key_failed_ttl
|
|
27
33
|
@rerun = rerun
|
|
28
34
|
@verbose = verbose
|
|
35
|
+
@report = report
|
|
29
36
|
@queue = queue || RedisQueue.new
|
|
30
37
|
@output = output
|
|
31
38
|
@batch_results = []
|
|
32
39
|
@accumulated_examples = []
|
|
40
|
+
@accumulated_failed_files = []
|
|
33
41
|
@accumulated_summary = { duration: 0.0, example_count: 0, failure_count: 0, pending_count: 0,
|
|
34
42
|
errors_outside_of_examples_count: 0 }
|
|
35
43
|
|
|
@@ -46,26 +54,14 @@ module Specbandit
|
|
|
46
54
|
#
|
|
47
55
|
# Returns 0 if all batches passed (or nothing to do), 1 if any batch failed.
|
|
48
56
|
def run
|
|
57
|
+
@run_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
49
58
|
adapter.setup
|
|
50
59
|
|
|
51
|
-
exit_code =
|
|
52
|
-
rerun_files = queue.read_all(key_rerun)
|
|
53
|
-
if rerun_files.any?
|
|
54
|
-
run_replay(rerun_files)
|
|
55
|
-
elsif rerun
|
|
56
|
-
output.puts "[specbandit] ERROR: --rerun flag is set but rerun key '#{key_rerun}' is empty."
|
|
57
|
-
output.puts '[specbandit] The rerun key may have expired (TTL) or Redis was flushed.'
|
|
58
|
-
output.puts '[specbandit] Cannot replay — failing to prevent silent false pass.'
|
|
59
|
-
1
|
|
60
|
-
else
|
|
61
|
-
run_steal(record: true)
|
|
62
|
-
end
|
|
63
|
-
else
|
|
64
|
-
run_steal(record: false)
|
|
65
|
-
end
|
|
60
|
+
exit_code = determine_exit_code
|
|
66
61
|
|
|
67
62
|
print_summary if @batch_results.any?
|
|
68
63
|
merge_json_results
|
|
64
|
+
write_report
|
|
69
65
|
exit_code
|
|
70
66
|
ensure
|
|
71
67
|
adapter.teardown
|
|
@@ -73,6 +69,43 @@ module Specbandit
|
|
|
73
69
|
|
|
74
70
|
private
|
|
75
71
|
|
|
72
|
+
# Decide the operating mode and execute it, returning the exit code.
|
|
73
|
+
# - no usable rerun key → steal mode (or crash if --rerun was requested)
|
|
74
|
+
# - rerun key has data → replay mode
|
|
75
|
+
# - rerun key but empty → record mode (or crash if --rerun was requested)
|
|
76
|
+
def determine_exit_code
|
|
77
|
+
unless key_present?(key_rerun)
|
|
78
|
+
return fail_stale_rerun if rerun
|
|
79
|
+
|
|
80
|
+
return run_steal(record: false)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
rerun_files = queue.read_all(key_rerun)
|
|
84
|
+
return run_replay(rerun_files) if rerun_files.any?
|
|
85
|
+
return fail_stale_rerun if rerun
|
|
86
|
+
|
|
87
|
+
run_steal(record: true)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A Redis key name is usable only when it is a non-nil, non-empty string.
|
|
91
|
+
# Guards the Ruby gotcha where "" is truthy (e.g. --key-rerun "$UNSET_VAR"),
|
|
92
|
+
# which would otherwise read/write against an empty key name.
|
|
93
|
+
def key_present?(value)
|
|
94
|
+
!value.nil? && !value.empty?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Emit the stale/missing-rerun error and return exit code 1.
|
|
98
|
+
def fail_stale_rerun
|
|
99
|
+
if key_present?(key_rerun)
|
|
100
|
+
output.puts "[specbandit] ERROR: --rerun flag is set but rerun key '#{key_rerun}' is empty."
|
|
101
|
+
else
|
|
102
|
+
output.puts '[specbandit] ERROR: --rerun flag is set but no rerun key is configured.'
|
|
103
|
+
end
|
|
104
|
+
output.puts '[specbandit] The rerun key may have expired (TTL) or Redis was flushed.'
|
|
105
|
+
output.puts '[specbandit] Cannot replay — failing to prevent silent false pass.'
|
|
106
|
+
1
|
|
107
|
+
end
|
|
108
|
+
|
|
76
109
|
# Replay mode: run a known list of files in local batches.
|
|
77
110
|
# Used when re-running a failed CI job -- the rerun key already
|
|
78
111
|
# contains the exact files this runner executed previously.
|
|
@@ -89,6 +122,7 @@ module Specbandit
|
|
|
89
122
|
batch.each { |f| output.puts " #{f}" } if verbose
|
|
90
123
|
|
|
91
124
|
result = adapter.run_batch(batch, batch_num)
|
|
125
|
+
record_failed_files(batch, result)
|
|
92
126
|
process_batch_result(result)
|
|
93
127
|
|
|
94
128
|
if result.exit_code != 0
|
|
@@ -132,6 +166,7 @@ module Specbandit
|
|
|
132
166
|
files.each { |f| output.puts " #{f}" } if verbose
|
|
133
167
|
|
|
134
168
|
result = adapter.run_batch(files, batch_num)
|
|
169
|
+
record_failed_files(files, result)
|
|
135
170
|
process_batch_result(result)
|
|
136
171
|
|
|
137
172
|
if result.exit_code != 0
|
|
@@ -151,11 +186,50 @@ module Specbandit
|
|
|
151
186
|
failed ? 1 : 0
|
|
152
187
|
end
|
|
153
188
|
|
|
189
|
+
# Record failed files to the failed key in Redis for later review.
|
|
190
|
+
# Called after each batch; only pushes when key_failed is configured
|
|
191
|
+
# and the batch had a non-zero exit code.
|
|
192
|
+
#
|
|
193
|
+
# For RSpec batches with JSON output, only the individual failed file
|
|
194
|
+
# paths are recorded (not the entire batch). For CLI adapter batches
|
|
195
|
+
# (no per-file granularity), the whole batch is recorded as fallback.
|
|
196
|
+
def record_failed_files(files, result)
|
|
197
|
+
return unless key_present?(key_failed)
|
|
198
|
+
return if result.exit_code.zero?
|
|
199
|
+
|
|
200
|
+
failed_files = extract_failed_files(result) || files
|
|
201
|
+
return if failed_files.empty?
|
|
202
|
+
|
|
203
|
+
queue.push(key_failed, failed_files, ttl: key_failed_ttl)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Extract individual failed file paths from an RspecBatchResult's JSON output.
|
|
207
|
+
# Returns nil when per-file data is not available (CLI adapter).
|
|
208
|
+
def extract_failed_files(result)
|
|
209
|
+
return nil unless result.is_a?(RspecBatchResult) && result.json_path && File.exist?(result.json_path)
|
|
210
|
+
|
|
211
|
+
data = JSON.parse(File.read(result.json_path))
|
|
212
|
+
failed = data.fetch('examples', [])
|
|
213
|
+
.select { |e| e['status'] == 'failed' }
|
|
214
|
+
.filter_map { |e| e['file_path'] }
|
|
215
|
+
.uniq
|
|
216
|
+
|
|
217
|
+
failed.empty? ? nil : failed
|
|
218
|
+
rescue JSON::ParserError
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
221
|
+
|
|
154
222
|
# Process a BatchResult: store it, and for RSpec batches,
|
|
155
223
|
# read the JSON output for rich reporting.
|
|
156
224
|
def process_batch_result(result)
|
|
157
225
|
@batch_results << result
|
|
158
226
|
|
|
227
|
+
# Accumulate failed files for the report while JSON data is still available.
|
|
228
|
+
if result.exit_code != 0
|
|
229
|
+
per_file = extract_failed_files(result)
|
|
230
|
+
@accumulated_failed_files.concat(per_file || result.files)
|
|
231
|
+
end
|
|
232
|
+
|
|
159
233
|
# If the adapter returned an RspecBatchResult with a json_path,
|
|
160
234
|
# accumulate the structured results for rich reporting.
|
|
161
235
|
return unless result.is_a?(RspecBatchResult) && result.json_path
|
|
@@ -242,6 +316,48 @@ module Specbandit
|
|
|
242
316
|
File.write(path, JSON.pretty_generate(merged))
|
|
243
317
|
end
|
|
244
318
|
|
|
319
|
+
# Write a JSON report file with run statistics when --report is set.
|
|
320
|
+
def write_report
|
|
321
|
+
return unless report
|
|
322
|
+
return if @batch_results.empty?
|
|
323
|
+
|
|
324
|
+
wall_time = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @run_start_time).round(2)
|
|
325
|
+
durations = batch_durations
|
|
326
|
+
failed_batches_count = @batch_results.count { |r| r.exit_code != 0 }
|
|
327
|
+
passed_batches_count = @batch_results.count { |r| r.exit_code == 0 }
|
|
328
|
+
|
|
329
|
+
data = {
|
|
330
|
+
specbandit_version: Specbandit::VERSION,
|
|
331
|
+
summary: {
|
|
332
|
+
total_files: @batch_results.sum { |r| r.files.size },
|
|
333
|
+
total_batches: @batch_results.size,
|
|
334
|
+
passed_batches: passed_batches_count,
|
|
335
|
+
failed_batches: failed_batches_count,
|
|
336
|
+
passed: failed_batches_count == 0
|
|
337
|
+
},
|
|
338
|
+
failed_files: @accumulated_failed_files.uniq,
|
|
339
|
+
total_wall_time: wall_time,
|
|
340
|
+
batch_timings: {
|
|
341
|
+
count: durations.size,
|
|
342
|
+
min: format('%.2f', durations.min || 0),
|
|
343
|
+
avg: format('%.2f', durations.empty? ? 0 : durations.sum / durations.size),
|
|
344
|
+
max: format('%.2f', durations.max || 0),
|
|
345
|
+
all: durations.map { |d| d.round(2) }
|
|
346
|
+
},
|
|
347
|
+
batches: @batch_results.map do |r|
|
|
348
|
+
{
|
|
349
|
+
batch_num: r.batch_num,
|
|
350
|
+
files: r.files,
|
|
351
|
+
exit_code: r.exit_code,
|
|
352
|
+
duration: r.duration.round(2),
|
|
353
|
+
passed: r.exit_code == 0
|
|
354
|
+
}
|
|
355
|
+
end
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
File.write(report, JSON.pretty_generate(data))
|
|
359
|
+
end
|
|
360
|
+
|
|
245
361
|
# Print a unified summary to the output stream after all batches.
|
|
246
362
|
def print_summary
|
|
247
363
|
output.puts ''
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: specbandit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ferran Basora
|
|
@@ -109,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
110
|
version: '0'
|
|
111
111
|
requirements: []
|
|
112
|
-
rubygems_version: 4.0.
|
|
112
|
+
rubygems_version: 4.0.10
|
|
113
113
|
specification_version: 4
|
|
114
114
|
summary: Distributed test runner using Redis as a work queue
|
|
115
115
|
test_files: []
|