specwrk 0.17.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93a17f70f0e768222876b22465e7b62d60deb9dbe9761eed9611f31638a7f7cb
4
- data.tar.gz: 9137ab50e74ea4e3e46d74496718ae2830af4bb3c1a51e4b0ac2ba53d1691617
3
+ metadata.gz: fc8e7bf6a7d8bebb0a79a536f403a51fd6cf88948e69e4d03304cd62a039290e
4
+ data.tar.gz: c0f58e84e321b6407cefb18bad2ad42344f8919ff754471725ab498072e54267
5
5
  SHA512:
6
- metadata.gz: cbcbb39d8d270fee9fed3a8befdbee10d7461e082ecd3e6bab0d24562dc00f53e7eb7bc2b8b75065cf2424456ecb1f9364d74658f28a7b49ffb2c2670ff5442b
7
- data.tar.gz: d6eb28d18cdf3aecde015c0da5b39ea529e6118249857e1ea312f4708efb98e3ce7e8a25fae9cc3948870e7afacfafa3e55db46106ae53dd62af8cd261ca1928
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("SPECWERK_SRV_GROUP_BY", "timings"), desc: "How examples will be grouped for workers; fallback to file if no timings are found. Overrides SPECWERK_SRV_GROUP_BY"
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 = Process.fork do
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
- @seed_pid = Process.fork do
392
- require "specwrk/seed_loop"
393
-
394
- ENV["SPECWRK_FORKED"] = "1"
395
- ENV["SPECWRK_SEED"] = "1"
396
-
397
- Specwrk::SeedLoop.loop!(ipc)
398
- end
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
- def initialize
6
- @parent_pid = Process.pid
5
+ attr_reader :parent_socket, :child_socket
7
6
 
8
- @parent_socket, @child_socket = UNIXSocket.pair
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, :parent_socket, :child_socket
36
+ attr_reader :parent_pid
27
37
 
28
38
  def socket
29
39
  child? ? child_socket : parent_socket
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Specwrk
4
- VERSION = "0.17.1"
4
+ VERSION = "0.18.0"
5
5
  end
@@ -9,13 +9,14 @@ module Specwrk
9
9
  EXAMPLE_STATUSES = %w[passed failed pending]
10
10
 
11
11
  def with_response
12
- completed.merge!(completed_examples)
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
- with_lock { pending.push_examples(expired_examples.values) }
21
- processing.delete(*expired_examples.keys.map(&:to_s))
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
 
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.17.1
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Westendorf