New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Parallel testing #31900
Parallel testing #31900
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# frozen_string_literal: true | ||
|
||
require "active_support/testing/parallelization" | ||
|
||
module ActiveRecord | ||
module TestDatabases # :nodoc: | ||
ActiveSupport::Testing::Parallelization.after_fork_hook do |i| | ||
create_and_migrate(i, spec_name: Rails.env) | ||
end | ||
|
||
ActiveSupport::Testing::Parallelization.run_cleanup_hook do |i| | ||
drop(i, spec_name: Rails.env) | ||
end | ||
|
||
def self.create_and_migrate(i, spec_name:) | ||
old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" | ||
|
||
connection_spec = ActiveRecord::Base.configurations[spec_name] | ||
|
||
connection_spec["database"] += "-#{i}" | ||
ActiveRecord::Tasks::DatabaseTasks.create(connection_spec) | ||
ActiveRecord::Base.establish_connection(connection_spec) | ||
ActiveRecord::Tasks::DatabaseTasks.migrate | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I plan on replacing this with a structure load or a straight copy of the database later on but currently structure load doesn't work with multiple databases so sticking with migrate for now. |
||
ensure | ||
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env]) | ||
ENV["VERBOSE"] = old | ||
end | ||
|
||
def self.drop(i, spec_name:) | ||
old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" | ||
connection_spec = ActiveRecord::Base.configurations[spec_name] | ||
|
||
ActiveRecord::Tasks::DatabaseTasks.drop(connection_spec) | ||
ensure | ||
ENV["VERBOSE"] = old | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,7 @@ | ||
* Adds parallel testing to Rails | ||
|
||
Parallelize your test suite with forked processes or threads. | ||
|
||
*Eileen M. Uchitelle*, *Aaron Patterson* | ||
|
||
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activesupport/CHANGELOG.md) for previous changes. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ | |
require "active_support/testing/constant_lookup" | ||
require "active_support/testing/time_helpers" | ||
require "active_support/testing/file_fixtures" | ||
require "active_support/testing/parallelization" | ||
|
||
module ActiveSupport | ||
class TestCase < ::Minitest::Test | ||
|
@@ -39,6 +40,91 @@ def test_order=(new_order) | |
def test_order | ||
ActiveSupport.test_order ||= :random | ||
end | ||
|
||
# Parallelizes the test suite. | ||
# | ||
# Takes a `workers` argument that controls how many times the process | ||
# is forked. For each process a new database will be created suffixed | ||
# with the worker number. | ||
# | ||
# test-database-0 | ||
# test-database-1 | ||
# | ||
# If `ENV["PARALLEL_WORKERS"]` is set the workers argument will be ignored | ||
# and the environment variable will be used instead. This is useful for CI | ||
# environments, or other environments where you may need more workers than | ||
# you do for local testing. | ||
# | ||
# If the number of workers is set to `1` or fewer, the tests will not be | ||
# parallelized. | ||
# | ||
# The default parallelization method is to fork processes. If you'd like to | ||
# use threads instead you can pass `with: :threads` to the `parallelize` | ||
# method. Note the threaded parallelization does not create multiple | ||
# database and will not work with system tests at this time. | ||
# | ||
# parallelize(workers: 2, with: :threads) | ||
# | ||
# The threaded parallelization uses Minitest's parallel exector directly. | ||
# The processes paralleliztion uses a Ruby Drb server. | ||
def parallelize(workers: 2, with: :processes) | ||
workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"] | ||
|
||
return if workers <= 1 | ||
|
||
executor = case with | ||
when :processes | ||
Testing::Parallelization.new(workers) | ||
when :threads | ||
Minitest::Parallel::Executor.new(workers) | ||
else | ||
raise ArgumentError, "#{with} is not a supported parallelization exectutor." | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was fixed by a4e226f |
||
end | ||
|
||
self.lock_threads = false if defined?(self.lock_threads) && with == :threads | ||
|
||
Minitest.parallel_executor = executor | ||
|
||
parallelize_me! | ||
end | ||
|
||
# Set up hook for parallel testing. This can be used if you have multiple | ||
# databases or any behavior that needs to be run after the process is forked | ||
# but before the tests run. | ||
# | ||
# Note: this feature is not available with the threaded parallelization. | ||
# | ||
# In your +test_helper.rb+ add the following: | ||
# | ||
# class ActiveSupport::TestCase | ||
# parallelize_setup do | ||
# # create databases | ||
# end | ||
# end | ||
def parallelize_setup(&block) | ||
ActiveSupport::Testing::Parallelization.after_fork_hook do |worker| | ||
yield worker | ||
end | ||
end | ||
|
||
# Clean up hook for parallel testing. This can be used to drop databases | ||
# if your app uses multiple write/read databases or other clean up before | ||
# the tests finish. This runs before the forked process is closed. | ||
# | ||
# Note: this feature is not available with the threaded parallelization. | ||
# | ||
# In your +test_helper.rb+ add the following: | ||
# | ||
# class ActiveSupport::TestCase | ||
# parallelize_teardown do | ||
# # drop databases | ||
# end | ||
# end | ||
def parallelize_teardown(&block) | ||
ActiveSupport::Testing::Parallelization.run_cleanup_hook do |worker| | ||
yield worker | ||
end | ||
end | ||
end | ||
|
||
alias_method :method_name, :name | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
# frozen_string_literal: true | ||
|
||
require "drb" | ||
require "drb/unix" | ||
|
||
module ActiveSupport | ||
module Testing | ||
class Parallelization # :nodoc: | ||
class Server | ||
include DRb::DRbUndumped | ||
|
||
def initialize | ||
@queue = Queue.new | ||
end | ||
|
||
def record(reporter, result) | ||
reporter.synchronize do | ||
reporter.record(result) | ||
end | ||
end | ||
|
||
def <<(o) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What not use delegator for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering what |
||
@queue << o | ||
end | ||
|
||
def pop; @queue.pop; end | ||
end | ||
|
||
@after_fork_hooks = [] | ||
|
||
def self.after_fork_hook(&blk) | ||
@after_fork_hooks << blk | ||
end | ||
|
||
def self.after_fork_hooks | ||
@after_fork_hooks | ||
end | ||
|
||
@run_cleanup_hooks = [] | ||
|
||
def self.run_cleanup_hook(&blk) | ||
@run_cleanup_hooks << blk | ||
end | ||
|
||
def self.run_cleanup_hooks | ||
@run_cleanup_hooks | ||
end | ||
|
||
def initialize(queue_size) | ||
@queue_size = queue_size | ||
@queue = Server.new | ||
@pool = [] | ||
|
||
@url = DRb.start_service("drbunix:", @queue).uri | ||
end | ||
|
||
def after_fork(worker) | ||
self.class.after_fork_hooks.each do |cb| | ||
cb.call(worker) | ||
end | ||
end | ||
|
||
def run_cleanup(worker) | ||
self.class.run_cleanup_hooks.each do |cb| | ||
cb.call(worker) | ||
end | ||
end | ||
|
||
def start | ||
@pool = @queue_size.times.map do |worker| | ||
fork do | ||
DRb.stop_service | ||
|
||
after_fork(worker) | ||
|
||
queue = DRbObject.new_with_uri(@url) | ||
|
||
while job = queue.pop | ||
klass = job[0] | ||
method = job[1] | ||
reporter = job[2] | ||
result = Minitest.run_one_method(klass, method) | ||
|
||
queue.record(reporter, result) | ||
end | ||
|
||
run_cleanup(worker) | ||
end | ||
end | ||
end | ||
|
||
def <<(work) | ||
@queue << work | ||
end | ||
|
||
def shutdown | ||
@queue_size.times { @queue << nil } | ||
@pool.each { |pid| Process.waitpid pid } | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about underscore
"_#{i}"
to keep file based databases using underscores in file names?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're temporary databases, unless it's going to break file based dbs I don't think underscore vs dash is a big deal.