specwrk 0.15.2 → 0.15.4
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 +114 -0
- data/README.md +17 -0
- data/lib/specwrk/cli.rb +18 -1
- data/lib/specwrk/cli_reporter.rb +0 -2
- data/lib/specwrk/list_examples.rb +2 -0
- data/lib/specwrk/version.rb +1 -1
- data/lib/specwrk/web/app.rb +7 -1
- data/lib/specwrk/web/endpoints/base.rb +130 -0
- data/lib/specwrk/web/endpoints/complete_and_pop.rb +85 -0
- data/lib/specwrk/web/endpoints/health.rb +15 -0
- data/lib/specwrk/web/endpoints/heartbeat.rb +15 -0
- data/lib/specwrk/web/endpoints/pop.rb +15 -0
- data/lib/specwrk/web/endpoints/popable.rb +50 -0
- data/lib/specwrk/web/endpoints/report.rb +19 -0
- data/lib/specwrk/web/endpoints/seed.rb +78 -0
- data/lib/specwrk/web/endpoints/shutdown.rb +25 -0
- metadata +11 -3
- data/lib/specwrk/web/endpoints.rb +0 -360
- /data/{LICENSE.txt → LICENSE} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e7c81e77211a27632313439e1fb98376798e5c606151fa0e110ae2f64823ccda
|
4
|
+
data.tar.gz: 9fdb03226d2d3a57e79ee3701ede4e36bf12a43369ecb298dca40999813d4606
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3822e6d7f2de884e56ce1374abf05809f7c7bc24c8c06fafc46e69a1b5a2ae796abedc7fc0b0be2ab6b468f36cf9370469ac1e5c0e9696b96496f21a5bf391e
|
7
|
+
data.tar.gz: ef4829b50269101858c6b95dfd51ed4671eda52319c552e624f47a8b7d028c8f4347d38fd8c8acfcc0853625dff6f3838d5ae2330b3db0a02a0d07d2ee35dedd
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,119 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
# Changelog
|
4
|
+
|
5
|
+
## v0.15.2 — 2025-08-15
|
6
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.15.1...v0.15.2)
|
7
|
+
- Fix bug where ruby objects were being written to ndjson files isntead of json — [#119](https://github.com/danielwestendorf/specwrk/pull/119) by @danielwestendorf
|
8
|
+
|
9
|
+
## v0.15.1 — 2025-08-14
|
10
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.15.0...v0.15.1)
|
11
|
+
- Add `watch` command to split spec files across processes as they change — [#117](https://github.com/danielwestendorf/specwrk/pull/117) by @danielwestendorf
|
12
|
+
- Readme formatting tweaks — @danielwestendorf
|
13
|
+
|
14
|
+
## v0.15.0 — 2025-08-08
|
15
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.14.1...v0.15.0)
|
16
|
+
- When output path is specified, write an `.ndjson` file per worker of all examples executed — [#113](https://github.com/danielwestendorf/specwrk/pull/113) (fixes [#10](https://github.com/danielwestendorf/specwrk/issues/10)) by @danielwestendorf
|
17
|
+
- Print re-run commands for failures — [#114](https://github.com/danielwestendorf/specwrk/pull/114) (fixes [#7](https://github.com/danielwestendorf/specwrk/issues/7)) by @danielwestendorf
|
18
|
+
- Show count of examples that did not execute; make runs resumable by only clearing stores on seed — [#115](https://github.com/danielwestendorf/specwrk/pull/115) by @danielwestendorf
|
19
|
+
- Report flake counts and provide flake re-run commands — [#116](https://github.com/danielwestendorf/specwrk/pull/116) (fixes [#112](https://github.com/danielwestendorf/specwrk/issues/112)) by @danielwestendorf
|
20
|
+
|
21
|
+
## v0.14.1 — 2025-08-07
|
22
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.14.0...v0.14.1)
|
23
|
+
- Fix CLI typo — @danielwestendorf
|
24
|
+
|
25
|
+
## v0.14.0 — 2025-08-07
|
26
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.13.1...v0.14.0)
|
27
|
+
- Add support for example retries (`--max-retries` for `start` and `seed`) — [#111](https://github.com/danielwestendorf/specwrk/pull/111) (addresses [#96](https://github.com/danielwestendorf/specwrk/issues/96)) by @danielwestendorf
|
28
|
+
|
29
|
+
## v0.13.1 — 2025-08-07
|
30
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.13.0...v0.13.1)
|
31
|
+
- Require `securerandom` in the FileAdapter to ensure availability — @danielwestendorf
|
32
|
+
|
33
|
+
## v0.13.0 — 2025-08-07
|
34
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.12.0...v0.13.0)
|
35
|
+
- Avoid multi-process tempfile conflicts by adding a UUID to tempfile names — [#108](https://github.com/danielwestendorf/specwrk/pull/108) (fixes [#107](https://github.com/danielwestendorf/specwrk/issues/107)) by @danielwestendorf
|
36
|
+
- Remove deprecated `complete` endpoint — [#109](https://github.com/danielwestendorf/specwrk/pull/109) (fixes [#95](https://github.com/danielwestendorf/specwrk/issues/95)) by @danielwestendorf
|
37
|
+
- Track worker success metrics (succeeded/pending/failed) and use them to instruct worker exit behavior — [#110](https://github.com/danielwestendorf/specwrk/pull/110) (fixes [#97](https://github.com/danielwestendorf/specwrk/issues/97)) by @danielwestendorf
|
38
|
+
|
39
|
+
## v0.12.0 — 2025-08-06
|
40
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.11.1...v0.12.0)
|
41
|
+
- Don’t return a body for `HEAD` requests in auth middleware — refs [#102](https://github.com/danielwestendorf/specwrk/issues/102) — @danielwestendorf
|
42
|
+
- Filter `specwrk` from backtraces — @danielwestendorf
|
43
|
+
- Move store order tracking to the only store class that needs it (performance improvement) — [#105](https://github.com/danielwestendorf/specwrk/pull/105) by @danielwestendorf
|
44
|
+
- Humanize seconds output — [#106](https://github.com/danielwestendorf/specwrk/pull/106) (fixes [#11](https://github.com/danielwestendorf/specwrk/issues/11)) by @danielwestendorf
|
45
|
+
|
46
|
+
## v0.11.1 — 2025-08-05
|
47
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.11.0...v0.11.1)
|
48
|
+
- Avoid doing work in `before_lock` for `complete` and `pop` endpoints — [#104](https://github.com/danielwestendorf/specwrk/pull/104) (fixes [#103](https://github.com/danielwestendorf/specwrk/issues/103)) by @danielwestendorf
|
49
|
+
|
50
|
+
## v0.10.2 — 2025-08-01
|
51
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.10.1...v0.10.2)
|
52
|
+
- Add `complete_and_pop` endpoint to reduce HTTP requests ~50% — [#88](https://github.com/danielwestendorf/specwrk/pull/88) by @danielwestendorf
|
53
|
+
- File-per-example file store — [#87](https://github.com/danielwestendorf/specwrk/pull/87) by @danielwestendorf
|
54
|
+
- Remove thread safety (follow-up to thread pool changes) — [#89](https://github.com/danielwestendorf/specwrk/pull/89) by @danielwestendorf
|
55
|
+
- Fix thread pool start — [#90](https://github.com/danielwestendorf/specwrk/pull/90) by @danielwestendorf
|
56
|
+
|
57
|
+
## v0.10.1 — 2025-08-01
|
58
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.10.0...v0.10.1)
|
59
|
+
- Internal cleanups and versioning adjustments for the 0.10.x line
|
60
|
+
|
61
|
+
## v0.10.0 — 2025-08-01
|
62
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.9.1...v0.10.0)
|
63
|
+
- Prep for 0.10 baseline (no PRs explicitly tied beyond those in 0.10.1/0.10.2)
|
64
|
+
|
65
|
+
## v0.9.1 — 2025-08-01
|
66
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.9.0...v0.9.1)
|
67
|
+
- Minor fixes and tag adjustments
|
68
|
+
|
69
|
+
## v0.9.0 — 2025-07-30
|
70
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.8.0...v0.9.0)
|
71
|
+
- File-per-example store groundwork — see [#87](https://github.com/danielwestendorf/specwrk/pull/87) by @danielwestendorf
|
72
|
+
|
73
|
+
## v0.8.0 — 2025-07-22
|
74
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.7.1...v0.8.0)
|
75
|
+
- Revert datastore and related adjustments — [#86](https://github.com/danielwestendorf/specwrk/pull/86) by @danielwestendorf
|
76
|
+
- Just return response if no `run_id` header — [#85](https://github.com/danielwestendorf/specwrk/pull/85) by @danielwestendorf
|
77
|
+
- On `INT`, if RSpec is defined set `wants_to_quit = true` — [#84](https://github.com/danielwestendorf/specwrk/pull/84) by @danielwestendorf
|
78
|
+
- Datastore for queues — [#83](https://github.com/danielwestendorf/specwrk/pull/83) by @danielwestendorf
|
79
|
+
|
80
|
+
## v0.7.1 — 2025-07-22
|
81
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.7.0...v0.7.1)
|
82
|
+
- Minor tag-only bump
|
83
|
+
|
84
|
+
## v0.7.0 — 2025-07-22
|
85
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.6.3...v0.7.0)
|
86
|
+
- Only exit 1 when no examples processed and run not completed — [#82](https://github.com/danielwestendorf/specwrk/pull/82) by @danielwestendorf
|
87
|
+
- Switch from Puma to Pitchfork — [#79](https://github.com/danielwestendorf/specwrk/pull/79) by @danielwestendorf
|
88
|
+
- Dump all runs as unique — [#78](https://github.com/danielwestendorf/specwrk/pull/78) by @danielwestendorf
|
89
|
+
|
90
|
+
## v0.6.3 — 2025-07-17
|
91
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.6.2...v0.6.3)
|
92
|
+
- Reap queues based on staleness of workers — [#76](https://github.com/danielwestendorf/specwrk/pull/76) by @danielwestendorf
|
93
|
+
- Track worker `first_seen_at` / `last_seen_at` — [#75](https://github.com/danielwestendorf/specwrk/pull/75) by @danielwestendorf
|
94
|
+
- Unique default ID when no `SPECWRK_ID` provided — [#74](https://github.com/danielwestendorf/specwrk/pull/74) by @danielwestendorf
|
95
|
+
- Send `X-Specwrk-Id` and `User-Agent` headers — [#71](https://github.com/danielwestendorf/specwrk/pull/71) by @danielwestendorf
|
96
|
+
|
97
|
+
## v0.6.2 — 2025-07-17
|
98
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.6.1...v0.6.2)
|
99
|
+
- Tag-only bump
|
100
|
+
|
101
|
+
## v0.6.1 — 2025-07-17
|
102
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.6.0...v0.6.1)
|
103
|
+
- Tag-only bump
|
104
|
+
|
105
|
+
## v0.6.0 — 2025-07-17
|
106
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.5.0...v0.6.0)
|
107
|
+
- Output success message when seeding succeeds — [#67](https://github.com/danielwestendorf/specwrk/pull/67) by @danielwestendorf
|
108
|
+
|
109
|
+
## v0.5.0 — 2025-07-15
|
110
|
+
[Compare](https://github.com/danielwestendorf/specwrk/compare/v0.4.11...v0.5.0)
|
111
|
+
- Support specifying if subsequent seeds should be ignored for a run — [#37](https://github.com/danielwestendorf/specwrk/pull/37) by @danielwestendorf
|
112
|
+
- Make number of seed waits configurable — [#35](https://github.com/danielwestendorf/specwrk/pull/35) by @danielwestendorf
|
113
|
+
- Worker wait for seeding — [#32](https://github.com/danielwestendorf/specwrk/pull/32) by @danielwestendorf
|
114
|
+
- Track if the worker has processed *any* examples — [#31](https://github.com/danielwestendorf/specwrk/pull/31) by @danielwestendorf
|
115
|
+
|
116
|
+
|
3
117
|
## v0.4.11
|
4
118
|
|
5
119
|
- Move logic out of CLI methods ([#64](https://github.com/danielwestendorf/specwrk/issues/64)) by @danielwestendorf
|
data/README.md
CHANGED
@@ -26,6 +26,7 @@ Commands:
|
|
26
26
|
specwrk serve # Start a queue server
|
27
27
|
specwrk start [DIR] # Start a server and workers, monitor until complete
|
28
28
|
specwrk version # Print version
|
29
|
+
specwrk watch # Start a server and workers, watch for file changes in the current directory, and execute specs
|
29
30
|
specwrk work # Start one or more worker processes
|
30
31
|
```
|
31
32
|
|
@@ -261,6 +262,22 @@ end
|
|
261
262
|
# end
|
262
263
|
```
|
263
264
|
|
265
|
+
## Prior/other works
|
266
|
+
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 invserly true. Others are comercial or impactical without making a purchase.
|
267
|
+
|
268
|
+
specwrk is different because it:
|
269
|
+
1. Puts your developer experience first. Easy execution. No messy outputs. Retries built in. Easy(er) debugging of flaky tests.
|
270
|
+
2. Is the same tool you use in development on your local box, in CI with a single runner, or CI with N number of runners.
|
271
|
+
3. Is easy to deploy and manage a queue server.
|
272
|
+
4. RSpec-only.
|
273
|
+
5. Is free.
|
274
|
+
|
275
|
+
[parallel_rspec](https://github.com/willbryant/parallel_rspec)
|
276
|
+
[Knapsack](https://github.com/KnapsackPro/knapsack)
|
277
|
+
[parallel_tests](https://github.com/grosser/parallel_tests)
|
278
|
+
[rspecq](https://github.com/skroutz/rspecq)
|
279
|
+
[RSpec ABQ](https://github.com/rwx-research/rspec-abq)
|
280
|
+
|
264
281
|
## Contributing
|
265
282
|
|
266
283
|
Bug reports and pull requests are welcome on GitHub at https://github.com/dwestendorf/specwrk.
|
data/lib/specwrk/cli.rb
CHANGED
@@ -288,6 +288,8 @@ module Specwrk
|
|
288
288
|
|
289
289
|
require "specwrk/cli_reporter"
|
290
290
|
|
291
|
+
title "👀 for changes"
|
292
|
+
|
291
293
|
loop do
|
292
294
|
status "👀 Watching for file changes..."
|
293
295
|
|
@@ -309,6 +311,7 @@ module Specwrk
|
|
309
311
|
end
|
310
312
|
|
311
313
|
next if example_count.zero?
|
314
|
+
title "👷 on #{example_count} examples"
|
312
315
|
|
313
316
|
return if Specwrk.force_quit
|
314
317
|
start_workers
|
@@ -318,8 +321,17 @@ module Specwrk
|
|
318
321
|
drain_outputs
|
319
322
|
return if Specwrk.force_quit
|
320
323
|
|
321
|
-
Specwrk::CLIReporter.new
|
324
|
+
reporter = Specwrk::CLIReporter.new
|
325
|
+
|
326
|
+
status = reporter.report
|
322
327
|
puts
|
328
|
+
|
329
|
+
if status.zero?
|
330
|
+
title "🟢 #{reporter.example_count} examples passed"
|
331
|
+
else
|
332
|
+
title " 🔴 #{reporter.failure_count}/#{reporter.example_count} examples failed"
|
333
|
+
end
|
334
|
+
|
323
335
|
$stdout.flush
|
324
336
|
end
|
325
337
|
|
@@ -329,6 +341,11 @@ module Specwrk
|
|
329
341
|
|
330
342
|
private
|
331
343
|
|
344
|
+
def title(str)
|
345
|
+
$stdout.write "\e]0;#{str}\a"
|
346
|
+
$stdout.flush
|
347
|
+
end
|
348
|
+
|
332
349
|
def web_pid
|
333
350
|
@web_pid ||= Process.fork do
|
334
351
|
require "specwrk/web"
|
data/lib/specwrk/cli_reporter.rb
CHANGED
data/lib/specwrk/version.rb
CHANGED
data/lib/specwrk/web/app.rb
CHANGED
@@ -16,7 +16,13 @@ end
|
|
16
16
|
require "specwrk/web"
|
17
17
|
require "specwrk/web/logger"
|
18
18
|
require "specwrk/web/auth"
|
19
|
-
require "specwrk/web/endpoints"
|
19
|
+
require "specwrk/web/endpoints/health"
|
20
|
+
require "specwrk/web/endpoints/heartbeat"
|
21
|
+
require "specwrk/web/endpoints/seed"
|
22
|
+
require "specwrk/web/endpoints/pop"
|
23
|
+
require "specwrk/web/endpoints/complete_and_pop"
|
24
|
+
require "specwrk/web/endpoints/report"
|
25
|
+
require "specwrk/web/endpoints/shutdown"
|
20
26
|
|
21
27
|
module Specwrk
|
22
28
|
class Web
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
require "specwrk/store"
|
6
|
+
|
7
|
+
module Specwrk
|
8
|
+
class Web
|
9
|
+
module Endpoints
|
10
|
+
class Base
|
11
|
+
attr_reader :started_at
|
12
|
+
|
13
|
+
def initialize(request)
|
14
|
+
@request = request
|
15
|
+
end
|
16
|
+
|
17
|
+
def response
|
18
|
+
before_lock
|
19
|
+
|
20
|
+
return with_response unless run_id # No run_id, no datastore usage in the endpoint
|
21
|
+
|
22
|
+
payload # parse the payload before any locking
|
23
|
+
|
24
|
+
worker[:first_seen_at] ||= Time.now.iso8601
|
25
|
+
worker[:last_seen_at] = Time.now.iso8601
|
26
|
+
|
27
|
+
final_response = with_lock do
|
28
|
+
started_at = metadata[:started_at] ||= Time.now.iso8601
|
29
|
+
@started_at = Time.parse(started_at)
|
30
|
+
|
31
|
+
with_response
|
32
|
+
end
|
33
|
+
|
34
|
+
after_lock
|
35
|
+
|
36
|
+
final_response[1]["x-specwrk-status"] = worker_status.to_s
|
37
|
+
|
38
|
+
final_response
|
39
|
+
end
|
40
|
+
|
41
|
+
def with_response
|
42
|
+
not_found
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :request
|
48
|
+
|
49
|
+
def before_lock
|
50
|
+
end
|
51
|
+
|
52
|
+
def after_lock
|
53
|
+
end
|
54
|
+
|
55
|
+
def not_found
|
56
|
+
if request.head?
|
57
|
+
[404, {}, []]
|
58
|
+
else
|
59
|
+
[404, {"content-type" => "text/plain"}, ["This is not the path you're looking for, 'ol chap..."]]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def ok
|
64
|
+
if request.head?
|
65
|
+
[200, {}, []]
|
66
|
+
else
|
67
|
+
[200, {"content-type" => "text/plain"}, ["OK, 'ol chap"]]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def payload
|
72
|
+
return unless request.content_type&.start_with?("application/json")
|
73
|
+
return unless request.post? || request.put? || request.delete?
|
74
|
+
return if body.empty?
|
75
|
+
|
76
|
+
@payload ||= JSON.parse(body, symbolize_names: true)
|
77
|
+
end
|
78
|
+
|
79
|
+
def body
|
80
|
+
@body ||= request.body.read
|
81
|
+
end
|
82
|
+
|
83
|
+
def pending
|
84
|
+
@pending ||= PendingStore.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "pending"))
|
85
|
+
end
|
86
|
+
|
87
|
+
def processing
|
88
|
+
@processing ||= ProcessingStore.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "processing"))
|
89
|
+
end
|
90
|
+
|
91
|
+
def completed
|
92
|
+
@completed ||= CompletedStore.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "completed"))
|
93
|
+
end
|
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
|
+
|
99
|
+
def metadata
|
100
|
+
@metadata ||= Store.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "metadata"))
|
101
|
+
end
|
102
|
+
|
103
|
+
def run_times
|
104
|
+
@run_times ||= Store.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "file://#{File.join(Dir.tmpdir, "specwrk")}"), "run_times")
|
105
|
+
end
|
106
|
+
|
107
|
+
def worker
|
108
|
+
@worker ||= Store.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "workers", request.get_header("HTTP_X_SPECWRK_ID").to_s))
|
109
|
+
end
|
110
|
+
|
111
|
+
def worker_status
|
112
|
+
return 0 if worker[:failed].nil? && completed.any? # worker starts after run has completed
|
113
|
+
|
114
|
+
worker[:failed] || 1
|
115
|
+
end
|
116
|
+
|
117
|
+
def run_id
|
118
|
+
request.get_header("HTTP_X_SPECWRK_RUN")
|
119
|
+
end
|
120
|
+
|
121
|
+
def with_lock
|
122
|
+
Store.with_lock(URI(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///")), "server") { yield }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Base default response is 404
|
127
|
+
NotFound = Class.new(Base)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "specwrk/web/endpoints/popable"
|
4
|
+
|
5
|
+
module Specwrk
|
6
|
+
class Web
|
7
|
+
module Endpoints
|
8
|
+
class CompleteAndPop < Popable
|
9
|
+
EXAMPLE_STATUSES = %w[passed failed pending]
|
10
|
+
|
11
|
+
def with_response
|
12
|
+
completed.merge!(completed_examples)
|
13
|
+
processing.delete(*(completed_examples.keys + retry_examples.keys))
|
14
|
+
pending.merge!(retry_examples)
|
15
|
+
failure_counts.merge!(retry_examples_new_failure_counts)
|
16
|
+
|
17
|
+
with_pop_response
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def all_examples
|
23
|
+
@all_examples ||= payload.map { |example| [example[:id], example] if processing[example[:id]] }.compact.to_h
|
24
|
+
end
|
25
|
+
|
26
|
+
def completed_examples
|
27
|
+
@completed_examples ||= all_examples.map do |id, example|
|
28
|
+
next if retry_example?(example)
|
29
|
+
|
30
|
+
[id, example]
|
31
|
+
end.compact.to_h
|
32
|
+
end
|
33
|
+
|
34
|
+
def retry_examples
|
35
|
+
@retry_examples ||= all_examples.map do |id, example|
|
36
|
+
next unless retry_example?(example)
|
37
|
+
|
38
|
+
[id, example]
|
39
|
+
end.compact.to_h
|
40
|
+
end
|
41
|
+
|
42
|
+
def retry_examples_new_failure_counts
|
43
|
+
@retry_examples_new_failure_counts ||= retry_examples.map do |id, _example|
|
44
|
+
[id, all_example_failure_counts.fetch(id, 0) + 1]
|
45
|
+
end.to_h
|
46
|
+
end
|
47
|
+
|
48
|
+
def retry_example?(example)
|
49
|
+
return false unless example[:status] == "failed"
|
50
|
+
return false unless pending.max_retries.positive?
|
51
|
+
|
52
|
+
example_failure_count = all_example_failure_counts.fetch(example[:id], 0)
|
53
|
+
|
54
|
+
example_failure_count < pending.max_retries
|
55
|
+
end
|
56
|
+
|
57
|
+
def all_example_failure_counts
|
58
|
+
@all_example_failure_counts ||= failure_counts.multi_read(*all_examples.keys)
|
59
|
+
end
|
60
|
+
|
61
|
+
def completed_examples_status_counts
|
62
|
+
@completed_examples_status_counts ||= completed_examples.values.map { |example| example[:status] }.tally
|
63
|
+
end
|
64
|
+
|
65
|
+
def after_lock
|
66
|
+
# We don't care about exact values here, just approximate run times are fine
|
67
|
+
# So if we overwrite run times from another process it is nbd
|
68
|
+
run_times.merge! run_time_data
|
69
|
+
|
70
|
+
# workers are single proces, single-threaded, so safe to do this work without the lock
|
71
|
+
existing_status_counts = worker.multi_read(*EXAMPLE_STATUSES)
|
72
|
+
new_status_counts = EXAMPLE_STATUSES.map do |status|
|
73
|
+
[status, existing_status_counts.fetch(status, 0) + completed_examples_status_counts.fetch(status, 0)]
|
74
|
+
end.to_h
|
75
|
+
|
76
|
+
worker.merge!(new_status_counts)
|
77
|
+
end
|
78
|
+
|
79
|
+
def run_time_data
|
80
|
+
@run_time_data ||= payload.map { |example| [example[:id], example[:run_time]] }.to_h
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "specwrk/web/endpoints/base"
|
4
|
+
|
5
|
+
module Specwrk
|
6
|
+
class Web
|
7
|
+
module Endpoints
|
8
|
+
class Popable < Base
|
9
|
+
private
|
10
|
+
|
11
|
+
def with_pop_response
|
12
|
+
if examples.any?
|
13
|
+
[200, {"content-type" => "application/json"}, [JSON.generate(examples)]]
|
14
|
+
elsif pending.empty? && processing.empty? && completed.empty?
|
15
|
+
[204, {"content-type" => "text/plain"}, ["Waiting for sample to be seeded."]]
|
16
|
+
elsif completed.any? && processing.empty?
|
17
|
+
[410, {"content-type" => "text/plain"}, ["That's a good lad. Run along now and go home."]]
|
18
|
+
elsif processing.any? && processing.expired.keys.any?
|
19
|
+
pending.merge!(processing.expired)
|
20
|
+
processing.delete(*processing.expired.keys)
|
21
|
+
@examples = nil
|
22
|
+
|
23
|
+
[200, {"content-type" => "application/json"}, [JSON.generate(examples)]]
|
24
|
+
else
|
25
|
+
not_found
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def examples
|
30
|
+
@examples ||= begin
|
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
|
+
|
36
|
+
processing_data = examples.map do |example|
|
37
|
+
[
|
38
|
+
example[:id], example.merge(completion_threshold: completion_threshold.to_f)
|
39
|
+
]
|
40
|
+
end
|
41
|
+
|
42
|
+
processing.merge!(processing_data.to_h)
|
43
|
+
|
44
|
+
examples
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "specwrk/web/endpoints/base"
|
4
|
+
|
5
|
+
module Specwrk
|
6
|
+
class Web
|
7
|
+
module Endpoints
|
8
|
+
class Report < Base
|
9
|
+
def with_response
|
10
|
+
completed_dump = completed.dump
|
11
|
+
completed_dump[:meta][:unexecuted] = pending.length + processing.length
|
12
|
+
completed_dump[:flakes] = failure_counts.to_h.reject { |id, _count| completed_dump.dig(:examples, id, :status) == "failed" }
|
13
|
+
|
14
|
+
[200, {"content-type" => "application/json"}, [JSON.generate(completed_dump)]]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "specwrk/web/endpoints/base"
|
4
|
+
|
5
|
+
module Specwrk
|
6
|
+
class Web
|
7
|
+
module Endpoints
|
8
|
+
class Seed < Base
|
9
|
+
def before_lock
|
10
|
+
examples_with_run_times
|
11
|
+
end
|
12
|
+
|
13
|
+
def with_response
|
14
|
+
pending.clear
|
15
|
+
processing.clear
|
16
|
+
failure_counts.clear
|
17
|
+
|
18
|
+
pending.max_retries = payload.fetch(:max_retries, "0").to_i
|
19
|
+
|
20
|
+
new_run_time_bucket_maximums = [pending.run_time_bucket_maximum, @seeds_run_time_bucket_maximum.to_f].compact
|
21
|
+
pending.run_time_bucket_maximum = new_run_time_bucket_maximums.sum.to_f / new_run_time_bucket_maximums.length.to_f
|
22
|
+
|
23
|
+
pending.merge!(examples_with_run_times)
|
24
|
+
processing.clear
|
25
|
+
completed.clear
|
26
|
+
|
27
|
+
ok
|
28
|
+
end
|
29
|
+
|
30
|
+
def examples_with_run_times
|
31
|
+
@examples_with_run_times ||= begin
|
32
|
+
unsorted_examples_with_run_times = []
|
33
|
+
all_ids = payload[:examples].map { |example| example[:id] }
|
34
|
+
all_run_times = run_times.multi_read(*all_ids)
|
35
|
+
|
36
|
+
payload[:examples].each do |example|
|
37
|
+
run_time = all_run_times[example[:id]]
|
38
|
+
|
39
|
+
unsorted_examples_with_run_times << [example[:id], example.merge(expected_run_time: run_time)]
|
40
|
+
end
|
41
|
+
|
42
|
+
sorted_examples_with_run_times = if sort_by == :timings
|
43
|
+
unsorted_examples_with_run_times.sort_by do |entry|
|
44
|
+
-(entry.last[:expected_run_time] || Float::INFINITY)
|
45
|
+
end
|
46
|
+
else
|
47
|
+
unsorted_examples_with_run_times.sort_by do |entry|
|
48
|
+
entry.last[:file_path]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
@seeds_run_time_bucket_maximum = run_time_bucket_maximum(all_run_times.values.compact)
|
53
|
+
@examples_with_run_times = sorted_examples_with_run_times.to_h
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Average + standard deviation
|
60
|
+
def run_time_bucket_maximum(values)
|
61
|
+
return 0 if values.length.zero?
|
62
|
+
|
63
|
+
mean = values.sum.to_f / values.size
|
64
|
+
variance = values.map { |v| (v - mean)**2 }.sum / values.size
|
65
|
+
(mean + Math.sqrt(variance)).round(2)
|
66
|
+
end
|
67
|
+
|
68
|
+
def sort_by
|
69
|
+
if ENV["SPECWRK_SRV_GROUP_BY"] == "file" || run_times.empty?
|
70
|
+
:file
|
71
|
+
else
|
72
|
+
:timings
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "specwrk/web/endpoints/base"
|
4
|
+
|
5
|
+
module Specwrk
|
6
|
+
class Web
|
7
|
+
module Endpoints
|
8
|
+
class Shutdown < Base
|
9
|
+
def with_response
|
10
|
+
interupt! if ENV["SPECWRK_SRV_SINGLE_RUN"]
|
11
|
+
|
12
|
+
[200, {"content-type" => "text/plain"}, ["✌️"]]
|
13
|
+
end
|
14
|
+
|
15
|
+
def interupt!
|
16
|
+
Thread.new do
|
17
|
+
# give the socket a moment to flush the response
|
18
|
+
sleep 0.2
|
19
|
+
Process.kill("INT", Process.pid)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
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.15.
|
4
|
+
version: 0.15.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Westendorf
|
@@ -202,7 +202,7 @@ files:
|
|
202
202
|
- ".rspec"
|
203
203
|
- ".standard.yml"
|
204
204
|
- CHANGELOG.md
|
205
|
-
- LICENSE
|
205
|
+
- LICENSE
|
206
206
|
- README.md
|
207
207
|
- Rakefile
|
208
208
|
- Specwrk.watchfile.rb
|
@@ -231,7 +231,15 @@ files:
|
|
231
231
|
- lib/specwrk/web.rb
|
232
232
|
- lib/specwrk/web/app.rb
|
233
233
|
- lib/specwrk/web/auth.rb
|
234
|
-
- lib/specwrk/web/endpoints.rb
|
234
|
+
- lib/specwrk/web/endpoints/base.rb
|
235
|
+
- lib/specwrk/web/endpoints/complete_and_pop.rb
|
236
|
+
- lib/specwrk/web/endpoints/health.rb
|
237
|
+
- lib/specwrk/web/endpoints/heartbeat.rb
|
238
|
+
- lib/specwrk/web/endpoints/pop.rb
|
239
|
+
- lib/specwrk/web/endpoints/popable.rb
|
240
|
+
- lib/specwrk/web/endpoints/report.rb
|
241
|
+
- lib/specwrk/web/endpoints/seed.rb
|
242
|
+
- lib/specwrk/web/endpoints/shutdown.rb
|
235
243
|
- lib/specwrk/web/logger.rb
|
236
244
|
- lib/specwrk/worker.rb
|
237
245
|
- lib/specwrk/worker/completion_formatter.rb
|
@@ -1,360 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "json"
|
4
|
-
|
5
|
-
require "specwrk/store"
|
6
|
-
|
7
|
-
module Specwrk
|
8
|
-
class Web
|
9
|
-
module Endpoints
|
10
|
-
class Base
|
11
|
-
attr_reader :started_at
|
12
|
-
|
13
|
-
def initialize(request)
|
14
|
-
@request = request
|
15
|
-
end
|
16
|
-
|
17
|
-
def response
|
18
|
-
before_lock
|
19
|
-
|
20
|
-
return with_response unless run_id # No run_id, no datastore usage in the endpoint
|
21
|
-
|
22
|
-
payload # parse the payload before any locking
|
23
|
-
|
24
|
-
worker[:first_seen_at] ||= Time.now.iso8601
|
25
|
-
worker[:last_seen_at] = Time.now.iso8601
|
26
|
-
|
27
|
-
final_response = with_lock do
|
28
|
-
started_at = metadata[:started_at] ||= Time.now.iso8601
|
29
|
-
@started_at = Time.parse(started_at)
|
30
|
-
|
31
|
-
with_response
|
32
|
-
end
|
33
|
-
|
34
|
-
after_lock
|
35
|
-
|
36
|
-
final_response[1]["x-specwrk-status"] = worker_status.to_s
|
37
|
-
|
38
|
-
final_response
|
39
|
-
end
|
40
|
-
|
41
|
-
def with_response
|
42
|
-
not_found
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
attr_reader :request
|
48
|
-
|
49
|
-
def before_lock
|
50
|
-
end
|
51
|
-
|
52
|
-
def after_lock
|
53
|
-
end
|
54
|
-
|
55
|
-
def not_found
|
56
|
-
if request.head?
|
57
|
-
[404, {}, []]
|
58
|
-
else
|
59
|
-
[404, {"content-type" => "text/plain"}, ["This is not the path you're looking for, 'ol chap..."]]
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def ok
|
64
|
-
if request.head?
|
65
|
-
[200, {}, []]
|
66
|
-
else
|
67
|
-
[200, {"content-type" => "text/plain"}, ["OK, 'ol chap"]]
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def payload
|
72
|
-
return unless request.content_type&.start_with?("application/json")
|
73
|
-
return unless request.post? || request.put? || request.delete?
|
74
|
-
return if body.empty?
|
75
|
-
|
76
|
-
@payload ||= JSON.parse(body, symbolize_names: true)
|
77
|
-
end
|
78
|
-
|
79
|
-
def body
|
80
|
-
@body ||= request.body.read
|
81
|
-
end
|
82
|
-
|
83
|
-
def pending
|
84
|
-
@pending ||= PendingStore.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "pending"))
|
85
|
-
end
|
86
|
-
|
87
|
-
def processing
|
88
|
-
@processing ||= ProcessingStore.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "processing"))
|
89
|
-
end
|
90
|
-
|
91
|
-
def completed
|
92
|
-
@completed ||= CompletedStore.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "completed"))
|
93
|
-
end
|
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
|
-
|
99
|
-
def metadata
|
100
|
-
@metadata ||= Store.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "metadata"))
|
101
|
-
end
|
102
|
-
|
103
|
-
def run_times
|
104
|
-
@run_times ||= Store.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "file://#{File.join(Dir.tmpdir, "specwrk")}"), "run_times")
|
105
|
-
end
|
106
|
-
|
107
|
-
def worker
|
108
|
-
@worker ||= Store.new(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///"), File.join(run_id, "workers", request.get_header("HTTP_X_SPECWRK_ID").to_s))
|
109
|
-
end
|
110
|
-
|
111
|
-
def worker_status
|
112
|
-
return 0 if worker[:failed].nil? && completed.any? # worker starts after run has completed
|
113
|
-
|
114
|
-
worker[:failed] || 1
|
115
|
-
end
|
116
|
-
|
117
|
-
def run_id
|
118
|
-
request.get_header("HTTP_X_SPECWRK_RUN")
|
119
|
-
end
|
120
|
-
|
121
|
-
def with_lock
|
122
|
-
Store.with_lock(URI(ENV.fetch("SPECWRK_SRV_STORE_URI", "memory:///")), "server") { yield }
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
# Base default response is 404
|
127
|
-
NotFound = Class.new(Base)
|
128
|
-
|
129
|
-
class Health < Base
|
130
|
-
def with_response
|
131
|
-
ok
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
class Heartbeat < Base
|
136
|
-
def with_response
|
137
|
-
ok
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
class Seed < Base
|
142
|
-
def before_lock
|
143
|
-
examples_with_run_times
|
144
|
-
end
|
145
|
-
|
146
|
-
def with_response
|
147
|
-
pending.clear
|
148
|
-
processing.clear
|
149
|
-
failure_counts.clear
|
150
|
-
|
151
|
-
pending.max_retries = payload.fetch(:max_retries, "0").to_i
|
152
|
-
|
153
|
-
new_run_time_bucket_maximums = [pending.run_time_bucket_maximum, @seeds_run_time_bucket_maximum.to_f].compact
|
154
|
-
pending.run_time_bucket_maximum = new_run_time_bucket_maximums.sum.to_f / new_run_time_bucket_maximums.length.to_f
|
155
|
-
|
156
|
-
pending.merge!(examples_with_run_times)
|
157
|
-
processing.clear
|
158
|
-
completed.clear
|
159
|
-
|
160
|
-
ok
|
161
|
-
end
|
162
|
-
|
163
|
-
def examples_with_run_times
|
164
|
-
@examples_with_run_times ||= begin
|
165
|
-
unsorted_examples_with_run_times = []
|
166
|
-
all_ids = payload[:examples].map { |example| example[:id] }
|
167
|
-
all_run_times = run_times.multi_read(*all_ids)
|
168
|
-
|
169
|
-
payload[:examples].each do |example|
|
170
|
-
run_time = all_run_times[example[:id]]
|
171
|
-
|
172
|
-
unsorted_examples_with_run_times << [example[:id], example.merge(expected_run_time: run_time)]
|
173
|
-
end
|
174
|
-
|
175
|
-
sorted_examples_with_run_times = if sort_by == :timings
|
176
|
-
unsorted_examples_with_run_times.sort_by do |entry|
|
177
|
-
-(entry.last[:expected_run_time] || Float::INFINITY)
|
178
|
-
end
|
179
|
-
else
|
180
|
-
unsorted_examples_with_run_times.sort_by do |entry|
|
181
|
-
entry.last[:file_path]
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
@seeds_run_time_bucket_maximum = run_time_bucket_maximum(all_run_times.values.compact)
|
186
|
-
@examples_with_run_times = sorted_examples_with_run_times.to_h
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
private
|
191
|
-
|
192
|
-
# Average + standard deviation
|
193
|
-
def run_time_bucket_maximum(values)
|
194
|
-
return 0 if values.length.zero?
|
195
|
-
|
196
|
-
mean = values.sum.to_f / values.size
|
197
|
-
variance = values.map { |v| (v - mean)**2 }.sum / values.size
|
198
|
-
(mean + Math.sqrt(variance)).round(2)
|
199
|
-
end
|
200
|
-
|
201
|
-
def sort_by
|
202
|
-
if ENV["SPECWRK_SRV_GROUP_BY"] == "file" || run_times.empty?
|
203
|
-
:file
|
204
|
-
else
|
205
|
-
:timings
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
class Popable < Base
|
211
|
-
private
|
212
|
-
|
213
|
-
def with_pop_response
|
214
|
-
if examples.any?
|
215
|
-
[200, {"content-type" => "application/json"}, [JSON.generate(examples)]]
|
216
|
-
elsif pending.empty? && processing.empty? && completed.empty?
|
217
|
-
[204, {"content-type" => "text/plain"}, ["Waiting for sample to be seeded."]]
|
218
|
-
elsif completed.any? && processing.empty?
|
219
|
-
[410, {"content-type" => "text/plain"}, ["That's a good lad. Run along now and go home."]]
|
220
|
-
elsif processing.any? && processing.expired.keys.any?
|
221
|
-
pending.merge!(processing.expired)
|
222
|
-
processing.delete(*processing.expired.keys)
|
223
|
-
@examples = nil
|
224
|
-
|
225
|
-
[200, {"content-type" => "application/json"}, [JSON.generate(examples)]]
|
226
|
-
else
|
227
|
-
not_found
|
228
|
-
end
|
229
|
-
end
|
230
|
-
|
231
|
-
def examples
|
232
|
-
@examples ||= begin
|
233
|
-
examples = pending.shift_bucket
|
234
|
-
maximum_completion_threshold = (Time.now + ((pending.run_time_bucket_maximum || 30) * 2)).to_i
|
235
|
-
|
236
|
-
processing_data = examples.map do |example|
|
237
|
-
example_run_time_completion_threshold = (Time.now + example[:expected_run_time].to_f * 2).to_i
|
238
|
-
|
239
|
-
[
|
240
|
-
example[:id], example.merge(completion_threshold: [maximum_completion_threshold, example_run_time_completion_threshold].compact.max)
|
241
|
-
]
|
242
|
-
end
|
243
|
-
|
244
|
-
processing.merge!(processing_data.to_h)
|
245
|
-
|
246
|
-
examples
|
247
|
-
end
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
class Pop < Popable
|
252
|
-
def with_response
|
253
|
-
with_pop_response
|
254
|
-
end
|
255
|
-
end
|
256
|
-
|
257
|
-
class CompleteAndPop < Popable
|
258
|
-
EXAMPLE_STATUSES = %w[passed failed pending]
|
259
|
-
|
260
|
-
def with_response
|
261
|
-
completed.merge!(completed_examples)
|
262
|
-
processing.delete(*(completed_examples.keys + retry_examples.keys))
|
263
|
-
pending.merge!(retry_examples)
|
264
|
-
failure_counts.merge!(retry_examples_new_failure_counts)
|
265
|
-
|
266
|
-
with_pop_response
|
267
|
-
end
|
268
|
-
|
269
|
-
private
|
270
|
-
|
271
|
-
def all_examples
|
272
|
-
@all_examples ||= payload.map { |example| [example[:id], example] if processing[example[:id]] }.compact.to_h
|
273
|
-
end
|
274
|
-
|
275
|
-
def completed_examples
|
276
|
-
@completed_examples ||= all_examples.map do |id, example|
|
277
|
-
next if retry_example?(example)
|
278
|
-
|
279
|
-
[id, example]
|
280
|
-
end.compact.to_h
|
281
|
-
end
|
282
|
-
|
283
|
-
def retry_examples
|
284
|
-
@retry_examples ||= all_examples.map do |id, example|
|
285
|
-
next unless retry_example?(example)
|
286
|
-
|
287
|
-
[id, example]
|
288
|
-
end.compact.to_h
|
289
|
-
end
|
290
|
-
|
291
|
-
def retry_examples_new_failure_counts
|
292
|
-
@retry_examples_new_failure_counts ||= retry_examples.map do |id, _example|
|
293
|
-
[id, all_example_failure_counts.fetch(id, 0) + 1]
|
294
|
-
end.to_h
|
295
|
-
end
|
296
|
-
|
297
|
-
def retry_example?(example)
|
298
|
-
return false unless example[:status] == "failed"
|
299
|
-
return false unless pending.max_retries.positive?
|
300
|
-
|
301
|
-
example_failure_count = all_example_failure_counts.fetch(example[:id], 0)
|
302
|
-
|
303
|
-
example_failure_count < pending.max_retries
|
304
|
-
end
|
305
|
-
|
306
|
-
def all_example_failure_counts
|
307
|
-
@all_example_failure_counts ||= failure_counts.multi_read(*all_examples.keys)
|
308
|
-
end
|
309
|
-
|
310
|
-
def completed_examples_status_counts
|
311
|
-
@completed_examples_status_counts ||= completed_examples.values.map { |example| example[:status] }.tally
|
312
|
-
end
|
313
|
-
|
314
|
-
def after_lock
|
315
|
-
# We don't care about exact values here, just approximate run times are fine
|
316
|
-
# So if we overwrite run times from another process it is nbd
|
317
|
-
run_times.merge! run_time_data
|
318
|
-
|
319
|
-
# workers are single proces, single-threaded, so safe to do this work without the lock
|
320
|
-
existing_status_counts = worker.multi_read(*EXAMPLE_STATUSES)
|
321
|
-
new_status_counts = EXAMPLE_STATUSES.map do |status|
|
322
|
-
[status, existing_status_counts.fetch(status, 0) + completed_examples_status_counts.fetch(status, 0)]
|
323
|
-
end.to_h
|
324
|
-
|
325
|
-
worker.merge!(new_status_counts)
|
326
|
-
end
|
327
|
-
|
328
|
-
def run_time_data
|
329
|
-
@run_time_data ||= payload.map { |example| [example[:id], example[:run_time]] }.to_h
|
330
|
-
end
|
331
|
-
end
|
332
|
-
|
333
|
-
class Report < Base
|
334
|
-
def with_response
|
335
|
-
completed_dump = completed.dump
|
336
|
-
completed_dump[:meta][:unexecuted] = pending.length + processing.length
|
337
|
-
completed_dump[:flakes] = failure_counts.to_h.reject { |id, _count| completed_dump.dig(:examples, id, :status) == "failed" }
|
338
|
-
|
339
|
-
[200, {"content-type" => "application/json"}, [JSON.generate(completed_dump)]]
|
340
|
-
end
|
341
|
-
end
|
342
|
-
|
343
|
-
class Shutdown < Base
|
344
|
-
def with_response
|
345
|
-
interupt! if ENV["SPECWRK_SRV_SINGLE_RUN"]
|
346
|
-
|
347
|
-
[200, {"content-type" => "text/plain"}, ["✌️"]]
|
348
|
-
end
|
349
|
-
|
350
|
-
def interupt!
|
351
|
-
Thread.new do
|
352
|
-
# give the socket a moment to flush the response
|
353
|
-
sleep 0.2
|
354
|
-
Process.kill("INT", Process.pid)
|
355
|
-
end
|
356
|
-
end
|
357
|
-
end
|
358
|
-
end
|
359
|
-
end
|
360
|
-
end
|
/data/{LICENSE.txt → LICENSE}
RENAMED
File without changes
|