specwrk 0.14.1 → 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: 29f7cb9c448edda0afc2ec56876818525e89a4a27fcacf14999124b299e43522
4
- data.tar.gz: b47405dbb97ede3bd1d97766b21fd8ce4e35638664a779ea4723eca75b55b685
3
+ metadata.gz: 3de4bb5f29a1c817db2c766d49fcd20d2153e6bf9708fc7b5ece2e1101fbb839
4
+ data.tar.gz: 3fb0dc079f9d85461ec245e2b1019d03e94ba7f3403813f2c7558504140b2c37
5
5
  SHA512:
6
- metadata.gz: 68e6b8e27da121cfd0b74142528ce9cb238c721a75e55dabca9a58fe35b63364c2315a9a0a1a239f2906c386682ca7ef5e6d9b78535ac3cfb9ffc9b8ec141f73
7
- data.tar.gz: 8b26d7d1def9ead69b0583786d54c03251edfe7944631a7f53cb882c655e798b8038bffdb97328ca1318d243079073824a5be5dbcb693f6d60bd0ceb25aa7164
6
+ metadata.gz: 3bfbec41f429426bcd85e81c2d879c1e3171ce2f0dba908a420890b5fa37373e803540ef92d4bfada74f4e465e4babc558c3e18cf8fa941376eb6e2d0d08cea2
7
+ data.tar.gz: 9096f33b527c0ae8233ae22a01d7e6b53e446b6684b63417120ba9181f7780e2590a4ff59ee5777a06d750341c5f3781f2f3f705c65874b69ae422db812a1ed2
data/.circleci/config.yml CHANGED
@@ -83,6 +83,17 @@ jobs:
83
83
  - .specwrk/report.json
84
84
  key: specwrk-{{ .Branch }}
85
85
  ## /SPECWRK STEP ##
86
+
87
+ - run:
88
+ name: Echo the workers output for giggles
89
+ command: |
90
+ shopt -s nullglob
91
+ for file in /tmp/specwrk/main/*.ndjson; do
92
+ echo "===== $file ====="
93
+ cat "$file"
94
+ echo
95
+ done
96
+
86
97
 
87
98
  specwrk-multi-node-prepare:
88
99
  docker:
@@ -116,7 +127,6 @@ jobs:
116
127
  --key "$SPECWRK_KEY" \
117
128
  --run "$CIRCLE_WORKFLOW_ID" \
118
129
  spec/
119
-
120
130
  ## /SPECWRK STEP ##
121
131
 
122
132
  specwrk-multi-node:
@@ -154,3 +164,14 @@ jobs:
154
164
  --run "$CIRCLE_WORKFLOW_ID" \
155
165
  --count 2
156
166
  ## /SPECWRK STEP ##
167
+
168
+ - run:
169
+ name: Echo the workers output for giggles
170
+ command: |
171
+ shopt -s nullglob
172
+ for file in /tmp/specwrk/main/*.ndjson; do
173
+ echo "===== $file ====="
174
+ cat "$file"
175
+ echo
176
+ done
177
+
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
@@ -19,10 +19,25 @@ module Specwrk
19
19
  puts "\nFinished in #{Specwrk.human_readable_duration total_duration} " \
20
20
  "(total execution time of #{Specwrk.human_readable_duration total_run_time})\n"
21
21
 
22
+ if flake_count.positive?
23
+ puts "\nFlaked examples:\n\n"
24
+ flake_reruns_lines.each { |(command, description)| print "#{colorizer.wrap(command, :magenta)} #{colorizer.wrap(description, :cyan)}\n" }
25
+ puts ""
26
+ end
27
+
22
28
  client.shutdown
23
29
 
24
30
  if failure_count.positive?
25
31
  puts colorizer.wrap(totals_line, :red)
32
+
33
+ puts "\nFailed examples:\n\n"
34
+ failure_reruns_lines.each { |(command, description)| print "#{colorizer.wrap(command, :red)} #{colorizer.wrap(description, :cyan)}\n" }
35
+ puts ""
36
+
37
+ 1
38
+ elsif unexecuted_count.positive?
39
+ puts colorizer.wrap(totals_line, :red)
40
+
26
41
  1
27
42
  elsif pending_count.positive?
28
43
  puts colorizer.wrap(totals_line, :yellow)
@@ -44,11 +59,29 @@ module Specwrk
44
59
  def totals_line
45
60
  summary = RSpec::Core::Formatters::Helpers.pluralize(example_count, "example") +
46
61
  ", " + RSpec::Core::Formatters::Helpers.pluralize(failure_count, "failure")
47
- summary += ", #{pending_count} pending" if pending_count > 0
62
+ summary += ", #{pending_count} pending" if pending_count.positive?
63
+ summary += ", #{RSpec::Core::Formatters::Helpers.pluralize(flake_count, "example")} flaked #{RSpec::Core::Formatters::Helpers.pluralize(total_flakes, "time")}" if flake_count.positive?
64
+ summary += ". #{RSpec::Core::Formatters::Helpers.pluralize(unexecuted_count, "example")} not executed" if unexecuted_count.positive?
48
65
 
49
66
  summary
50
67
  end
51
68
 
69
+ def failure_reruns_lines
70
+ @failure_reruns_lines ||= report_data.dig(:examples).values.map do |example|
71
+ next unless example[:status] == "failed"
72
+
73
+ ["rspec #{example[:file_path]}:#{example[:line_number]}", "# #{example[:full_description]}"]
74
+ end.compact
75
+ end
76
+
77
+ def flake_reruns_lines
78
+ @flake_reruns_lines ||= report_data.dig(:flakes).map do |example_id, count|
79
+ example = report_data.dig(:examples, example_id)
80
+
81
+ ["rspec #{example[:file_path]}:#{example[:line_number]}", "# #{example[:full_description]}. Failed #{RSpec::Core::Formatters::Helpers.pluralize(count, "time")} before passing."]
82
+ end.compact
83
+ end
84
+
52
85
  def report_data
53
86
  @report_data ||= client.report
54
87
  end
@@ -69,10 +102,22 @@ module Specwrk
69
102
  report_data.dig(:meta, :pending)
70
103
  end
71
104
 
105
+ def unexecuted_count
106
+ report_data.dig(:meta, :unexecuted)
107
+ end
108
+
72
109
  def example_count
73
110
  report_data.dig(:examples).length
74
111
  end
75
112
 
113
+ def flake_count
114
+ report_data.dig(:flakes).length
115
+ end
116
+
117
+ def total_flakes
118
+ @total_flakes ||= report_data.dig(:flakes).values.sum
119
+ end
120
+
76
121
  def client
77
122
  @client ||= Client.new
78
123
  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.14.1"
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
@@ -145,6 +145,7 @@ module Specwrk
145
145
 
146
146
  def with_response
147
147
  pending.clear
148
+ processing.clear
148
149
  failure_counts.clear
149
150
 
150
151
  pending.max_retries = payload.fetch(:max_retries, "0").to_i
@@ -331,15 +332,16 @@ module Specwrk
331
332
 
332
333
  class Report < Base
333
334
  def with_response
334
- [200, {"content-type" => "application/json"}, [JSON.generate(completed.dump)]]
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)]]
335
340
  end
336
341
  end
337
342
 
338
343
  class Shutdown < Base
339
344
  def with_response
340
- pending.clear
341
- processing.clear
342
-
343
345
  interupt! if ENV["SPECWRK_SRV_SINGLE_RUN"]
344
346
 
345
347
  [200, {"content-type" => "text/plain"}, ["✌️"]]
@@ -13,7 +13,7 @@ module Specwrk
13
13
 
14
14
  def stop(group_notification)
15
15
  group_notification.notifications.map do |notification|
16
- examples << {
16
+ hash = {
17
17
  id: notification.example.id,
18
18
  full_description: notification.example.full_description,
19
19
  status: notification.example.execution_result.status,
@@ -23,6 +23,16 @@ module Specwrk
23
23
  finished_at: notification.example.execution_result.finished_at.iso8601(6),
24
24
  run_time: notification.example.execution_result.run_time
25
25
  }
26
+
27
+ if (e = notification.example.exception)
28
+ hash[:exception] = {
29
+ class: e.class.name,
30
+ message: e.message,
31
+ backtrace: notification.formatted_backtrace
32
+ }
33
+ end
34
+
35
+ examples << hash
26
36
  end
27
37
  end
28
38
  end
@@ -24,12 +24,13 @@ module Specwrk
24
24
 
25
25
  example_ids = examples.map { |example| example[:id] }
26
26
 
27
- options = RSpec::Core::ConfigurationOptions.new rspec_options + example_ids
27
+ options = RSpec::Core::ConfigurationOptions.new ["--format", "Specwrk::Worker::NullFormatter"] + example_ids
28
28
  RSpec::Core::Runner.new(options).run($stderr, $stdout)
29
29
  end
30
30
 
31
31
  # https://github.com/skroutz/rspecq/blob/341383ce3ca25f42fad5483cbb6a00ba1c405570/lib/rspecq/worker.rb#L208-L224
32
32
  def reset!
33
+ flush_log
33
34
  completion_formatter.examples.clear
34
35
 
35
36
  RSpec.clear_examples
@@ -71,13 +72,25 @@ module Specwrk
71
72
  @completion_formatter ||= CompletionFormatter.new
72
73
  end
73
74
 
74
- def rspec_options
75
- @rspec_options ||= if ENV["SPECWRK_OUT"]
76
- ["--format", "json", "--out", File.join(ENV["SPECWRK_OUT"], "#{ENV.fetch("SPECWRK_ID", "specwrk-worker")}.json")]
75
+ def flush_log
76
+ completion_formatter.examples.each { |example| json_log_file.puts example }
77
+ end
78
+
79
+ def json_log_file
80
+ @json_log_file ||= if json_log_file_path
81
+ FileUtils.mkdir_p(File.dirname(json_log_file_path))
82
+ File.truncate(json_log_file_path, 0) if File.exist?(json_log_file_path)
83
+ File.open(json_log_file_path, "a", sync: true)
77
84
  else
78
- ["--format", "Specwrk::Worker::NullFormatter"]
85
+ File.open(File::NULL, "a")
79
86
  end
80
87
  end
88
+
89
+ def json_log_file_path
90
+ return unless ENV["SPECWRK_OUT"]
91
+
92
+ @json_log_file_path ||= File.join(ENV["SPECWRK_OUT"], ENV["SPECWRK_RUN"], "#{ENV["SPECWRK_FORKED"]}.ndjson")
93
+ end
81
94
  end
82
95
  end
83
96
  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.14.1
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