specwrk 0.17.0 → 0.18.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/lib/specwrk/cli.rb +142 -114
- data/lib/specwrk/ipc.rb +14 -4
- data/lib/specwrk/version.rb +1 -1
- data/lib/specwrk/web/endpoints/complete_and_pop.rb +3 -2
- data/lib/specwrk/web/endpoints/popable.rb +7 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc8e7bf6a7d8bebb0a79a536f403a51fd6cf88948e69e4d03304cd62a039290e
|
|
4
|
+
data.tar.gz: c0f58e84e321b6407cefb18bad2ad42344f8919ff754471725ab498072e54267
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22fc0af6d23a344521e870194f6ea6c943b313c6596ec3b420a103e533a34ad60fe262b5138b8d0dcdbe1644850cdc633e28510c30d4c0ecea07e9607a4c23e3
|
|
7
|
+
data.tar.gz: 2a0b62e8140bd41efdbc62838bc8972ce56d9b7f3d7cd316479d5264d16909af2aef9f699d4262cb8dcc2960fdd5ae361a978729403a960b3fd55e0e880060fa
|
data/lib/specwrk/cli.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "pathname"
|
|
4
4
|
require "securerandom"
|
|
5
|
+
require "json"
|
|
5
6
|
|
|
6
7
|
require "dry/cli"
|
|
7
8
|
|
|
@@ -12,6 +13,78 @@ module Specwrk
|
|
|
12
13
|
module CLI
|
|
13
14
|
extend Dry::CLI::Registry
|
|
14
15
|
|
|
16
|
+
module WorkerProcesses
|
|
17
|
+
WORKER_INIT_SCRIPT = <<~RUBY
|
|
18
|
+
writer = IO.for_fd(Integer(ENV.fetch("SPECWRK_FINAL_FD")))
|
|
19
|
+
$final_output = writer # standard:disable Style/GlobalVars
|
|
20
|
+
$final_output.sync = true # standard:disable Style/GlobalVars
|
|
21
|
+
$stdout.sync = true
|
|
22
|
+
$stderr.sync = true
|
|
23
|
+
|
|
24
|
+
require "specwrk/worker"
|
|
25
|
+
|
|
26
|
+
trap("INT") do
|
|
27
|
+
RSpec.world.wants_to_quit = true if defined?(RSpec)
|
|
28
|
+
exit(1) if Specwrk.force_quit
|
|
29
|
+
Specwrk.force_quit = true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
status = Specwrk::Worker.run!
|
|
33
|
+
$final_output.close # standard:disable Style/GlobalVars
|
|
34
|
+
exit(status)
|
|
35
|
+
RUBY
|
|
36
|
+
|
|
37
|
+
def start_workers
|
|
38
|
+
@final_outputs = []
|
|
39
|
+
@worker_pids = worker_count.times.map do |i|
|
|
40
|
+
reader, writer = IO.pipe
|
|
41
|
+
@final_outputs << reader
|
|
42
|
+
|
|
43
|
+
env = worker_env_for(i + 1).merge(
|
|
44
|
+
"SPECWRK_FINAL_FD" => writer.fileno.to_s
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
Process.spawn(
|
|
48
|
+
env, RbConfig.ruby, "-e", WORKER_INIT_SCRIPT,
|
|
49
|
+
writer.fileno => writer,
|
|
50
|
+
:in => :close,
|
|
51
|
+
:close_others => false
|
|
52
|
+
).tap { writer.close }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def drain_outputs
|
|
57
|
+
@final_outputs.each do |reader|
|
|
58
|
+
reader.each_line { |line| $stdout.print line }
|
|
59
|
+
reader.close
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def worker_count
|
|
64
|
+
@worker_count ||= [1, ENV["SPECWRK_COUNT"].to_i].max
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def worker_env_for(idx)
|
|
68
|
+
{
|
|
69
|
+
"TEST_ENV_NUMBER" => idx.to_s,
|
|
70
|
+
"SPECWRK_FORKED" => idx.to_s,
|
|
71
|
+
"SPECWRK_ID" => "#{ENV.fetch("SPECWRK_ID", "specwrk-worker")}-#{idx}"
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
module PortDiscoverable
|
|
77
|
+
def find_open_port
|
|
78
|
+
require "socket"
|
|
79
|
+
|
|
80
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
81
|
+
port = server.addr[1]
|
|
82
|
+
server.close
|
|
83
|
+
|
|
84
|
+
port
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
15
88
|
module Clientable
|
|
16
89
|
extend Hookable
|
|
17
90
|
|
|
@@ -34,6 +107,7 @@ module Specwrk
|
|
|
34
107
|
|
|
35
108
|
module Workable
|
|
36
109
|
extend Hookable
|
|
110
|
+
include WorkerProcesses
|
|
37
111
|
|
|
38
112
|
on_included do |base|
|
|
39
113
|
base.unique_option :id, type: :string, desc: "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"
|
|
@@ -49,44 +123,11 @@ module Specwrk
|
|
|
49
123
|
ENV["SPECWRK_SEED_WAITS"] = seed_waits.to_s
|
|
50
124
|
ENV["SPECWRK_OUT"] = Pathname.new(output).expand_path(Dir.pwd).to_s
|
|
51
125
|
end
|
|
52
|
-
|
|
53
|
-
def start_workers
|
|
54
|
-
@final_outputs = []
|
|
55
|
-
@worker_pids = worker_count.times.map do |i|
|
|
56
|
-
reader, writer = IO.pipe
|
|
57
|
-
@final_outputs << reader
|
|
58
|
-
|
|
59
|
-
Process.fork do
|
|
60
|
-
ENV["TEST_ENV_NUMBER"] = ENV["SPECWRK_FORKED"] = (i + 1).to_s
|
|
61
|
-
ENV["SPECWRK_ID"] = ENV["SPECWRK_ID"] + "-#{i + 1}"
|
|
62
|
-
|
|
63
|
-
$final_output = writer # standard:disable Style/GlobalVars
|
|
64
|
-
$final_output.sync = true # standard:disable Style/GlobalVars
|
|
65
|
-
reader.close
|
|
66
|
-
|
|
67
|
-
require "specwrk/worker"
|
|
68
|
-
|
|
69
|
-
status = Specwrk::Worker.run!
|
|
70
|
-
$final_output.close # standard:disable Style/GlobalVars
|
|
71
|
-
exit(status)
|
|
72
|
-
end.tap { writer.close }
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def drain_outputs
|
|
77
|
-
@final_outputs.each do |reader|
|
|
78
|
-
reader.each_line { |line| $stdout.print line }
|
|
79
|
-
reader.close
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def worker_count
|
|
84
|
-
@worker_count ||= [1, ENV["SPECWRK_COUNT"].to_i].max
|
|
85
|
-
end
|
|
86
126
|
end
|
|
87
127
|
|
|
88
128
|
module Servable
|
|
89
129
|
extend Hookable
|
|
130
|
+
include PortDiscoverable
|
|
90
131
|
|
|
91
132
|
on_included do |base|
|
|
92
133
|
base.unique_option :port, type: :integer, default: ENV.fetch("SPECWRK_SRV_PORT", "5138"), aliases: ["-p"], desc: "Server port. Overrides SPECWRK_SRV_PORT"
|
|
@@ -94,7 +135,7 @@ module Specwrk
|
|
|
94
135
|
base.unique_option :key, type: :string, aliases: ["-k"], default: ENV.fetch("SPECWRK_SRV_KEY", ""), desc: "Authentication key clients must use for access. Overrides SPECWRK_SRV_KEY"
|
|
95
136
|
base.unique_option :output, type: :string, default: ENV.fetch("SPECWRK_OUT", ".specwrk/"), aliases: ["-o"], desc: "Directory where worker or server output is stored. Overrides SPECWRK_OUT"
|
|
96
137
|
base.unique_option :store_uri, type: :string, desc: "Directory where server state is stored. Required for multi-node or multi-process servers."
|
|
97
|
-
base.unique_option :group_by, values: %w[file timings], default: ENV.fetch("
|
|
138
|
+
base.unique_option :group_by, values: %w[file timings], default: ENV.fetch("SPECWRK_SRV_GROUP_BY", "timings"), desc: "How examples will be grouped for workers; fallback to file if no timings are found. Overrides SPECWRK_SRV_GROUP_BY"
|
|
98
139
|
base.unique_option :verbose, type: :boolean, default: false, desc: "Run in verbose mode"
|
|
99
140
|
end
|
|
100
141
|
|
|
@@ -108,16 +149,6 @@ module Specwrk
|
|
|
108
149
|
ENV["SPECWRK_SRV_KEY"] = key
|
|
109
150
|
ENV["SPECWRK_SRV_GROUP_BY"] = group_by
|
|
110
151
|
end
|
|
111
|
-
|
|
112
|
-
def find_open_port
|
|
113
|
-
require "socket"
|
|
114
|
-
|
|
115
|
-
server = TCPServer.new("127.0.0.1", 0)
|
|
116
|
-
port = server.addr[1]
|
|
117
|
-
server.close
|
|
118
|
-
|
|
119
|
-
port
|
|
120
|
-
end
|
|
121
152
|
end
|
|
122
153
|
|
|
123
154
|
class Version < Dry::CLI::Command
|
|
@@ -210,6 +241,31 @@ module Specwrk
|
|
|
210
241
|
include Workable
|
|
211
242
|
include Servable
|
|
212
243
|
|
|
244
|
+
SEED_INIT_SCRIPT = <<~'RUBY'
|
|
245
|
+
require "json"
|
|
246
|
+
require "specwrk/list_examples"
|
|
247
|
+
require "specwrk/client"
|
|
248
|
+
|
|
249
|
+
def status(msg)
|
|
250
|
+
print "\e[2K\r#{msg}"
|
|
251
|
+
$stdout.flush
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
dir = JSON.parse(ENV.fetch("SPECWRK_SEED_DIRS"))
|
|
255
|
+
max_retries = Integer(ENV.fetch("SPECWRK_MAX_RETRIES", "0"))
|
|
256
|
+
|
|
257
|
+
examples = Specwrk::ListExamples.new(dir).examples
|
|
258
|
+
|
|
259
|
+
status "Waiting for server to respond..."
|
|
260
|
+
Specwrk::Client.wait_for_server!
|
|
261
|
+
status "Server responding ✓"
|
|
262
|
+
status "Seeding #{examples.length} examples..."
|
|
263
|
+
Specwrk::Client.new.seed(examples, max_retries)
|
|
264
|
+
file_count = examples.group_by { |e| e[:file_path] }.keys.size
|
|
265
|
+
status "🌱 Seeded #{examples.size} examples across #{file_count} files"
|
|
266
|
+
exit(1) if examples.size.zero?
|
|
267
|
+
RUBY
|
|
268
|
+
|
|
213
269
|
desc "Start a server and workers, monitor until complete"
|
|
214
270
|
option :max_retries, default: 0, desc: "Number of times an example will be re-run should it fail"
|
|
215
271
|
argument :dir, type: :array, required: false, desc: "Relative spec directory to run against, default: spec/"
|
|
@@ -238,23 +294,7 @@ module Specwrk
|
|
|
238
294
|
end
|
|
239
295
|
|
|
240
296
|
return if Specwrk.force_quit
|
|
241
|
-
seed_pid =
|
|
242
|
-
require "specwrk/list_examples"
|
|
243
|
-
require "specwrk/client"
|
|
244
|
-
|
|
245
|
-
ENV["SPECWRK_FORKED"] = "1"
|
|
246
|
-
ENV["SPECWRK_SEED"] = "1"
|
|
247
|
-
examples = ListExamples.new(dir).examples
|
|
248
|
-
|
|
249
|
-
status "Waiting for server to respond..."
|
|
250
|
-
Client.wait_for_server!
|
|
251
|
-
status "Server responding ✓"
|
|
252
|
-
status "Seeding #{examples.length} examples..."
|
|
253
|
-
Client.new.seed(examples, max_retries)
|
|
254
|
-
file_count = examples.group_by { |e| e[:file_path] }.keys.size
|
|
255
|
-
status "🌱 Seeded #{examples.size} examples across #{file_count} files"
|
|
256
|
-
exit(1) if examples.size.zero?
|
|
257
|
-
end
|
|
297
|
+
seed_pid = spawn_seed_process(dir, max_retries)
|
|
258
298
|
|
|
259
299
|
if Specwrk.wait_for_pids_exit([seed_pid]).value?(1)
|
|
260
300
|
status "Seeding examples failed, exiting."
|
|
@@ -279,6 +319,19 @@ module Specwrk
|
|
|
279
319
|
exit(status)
|
|
280
320
|
end
|
|
281
321
|
|
|
322
|
+
def spawn_seed_process(dir, max_retries)
|
|
323
|
+
Process.spawn(
|
|
324
|
+
{
|
|
325
|
+
"SPECWRK_FORKED" => "1",
|
|
326
|
+
"SPECWRK_SEED" => "1",
|
|
327
|
+
"SPECWRK_SEED_DIRS" => JSON.dump(dir),
|
|
328
|
+
"SPECWRK_MAX_RETRIES" => max_retries.to_s
|
|
329
|
+
},
|
|
330
|
+
RbConfig.ruby, "-e", SEED_INIT_SCRIPT,
|
|
331
|
+
close_others: false
|
|
332
|
+
)
|
|
333
|
+
end
|
|
334
|
+
|
|
282
335
|
def status(msg)
|
|
283
336
|
print "\e[2K\r#{msg}"
|
|
284
337
|
$stdout.flush
|
|
@@ -286,6 +339,20 @@ module Specwrk
|
|
|
286
339
|
end
|
|
287
340
|
|
|
288
341
|
class Watch < Dry::CLI::Command
|
|
342
|
+
include WorkerProcesses
|
|
343
|
+
include PortDiscoverable
|
|
344
|
+
|
|
345
|
+
SEED_LOOP_INIT_SCRIPT = <<~RUBY
|
|
346
|
+
require "specwrk/ipc"
|
|
347
|
+
require "specwrk/seed_loop"
|
|
348
|
+
|
|
349
|
+
parent_pid = Integer(ENV.fetch("SPECWRK_IPC_PARENT_PID"))
|
|
350
|
+
fd = Integer(ENV.fetch("SPECWRK_IPC_FD"))
|
|
351
|
+
ipc = Specwrk::IPC.from_child_fd(fd, parent_pid: parent_pid)
|
|
352
|
+
|
|
353
|
+
Specwrk::SeedLoop.loop!(ipc)
|
|
354
|
+
RUBY
|
|
355
|
+
|
|
289
356
|
desc "Start a server and workers, watch for file changes in the current directory, and execute specs"
|
|
290
357
|
option :watchfile, type: :string, default: "Specwrk.watchfile.rb", desc: "Path to watchfile configuration"
|
|
291
358
|
option :count, type: :integer, default: 1, aliases: ["-c"], desc: "The number of worker processes you want to start"
|
|
@@ -388,14 +455,19 @@ module Specwrk
|
|
|
388
455
|
@seed_pid ||= begin
|
|
389
456
|
ipc # must be initialized in the parent process
|
|
390
457
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
458
|
+
ipc.child_socket.close_on_exec = false
|
|
459
|
+
|
|
460
|
+
Process.spawn(
|
|
461
|
+
{
|
|
462
|
+
"SPECWRK_FORKED" => "1",
|
|
463
|
+
"SPECWRK_SEED" => "1",
|
|
464
|
+
"SPECWRK_IPC_FD" => ipc.child_socket.fileno.to_s,
|
|
465
|
+
"SPECWRK_IPC_PARENT_PID" => Process.pid.to_s
|
|
466
|
+
},
|
|
467
|
+
RbConfig.ruby, "-e", SEED_LOOP_INIT_SCRIPT,
|
|
468
|
+
ipc.child_socket => ipc.child_socket,
|
|
469
|
+
:close_others => false
|
|
470
|
+
)
|
|
399
471
|
end
|
|
400
472
|
end
|
|
401
473
|
|
|
@@ -421,50 +493,6 @@ module Specwrk
|
|
|
421
493
|
print "\e[2K\r#{msg}"
|
|
422
494
|
$stdout.flush
|
|
423
495
|
end
|
|
424
|
-
|
|
425
|
-
def start_workers
|
|
426
|
-
@final_outputs = []
|
|
427
|
-
@worker_pids = worker_count.times.map do |i|
|
|
428
|
-
reader, writer = IO.pipe
|
|
429
|
-
@final_outputs << reader
|
|
430
|
-
|
|
431
|
-
Process.fork do
|
|
432
|
-
ENV["TEST_ENV_NUMBER"] = ENV["SPECWRK_FORKED"] = (i + 1).to_s
|
|
433
|
-
ENV["SPECWRK_ID"] = "specwrk-worker-#{i + 1}"
|
|
434
|
-
|
|
435
|
-
$final_output = writer # standard:disable Style/GlobalVars
|
|
436
|
-
$final_output.sync = true # standard:disable Style/GlobalVars
|
|
437
|
-
reader.close
|
|
438
|
-
|
|
439
|
-
require "specwrk/worker"
|
|
440
|
-
|
|
441
|
-
status = Specwrk::Worker.run!
|
|
442
|
-
$final_output.close # standard:disable Style/GlobalVars
|
|
443
|
-
exit(status)
|
|
444
|
-
end.tap { writer.close }
|
|
445
|
-
end
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
def drain_outputs
|
|
449
|
-
@final_outputs.each do |reader|
|
|
450
|
-
reader.each_line { |line| $stdout.print line }
|
|
451
|
-
reader.close
|
|
452
|
-
end
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
def worker_count
|
|
456
|
-
@worker_count ||= [1, ENV["SPECWRK_COUNT"].to_i].max
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
def find_open_port
|
|
460
|
-
require "socket"
|
|
461
|
-
|
|
462
|
-
server = TCPServer.new("127.0.0.1", 0)
|
|
463
|
-
port = server.addr[1]
|
|
464
|
-
server.close
|
|
465
|
-
|
|
466
|
-
port
|
|
467
|
-
end
|
|
468
496
|
end
|
|
469
497
|
|
|
470
498
|
register "version", Version, aliases: ["v", "-v", "--version"]
|
data/lib/specwrk/ipc.rb
CHANGED
|
@@ -2,10 +2,20 @@ require "socket"
|
|
|
2
2
|
|
|
3
3
|
module Specwrk
|
|
4
4
|
class IPC
|
|
5
|
-
|
|
6
|
-
@parent_pid = Process.pid
|
|
5
|
+
attr_reader :parent_socket, :child_socket
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
def self.from_child_fd(fd, parent_pid:)
|
|
8
|
+
new(
|
|
9
|
+
parent_pid: parent_pid,
|
|
10
|
+
child_socket: UNIXSocket.for_fd(fd)
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(parent_pid: Process.pid, parent_socket: nil, child_socket: nil)
|
|
15
|
+
@parent_pid = parent_pid
|
|
16
|
+
|
|
17
|
+
@parent_socket, @child_socket = parent_socket, child_socket
|
|
18
|
+
@parent_socket, @child_socket = UNIXSocket.pair if @parent_socket.nil? && @child_socket.nil?
|
|
9
19
|
end
|
|
10
20
|
|
|
11
21
|
def write(msg)
|
|
@@ -23,7 +33,7 @@ module Specwrk
|
|
|
23
33
|
|
|
24
34
|
private
|
|
25
35
|
|
|
26
|
-
attr_reader :parent_pid
|
|
36
|
+
attr_reader :parent_pid
|
|
27
37
|
|
|
28
38
|
def socket
|
|
29
39
|
child? ? child_socket : parent_socket
|
data/lib/specwrk/version.rb
CHANGED
|
@@ -9,13 +9,14 @@ module Specwrk
|
|
|
9
9
|
EXAMPLE_STATUSES = %w[passed failed pending]
|
|
10
10
|
|
|
11
11
|
def with_response
|
|
12
|
-
|
|
13
|
-
processing.delete(*(completed_examples.keys + retry_examples.keys))
|
|
12
|
+
retry_examples # pre-calculate before lock
|
|
14
13
|
|
|
15
14
|
with_lock do
|
|
15
|
+
processing.delete(*(completed_examples.keys + retry_examples.keys))
|
|
16
16
|
pending.merge!(retry_examples)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
completed.merge!(completed_examples)
|
|
19
20
|
failure_counts.merge!(retry_examples_new_failure_counts)
|
|
20
21
|
|
|
21
22
|
update_run_times
|
|
@@ -17,8 +17,12 @@ module Specwrk
|
|
|
17
17
|
[410, {"content-type" => "text/plain"}, ["That's a good lad. Run along now and go home."]]
|
|
18
18
|
elsif expired_examples.length.positive?
|
|
19
19
|
expired_examples.each { |_id, example| example[:worker_id] = worker_id }
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
|
|
21
|
+
with_lock do
|
|
22
|
+
pending.push_examples(expired_examples.values)
|
|
23
|
+
processing.delete(*expired_examples.keys.map(&:to_s))
|
|
24
|
+
end
|
|
25
|
+
|
|
22
26
|
@examples = nil
|
|
23
27
|
|
|
24
28
|
[200, {"content-type" => "application/json"}, [JSON.generate(examples)]]
|
|
@@ -29,6 +33,7 @@ module Specwrk
|
|
|
29
33
|
|
|
30
34
|
def examples
|
|
31
35
|
@examples ||= begin
|
|
36
|
+
return [] if pending.empty?
|
|
32
37
|
bucket_id = with_lock { pending.shift_bucket }
|
|
33
38
|
return [] if bucket_id.nil?
|
|
34
39
|
|