specwrk 0.15.10 → 0.16.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/README.md +12 -9
- data/lib/specwrk/cli.rb +3 -1
- data/lib/specwrk/client.rb +10 -10
- data/lib/specwrk/store.rb +41 -16
- data/lib/specwrk/version.rb +1 -1
- data/lib/specwrk/web/endpoints/base.rb +11 -3
- data/lib/specwrk/web/endpoints/popable.rb +25 -7
- 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: 1736573f9c3fc84992518a7a2ea486c03eefe4885b8b857a79b15551533deec0
|
4
|
+
data.tar.gz: 7106e3b4c856bfd9ff89e5fe562ba3da45ad5981356b295e24b55579aa48609f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 067222d93a38d847886d40b5a5121e24d68f633115ecfddccdd7c199adf598fd780cb8ca5fd1110028679828436ac3bc8a4335ca54c7a481aeaa7dc85938d0a4
|
7
|
+
data.tar.gz: 5d10c755bb71d7ace592e33c5fa8b2c4a2d9a349805870914ca0c9ac3410306765324211820c1283c8c2d8846ddbbcf5d1c88f89f2d5b89899f554a6e5c187dd
|
data/README.md
CHANGED
@@ -34,7 +34,7 @@ Commands:
|
|
34
34
|
Intended for quick ad-hoc local host development or single-node CI runs. This command starts a queue server, seeds it with examples from the `spec/` directory, and starts `8` worker processes. It will report the ultimate success or failure.
|
35
35
|
|
36
36
|
```sh
|
37
|
-
$ start --help
|
37
|
+
$ specwrk start --help
|
38
38
|
Command:
|
39
39
|
specwrk start
|
40
40
|
|
@@ -52,6 +52,7 @@ Options:
|
|
52
52
|
--key=VALUE, -k VALUE # Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY, default: ""
|
53
53
|
--run=VALUE, -r VALUE # The run identifier for this job execution. Overrides SPECWRK_RUN, default: "main"
|
54
54
|
--timeout=VALUE, -t VALUE # The amount of time to wait for the server to respond. Overrides SPECWRK_TIMEOUT, default: "5"
|
55
|
+
--network-retries=VALUE # The number of times to retry in the event of a network failure. Overrides SPECWRK_NETWORK_RETRIES, default: "1"
|
55
56
|
--id=VALUE # The identifier for this worker. Overrides SPECWRK_ID. If none provided one in the format of specwrk-worker-8_RAND_CHARS-COUNT_INDEX will be used
|
56
57
|
--count=VALUE, -c VALUE # The number of worker processes you want to start, default: 1
|
57
58
|
--output=VALUE, -o VALUE # Directory where worker output is stored. Overrides SPECWRK_OUT, default: ".specwrk/"
|
@@ -113,6 +114,7 @@ Options:
|
|
113
114
|
--key=VALUE, -k VALUE # Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY, default: ""
|
114
115
|
--run=VALUE, -r VALUE # The run identifier for this job execution. Overrides SPECWRK_RUN, default: "main"
|
115
116
|
--timeout=VALUE, -t VALUE # The amount of time to wait for the server to respond. Overrides SPECWRK_TIMEOUT, default: "5"
|
117
|
+
--network-retries=VALUE # The number of times to retry in the event of a network failure. Overrides SPECWRK_NETWORK_RETRIES, default: "1"
|
116
118
|
--max-retries=VALUE # Number of times an example will be re-run should it fail, default: 0
|
117
119
|
--help, -h # Print this help
|
118
120
|
```
|
@@ -140,6 +142,7 @@ Options:
|
|
140
142
|
--key=VALUE, -k VALUE # Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY, default: ""
|
141
143
|
--run=VALUE, -r VALUE # The run identifier for this job execution. Overrides SPECWRK_RUN, default: "main"
|
142
144
|
--timeout=VALUE, -t VALUE # The amount of time to wait for the server to respond. Overrides SPECWRK_TIMEOUT, default: "5"
|
145
|
+
--network-retries=VALUE # The number of times to retry in the event of a network failure. Overrides SPECWRK_NETWORK_RETRIES, default: "1"
|
143
146
|
--help, -h # Print this help
|
144
147
|
```
|
145
148
|
|
@@ -164,13 +167,13 @@ Options:
|
|
164
167
|
```
|
165
168
|
|
166
169
|
## Configuring your test environment
|
167
|
-
If
|
170
|
+
If your test suite tracks state, starts servers, etc. and you plan on running many processes on the same node, you'll need to make
|
168
171
|
adjustments to avoid conflicting port usage or database/state mutations.
|
169
172
|
|
170
173
|
`specwrk` workers will have `TEST_ENV_NUMBER={i}` set to help you configure approriately.
|
171
174
|
|
172
175
|
### Rails
|
173
|
-
Rails has had easy multi-process test setup for a while now by creating unique test databases per process. For my rails v7.2 app which uses PostgreSQL and
|
176
|
+
Rails has had easy multi-process test setup for a while now by creating unique test databases per process. For my rails v7.2 app which uses PostgreSQL and Capybara, I made these changes to my `spec/rails_helper.rb`:
|
174
177
|
|
175
178
|
```diff
|
176
179
|
++ if ENV["TEST_ENV_NUMBER"]
|
@@ -200,7 +203,7 @@ Make sure to persist `$SPECWRK_OUT/report.json` between runs so that subsequent
|
|
200
203
|
[CircleCI Example](https://github.com/danielwestendorf/specwrk/blob/main/.circleci/config.yml) (specwrk-single-node job)
|
201
204
|
|
202
205
|
### Multi-node, multi-process
|
203
|
-
Multi-node, multi-process works best when have many nodes running tests. This distributes the test execution across the nodes until the queue is for the run is empty, optimizing for slowest specs first. This distributes test execution across all nodes evenly(-ish).
|
206
|
+
Multi-node, multi-process works best when you have many nodes running tests. This distributes the test execution across the nodes until the queue is for the run is empty, optimizing for slowest specs first. This distributes test execution across all nodes evenly(-ish).
|
204
207
|
|
205
208
|
To accomplish this, a central queue server is required, examples must be explicitly seeded, and workers explicitly started.
|
206
209
|
|
@@ -221,7 +224,7 @@ Start a persistent Queue Server given one of the following methods
|
|
221
224
|
|
222
225
|
### Configuring your Queue Server
|
223
226
|
- Secure your server with a key either with the `SPECWRK_SRV_KEY` environment variable or `--key` CLI option
|
224
|
-
- Configure the server output to be a persisted volume so your timings survive between system restarts with the `SPECWRK_SRV_STORE_URI` environment variable or `--store-uri` CLI option. By default, `memory:///` will be used for the run's data stores (so run data will
|
227
|
+
- Configure the server output to be a persisted volume so your timings survive between system restarts with the `SPECWRK_SRV_STORE_URI` environment variable or `--store-uri` CLI option. By default, `memory:///` will be used for the run's data stores (so run data will not survive server restarts) while `file://#{Dir.tmpdir}` will be used for run timings. Pass `--store-uri file:///whatever/absolute/path` to store all data on disk (required for multiple server processes).
|
225
228
|
|
226
229
|
See [specwrk-store-redis_adapter](https://github.com/danielwestendorf/specwrk-store-redis_adapter) for Redis-compatible backed storage.
|
227
230
|
|
@@ -245,7 +248,7 @@ map(/_spec\.rb$/) do |spec_path|
|
|
245
248
|
spec_path
|
246
249
|
end
|
247
250
|
|
248
|
-
# If a file in lib changes, map it to the spec folder for
|
251
|
+
# If a file in lib changes, map it to the spec folder for its spec file
|
249
252
|
map(/lib\/.*\.rb$/) do |path|
|
250
253
|
path.gsub(/lib\/(.+)\.rb/, "spec/\\1_spec.rb")
|
251
254
|
end
|
@@ -255,7 +258,7 @@ end
|
|
255
258
|
# path.gsub(/app\/models\/(.+)\.rb/, "spec/models/\\1_spec.rb")
|
256
259
|
# end
|
257
260
|
#
|
258
|
-
# If a
|
261
|
+
# If a controller file changes (assuming rails app structure), run the controller and system specs file
|
259
262
|
# map(/app\/controllers\/.*.rb$/) do |path|
|
260
263
|
# [
|
261
264
|
# path.gsub(/app\/controllers\/(.+)\.rb/, "spec/controllers/\\1_spec.rb"),
|
@@ -265,7 +268,7 @@ end
|
|
265
268
|
```
|
266
269
|
|
267
270
|
## Prior/other works
|
268
|
-
There are many prior works for running rspec tests across multiple processes. Most of them combine process output making failures hard to grok. Some are good at running tests locally, but not on CI, while others are inversely true. Others are
|
271
|
+
There are many prior works for running rspec tests across multiple processes. Most of them combine process output making failures hard to grok. Some are good at running tests locally, but not on CI, while others are inversely true. Others are commercial or impractical without making a purchase.
|
269
272
|
|
270
273
|
specwrk is different because it:
|
271
274
|
1. Puts your developer experience first. Easy execution. No messy outputs. Retries built in. Easy(er) debugging of flaky tests.
|
@@ -287,7 +290,7 @@ specwrk is different because it:
|
|
287
290
|
|
288
291
|
## Contributing
|
289
292
|
|
290
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
293
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/danielwestendorf/specwrk.
|
291
294
|
|
292
295
|
## License
|
293
296
|
|
data/lib/specwrk/cli.rb
CHANGED
@@ -20,13 +20,15 @@ module Specwrk
|
|
20
20
|
base.unique_option :key, type: :string, default: ENV.fetch("SPECWRK_SRV_KEY", ""), aliases: ["-k"], desc: "Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY"
|
21
21
|
base.unique_option :run, type: :string, default: ENV.fetch("SPECWRK_RUN", "main"), aliases: ["-r"], desc: "The run identifier for this job execution. Overrides SPECWRK_RUN"
|
22
22
|
base.unique_option :timeout, type: :integer, default: ENV.fetch("SPECWRK_TIMEOUT", "5"), aliases: ["-t"], desc: "The amount of time to wait for the server to respond. Overrides SPECWRK_TIMEOUT"
|
23
|
+
base.unique_option :network_retries, type: :integer, default: ENV.fetch("SPECWRK_NETWORK_RETRIES", "1"), desc: "The number of times to retry in the event of a network failure. Overrides SPECWRK_NETWORK_RETRIES"
|
23
24
|
end
|
24
25
|
|
25
|
-
on_setup do |uri:, key:, run:, timeout:, **|
|
26
|
+
on_setup do |uri:, key:, run:, timeout:, network_retries:, **|
|
26
27
|
ENV["SPECWRK_SRV_URI"] = uri
|
27
28
|
ENV["SPECWRK_SRV_KEY"] = key
|
28
29
|
ENV["SPECWRK_RUN"] = run
|
29
30
|
ENV["SPECWRK_TIMEOUT"] = timeout
|
31
|
+
ENV["SPECWRK_NETWORK_RETRIES"] = network_retries
|
30
32
|
end
|
31
33
|
end
|
32
34
|
|
data/lib/specwrk/client.rb
CHANGED
@@ -115,16 +115,6 @@ module Specwrk
|
|
115
115
|
else
|
116
116
|
raise UnhandledResponseError.new("#{response.code}: #{response.body}")
|
117
117
|
end
|
118
|
-
rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout => e
|
119
|
-
@retry_count ||= 0
|
120
|
-
@retry_count += 1
|
121
|
-
|
122
|
-
raise e if @retry_count == 5
|
123
|
-
|
124
|
-
warn e
|
125
|
-
sleep @retry_count
|
126
|
-
|
127
|
-
retry
|
128
118
|
end
|
129
119
|
|
130
120
|
def seed(examples, max_retries)
|
@@ -172,6 +162,16 @@ module Specwrk
|
|
172
162
|
@worker_status = response["x-specwrk-status"].to_i if response["x-specwrk-status"]
|
173
163
|
end
|
174
164
|
end
|
165
|
+
rescue Net::ReadTimeout, Net::WriteTimeout => e
|
166
|
+
@retry_count ||= 0
|
167
|
+
|
168
|
+
raise e if @retry_count == ENV["SPECWRK_NETWORK_RETRIES"].to_i
|
169
|
+
@retry_count += 1
|
170
|
+
|
171
|
+
warn e
|
172
|
+
sleep @retry_count
|
173
|
+
|
174
|
+
retry
|
175
175
|
end
|
176
176
|
|
177
177
|
def default_headers
|
data/lib/specwrk/store.rb
CHANGED
@@ -105,6 +105,47 @@ module Specwrk
|
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
108
|
+
class WorkerStore < Store
|
109
|
+
FIRST_SEEN_AT_KEY = :____first_seen_at_key
|
110
|
+
LAST_SEEN_AT_KEY = :____last_seen_at_key
|
111
|
+
|
112
|
+
def first_seen_at=(val)
|
113
|
+
@first_seen_at = nil
|
114
|
+
|
115
|
+
self[FIRST_SEEN_AT_KEY] = val.to_i
|
116
|
+
end
|
117
|
+
|
118
|
+
def first_seen_at
|
119
|
+
@first_seen_at ||= begin
|
120
|
+
value = self[FIRST_SEEN_AT_KEY]
|
121
|
+
return @first_seen_at = value unless value
|
122
|
+
|
123
|
+
@first_seen_at = Time.at(value.to_i)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def last_seen_at=(val)
|
128
|
+
@last_seen_at = nil
|
129
|
+
|
130
|
+
self[LAST_SEEN_AT_KEY] = val.to_i
|
131
|
+
end
|
132
|
+
|
133
|
+
def last_seen_at
|
134
|
+
@last_seen_at ||= begin
|
135
|
+
value = self[LAST_SEEN_AT_KEY]
|
136
|
+
return @last_seen_at = value unless value
|
137
|
+
|
138
|
+
@last_seen_at = Time.at(value.to_i)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def reload
|
143
|
+
@last_seen_at = nil
|
144
|
+
@first_seen_at = nil
|
145
|
+
super
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
108
149
|
class PendingStore < Store
|
109
150
|
RUN_TIME_BUCKET_MAXIMUM_KEY = :____run_time_bucket_maximum
|
110
151
|
ORDER_KEY = :____order
|
@@ -234,22 +275,6 @@ module Specwrk
|
|
234
275
|
end
|
235
276
|
|
236
277
|
class ProcessingStore < Store
|
237
|
-
def expired
|
238
|
-
@expired ||= begin
|
239
|
-
bucket = []
|
240
|
-
|
241
|
-
keys.each_slice(24).each do |key_group|
|
242
|
-
examples = multi_read(*key_group)
|
243
|
-
examples.each do |id, example|
|
244
|
-
next if example[:completion_threshold].nil?
|
245
|
-
|
246
|
-
bucket << [id, example] if example[:completion_threshold] < Time.now.to_i
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
bucket.to_h
|
251
|
-
end
|
252
|
-
end
|
253
278
|
end
|
254
279
|
|
255
280
|
class CompletedStore < Store
|
data/lib/specwrk/version.rb
CHANGED
@@ -21,8 +21,8 @@ module Specwrk
|
|
21
21
|
|
22
22
|
before_lock
|
23
23
|
|
24
|
-
worker
|
25
|
-
worker
|
24
|
+
worker.first_seen_at ||= Time.now
|
25
|
+
worker.last_seen_at = Time.now
|
26
26
|
|
27
27
|
final_response = with_lock do
|
28
28
|
started_at = metadata[:started_at] ||= Time.now.iso8601
|
@@ -109,7 +109,11 @@ module Specwrk
|
|
109
109
|
end
|
110
110
|
|
111
111
|
def worker
|
112
|
-
@worker ||=
|
112
|
+
@worker ||= worker_store_for(worker_id)
|
113
|
+
end
|
114
|
+
|
115
|
+
def worker_id
|
116
|
+
request.get_header("HTTP_X_SPECWRK_ID").to_s
|
113
117
|
end
|
114
118
|
|
115
119
|
def worker_status
|
@@ -118,6 +122,10 @@ module Specwrk
|
|
118
122
|
worker[:failed] || 1
|
119
123
|
end
|
120
124
|
|
125
|
+
def worker_store_for(id)
|
126
|
+
WorkerStore.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "workers", id))
|
127
|
+
end
|
128
|
+
|
121
129
|
def run_id
|
122
130
|
request.get_header("HTTP_X_SPECWRK_RUN")
|
123
131
|
end
|
@@ -15,9 +15,9 @@ module Specwrk
|
|
15
15
|
[204, {"content-type" => "text/plain"}, ["Waiting for sample to be seeded."]]
|
16
16
|
elsif completed.any? && processing.empty?
|
17
17
|
[410, {"content-type" => "text/plain"}, ["That's a good lad. Run along now and go home."]]
|
18
|
-
elsif
|
19
|
-
pending.merge!(
|
20
|
-
processing.delete(*
|
18
|
+
elsif expired_examples.length.positive?
|
19
|
+
pending.merge!(expired_examples.each { |_id, example| example[:worker_id] = worker_id })
|
20
|
+
processing.delete(*expired_examples.keys)
|
21
21
|
@examples = nil
|
22
22
|
|
23
23
|
[200, {"content-type" => "application/json"}, [JSON.generate(examples)]]
|
@@ -29,13 +29,10 @@ module Specwrk
|
|
29
29
|
def examples
|
30
30
|
@examples ||= begin
|
31
31
|
examples = pending.shift_bucket
|
32
|
-
bucket_run_time_total = examples.map { |example| example.fetch(:expected_run_time, 10.0) }.compact.sum * 2
|
33
|
-
maximum_completion_threshold = (pending.run_time_bucket_maximum || 30.0) * 2
|
34
|
-
completion_threshold = Time.now + [bucket_run_time_total, maximum_completion_threshold, 20.0].max
|
35
32
|
|
36
33
|
processing_data = examples.map do |example|
|
37
34
|
[
|
38
|
-
example[:id], example.merge(
|
35
|
+
example[:id], example.merge(worker_id: worker_id, processing_started_at: Time.now.to_i)
|
39
36
|
]
|
40
37
|
end
|
41
38
|
|
@@ -44,6 +41,27 @@ module Specwrk
|
|
44
41
|
examples
|
45
42
|
end
|
46
43
|
end
|
44
|
+
|
45
|
+
def expired_examples
|
46
|
+
return unless processing.any?
|
47
|
+
|
48
|
+
@expired_examples ||= processing.to_h.select { |_id, example| expired?(example) }
|
49
|
+
end
|
50
|
+
|
51
|
+
# Has the worker missed two heartbeat check-ins?
|
52
|
+
def expired?(example)
|
53
|
+
return false unless example[:worker_id]
|
54
|
+
return false unless example[:processing_started_at]
|
55
|
+
return false unless example[:processing_started_at] < (Time.now - 20).to_i
|
56
|
+
|
57
|
+
workers_last_heartbeats[example[:worker_id]] < Time.now - 20
|
58
|
+
end
|
59
|
+
|
60
|
+
def workers_last_heartbeats
|
61
|
+
@workers_last_heartbeats ||= Hash.new do |h, k|
|
62
|
+
h[k] = worker_store_for(k).last_seen_at || Time.at(0)
|
63
|
+
end
|
64
|
+
end
|
47
65
|
end
|
48
66
|
end
|
49
67
|
end
|