specwrk 0.15.0 → 0.15.1

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: 63eedb1506644bd71c457579fb2de19779c66c565b2c614284272152506648ca
4
- data.tar.gz: eb5c83efcd7a9d76623867ec42d48a335e2f349c6608326abba643252a31e486
3
+ metadata.gz: 3de4bb5f29a1c817db2c766d49fcd20d2153e6bf9708fc7b5ece2e1101fbb839
4
+ data.tar.gz: 3fb0dc079f9d85461ec245e2b1019d03e94ba7f3403813f2c7558504140b2c37
5
5
  SHA512:
6
- metadata.gz: 8235b7ddcd7bbee6acc73ec3c53bf8bd9987d769babc42159248b514f326b381ad49a4247e12c649d21d700d158c7f1b83b1b5c93ff6200eb38f8e41b46f4df1
7
- data.tar.gz: f98b5df6bb5a72bd515c5324c17c8c76daf1a08c876fc6d4ab8b7a9f9f6a6477248b34dc9a82c73f289f417a8026bd7b2cc6eb367c25948d17c5f0c5d918844d
6
+ metadata.gz: 3bfbec41f429426bcd85e81c2d879c1e3171ce2f0dba908a420890b5fa37373e803540ef92d4bfada74f4e465e4babc558c3e18cf8fa941376eb6e2d0d08cea2
7
+ data.tar.gz: 9096f33b527c0ae8233ae22a01d7e6b53e446b6684b63417120ba9181f7780e2590a4ff59ee5777a06d750341c5f3781f2f3f705c65874b69ae422db812a1ed2
data/README.md CHANGED
@@ -142,6 +142,26 @@ Options:
142
142
  --help, -h # Print this help
143
143
  ```
144
144
 
145
+ ### `specwrk watch -c 8`
146
+ Starts `8` worker processes in watch mode for the current directory. Watched spec files will be distributed across the processes. By default, only looks at `_spec.rb` files. Configure a [watchfile](#create-a-watchfile-for-the-watch-command) to map file changes to spec files (i.e. modification of `app/models/user.rb` should run `spec/models/user_spec.rb` and `spec/system/users_spec.rb`).
147
+
148
+ ```sh
149
+ $ specwrk watch --help
150
+ Command:
151
+ specwrk watch
152
+
153
+ Usage:
154
+ specwrk watch
155
+
156
+ Description:
157
+ Start a server and workers, watch for file changes in the current directory, and execute specs
158
+
159
+ Options:
160
+ --watchfile=VALUE # Path to watchfile configuration, default: "Specwrk.watchfile.rb"
161
+ --count=VALUE, -c VALUE # The number of worker processes you want to start, default: 1
162
+ --help, -h # Print this help
163
+ ```
164
+
145
165
  ## Configuring your test environment
146
166
  If you test suite tracks state, starts servers, etc. and you plan on running many processes on the same node, you'll need to make
147
167
  adjustments to avoid conflicting port usage or database/state mutations.
@@ -204,6 +224,43 @@ Start a persistent Queue Server given one of the following methods
204
224
 
205
225
  See [specwrk serve --help](#specwrk-serve) for all possible configuration options.
206
226
 
227
+ ### Create a watchfile for the `watch` command
228
+ Watch file (default path is `Specwrk.watchfile.rb` in the current directory) is a ruby file that will be instance eval'd to configure the watcher. There are two commands available:
229
+
230
+ 1. `ignore(Regexp)` to define files that should never trigger a run
231
+ 2. `map(Regexp, &blk)` to map a file change to the spec files that should be run to that file change
232
+
233
+ By default, files without a `.rb` extension will be ignored and files ending with `_spec.rb` will be run. Presence of a watchfile will override these defaults.
234
+
235
+ ```ruby
236
+ # Specwrk.watchfile.rb
237
+ # Ignore all files which don't have an .rb extension
238
+ ignore(/^(?!.*\.rb$).+/)
239
+
240
+ # When a _spec.rb file changes, it should be run
241
+ map(/_spec\.rb$/) do |spec_path|
242
+ spec_path
243
+ end
244
+
245
+ # If a file in lib changes, map it to the spec folder for it's spec file
246
+ map(/lib\/.*\.rb$/) do |path|
247
+ path.gsub(/lib\/(.+)\.rb/, "spec/\\1_spec.rb")
248
+ end
249
+
250
+ # If a model file changes (assuming rails app structure), run the model's spec file
251
+ # map(/app\/models\/.*.rb$/) do |path|
252
+ # path.gsub(/app\/models\/(.+)\.rb/, "spec/models/\\1_spec.rb")
253
+ # end
254
+ #
255
+ # If a controlelr file changes (assuming rails app structure), run the controller and system specs file
256
+ # map(/app\/controllers\/.*.rb$/) do |path|
257
+ # [
258
+ # path.gsub(/app\/controllers\/(.+)\.rb/, "spec/controllers/\\1_spec.rb"),
259
+ # path.gsub(/app\/controllers\/(.+)\.rb/, "spec/system/\\1_spec.rb")
260
+ # ]
261
+ # end
262
+ ```
263
+
207
264
  ## Contributing
208
265
 
209
266
  Bug reports and pull requests are welcome on GitHub at https://github.com/dwestendorf/specwrk.
@@ -0,0 +1,25 @@
1
+ # Ignore all files which don't have an .rb extension
2
+ ignore(/^(?!.*\.rb$).+/)
3
+
4
+ # When a _spec.rb file changes, it should be run
5
+ map(/_spec\.rb$/) do |spec_path|
6
+ spec_path
7
+ end
8
+
9
+ # If a file in lib changes, map it to the spec folder for it's spec file
10
+ map(/lib\/.*\.rb$/) do |path|
11
+ path.gsub(/lib\/(.+)\.rb/, "spec/\\1_spec.rb")
12
+ end
13
+
14
+ # If a model file changes (assuming rails app structure), run the model's spec file
15
+ # map(/app\/models\/.*.rb$/) do |path|
16
+ # path.gsub(/app\/models\/(.+)\.rb/, "spec/models/\\1_spec.rb")
17
+ # end
18
+ #
19
+ # If a controlelr file changes (assuming rails app structure), run the controller and system specs file
20
+ # map(/app\/controllers\/.*.rb$/) do |path|
21
+ # [
22
+ # path.gsub(/app\/controllers\/(.+)\.rb/, "spec/controllers/\\1_spec.rb"),
23
+ # path.gsub(/app\/controllers\/(.+)\.rb/, "spec/system/\\1_spec.rb")
24
+ # ]
25
+ # end
data/lib/specwrk/cli.rb CHANGED
@@ -74,6 +74,7 @@ module Specwrk
74
74
  def drain_outputs
75
75
  @final_outputs.each do |reader|
76
76
  reader.each_line { |line| $stdout.print line }
77
+ reader.close
77
78
  end
78
79
  end
79
80
 
@@ -252,7 +253,7 @@ module Specwrk
252
253
  require "specwrk/cli_reporter"
253
254
  status = Specwrk::CLIReporter.new.report
254
255
 
255
- Specwrk.wait_for_pids_exit([web_pid, seed_pid] + @worker_pids)
256
+ Specwrk.wait_for_pids_exit([web_pid, seed_pid])
256
257
  exit(status)
257
258
  end
258
259
 
@@ -262,10 +263,161 @@ module Specwrk
262
263
  end
263
264
  end
264
265
 
266
+ class Watch < Dry::CLI::Command
267
+ desc "Start a server and workers, watch for file changes in the current directory, and execute specs"
268
+ option :watchfile, type: :string, default: "Specwrk.watchfile.rb", desc: "Path to watchfile configuration"
269
+ option :count, type: :integer, default: 1, aliases: ["-c"], desc: "The number of worker processes you want to start"
270
+
271
+ def call(count:, watchfile:, **args)
272
+ $stdout.sync = true
273
+
274
+ # nil this env var if it exists to prevent never-ending workers
275
+ ENV["SPECWRK_SRV_URI"] = nil
276
+ ENV["SPECWRK_SEED_WAITS"] = "0"
277
+ ENV["SPECWRK_MAX_BUCKET_SIZE"] = "1"
278
+ ENV["SPECWRK_COUNT"] = count.to_s
279
+ ENV["SPECWRK_RUN"] = "watch"
280
+
281
+ web_pid
282
+
283
+ return if Specwrk.force_quit
284
+
285
+ seed_pid
286
+
287
+ start_watcher(watchfile)
288
+
289
+ require "specwrk/cli_reporter"
290
+
291
+ loop do
292
+ status "👀 Watching for file changes..."
293
+
294
+ @worker_pids = nil
295
+ Thread.pass until file_queue.length.positive? || Specwrk.force_quit
296
+
297
+ break if Specwrk.force_quit
298
+
299
+ files = []
300
+ files.push(file_queue.pop) until file_queue.length.zero?
301
+ status "Running specs for #{files.join(" ")}..."
302
+ ipc.write(files.join(" "))
303
+
304
+ example_count = ipc.read.to_i
305
+ if example_count.positive?
306
+ puts "\n🌱 Seeded #{example_count} examples for execution\n"
307
+ else
308
+ puts "\n🙅 No examples to seed for execution\n"
309
+ end
310
+
311
+ next if example_count.zero?
312
+
313
+ return if Specwrk.force_quit
314
+ start_workers
315
+
316
+ Specwrk.wait_for_pids_exit(@worker_pids)
317
+
318
+ drain_outputs
319
+ return if Specwrk.force_quit
320
+
321
+ Specwrk::CLIReporter.new.report
322
+ puts
323
+ $stdout.flush
324
+ end
325
+
326
+ ipc.write "INT" # wakes the socket
327
+ Specwrk.wait_for_pids_exit([web_pid, seed_pid])
328
+ end
329
+
330
+ private
331
+
332
+ def web_pid
333
+ @web_pid ||= Process.fork do
334
+ require "specwrk/web"
335
+ require "specwrk/web/app"
336
+
337
+ ENV["SPECWRK_FORKED"] = "1"
338
+ status "Starting queue server..."
339
+ Specwrk::Web::App.run!
340
+ end
341
+ end
342
+
343
+ def seed_pid
344
+ @seed_pid ||= begin
345
+ ipc # must be initialized in the parent process
346
+
347
+ @seed_pid = Process.fork do
348
+ require "specwrk/seed_loop"
349
+
350
+ ENV["SPECWRK_FORKED"] = "1"
351
+ ENV["SPECWRK_SEED"] = "1"
352
+
353
+ Specwrk::SeedLoop.loop!(ipc)
354
+ end
355
+ end
356
+ end
357
+
358
+ def ipc
359
+ @ipc ||= begin
360
+ require "specwrk/ipc"
361
+
362
+ Specwrk::IPC.new
363
+ end
364
+ end
365
+
366
+ def start_watcher(watchfile)
367
+ require "specwrk/watcher"
368
+
369
+ Specwrk::Watcher.watch(Dir.pwd, file_queue, watchfile)
370
+ end
371
+
372
+ def file_queue
373
+ @file_queue ||= Queue.new
374
+ end
375
+
376
+ def status(msg)
377
+ print "\e[2K\r#{msg}"
378
+ $stdout.flush
379
+ end
380
+
381
+ def start_workers
382
+ @final_outputs = []
383
+ @worker_pids = worker_count.times.map do |i|
384
+ reader, writer = IO.pipe
385
+ @final_outputs << reader
386
+
387
+ Process.fork do
388
+ ENV["TEST_ENV_NUMBER"] = ENV["SPECWRK_FORKED"] = (i + 1).to_s
389
+ ENV["SPECWRK_ID"] = "specwrk-worker-#{i + 1}"
390
+
391
+ $final_output = writer # standard:disable Style/GlobalVars
392
+ $final_output.sync = true # standard:disable Style/GlobalVars
393
+ reader.close
394
+
395
+ require "specwrk/worker"
396
+
397
+ status = Specwrk::Worker.run!
398
+ $final_output.close # standard:disable Style/GlobalVars
399
+ exit(status)
400
+ end.tap { writer.close }
401
+ end
402
+ end
403
+
404
+ def drain_outputs
405
+ @final_outputs.each do |reader|
406
+ reader.each_line { |line| $stdout.print line }
407
+ reader.close
408
+ end
409
+ end
410
+
411
+ def worker_count
412
+ @worker_count ||= [1, ENV["SPECWRK_COUNT"].to_i].max
413
+ end
414
+ end
415
+
265
416
  register "version", Version, aliases: ["v", "-v", "--version"]
266
417
  register "work", Work, aliases: ["wrk", "twerk", "w"]
267
418
  register "serve", Serve, aliases: ["srv", "s"]
268
419
  register "seed", Seed
269
420
  register "start", Start
421
+ register "watch", Watch, aliases: ["w", "👀"]
270
422
  end
271
423
  end
@@ -0,0 +1,36 @@
1
+ require "socket"
2
+
3
+ module Specwrk
4
+ class IPC
5
+ def initialize
6
+ @parent_pid = Process.pid
7
+
8
+ @parent_socket, @child_socket = UNIXSocket.pair
9
+ end
10
+
11
+ def write(msg)
12
+ socket.puts msg.to_s
13
+ end
14
+
15
+ def read
16
+ IO.select([socket])
17
+
18
+ data = socket.gets&.chomp
19
+ return if data.nil? || data.length.zero? || data == "INT"
20
+
21
+ data
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :parent_pid, :parent_socket, :child_socket
27
+
28
+ def socket
29
+ child? ? child_socket : parent_socket
30
+ end
31
+
32
+ def child?
33
+ Process.pid != parent_pid
34
+ end
35
+ end
36
+ end
@@ -11,6 +11,7 @@ module Specwrk
11
11
  end
12
12
 
13
13
  def examples
14
+ reset!
14
15
  return @examples if defined?(@examples)
15
16
 
16
17
  @examples = []
@@ -38,6 +39,30 @@ module Specwrk
38
39
 
39
40
  private
40
41
 
42
+ def reset!
43
+ return unless ENV["SPECWRK_SEED"]
44
+ RSpec.clear_examples
45
+
46
+ # see https://github.com/rspec/rspec-core/pull/2723
47
+ if Gem::Version.new(RSpec::Core::Version::STRING) <= Gem::Version.new("3.9.1")
48
+ RSpec.world.instance_variable_set(
49
+ :@example_group_counts_by_spec_file, Hash.new(0)
50
+ )
51
+ end
52
+
53
+ # RSpec.clear_examples does not reset those, which causes issues when
54
+ # a non-example error occurs (subsequent jobs are not executed)
55
+ RSpec.world.non_example_failure = false
56
+
57
+ # we don't want an error that occured outside of the examples (which
58
+ # would set this to `true`) to stop the worker
59
+ RSpec.world.wants_to_quit = Specwrk.force_quit
60
+
61
+ RSpec.configuration.silence_filter_announcements = true
62
+
63
+ true
64
+ end
65
+
41
66
  def out
42
67
  @out ||= Tempfile.new.tap do |f|
43
68
  f.define_singleton_method(:tty?) { true }
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specwrk/list_examples"
4
+ require "specwrk/client"
5
+
6
+ module Specwrk
7
+ class SeedLoop
8
+ def self.loop!(ipc)
9
+ Client.wait_for_server!
10
+
11
+ loop do
12
+ break if Specwrk.force_quit
13
+
14
+ files = ipc.read
15
+
16
+ next unless files
17
+ examples = ListExamples.new(files.split(" ")).examples
18
+
19
+ client = Client.new
20
+ client.seed(examples, 0)
21
+ client.close
22
+
23
+ ipc.write examples.length
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Specwrk
4
- VERSION = "0.15.0"
4
+ VERSION = "0.15.1"
5
5
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "listen"
4
+
5
+ module Specwrk
6
+ class Watcher
7
+ class Config
8
+ def self.load(file)
9
+ if file && File.exist?(file)
10
+ new(false).tap { |instance| instance.instance_eval(File.read(file), file, 1) }
11
+ else
12
+ new
13
+ end
14
+ end
15
+
16
+ def initialize(default_config = true)
17
+ if default_config
18
+ @mappings = [
19
+ [/_spec\.rb$/, proc { |changed_file_path| changed_file_path }]
20
+ ]
21
+
22
+ @ignore_patterns = [/^(?!.*\.rb$).+/]
23
+ else
24
+ @mappings = []
25
+ @ignore_patterns = []
26
+ end
27
+ end
28
+
29
+ def map(pattern, &block)
30
+ @mappings << [pattern, block]
31
+ end
32
+
33
+ def ignore(*patterns)
34
+ @ignore_patterns.concat(patterns)
35
+ end
36
+
37
+ def spec_files_for(path)
38
+ return [] if @ignore_patterns.any? { |pattern| pattern.match? path }
39
+
40
+ @mappings.map do |pattern, block|
41
+ next unless pattern.match? path
42
+
43
+ block.call(path)
44
+ end.flatten.compact.uniq
45
+ end
46
+ end
47
+
48
+ def self.watch(dir, queue, watchfile)
49
+ instance = new(dir, queue, watchfile)
50
+
51
+ instance.start
52
+ end
53
+
54
+ def initialize(dir, queue, watchfile = "Specwrk.watchfile.rb")
55
+ @dir = dir
56
+ @queue = queue
57
+ @config = Config.load(watchfile)
58
+ end
59
+
60
+ def start
61
+ listener.start
62
+ end
63
+
64
+ def push(paths)
65
+ paths.each do |path|
66
+ relative_path = Pathname.new(path).relative_path_from(Pathname.new(dir)).to_s
67
+
68
+ spec_files = config.spec_files_for(relative_path)
69
+
70
+ spec_files.each do |spec_file_path|
71
+ queue.push(spec_file_path) if File.exist?(spec_file_path)
72
+ end
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :dir, :queue, :config
79
+
80
+ def listener
81
+ @listener ||= Listen.to(dir) do |modified, added|
82
+ push(modified)
83
+ push(added)
84
+ end
85
+ end
86
+ end
87
+ 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.0
4
+ version: 0.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Westendorf
@@ -79,6 +79,34 @@ dependencies:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: listen
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: logger
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
82
110
  - !ruby/object:Gem::Dependency
83
111
  name: rackup
84
112
  requirement: !ruby/object:Gem::Requirement
@@ -177,6 +205,7 @@ files:
177
205
  - LICENSE.txt
178
206
  - README.md
179
207
  - Rakefile
208
+ - Specwrk.watchfile.rb
180
209
  - config.ru
181
210
  - docker/Dockerfile.server
182
211
  - docker/entrypoint.server.sh
@@ -190,12 +219,15 @@ files:
190
219
  - lib/specwrk/cli_reporter.rb
191
220
  - lib/specwrk/client.rb
192
221
  - lib/specwrk/hookable.rb
222
+ - lib/specwrk/ipc.rb
193
223
  - lib/specwrk/list_examples.rb
224
+ - lib/specwrk/seed_loop.rb
194
225
  - lib/specwrk/store.rb
195
226
  - lib/specwrk/store/base_adapter.rb
196
227
  - lib/specwrk/store/file_adapter.rb
197
228
  - lib/specwrk/store/memory_adapter.rb
198
229
  - lib/specwrk/version.rb
230
+ - lib/specwrk/watcher.rb
199
231
  - lib/specwrk/web.rb
200
232
  - lib/specwrk/web/app.rb
201
233
  - lib/specwrk/web/auth.rb