Skip to content

Commit c183397

Browse files
authored
Merge pull request rails#42761 from basecamp/smart-parallel-tests
Parallelize tests only when there are enough to justify the parallelization overhead
2 parents 801980a + ecc5afe commit c183397

File tree

8 files changed

+140
-15
lines changed

8 files changed

+140
-15
lines changed

activesupport/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
* Parallelize tests only when overhead is justified by the number of them
2+
3+
Running tests in parallel adds overhead in terms of database
4+
setup and fixture loading. Now, Rails will only parallelize test executions when
5+
there are enough tests to make it worth it.
6+
7+
This threshold is 50 by default, and is configurable via:
8+
9+
```ruby
10+
config.active_support.test_parallelization_minimum_number_of_tests = 100
11+
```
12+
13+
*Jorge Manrubia*
14+
115
* OpenSSL constants are now used for Digest computations.
216

317
*Dirkjan Bussink*

activesupport/lib/active_support.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def self.eager_load!
8888

8989
cattr_accessor :test_order # :nodoc:
9090
cattr_accessor :test_parallelization_disabled, default: false # :nodoc:
91+
cattr_accessor :test_parallelization_minimum_number_of_tests, default: 50 # :nodoc:
9192

9293
def self.disable_test_parallelization!
9394
self.test_parallelization_disabled = true unless ENV["PARALLEL_WORKERS"]

activesupport/lib/active_support/test_case.rb

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
require "active_support/testing/time_helpers"
1313
require "active_support/testing/file_fixtures"
1414
require "active_support/testing/parallelization"
15+
require "active_support/testing/parallelize_executor"
1516
require "concurrent/utility/processor_counter"
1617

1718
module ActiveSupport
@@ -77,20 +78,7 @@ def parallelize(workers: :number_of_processors, with: :processes)
7778

7879
return if workers <= 1 || ActiveSupport.test_parallelization_disabled
7980

80-
executor = case with
81-
when :processes
82-
Testing::Parallelization.new(workers)
83-
when :threads
84-
Minitest::Parallel::Executor.new(workers)
85-
else
86-
raise ArgumentError, "#{with} is not a supported parallelization executor."
87-
end
88-
89-
self.lock_threads = false if defined?(self.lock_threads) && with == :threads
90-
91-
Minitest.parallel_executor = executor
92-
93-
parallelize_me!
81+
Minitest.parallel_executor = ActiveSupport::Testing::ParallelizeExecutor.new(size: workers, with: with)
9482
end
9583

9684
# Set up hook for parallel testing. This can be used if you have multiple

activesupport/lib/active_support/testing/parallelization.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ def <<(work)
4242
@queue_server << work
4343
end
4444

45+
def size
46+
@worker_count
47+
end
48+
4549
def shutdown
4650
@queue_server.shutdown
4751
@worker_pool.each { |pid| Process.waitpid pid }
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveSupport
4+
module Testing
5+
class ParallelizeExecutor # :nodoc:
6+
attr_reader :size, :parallelize_with, :parallel_executor
7+
8+
def initialize(size:, with:)
9+
@size = size
10+
@parallelize_with = with
11+
@parallel_executor = build_parallel_executor
12+
end
13+
14+
def start
15+
parallelize if should_parallelize?
16+
show_execution_info
17+
18+
parallel_executor.start if parallelized?
19+
end
20+
21+
def <<(work)
22+
parallel_executor << work if parallelized?
23+
end
24+
25+
def shutdown
26+
parallel_executor.shutdown if parallelized?
27+
end
28+
29+
private
30+
def build_parallel_executor
31+
case parallelize_with
32+
when :processes
33+
Testing::Parallelization.new(size)
34+
when :threads
35+
ActiveSupport::TestCase.lock_threads = false if defined?(ActiveSupport::TestCase.lock_threads)
36+
Minitest::Parallel::Executor.new(size)
37+
else
38+
raise ArgumentError, "#{parallelize_with} is not a supported parallelization executor."
39+
end
40+
end
41+
42+
def parallelize
43+
@parallelized = true
44+
Minitest::Test.parallelize_me!
45+
end
46+
47+
def parallelized?
48+
@parallelized
49+
end
50+
51+
def should_parallelize?
52+
ENV["PARALLEL_WORKERS"] || tests_count > ActiveSupport.test_parallelization_minimum_number_of_tests
53+
end
54+
55+
def tests_count
56+
@tests_count ||= Minitest::Runnable.runnables.sum { |runnable| runnable.runnable_methods.size }
57+
end
58+
59+
def show_execution_info
60+
puts execution_info
61+
end
62+
63+
def execution_info
64+
if should_parallelize?
65+
"Running #{tests_count} tests in parallel using #{parallel_executor.size} #{parallelize_with}"
66+
else
67+
"Running #{tests_count} tests in a single process (parallelization threshold is #{ActiveSupport.test_parallelization_minimum_number_of_tests})"
68+
end
69+
end
70+
end
71+
end
72+
end

guides/source/testing.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,16 @@ end
569569
NOTE: With disabled transactional tests, you have to clean up any data tests
570570
create as changes are not automatically rolled back after the test completes.
571571

572+
### Threshold to parallelize tests
573+
574+
Running tests in parallel adds an overhead in terms of database setup and
575+
fixture loading. Because of this, Rails won't parallelize executions that involve
576+
fewer than 50 tests. You can configure this threshold in your `test.rb`:
577+
578+
```ruby
579+
config.active_support.test_parallelization_minimum_number_of_tests = 100
580+
```
581+
572582
The Test Database
573583
-----------------
574584

railties/test/application/configuration_test.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2421,6 +2421,20 @@ class D < C
24212421
assert_equal OpenSSL::Digest::SHA256, ActiveSupport::KeyGenerator.hash_digest_class
24222422
end
24232423

2424+
test "ActiveSupport.test_parallelization_minimum_number_of_tests can be configured via config.active_support.test_parallelization_minimum_number_of_tests" do
2425+
remove_from_config '.*config\.load_defaults.*\n'
2426+
2427+
app_file "config/environments/test.rb", <<-RUBY
2428+
Rails.application.configure do
2429+
config.active_support.test_parallelization_minimum_number_of_tests = 1234
2430+
end
2431+
RUBY
2432+
2433+
app "test"
2434+
2435+
assert_equal 1234, ActiveSupport.test_parallelization_minimum_number_of_tests
2436+
end
2437+
24242438
test "custom serializers should be able to set via config.active_job.custom_serializers in an initializer" do
24252439
class ::DummySerializer < ActiveJob::Serializers::ObjectSerializer; end
24262440

railties/test/application/test_runner_test.rb

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,9 +567,29 @@ def test_run_in_parallel_with_processes
567567
output = run_test_command(file_name)
568568

569569
assert_match %r{Finished in.*\n2 runs, 2 assertions}, output
570+
assert_match %r{Running \d+ tests in parallel using \d+ processes}, output
570571
assert_no_match "create_table(:users)", output
571572
end
572573

574+
def test_avoid_paralleling_when_number_of_tests_if_below_threshold
575+
exercise_parallelization_regardless_of_machine_core_count(with: :processes, threshold: 100)
576+
577+
file_name = create_parallel_processes_test_file
578+
579+
app_file "db/schema.rb", <<-RUBY
580+
ActiveRecord::Schema.define(version: 1) do
581+
create_table :users do |t|
582+
t.string :name
583+
end
584+
end
585+
RUBY
586+
587+
output = run_test_command(file_name)
588+
589+
assert_match %r{Running \d+ tests in a single process}, output
590+
assert_no_match %r{Running \d+ tests in parallel using \d+ processes}, output
591+
end
592+
573593
def test_parallel_is_disabled_when_single_file_is_run
574594
exercise_parallelization_regardless_of_machine_core_count(with: :processes, force: false)
575595

@@ -1140,12 +1160,14 @@ class ParallelTest < ActiveSupport::TestCase
11401160
RUBY
11411161
end
11421162

1143-
def exercise_parallelization_regardless_of_machine_core_count(with:, force: true)
1163+
def exercise_parallelization_regardless_of_machine_core_count(with:, force: true, threshold: 0)
11441164
file_content = ERB.new(<<-ERB, trim_mode: "-").result_with_hash(with: with.to_s, force: force)
11451165
ENV["RAILS_ENV"] ||= "test"
11461166
require_relative "../config/environment"
11471167
require "rails/test_help"
11481168
1169+
ActiveSupport.test_parallelization_minimum_number_of_tests = #{threshold}
1170+
11491171
class ActiveSupport::TestCase
11501172
<%- if force -%>
11511173
# Force parallelization, even with single files

0 commit comments

Comments
 (0)