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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94a0d37170078c212f345df5e4a1b014b5322856ed8f36305014ce55fd690aba
4
- data.tar.gz: 2fa1e62b735f22f1342ae7e56634c257db48e9d6f72842328568293d59ecb764
3
+ metadata.gz: ec2663b93b4ed3469ecfaebea825d97a2bd2b6764ccae67bc316efd6de2f53a6
4
+ data.tar.gz: 23e5e30cb0e6d9644085b0597e36f3c7380baea67ee01aaad70af9341b347fd8
5
5
  SHA512:
6
- metadata.gz: 298ce73183966b8e89ce38e7cee4ee4da184ee0bfccc114574a64ebf1fc55fe76bde02da0189060946db29913c9351e9fd84669ef23967433adb4f3b668a9fce
7
- data.tar.gz: 74263602ada554af6970d5f58dc872e744c7517e9fd97141a8eb4ebec725f888440876dee810498be6a9230288325e85f38d8cf4db082a08d6fb94f0b1152f45
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.20.2...main)
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
- @exited_pids = Specwrk.wait_for_pids_exit(@worker_pids)
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
 
@@ -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
- request = Specwrk.net_http::Get.new(path, headers)
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
- request = Specwrk.net_http::Post.new(path, headers)
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
- request = Specwrk.net_http::Put.new(path, headers)
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
- request = Specwrk.net_http::Delete.new(path, headers)
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(request)
163
- @mutex.synchronize do
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
- @worker_status = response["x-specwrk-status"].to_i if response["x-specwrk-status"]
170
- end
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
- raise e if @retry_count == ENV["SPECWRK_NETWORK_RETRIES"].to_i
179
- @retry_count += 1
160
+ request = request_class.new(path, headers)
161
+ request.body = body if body
180
162
 
181
- warn e
182
- sleep @retry_count
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
- retry
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Specwrk
4
- VERSION = "0.20.2"
4
+ VERSION = "0.21.0"
5
5
  end
@@ -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 ||= request.body.read
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
- RSpec::Core::Runner.new(options).run($stderr, $stdout)
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.20.2
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