specwrk 0.20.2 → 0.21.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/CHANGELOG.md +7 -2
- data/README.md +25 -0
- data/Specwrk.hooks.rb +68 -0
- data/lib/specwrk/cli.rb +41 -3
- data/lib/specwrk/client.rb +44 -34
- data/lib/specwrk/hooks.rb +33 -0
- data/lib/specwrk/seed_loop.rb +2 -0
- data/lib/specwrk/version.rb +1 -1
- data/lib/specwrk/web/app.rb +3 -0
- data/lib/specwrk/web/endpoints/base.rb +22 -1
- data/lib/specwrk/web/endpoints/seed.rb +2 -0
- data/lib/specwrk/worker/executor.rb +4 -1
- data/lib/specwrk.rb +41 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec2663b93b4ed3469ecfaebea825d97a2bd2b6764ccae67bc316efd6de2f53a6
|
|
4
|
+
data.tar.gz: 23e5e30cb0e6d9644085b0597e36f3c7380baea67ee01aaad70af9341b347fd8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99066768c088081d7c3c69a52ce51898a4ea7cd533581fc70829f70edc25b423b583d9883dda079070a17c5ef5f135ca330afeb62a6027897650cfc1f2b82318
|
|
7
|
+
data.tar.gz: c3a606f594a5dc018556149b79846ec3ccee01b89ef016396c327e250aeba6d84cfcb7c1bdfd7b7a9c121f835f5c7818356be5c5cdbef826cc794506dd4bc55b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## Unreleased
|
|
4
|
-
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.
|
|
3
|
+
## Unreleased
|
|
4
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.21.0...main)
|
|
5
|
+
|
|
6
|
+
## v0.21.0 — 2026-09-13
|
|
7
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.20.2...v0.21.0)
|
|
8
|
+
- Add configurable lifecycle hooks — [#193](https://github.com/danielwestendorf/specwrk/pull/193) by [@danielwestendorf](https://github.com/danielwestendorf)
|
|
9
|
+
- Add opt-in gzip request compression and negotiated gzip responses — [#195](https://github.com/danielwestendorf/specwrk/pull/195) by [@danielwestendorf](https://github.com/danielwestendorf)
|
|
5
10
|
|
|
6
11
|
## v0.20.2 — 2026-08-27
|
|
7
12
|
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.20.1...v0.20.2)
|
data/README.md
CHANGED
|
@@ -53,6 +53,7 @@ Options:
|
|
|
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
55
|
--network-retries=VALUE # The number of times to retry in the event of a network failure. Overrides SPECWRK_NETWORK_RETRIES, default: "1"
|
|
56
|
+
--[no-]http-compression # Compress large JSON request bodies sent to the server. Overrides SPECWRK_HTTP_COMPRESSION, default: false
|
|
56
57
|
--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
|
|
57
58
|
--count=VALUE, -c VALUE # The number of worker processes you want to start, default: 1
|
|
58
59
|
--output=VALUE, -o VALUE # Directory where worker output is stored. Overrides SPECWRK_OUT, default: ".specwrk/"
|
|
@@ -115,6 +116,7 @@ Options:
|
|
|
115
116
|
--run=VALUE, -r VALUE # The run identifier for this job execution. Overrides SPECWRK_RUN, default: "main"
|
|
116
117
|
--timeout=VALUE, -t VALUE # The amount of time to wait for the server to respond. Overrides SPECWRK_TIMEOUT, default: "5"
|
|
117
118
|
--network-retries=VALUE # The number of times to retry in the event of a network failure. Overrides SPECWRK_NETWORK_RETRIES, default: "1"
|
|
119
|
+
--[no-]http-compression # Compress large JSON request bodies sent to the server. Overrides SPECWRK_HTTP_COMPRESSION, default: false
|
|
118
120
|
--max-retries=VALUE # Number of times an example will be re-run should it fail, default: 0
|
|
119
121
|
--target-bucket-timing-duration=VALUE # Target runtime duration per bucket in seconds, overriding the average timings calculation. Overrides SPECWRK_TARGET_BUCKET_TIMING_DURATION, default: "0"
|
|
120
122
|
--help, -h # Print this help
|
|
@@ -144,6 +146,7 @@ Options:
|
|
|
144
146
|
--run=VALUE, -r VALUE # The run identifier for this job execution. Overrides SPECWRK_RUN, default: "main"
|
|
145
147
|
--timeout=VALUE, -t VALUE # The amount of time to wait for the server to respond. Overrides SPECWRK_TIMEOUT, default: "5"
|
|
146
148
|
--network-retries=VALUE # The number of times to retry in the event of a network failure. Overrides SPECWRK_NETWORK_RETRIES, default: "1"
|
|
149
|
+
--[no-]http-compression # Compress large JSON request bodies sent to the server. Overrides SPECWRK_HTTP_COMPRESSION, default: false
|
|
147
150
|
--help, -h # Print this help
|
|
148
151
|
```
|
|
149
152
|
|
|
@@ -164,9 +167,31 @@ Description:
|
|
|
164
167
|
Options:
|
|
165
168
|
--watchfile=VALUE # Path to watchfile configuration, default: "Specwrk.watchfile.rb"
|
|
166
169
|
--count=VALUE, -c VALUE # The number of worker processes you want to start, default: 1
|
|
170
|
+
--[no-]http-compression # Compress large JSON request bodies sent to the server. Overrides SPECWRK_HTTP_COMPRESSION, default: false
|
|
167
171
|
--help, -h # Print this help
|
|
168
172
|
```
|
|
169
173
|
|
|
174
|
+
## Lifecycle hooks
|
|
175
|
+
|
|
176
|
+
Lifecycle hooks run synchronously in the process performing the corresponding work. Add hook registrations to `Specwrk.hooks.rb` in the directory where you run `specwrk`; the file is loaded automatically by the `seed`, `work`, `serve`, `start`, and `watch` commands. Use `--hooks=path/to/hooks.rb` to load a different file.
|
|
177
|
+
|
|
178
|
+
The hooks file is ordinary Ruby and is loaded into the parent, server, seed, and worker processes as applicable. Hooks remain process-local and are not serialized between processes.
|
|
179
|
+
|
|
180
|
+
| Hook Method | Description | Arguments |
|
|
181
|
+
| --- | --- | --- |
|
|
182
|
+
| `Specwrk.before_seed(&blk)` | Runs before seeding examples to the server | none |
|
|
183
|
+
| `Specwrk.after_seed(&blk)` | Runs after seeding examples to the server | `examples` RSpec examples |
|
|
184
|
+
| `Specwrk.before_server_seed(&blk)` | Runs in the server before seeding examples | none |
|
|
185
|
+
| `Specwrk.after_server_seed(&blk)` | Runs in the server after seeding examples | `examples` RSpec examples |
|
|
186
|
+
| `Specwrk.before_worker_fork(&blk)` | Runs in the parent process before spawning each worker | none |
|
|
187
|
+
| `Specwrk.after_worker_fork(&blk)` | Runs in each worker process immediately after it is spawned | none |
|
|
188
|
+
| `Specwrk.before_worker_examples_execute(&blk)` | Runs in a worker before an examples group is passed to the RSpec runner | `examples` RSpec examples in the group |
|
|
189
|
+
| `Specwrk.after_worker_examples_execute(&blk)` | Runs in a worker after an examples group is executed by the RSpec runner | `examples` RSpec examples in the group |
|
|
190
|
+
| `Specwrk.before_worker_exit(&blk)` | Runs in each worker after work completes and before the process exits | `status` Worker exit status |
|
|
191
|
+
| `Specwrk.after_all_workers_exit(&blk)` | Runs in the parent process after all workers exit | `status` Overall worker exit status |
|
|
192
|
+
|
|
193
|
+
Response compression is negotiated automatically by HTTP clients and the queue server. Request compression is disabled by default; enable it with `--http-compression` or `SPECWRK_HTTP_COMPRESSION=1` on client commands to gzip seed data and worker results that are at least 1 KiB. Set `SPECWRK_HTTP_COMPRESSION_MINIMUM_SIZE` to change this byte threshold. Upgrade the queue server before enabling request compression because it does not fall back when a server cannot decode gzip.
|
|
194
|
+
|
|
170
195
|
## Configuring your test environment
|
|
171
196
|
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
|
|
172
197
|
adjustments to avoid conflicting port usage or database/state mutations.
|
data/Specwrk.hooks.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# specwrk_hook_log = lambda do |event, **details|
|
|
2
|
+
# specwrk_env = ENV.each_with_object({}) do |(name, value), values|
|
|
3
|
+
# next unless name.start_with?("SPECWRK_")
|
|
4
|
+
#
|
|
5
|
+
# values[name] = if name.match?(/KEY|TOKEN|SECRET|PASSWORD/)
|
|
6
|
+
# "[REDACTED]"
|
|
7
|
+
# else
|
|
8
|
+
# value
|
|
9
|
+
# end
|
|
10
|
+
# end.sort.to_h
|
|
11
|
+
#
|
|
12
|
+
# context = {
|
|
13
|
+
# event: event,
|
|
14
|
+
# pid: Process.pid,
|
|
15
|
+
# ppid: Process.ppid,
|
|
16
|
+
# specwrk_env: specwrk_env
|
|
17
|
+
# }.merge(details)
|
|
18
|
+
#
|
|
19
|
+
# puts context.map { |name, value| "#{name}=#{value.inspect}" }.join(" ")
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# Specwrk.before_seed do
|
|
23
|
+
# specwrk_hook_log.call("before_seed")
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# Specwrk.after_seed do |examples|
|
|
27
|
+
# specwrk_hook_log.call("after_seed", example_count: examples.length)
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# Specwrk.before_server_seed do
|
|
31
|
+
# specwrk_hook_log.call("before_server_seed")
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# Specwrk.after_server_seed do |examples|
|
|
35
|
+
# specwrk_hook_log.call("after_server_seed", example_count: examples.length)
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# Specwrk.before_worker_fork do
|
|
39
|
+
# specwrk_hook_log.call("before_worker_fork")
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# Specwrk.after_worker_fork do
|
|
43
|
+
# specwrk_hook_log.call("after_worker_fork")
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# Specwrk.before_worker_examples_execute do |examples|
|
|
47
|
+
# specwrk_hook_log.call(
|
|
48
|
+
# "before_worker_examples_execute",
|
|
49
|
+
# example_count: examples.length,
|
|
50
|
+
# example_ids: examples.map { |example| example[:id] }
|
|
51
|
+
# )
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# Specwrk.after_worker_examples_execute do |examples|
|
|
55
|
+
# specwrk_hook_log.call(
|
|
56
|
+
# "after_worker_examples_execute",
|
|
57
|
+
# example_count: examples.length,
|
|
58
|
+
# example_ids: examples.map { |example| example[:id] }
|
|
59
|
+
# )
|
|
60
|
+
# end
|
|
61
|
+
#
|
|
62
|
+
# Specwrk.before_worker_exit do |status|
|
|
63
|
+
# specwrk_hook_log.call("before_worker_exit", status: status)
|
|
64
|
+
# end
|
|
65
|
+
#
|
|
66
|
+
# Specwrk.after_all_workers_exit do |status|
|
|
67
|
+
# specwrk_hook_log.call("after_all_workers_exit", status: status)
|
|
68
|
+
# end
|
data/lib/specwrk/cli.rb
CHANGED
|
@@ -14,8 +14,26 @@ module Specwrk
|
|
|
14
14
|
module CLI
|
|
15
15
|
extend Dry::CLI::Registry
|
|
16
16
|
|
|
17
|
+
module HooksConfigurable
|
|
18
|
+
extend Hookable
|
|
19
|
+
|
|
20
|
+
on_included do |base|
|
|
21
|
+
base.unique_option :hooks, type: :string, default: "Specwrk.hooks.rb", desc: "Path to lifecycle hooks configuration"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
on_setup do |hooks: "Specwrk.hooks.rb", **|
|
|
25
|
+
hooks_file = Pathname.new(hooks).expand_path(Dir.pwd).to_s
|
|
26
|
+
ENV["SPECWRK_HOOKS_FILE"] = hooks_file
|
|
27
|
+
Hooks.load_file(hooks_file)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
17
31
|
module WorkerProcesses
|
|
18
32
|
WORKER_INIT_SCRIPT = <<~RUBY
|
|
33
|
+
require "specwrk"
|
|
34
|
+
Specwrk::Hooks.load_file(ENV["SPECWRK_HOOKS_FILE"])
|
|
35
|
+
Specwrk::Hooks.run(:after_worker_fork)
|
|
36
|
+
|
|
19
37
|
writer = IO.for_fd(Integer(ENV.fetch("SPECWRK_FINAL_FD")))
|
|
20
38
|
$final_output = writer # standard:disable Style/GlobalVars
|
|
21
39
|
$final_output.sync = true # standard:disable Style/GlobalVars
|
|
@@ -31,6 +49,7 @@ module Specwrk
|
|
|
31
49
|
end
|
|
32
50
|
|
|
33
51
|
status = Specwrk::Worker.run!
|
|
52
|
+
Specwrk::Hooks.run(:before_worker_exit, status)
|
|
34
53
|
$final_output.close # standard:disable Style/GlobalVars
|
|
35
54
|
exit(status)
|
|
36
55
|
RUBY
|
|
@@ -45,6 +64,7 @@ module Specwrk
|
|
|
45
64
|
"SPECWRK_FINAL_FD" => writer.fileno.to_s
|
|
46
65
|
)
|
|
47
66
|
|
|
67
|
+
Hooks.run(:before_worker_fork)
|
|
48
68
|
Process.spawn(
|
|
49
69
|
env, RbConfig.ruby, "-e", WORKER_INIT_SCRIPT,
|
|
50
70
|
writer.fileno => writer,
|
|
@@ -95,14 +115,16 @@ module Specwrk
|
|
|
95
115
|
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"
|
|
96
116
|
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"
|
|
97
117
|
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"
|
|
118
|
+
base.unique_option :http_compression, type: :boolean, default: ENV.fetch("SPECWRK_HTTP_COMPRESSION", "0") == "1", desc: "Compress large JSON request bodies sent to the server. Overrides SPECWRK_HTTP_COMPRESSION"
|
|
98
119
|
end
|
|
99
120
|
|
|
100
|
-
on_setup do |uri:, key:, run:, timeout:, network_retries:, **|
|
|
121
|
+
on_setup do |uri:, key:, run:, timeout:, network_retries:, http_compression: false, **|
|
|
101
122
|
ENV["SPECWRK_SRV_URI"] = uri
|
|
102
123
|
ENV["SPECWRK_SRV_KEY"] = key
|
|
103
124
|
ENV["SPECWRK_RUN"] = run
|
|
104
125
|
ENV["SPECWRK_TIMEOUT"] = timeout
|
|
105
126
|
ENV["SPECWRK_NETWORK_RETRIES"] = network_retries
|
|
127
|
+
ENV["SPECWRK_HTTP_COMPRESSION"] = http_compression ? "1" : "0"
|
|
106
128
|
end
|
|
107
129
|
end
|
|
108
130
|
|
|
@@ -164,6 +186,7 @@ module Specwrk
|
|
|
164
186
|
|
|
165
187
|
class Seed < Dry::CLI::Command
|
|
166
188
|
include Clientable
|
|
189
|
+
include HooksConfigurable
|
|
167
190
|
|
|
168
191
|
desc "Seed the server with a list of specs for the run"
|
|
169
192
|
option :max_retries, default: 0, desc: "Number of times an example will be re-run should it fail"
|
|
@@ -199,6 +222,7 @@ module Specwrk
|
|
|
199
222
|
class Work < Dry::CLI::Command
|
|
200
223
|
include Workable
|
|
201
224
|
include Clientable
|
|
225
|
+
include HooksConfigurable
|
|
202
226
|
|
|
203
227
|
desc "Start one or more worker processes"
|
|
204
228
|
|
|
@@ -216,7 +240,10 @@ module Specwrk
|
|
|
216
240
|
end
|
|
217
241
|
|
|
218
242
|
def wait_for_workers_exit
|
|
219
|
-
|
|
243
|
+
Specwrk.wait_for_pids_exit(@worker_pids).tap do |exited_pids|
|
|
244
|
+
@exited_pids = exited_pids
|
|
245
|
+
Hooks.run(:after_all_workers_exit, status)
|
|
246
|
+
end
|
|
220
247
|
end
|
|
221
248
|
|
|
222
249
|
def status
|
|
@@ -226,6 +253,7 @@ module Specwrk
|
|
|
226
253
|
|
|
227
254
|
class Serve < Dry::CLI::Command
|
|
228
255
|
include Servable
|
|
256
|
+
include HooksConfigurable
|
|
229
257
|
|
|
230
258
|
desc "Start a queue server"
|
|
231
259
|
option :single_run, type: :boolean, default: false, desc: "Act on shutdown requests from clients"
|
|
@@ -246,9 +274,12 @@ module Specwrk
|
|
|
246
274
|
include Clientable
|
|
247
275
|
include Workable
|
|
248
276
|
include Servable
|
|
277
|
+
include HooksConfigurable
|
|
249
278
|
|
|
250
279
|
SEED_INIT_SCRIPT = <<~'RUBY'
|
|
251
280
|
require "json"
|
|
281
|
+
require "specwrk"
|
|
282
|
+
Specwrk::Hooks.load_file(ENV["SPECWRK_HOOKS_FILE"])
|
|
252
283
|
require "specwrk/list_examples"
|
|
253
284
|
require "specwrk/client"
|
|
254
285
|
|
|
@@ -347,8 +378,11 @@ module Specwrk
|
|
|
347
378
|
class Watch < Dry::CLI::Command
|
|
348
379
|
include WorkerProcesses
|
|
349
380
|
include PortDiscoverable
|
|
381
|
+
include HooksConfigurable
|
|
350
382
|
|
|
351
383
|
SEED_LOOP_INIT_SCRIPT = <<~RUBY
|
|
384
|
+
require "specwrk"
|
|
385
|
+
Specwrk::Hooks.load_file(ENV["SPECWRK_HOOKS_FILE"])
|
|
352
386
|
require "specwrk/ipc"
|
|
353
387
|
require "specwrk/seed_loop"
|
|
354
388
|
|
|
@@ -362,8 +396,9 @@ module Specwrk
|
|
|
362
396
|
desc "Start a server and workers, watch for file changes in the current directory, and execute specs"
|
|
363
397
|
option :watchfile, type: :string, default: "Specwrk.watchfile.rb", desc: "Path to watchfile configuration"
|
|
364
398
|
option :count, type: :integer, default: 1, aliases: ["-c"], desc: "The number of worker processes you want to start"
|
|
399
|
+
option :http_compression, type: :boolean, default: ENV.fetch("SPECWRK_HTTP_COMPRESSION", "0") == "1", desc: "Compress large JSON request bodies sent to the server. Overrides SPECWRK_HTTP_COMPRESSION"
|
|
365
400
|
|
|
366
|
-
def call(count:, watchfile:, **args)
|
|
401
|
+
def call(count:, watchfile:, http_compression: false, **args)
|
|
367
402
|
$stdout.sync = true
|
|
368
403
|
|
|
369
404
|
# nil this env var if it exists to prevent never-ending workers
|
|
@@ -377,6 +412,9 @@ module Specwrk
|
|
|
377
412
|
ENV["SPECWRK_MAX_BUCKET_SIZE"] = "1"
|
|
378
413
|
ENV["SPECWRK_COUNT"] = count.to_s
|
|
379
414
|
ENV["SPECWRK_RUN"] = "watch"
|
|
415
|
+
ENV["SPECWRK_HTTP_COMPRESSION"] = http_compression ? "1" : "0"
|
|
416
|
+
|
|
417
|
+
self.class.setup(**args)
|
|
380
418
|
|
|
381
419
|
web_pid
|
|
382
420
|
|
data/lib/specwrk/client.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "uri"
|
|
4
4
|
require "net/http"
|
|
5
5
|
require "json"
|
|
6
|
+
require "zlib"
|
|
6
7
|
|
|
7
8
|
require "specwrk"
|
|
8
9
|
|
|
@@ -13,6 +14,7 @@ Specwrk.net_http = Net::HTTP
|
|
|
13
14
|
module Specwrk
|
|
14
15
|
class Client
|
|
15
16
|
LockedResponseError = Class.new(StandardError)
|
|
17
|
+
HTTP_COMPRESSION_MINIMUM_SIZE = 1024
|
|
16
18
|
|
|
17
19
|
def self.connect?
|
|
18
20
|
http = build_http
|
|
@@ -132,56 +134,64 @@ module Specwrk
|
|
|
132
134
|
private
|
|
133
135
|
|
|
134
136
|
def get(path, headers: default_headers, body: nil)
|
|
135
|
-
|
|
136
|
-
request.body = body if body
|
|
137
|
-
|
|
138
|
-
make_request(request)
|
|
137
|
+
make_request Specwrk.net_http::Get, path, headers: headers, body: body
|
|
139
138
|
end
|
|
140
139
|
|
|
141
140
|
def post(path, headers: default_headers, body: nil)
|
|
142
|
-
|
|
143
|
-
request.body = body if body
|
|
144
|
-
|
|
145
|
-
make_request(request)
|
|
141
|
+
make_request Specwrk.net_http::Post, path, headers: headers, body: body
|
|
146
142
|
end
|
|
147
143
|
|
|
148
144
|
def put(path, headers: default_headers, body: nil)
|
|
149
|
-
|
|
150
|
-
request.body = body if body
|
|
151
|
-
|
|
152
|
-
make_request(request)
|
|
145
|
+
make_request Specwrk.net_http::Put, path, headers: headers, body: body
|
|
153
146
|
end
|
|
154
147
|
|
|
155
148
|
def delete(path, headers: default_headers, body: nil)
|
|
156
|
-
|
|
157
|
-
request.body = body if body
|
|
158
|
-
|
|
159
|
-
make_request(request)
|
|
149
|
+
make_request Specwrk.net_http::Delete, path, headers: headers, body: body
|
|
160
150
|
end
|
|
161
151
|
|
|
162
|
-
def make_request(
|
|
163
|
-
|
|
164
|
-
@last_request_at = Time.now
|
|
165
|
-
@http.request(request).tap do |response|
|
|
166
|
-
@retry_count = 0
|
|
167
|
-
raise LockedResponseError if response.code == "423"
|
|
152
|
+
def make_request(request_class, path, headers:, body:)
|
|
153
|
+
headers = headers.dup
|
|
168
154
|
|
|
169
|
-
|
|
170
|
-
|
|
155
|
+
if http_compression? && headers["Content-Type"] == "application/json" && body&.bytesize.to_i >= http_compression_minimum_size
|
|
156
|
+
body = Zlib.gzip(body)
|
|
157
|
+
headers["Content-Encoding"] = "gzip"
|
|
171
158
|
end
|
|
172
|
-
rescue LockedResponseError
|
|
173
|
-
sleep rand
|
|
174
|
-
retry
|
|
175
|
-
rescue Net::ReadTimeout, Net::WriteTimeout => e
|
|
176
|
-
@retry_count ||= 0
|
|
177
159
|
|
|
178
|
-
|
|
179
|
-
|
|
160
|
+
request = request_class.new(path, headers)
|
|
161
|
+
request.body = body if body
|
|
180
162
|
|
|
181
|
-
|
|
182
|
-
|
|
163
|
+
begin
|
|
164
|
+
@mutex.synchronize do
|
|
165
|
+
@last_request_at = Time.now
|
|
166
|
+
@http.request(request).tap do |response|
|
|
167
|
+
@retry_count = 0
|
|
168
|
+
raise LockedResponseError if response.code == "423"
|
|
169
|
+
|
|
170
|
+
@worker_status = response["x-specwrk-status"].to_i if response["x-specwrk-status"]
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
rescue LockedResponseError
|
|
174
|
+
sleep rand
|
|
175
|
+
retry
|
|
176
|
+
rescue Net::ReadTimeout, Net::WriteTimeout => e
|
|
177
|
+
@retry_count ||= 0
|
|
178
|
+
|
|
179
|
+
raise e if @retry_count == ENV["SPECWRK_NETWORK_RETRIES"].to_i
|
|
180
|
+
@retry_count += 1
|
|
181
|
+
|
|
182
|
+
warn e
|
|
183
|
+
sleep @retry_count
|
|
184
|
+
|
|
185
|
+
retry
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def http_compression?
|
|
190
|
+
ENV.fetch("SPECWRK_HTTP_COMPRESSION", "0") == "1"
|
|
191
|
+
end
|
|
183
192
|
|
|
184
|
-
|
|
193
|
+
def http_compression_minimum_size
|
|
194
|
+
ENV.fetch("SPECWRK_HTTP_COMPRESSION_MINIMUM_SIZE", HTTP_COMPRESSION_MINIMUM_SIZE).to_i
|
|
185
195
|
end
|
|
186
196
|
|
|
187
197
|
def default_headers
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Specwrk
|
|
4
|
+
module Hooks
|
|
5
|
+
class << self
|
|
6
|
+
def register(name, &block)
|
|
7
|
+
raise ArgumentError, "a block is required" unless block
|
|
8
|
+
|
|
9
|
+
hooks[name] << block
|
|
10
|
+
block
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(name, *objects)
|
|
14
|
+
hooks.fetch(name, []).dup.each { |hook| hook.call(*objects) }
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def load_file(file)
|
|
19
|
+
Kernel.load(file) if file && File.file?(file)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset!
|
|
23
|
+
@hooks = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def hooks
|
|
29
|
+
@hooks ||= Hash.new { |registry, name| registry[name] = [] }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/specwrk/seed_loop.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "specwrk/client"
|
|
|
6
6
|
module Specwrk
|
|
7
7
|
class SeedLoop
|
|
8
8
|
def self.loop!(ipc)
|
|
9
|
+
Hooks.run(:before_seed)
|
|
9
10
|
Client.wait_for_server!
|
|
10
11
|
|
|
11
12
|
loop do
|
|
@@ -18,6 +19,7 @@ module Specwrk
|
|
|
18
19
|
|
|
19
20
|
client = Client.new
|
|
20
21
|
client.seed(examples, 0)
|
|
22
|
+
Hooks.run(:after_seed, examples)
|
|
21
23
|
client.close
|
|
22
24
|
|
|
23
25
|
ipc.write examples.length
|
data/lib/specwrk/version.rb
CHANGED
data/lib/specwrk/web/app.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "fileutils"
|
|
|
5
5
|
|
|
6
6
|
require "webrick"
|
|
7
7
|
require "rack"
|
|
8
|
+
require "rack/deflater"
|
|
8
9
|
|
|
9
10
|
# rack v3 or v2
|
|
10
11
|
begin
|
|
@@ -67,6 +68,8 @@ module Specwrk
|
|
|
67
68
|
|
|
68
69
|
def rackup
|
|
69
70
|
Rack::Builder.new do
|
|
71
|
+
use Rack::Deflater, include: ["application/json"]
|
|
72
|
+
|
|
70
73
|
if ENV["SPECWRK_SRV_VERBOSE"]
|
|
71
74
|
use Rack::Runtime
|
|
72
75
|
use Specwrk::Web::Logger, $stdout, %w[/health]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "zlib"
|
|
4
5
|
|
|
5
6
|
require "specwrk/store"
|
|
6
7
|
|
|
@@ -9,6 +10,8 @@ module Specwrk
|
|
|
9
10
|
module Endpoints
|
|
10
11
|
class Base
|
|
11
12
|
MUTEX = Mutex.new
|
|
13
|
+
InvalidGzipError = Class.new(StandardError)
|
|
14
|
+
UnsupportedContentEncodingError = Class.new(StandardError)
|
|
12
15
|
|
|
13
16
|
attr_reader :started_at
|
|
14
17
|
|
|
@@ -32,6 +35,10 @@ module Specwrk
|
|
|
32
35
|
end
|
|
33
36
|
rescue Store::LockUnavailableError
|
|
34
37
|
locked
|
|
38
|
+
rescue InvalidGzipError
|
|
39
|
+
[400, {"content-type" => "text/plain"}, ["Invalid gzip request body"]]
|
|
40
|
+
rescue UnsupportedContentEncodingError
|
|
41
|
+
[415, {"content-type" => "text/plain"}, ["Unsupported content encoding"]]
|
|
35
42
|
end
|
|
36
43
|
|
|
37
44
|
def with_response
|
|
@@ -75,7 +82,21 @@ module Specwrk
|
|
|
75
82
|
end
|
|
76
83
|
|
|
77
84
|
def body
|
|
78
|
-
@body ||=
|
|
85
|
+
@body ||= begin
|
|
86
|
+
body = request.body.read
|
|
87
|
+
encoding = request.get_header("HTTP_CONTENT_ENCODING")&.strip&.downcase
|
|
88
|
+
|
|
89
|
+
case encoding
|
|
90
|
+
when nil, "", "identity"
|
|
91
|
+
body
|
|
92
|
+
when "gzip"
|
|
93
|
+
Zlib.gunzip(body)
|
|
94
|
+
else
|
|
95
|
+
raise UnsupportedContentEncodingError
|
|
96
|
+
end
|
|
97
|
+
rescue Zlib::Error
|
|
98
|
+
raise InvalidGzipError
|
|
99
|
+
end
|
|
79
100
|
end
|
|
80
101
|
|
|
81
102
|
def pending
|
|
@@ -7,6 +7,7 @@ module Specwrk
|
|
|
7
7
|
module Endpoints
|
|
8
8
|
class Seed < Base
|
|
9
9
|
def with_response
|
|
10
|
+
Hooks.run(:before_server_seed)
|
|
10
11
|
examples_with_run_times
|
|
11
12
|
|
|
12
13
|
pending.clear
|
|
@@ -25,6 +26,7 @@ module Specwrk
|
|
|
25
26
|
pending.merge!(examples_with_run_times)
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
Hooks.run(:after_server_seed, payload[:examples])
|
|
28
30
|
ok
|
|
29
31
|
end
|
|
30
32
|
|
|
@@ -25,7 +25,10 @@ module Specwrk
|
|
|
25
25
|
example_ids = examples.map { |example| example[:id] }
|
|
26
26
|
|
|
27
27
|
options = RSpec::Core::ConfigurationOptions.new ["--format", "Specwrk::Worker::NullFormatter"] + example_ids
|
|
28
|
-
|
|
28
|
+
Hooks.run(:before_worker_examples_execute, examples)
|
|
29
|
+
RSpec::Core::Runner.new(options).run($stderr, $stdout).tap do
|
|
30
|
+
Hooks.run(:after_worker_examples_execute, examples)
|
|
31
|
+
end
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
# https://github.com/skroutz/rspecq/blob/341383ce3ca25f42fad5483cbb6a00ba1c405570/lib/rspecq/worker.rb#L208-L224
|
data/lib/specwrk.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "specwrk/version"
|
|
4
|
+
require "specwrk/hooks"
|
|
4
5
|
|
|
5
6
|
module Specwrk
|
|
6
7
|
Error = Class.new(StandardError)
|
|
@@ -19,6 +20,46 @@ module Specwrk
|
|
|
19
20
|
attr_accessor :force_quit, :net_http
|
|
20
21
|
attr_reader :starting_pid
|
|
21
22
|
|
|
23
|
+
def before_seed(&block)
|
|
24
|
+
Hooks.register(:before_seed, &block)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def after_seed(&block)
|
|
28
|
+
Hooks.register(:after_seed, &block)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def before_server_seed(&block)
|
|
32
|
+
Hooks.register(:before_server_seed, &block)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def after_server_seed(&block)
|
|
36
|
+
Hooks.register(:after_server_seed, &block)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def before_worker_fork(&block)
|
|
40
|
+
Hooks.register(:before_worker_fork, &block)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def after_worker_fork(&block)
|
|
44
|
+
Hooks.register(:after_worker_fork, &block)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def before_worker_examples_execute(&block)
|
|
48
|
+
Hooks.register(:before_worker_examples_execute, &block)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def after_worker_examples_execute(&block)
|
|
52
|
+
Hooks.register(:after_worker_examples_execute, &block)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def before_worker_exit(&block)
|
|
56
|
+
Hooks.register(:before_worker_exit, &block)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def after_all_workers_exit(&block)
|
|
60
|
+
Hooks.register(:after_all_workers_exit, &block)
|
|
61
|
+
end
|
|
62
|
+
|
|
22
63
|
def wait_for_pids_exit(pids)
|
|
23
64
|
exited_pids = {}
|
|
24
65
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: specwrk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Westendorf
|
|
@@ -206,6 +206,7 @@ files:
|
|
|
206
206
|
- README.md
|
|
207
207
|
- RELEASING.md
|
|
208
208
|
- Rakefile
|
|
209
|
+
- Specwrk.hooks.rb
|
|
209
210
|
- Specwrk.watchfile.rb
|
|
210
211
|
- config.ru
|
|
211
212
|
- docker/Dockerfile.server
|
|
@@ -220,6 +221,7 @@ files:
|
|
|
220
221
|
- lib/specwrk/cli_reporter.rb
|
|
221
222
|
- lib/specwrk/client.rb
|
|
222
223
|
- lib/specwrk/hookable.rb
|
|
224
|
+
- lib/specwrk/hooks.rb
|
|
223
225
|
- lib/specwrk/ipc.rb
|
|
224
226
|
- lib/specwrk/list_examples.rb
|
|
225
227
|
- lib/specwrk/seed_loop.rb
|