rspec-core-parallel 0.2.1 → 0.4.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: 77f7b9d3542750b2b8a90a8f2ec8c8730205432dbdcd2615516684939343d300
4
- data.tar.gz: 7107cf809562b46795cb35f720b709d6892ec758bedfeba55e3df22e9214272f
3
+ metadata.gz: 959cdd2e6744a83648486825d673ea9164f39f2aa0fbafafd2f4b49eda5163ae
4
+ data.tar.gz: 8657334668c6e6aaeb8ceca2fabf5c413d1860f19379e34d463a26b01bcdb837
5
5
  SHA512:
6
- metadata.gz: c5243cfee9b3851588bd9cd041a34e9f0f090568b4519bb08942f6f4f82e3ca891b2e99f851192922f7f623728e667cfd6ffe489487733b71271b01e72008114
7
- data.tar.gz: 0672ec4583dacff0e6e8a69e1b394b3bb197bc6ff647ef868c1db2a9ba5d7217a55c32d1c30bf6badbd8fd1bd42517049cab0a59029e35a2058ecd66fa33ab68
6
+ metadata.gz: d12df7ed1e4abe97795bc1643779d16dd9c348458121952733e974340ae5024abe3e7b18840c308294b8955b5294cb77353506276e0428dc5dfd6775f9080430
7
+ data.tar.gz: '02925aa2a3aff6acbe74d6c499802d247942cbc83336cba8dce061cba2d41fe73a62d230cac84f26e1b2e70ed73f94414a5c264cd509735fa1226644c6816b2e'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ * In Rails apps every worker writes to its own log file (`log/test-1.log`,
6
+ `log/test-2.log`...) instead of `log/test.log`.
7
+
8
+ ## 0.3.0
9
+
10
+ * `before(:run)` and `after(:run)` hooks, which run once per run: in the main
11
+ process around the workers in parallel runs, around the `:suite` hooks in
12
+ sequential runs.
13
+ * SimpleCov merges the coverage of workers that finished more than its
14
+ `merge_timeout` (10 minutes by default) before the slowest one.
15
+
3
16
  ## 0.2.1
4
17
 
5
18
  * No deprecation warning with SimpleCov 1.x, which renamed
data/README.md CHANGED
@@ -68,6 +68,9 @@ loaded from the schema when needed, like `rails test` does in parallel. Set
68
68
  (`maintain_test_schema!` runs before the workers are forked), so on CI create it as usual, e.g. with
69
69
  `bin/rails db:create db:schema:load`, rather than with `parallel:create`.
70
70
 
71
+ **Logs.** In Rails apps every worker writes to its own log file (`log/test-1.log`, `log/test-2.log`, ...), so that the
72
+ lines of the workers don't mix in `log/test.log`, which keeps the lines of the main process.
73
+
71
74
  **Other resources.** Each worker gets a `TEST_ENV_NUMBER` environment variable (`""`, `"2"`, `"3"`, ...) as with
72
75
  parallel_tests, and runs the `parallel_worker_setup` hooks. Workers are forked after the suite is loaded, so anything
73
76
  set up while loading, like a constant with a directory or a Redis client, is the same in all of them. This also applies
@@ -83,7 +86,18 @@ RSpec.configure do |config|
83
86
  end
84
87
  ```
85
88
 
86
- **Hooks.** `before(:suite)`, `after(:suite)` and `at_exit` hooks run in every worker.
89
+ **Hooks.** `before(:suite)`, `after(:suite)` and `at_exit` hooks run in every worker. For what all the workers share,
90
+ like starting a service or cleaning it up, use `before(:run)` and `after(:run)`: they run once, in the main process,
91
+ before the workers are forked and after they have exited. In sequential runs they run around the `:suite` hooks.
92
+
93
+ ```ruby
94
+ RSpec.configure do |config|
95
+ config.before(:run) { TestServer.start }
96
+ config.after(:run) { TestServer.stop }
97
+ end
98
+ ```
99
+
100
+ When a `before(:run)` hook fails, no example runs, but the `after(:run)` hooks still do.
87
101
 
88
102
  ## Code coverage
89
103
 
@@ -107,8 +107,74 @@ module RSpec
107
107
  Parallel.supported? ? Parallel.default_worker_count : 1
108
108
  end
109
109
 
110
+ # `before(:run)` and `after(:run)` hooks run once per run: around the
111
+ # `:suite` hooks in sequential runs, and in the main process around
112
+ # the workers in parallel runs, where `:suite` hooks run in every
113
+ # worker. Use them for what all the workers share, like starting a
114
+ # service or cleaning it up.
115
+ def before(scope=nil, *meta, &block)
116
+ return super unless scope == :run
117
+
118
+ run_hooks(:before) << Hooks::BeforeHook.new(block, {})
119
+ end
120
+ alias_method :append_before, :before
121
+
122
+ def prepend_before(scope=nil, *meta, &block)
123
+ return super unless scope == :run
124
+
125
+ run_hooks(:before).unshift Hooks::BeforeHook.new(block, {})
126
+ end
127
+
128
+ def after(scope=nil, *meta, &block)
129
+ return super unless scope == :run
130
+
131
+ run_hooks(:after).unshift Hooks::AfterHook.new(block, {})
132
+ end
133
+ alias_method :prepend_after, :after
134
+
135
+ def append_after(scope=nil, *meta, &block)
136
+ return super unless scope == :run
137
+
138
+ run_hooks(:after) << Hooks::AfterHook.new(block, {})
139
+ end
140
+
141
+ def with_suite_hooks
142
+ return super if parallel_worker_number
143
+
144
+ with_run_hooks { super }
145
+ end
146
+
147
+ # @private
148
+ #
149
+ # Examples are not run when a `before(:run)` hook fails, but all the
150
+ # `after(:run)` hooks are, so that everything started is stopped.
151
+ def with_run_hooks
152
+ return yield if dry_run?
153
+
154
+ begin
155
+ run_run_hooks('a `before(:run)` hook', run_hooks(:before), :stop_on_failure) ? yield : false
156
+ ensure
157
+ run_run_hooks('an `after(:run)` hook', run_hooks(:after))
158
+ end
159
+ end
160
+
110
161
  private
111
162
 
163
+ def run_hooks(position)
164
+ (@run_hooks ||= { :before => [], :after => [] })[position]
165
+ end
166
+
167
+ def run_run_hooks(description, hooks, stop_on_failure=false)
168
+ context = SuiteHookContext.new(description, reporter)
169
+ hooks.each do |hook|
170
+ hook.run(context)
171
+ rescue Support::AllExceptionsExceptOnesWeMustNotRescue => e
172
+ context.set_exception(e)
173
+ return false if stop_on_failure
174
+ end
175
+ true
176
+ end
177
+
112
178
  # rspec-core runs the default path (`spec`) when no files are given
113
179
  # only when its command is `rspec`.
114
180
  def command
@@ -148,7 +214,7 @@ module RSpec
148
214
 
149
215
  examples_count = @world.example_count(example_groups)
150
216
  examples_passed = @configuration.reporter.report(examples_count) do |reporter|
151
- Parallel.run(@configuration, @world, parallel_units, reporter)
217
+ @configuration.with_run_hooks { Parallel.run(@configuration, @world, parallel_units, reporter) }
152
218
  end
153
219
 
154
220
  exit_code(examples_passed)
@@ -11,6 +11,8 @@ module RSpec
11
11
  # `ActiveSupport.parallelize_test_databases` to `false` to opt out.
12
12
  # Workers are numbered from 0 for these hooks, as in Rails. Rails
13
13
  # versions before 8.1 have no hooks to run before forking.
14
+ # * gives every worker its own log file (`log/test-1.log`, `log/test-2.log`...),
15
+ # rather than mixing the lines of all the workers in `log/test.log`.
14
16
  # * loads the translations before forking, as Rails does when eager
15
17
  # loading, rather than in every worker.
16
18
  #
@@ -22,9 +24,39 @@ module RSpec
22
24
  end
23
25
 
24
26
  def self.after_fork(worker_number)
27
+ split_log(worker_number)
25
28
  hooks(:after_fork_hooks).each { |hook| hook.call(worker_number - 1) }
26
29
  end
27
30
 
31
+ # Rails opens the log file itself and hands the open file to its
32
+ # logger, so the path comes from the file the logger writes to.
33
+ # Loggers writing elsewhere, like to STDOUT, are left as they are.
34
+ def self.split_log(worker_number)
35
+ return unless defined?(::Rails.logger) && ::Rails.logger
36
+
37
+ loggers = ::Rails.logger.respond_to?(:broadcasts) ? ::Rails.logger.broadcasts : [::Rails.logger]
38
+ loggers.each do |logger|
39
+ device = logger.instance_variable_get(:@logdev)&.dev
40
+ next unless device.is_a?(::File)
41
+
42
+ logger.reopen(open_log_file(worker_log_path(device.path, worker_number)))
43
+ end
44
+ end
45
+
46
+ def self.worker_log_path(path, worker_number)
47
+ extension = ::File.extname(path)
48
+ "#{path.delete_suffix(extension)}-#{worker_number}#{extension}"
49
+ end
50
+
51
+ # As Rails opens its own log file: in binary mode, so that lines in
52
+ # any encoding can be written, and without buffering.
53
+ def self.open_log_file(path)
54
+ file = ::File.open(path, 'a')
55
+ file.binmode
56
+ file.sync = true
57
+ file
58
+ end
59
+
28
60
  def self.run_cleanup(worker_number)
29
61
  hooks(:run_cleanup_hooks).each { |hook| hook.call(worker_number - 1) }
30
62
  end
@@ -16,6 +16,8 @@ module RSpec
16
16
  # `TEST_ENV_NUMBER`), so workers finishing early checked the coverage
17
17
  # collected so far and printed false "below minimum coverage"
18
18
  # warnings. The main process exits after all the workers.
19
+ # * extends SimpleCov's `merge_timeout` by the time the workers ran, so
20
+ # that the main process merges the results of all of them.
19
21
  #
20
22
  # @private
21
23
  module SimpleCovIntegration
@@ -28,7 +30,10 @@ module RSpec
28
30
 
29
31
  def self.before_fork
30
32
  @command_name = nil
33
+ @forked_at = nil
31
34
  return unless defined?(::SimpleCov) && defined?(::Coverage.running?) && ::Coverage.running?
35
+
36
+ @forked_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
32
37
  return if ::SimpleCov.enabled_for_subprocesses?
33
38
 
34
39
  @command_name = ::SimpleCov.command_name
@@ -47,6 +52,15 @@ module RSpec
47
52
  ::SimpleCov.command_name "#{@command_name} (worker #{worker_number})" if @command_name
48
53
  ::SimpleCov.singleton_class.__send__(:prepend, NotFinalResultProcess)
49
54
  end
55
+
56
+ # SimpleCov merges only results younger than `merge_timeout`: those of
57
+ # workers finished long before the slowest one would be left out.
58
+ def self.after_workers
59
+ return unless @forked_at && ::SimpleCov.respond_to?(:merge_timeout)
60
+
61
+ workers_time = (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @forked_at).ceil
62
+ ::SimpleCov.merge_timeout(::SimpleCov.merge_timeout + workers_time)
63
+ end
50
64
  end
51
65
  end
52
66
  end
@@ -1,7 +1,7 @@
1
1
  module RSpec
2
2
  module Core
3
3
  module Parallel
4
- VERSION = '0.2.1'
4
+ VERSION = '0.4.0'
5
5
  end
6
6
  end
7
7
  end
@@ -51,6 +51,7 @@ module RSpec
51
51
  def self.run(configuration, world, units, reporter)
52
52
  worker_count = [configuration.parallel_workers.to_i, units.size].min
53
53
  passed = Coordinator.new(configuration, world, worker_count).run(units, reporter)
54
+ SimpleCovIntegration.after_workers
54
55
  persist_run_times(configuration, world)
55
56
  passed
56
57
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-core-parallel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Rodi