specwrk 0.13.1 → 0.14.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/.circleci/config.yml +2 -1
- data/README.md +3 -1
- data/lib/specwrk/cli.rb +25 -6
- data/lib/specwrk/client.rb +2 -2
- data/lib/specwrk/store.rb +12 -1
- data/lib/specwrk/version.rb +1 -1
- data/lib/specwrk/web/endpoints.rb +49 -4
- data/lib/specwrk/worker.rb +5 -1
- 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: 7890636149b3658e663cfc5957522e9147f0ede2a6ff4e28c27b6966c1b45b40
|
4
|
+
data.tar.gz: a76c0bd696a11d4b1703dabd156466611678bcec30b35fd6994ced48444e9dcf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ad2c96e0111c5b523836f22a3eda9133f7ef838b5239c8355118a299c9ac6ea523bdb524955bf7b66caffea1397d5470cfd3554e877247268fce332ba34ac43
|
7
|
+
data.tar.gz: dec3475151008f2e4452ed4f5e96c413d63d61c4b2ebe38d315a3d795bed58247341c53c60b8dd7f15385860a1d381d77f60cdba61ce5738c24fb0f27a50b72f
|
data/.circleci/config.yml
CHANGED
@@ -73,7 +73,8 @@ jobs:
|
|
73
73
|
## SPECWRK STEP ##
|
74
74
|
- run:
|
75
75
|
name: Run tests via specwrk start
|
76
|
-
|
76
|
+
# This run will output a single failure when healthy, but because of retries the exit code will be zero
|
77
|
+
command: bundle exec specwrk start --count 2 --max-retries 1 spec/
|
77
78
|
## /SPECWRK STEP ##
|
78
79
|
|
79
80
|
## SPECWRK STEP ##
|
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# Specwrk
|
2
|
-
Run your [RSpec](https://github.com/rspec/rspec) examples across many processors and many nodes for a single build. Or just many processes on a single node. Speeds up your *slow* (minutes/hours not seconds) test suite by running multiple examples in parallel.
|
2
|
+
Run your [RSpec](https://github.com/rspec/rspec) examples across many processors and many nodes for a single build. Or just many processes on a single node. Speeds up your *slow* (minutes/hours not seconds) test suite by running multiple examples in parallel. Optionally, retry failed examples up-to N-times to avoid breaking deployments.
|
3
3
|
|
4
4
|
One CLI command to:
|
5
5
|
|
@@ -60,6 +60,7 @@ Options:
|
|
60
60
|
--store-uri=VALUE # Directory where server state is stored. Required for multi-node or multi-process servers.
|
61
61
|
--group-by=VALUE # How examples will be grouped for workers; fallback to file if no timings are found. Overrides SPECWERK_SRV_GROUP_BY: (file/timings), default: "timings"
|
62
62
|
--[no-]verbose # Run in verbose mode, default: false
|
63
|
+
--max-retries=VALUE # Number of times an example will be re-run should it fail, default: 0
|
63
64
|
--help, -h # Print this help
|
64
65
|
```
|
65
66
|
|
@@ -111,6 +112,7 @@ Options:
|
|
111
112
|
--key=VALUE, -k VALUE # Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY, default: ""
|
112
113
|
--run=VALUE, -r VALUE # The run identifier for this job execution. Overrides SPECWRK_RUN, default: "main"
|
113
114
|
--timeout=VALUE, -t VALUE # The amount of time to wait for the server to respond. Overrides SPECWRK_TIMEOUT, default: "5"
|
115
|
+
--max-retries=VALUE # Number of times an example will be re-run should it fail, default: 0
|
114
116
|
--help, -h # Print this help
|
115
117
|
```
|
116
118
|
|
data/lib/specwrk/cli.rb
CHANGED
@@ -49,16 +49,31 @@ module Specwrk
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def start_workers
|
52
|
+
@final_outputs = []
|
52
53
|
@worker_pids = worker_count.times.map do |i|
|
54
|
+
reader, writer = IO.pipe
|
55
|
+
@final_outputs << reader
|
56
|
+
|
53
57
|
Process.fork do
|
54
58
|
ENV["TEST_ENV_NUMBER"] = ENV["SPECWRK_FORKED"] = (i + 1).to_s
|
55
59
|
ENV["SPECWRK_ID"] = ENV["SPECWRK_ID"] + "-#{i + 1}"
|
56
60
|
|
61
|
+
$final_output = writer # standard:disable Style/GlobalVars
|
62
|
+
$final_output.sync = true # standard:disable Style/GlobalVars
|
63
|
+
reader.close
|
64
|
+
|
57
65
|
require "specwrk/worker"
|
58
66
|
|
59
67
|
status = Specwrk::Worker.run!
|
68
|
+
$final_output.close # standard:disable Style/GlobalVars
|
60
69
|
exit(status)
|
61
|
-
end
|
70
|
+
end.tap { writer.close }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def drain_outputs
|
75
|
+
@final_outputs.each do |reader|
|
76
|
+
reader.each_line { |line| $stdout.print line }
|
62
77
|
end
|
63
78
|
end
|
64
79
|
|
@@ -104,10 +119,10 @@ module Specwrk
|
|
104
119
|
include Clientable
|
105
120
|
|
106
121
|
desc "Seed the server with a list of specs for the run"
|
107
|
-
|
122
|
+
option :max_retries, default: 0, desc: "Number of times an example will be re-run should it fail"
|
108
123
|
argument :dir, required: false, default: "spec", desc: "Relative spec directory to run against"
|
109
124
|
|
110
|
-
def call(dir:, **args)
|
125
|
+
def call(max_retries:, dir:, **args)
|
111
126
|
self.class.setup(**args)
|
112
127
|
|
113
128
|
require "specwrk/list_examples"
|
@@ -117,7 +132,7 @@ module Specwrk
|
|
117
132
|
examples = ListExamples.new(dir).examples
|
118
133
|
|
119
134
|
Client.wait_for_server!
|
120
|
-
Client.new.seed(examples)
|
135
|
+
Client.new.seed(examples, max_retries: max_retries)
|
121
136
|
file_count = examples.group_by { |e| e[:file_path] }.keys.size
|
122
137
|
puts "🌱 Seeded #{examples.size} examples across #{file_count} files"
|
123
138
|
rescue Errno::ECONNREFUSED
|
@@ -140,6 +155,7 @@ module Specwrk
|
|
140
155
|
|
141
156
|
start_workers
|
142
157
|
wait_for_workers_exit
|
158
|
+
drain_outputs
|
143
159
|
|
144
160
|
require "specwrk/cli_reporter"
|
145
161
|
Specwrk::CLIReporter.new.report
|
@@ -180,9 +196,10 @@ module Specwrk
|
|
180
196
|
include Servable
|
181
197
|
|
182
198
|
desc "Start a server and workers, monitor until complete"
|
199
|
+
option :max_retries, default: 0, desc: "Number of times an example will be re-run should it fail"
|
183
200
|
argument :dir, required: false, default: "spec", desc: "Relative spec directory to run against"
|
184
201
|
|
185
|
-
def call(dir:, **args)
|
202
|
+
def call(max_retries:, dir:, **args)
|
186
203
|
self.class.setup(**args)
|
187
204
|
$stdout.sync = true
|
188
205
|
|
@@ -204,6 +221,7 @@ module Specwrk
|
|
204
221
|
require "specwrk/list_examples"
|
205
222
|
require "specwrk/client"
|
206
223
|
|
224
|
+
ENV["SPECWRK_FORKED"] = "1"
|
207
225
|
ENV["SPECWRK_SEED"] = "1"
|
208
226
|
examples = ListExamples.new(dir).examples
|
209
227
|
|
@@ -211,7 +229,7 @@ module Specwrk
|
|
211
229
|
Client.wait_for_server!
|
212
230
|
status "Server responding ✓"
|
213
231
|
status "Seeding #{examples.length} examples..."
|
214
|
-
Client.new.seed(examples)
|
232
|
+
Client.new.seed(examples, max_retries)
|
215
233
|
file_count = examples.group_by { |e| e[:file_path] }.keys.size
|
216
234
|
status "🌱 Seeded #{examples.size} examples across #{file_count} files"
|
217
235
|
end
|
@@ -228,6 +246,7 @@ module Specwrk
|
|
228
246
|
status "#{worker_count} workers started ✓\n"
|
229
247
|
Specwrk.wait_for_pids_exit(@worker_pids)
|
230
248
|
|
249
|
+
drain_outputs
|
231
250
|
return if Specwrk.force_quit
|
232
251
|
|
233
252
|
require "specwrk/cli_reporter"
|
data/lib/specwrk/client.rb
CHANGED
@@ -127,8 +127,8 @@ module Specwrk
|
|
127
127
|
retry
|
128
128
|
end
|
129
129
|
|
130
|
-
def seed(examples)
|
131
|
-
response = post "/seed", body: examples.to_json
|
130
|
+
def seed(examples, max_retries)
|
131
|
+
response = post "/seed", body: {max_retries: max_retries, examples: examples}.to_json
|
132
132
|
|
133
133
|
(response.code == "200") ? true : raise(UnhandledResponseError.new("#{response.code}: #{response.body}"))
|
134
134
|
end
|
data/lib/specwrk/store.rb
CHANGED
@@ -99,6 +99,7 @@ module Specwrk
|
|
99
99
|
class PendingStore < Store
|
100
100
|
RUN_TIME_BUCKET_MAXIMUM_KEY = :____run_time_bucket_maximum
|
101
101
|
ORDER_KEY = :____order
|
102
|
+
MAX_RETRIES_KEY = :____max_retries
|
102
103
|
|
103
104
|
def run_time_bucket_maximum=(val)
|
104
105
|
@run_time_bucket_maximum = self[RUN_TIME_BUCKET_MAXIMUM_KEY] = val
|
@@ -122,6 +123,14 @@ module Specwrk
|
|
122
123
|
@order ||= self[ORDER_KEY] || []
|
123
124
|
end
|
124
125
|
|
126
|
+
def max_retries=(val)
|
127
|
+
@max_retries = self[MAX_RETRIES_KEY] = val
|
128
|
+
end
|
129
|
+
|
130
|
+
def max_retries
|
131
|
+
@max_retries ||= self[MAX_RETRIES_KEY] || 0
|
132
|
+
end
|
133
|
+
|
125
134
|
def keys
|
126
135
|
return super if order.length.zero?
|
127
136
|
|
@@ -130,7 +139,8 @@ module Specwrk
|
|
130
139
|
|
131
140
|
def merge!(hash)
|
132
141
|
super
|
133
|
-
|
142
|
+
|
143
|
+
self.order = order + (hash.keys - order)
|
134
144
|
end
|
135
145
|
|
136
146
|
def clear
|
@@ -140,6 +150,7 @@ module Specwrk
|
|
140
150
|
|
141
151
|
def reload
|
142
152
|
@order = nil
|
153
|
+
@max_retries = nil
|
143
154
|
super
|
144
155
|
end
|
145
156
|
|
data/lib/specwrk/version.rb
CHANGED
@@ -92,6 +92,10 @@ module Specwrk
|
|
92
92
|
@completed ||= CompletedStore.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "completed"))
|
93
93
|
end
|
94
94
|
|
95
|
+
def failure_counts
|
96
|
+
@failure_counts ||= Store.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "failure_counts"))
|
97
|
+
end
|
98
|
+
|
95
99
|
def metadata
|
96
100
|
@metadata ||= Store.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "metadata"))
|
97
101
|
end
|
@@ -141,6 +145,10 @@ module Specwrk
|
|
141
145
|
|
142
146
|
def with_response
|
143
147
|
pending.clear
|
148
|
+
failure_counts.clear
|
149
|
+
|
150
|
+
pending.max_retries = payload.fetch(:max_retries, "0").to_i
|
151
|
+
|
144
152
|
new_run_time_bucket_maximums = [pending.run_time_bucket_maximum, @seeds_run_time_bucket_maximum.to_f].compact
|
145
153
|
pending.run_time_bucket_maximum = new_run_time_bucket_maximums.sum.to_f / new_run_time_bucket_maximums.length.to_f
|
146
154
|
|
@@ -154,10 +162,10 @@ module Specwrk
|
|
154
162
|
def examples_with_run_times
|
155
163
|
@examples_with_run_times ||= begin
|
156
164
|
unsorted_examples_with_run_times = []
|
157
|
-
all_ids = payload.map { |example| example[:id] }
|
165
|
+
all_ids = payload[:examples].map { |example| example[:id] }
|
158
166
|
all_run_times = run_times.multi_read(*all_ids)
|
159
167
|
|
160
|
-
payload.each do |example|
|
168
|
+
payload[:examples].each do |example|
|
161
169
|
run_time = all_run_times[example[:id]]
|
162
170
|
|
163
171
|
unsorted_examples_with_run_times << [example[:id], example.merge(expected_run_time: run_time)]
|
@@ -250,15 +258,52 @@ module Specwrk
|
|
250
258
|
|
251
259
|
def with_response
|
252
260
|
completed.merge!(completed_examples)
|
253
|
-
processing.delete(*completed_examples.keys)
|
261
|
+
processing.delete(*(completed_examples.keys + retry_examples.keys))
|
262
|
+
pending.merge!(retry_examples)
|
263
|
+
failure_counts.merge!(retry_examples_new_failure_counts)
|
254
264
|
|
255
265
|
with_pop_response
|
256
266
|
end
|
257
267
|
|
258
268
|
private
|
259
269
|
|
270
|
+
def all_examples
|
271
|
+
@all_examples ||= payload.map { |example| [example[:id], example] if processing[example[:id]] }.compact.to_h
|
272
|
+
end
|
273
|
+
|
260
274
|
def completed_examples
|
261
|
-
@
|
275
|
+
@completed_examples ||= all_examples.map do |id, example|
|
276
|
+
next if retry_example?(example)
|
277
|
+
|
278
|
+
[id, example]
|
279
|
+
end.compact.to_h
|
280
|
+
end
|
281
|
+
|
282
|
+
def retry_examples
|
283
|
+
@retry_examples ||= all_examples.map do |id, example|
|
284
|
+
next unless retry_example?(example)
|
285
|
+
|
286
|
+
[id, example]
|
287
|
+
end.compact.to_h
|
288
|
+
end
|
289
|
+
|
290
|
+
def retry_examples_new_failure_counts
|
291
|
+
@retry_examples_new_failure_counts ||= retry_examples.map do |id, _example|
|
292
|
+
[id, all_example_failure_counts.fetch(id, 0) + 1]
|
293
|
+
end.to_h
|
294
|
+
end
|
295
|
+
|
296
|
+
def retry_example?(example)
|
297
|
+
return false unless example[:status] == "failed"
|
298
|
+
return false unless pending.max_retries.positive?
|
299
|
+
|
300
|
+
example_failure_count = all_example_failure_counts.fetch(example[:id], 0)
|
301
|
+
|
302
|
+
example_failure_count < pending.max_retries
|
303
|
+
end
|
304
|
+
|
305
|
+
def all_example_failure_counts
|
306
|
+
@all_example_failure_counts ||= failure_counts.multi_read(*all_examples.keys)
|
262
307
|
end
|
263
308
|
|
264
309
|
def completed_examples_status_counts
|
data/lib/specwrk/worker.rb
CHANGED
@@ -54,7 +54,7 @@ module Specwrk
|
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
57
|
-
executor.final_output.tap(&:rewind).each_line { |line|
|
57
|
+
executor.final_output.tap(&:rewind).each_line { |line| final_output.write line }
|
58
58
|
|
59
59
|
@heartbeat_thread.kill
|
60
60
|
client.close
|
@@ -109,6 +109,10 @@ module Specwrk
|
|
109
109
|
|
110
110
|
attr_reader :running, :client, :executor
|
111
111
|
|
112
|
+
def final_output
|
113
|
+
$final_output || $stdout # standard:disable Style/GlobalVars
|
114
|
+
end
|
115
|
+
|
112
116
|
def status
|
113
117
|
return 0 if @all_examples_completed && client.worker_status.zero?
|
114
118
|
return 1 if Specwrk.force_quit
|