minitest/minitest-rails

Parallel testing causes DRb error in Rails 6.0.0.rc1

Closed this issue · 6 comments

I've been using your gem on a project (Rails 6.0.0.rc1) but had to set parallelize(workers: 1) because I keep on getting the following error.

/.rbenv/versions/2.5.1/lib/ruby/2.5.0/drb/drb.rb:1733:in 'current_server': DRb::DRbServerNotFound (DRb::DRbConnError)

I've created an app that errors here - https://github.com/benaldred/rails-6-minitest-drb-error

  1. It's a vanilla rails 6 rc1 app with the install instructions followed from this projects README.
  2. Ran tests and then ran fine with no DB
  3. Ran the generator to add a User model and un commented the tests
  4. Ran tests rails test and I got the error

I had to remove spring. It kept on hanging and I think I read that parallel tests don't work with Spring.

If I set parallelize(workers: 1) all works as expected

Any ideas?

Very interesting. If you change the describe User do in your user_test.rb file to class UserTest < ActiveSupport::TestCase then it works just fine. So something is definitely up with how the spec DSL is interacting with parallelize.

Thanks for the report!

I think this has to do with how the spec DSL creates anonymous classes for tests. When defining the test using a class the job that is being pushed onto the Drb queue looks like this:

[UserTest, "test_0001_does a thing", #<Minitest::CompositeReporter:0x...>]

And when it is popped off the queue it looks like this:

[UserTest, "test_0001_does a thing", #<DRb::DRbObject:0x...>]

Notice how UserTest is the same because it is a class and ruby knows about the class. But, when defining the test using describe (the spec DSL), the job that is pushed onto the Drb queue includes the test class as defined by the spec DSL, which is not named:

[#<Class:0x00007f8fc6d0aab8>, "test_0001_does a thing", #<Minitest::CompositeReporter:0x...>]

And when this spec test array is popped off the queue it looks like this:

#<DRb::DRbObject:0x00007fd32fc5e2c0 @uri="drbunix:/var/folders/n4/hn9lkdc93xq18x8b50wggy_r0000gn/T/druby54823.0", @ref=70272544215120>

But we can still treat it like an array, this is not where the error is coming from. The first element is the class, but it is now a DRbObject. The second element is a string of the test name, and the third is the reporter as a DRbObject object.

The issue seems to be passing the test class as defined by the spec DSL through Drb.

@benaldred Can you try adding the following to your test/test_helper.rb file?

module Minitest
  module Rails
    module SpecTests
    end
  end
end

module Kernel
  alias_method :describe_before_minitest_spec_constant_fix, :describe
  private :describe_before_minitest_spec_constant_fix
  def describe *args, &block
    cls = describe_before_minitest_spec_constant_fix *args, &block
    ::Minitest::Rails::SpecTests.const_set "Test__#{cls.object_id}", cls
    cls
  end
end

Here is another option:

module Minitest
  module Rails
    module SpecTests
    end
  end
end

module Kernel
  alias_method :describe_before_minitest_spec_constant_fix, :describe
  private :describe_before_minitest_spec_constant_fix
  def describe *args, &block
    cls = describe_before_minitest_spec_constant_fix *args, &block
    cls_const = "Test__#{cls.name.to_s.split(/\W/).reject(&:empty?).join("_".freeze)}"
    if block.source_location
      source_path, line_num = block.source_location
      source_path = Pathname.new(source_path).relative_path_from(Rails.root).to_s
      source_path = source_path.split(/\W/).reject(&:empty?).join("_".freeze)
      cls_const += "__#{source_path}__#{line_num}"
    end
    while Minitest::Rails::SpecTests.const_defined? cls_const
      cls_const += "_1"
    end
    Minitest::Rails::SpecTests.const_set cls_const, cls
    cls
  end
end

When changing the test/models/user_test.rb file to this:

require "test_helper"

describe User do
  it "does a thing" do
    value(1+1).must_equal 2
  end
end

describe User do
  it "does another thing" do
    value(1+1).must_equal 2
  end
end

describe User do
  it "does a third thing" do
    value(1+1).must_equal 2
  end
end

[1, 2, 3].each do |num|
  describe User, :model do
    it "works when recursed" do
      value(num).wont_be_nil
    end

    describe :nested do
      it "still works" do
        assert true
      end
    end
  end
end

It created the following constants:

  • Minitest::Rails::SpecTests::Test__User__test_models_user_test_rb__3
  • Minitest::Rails::SpecTests::Test__User__test_models_user_test_rb__9
  • Minitest::Rails::SpecTests::Test__User__test_models_user_test_rb__15
  • Minitest::Rails::SpecTests::Test__User_model_nested__test_models_user_test_rb__27
  • Minitest::Rails::SpecTests::Test__User_model__test_models_user_test_rb__22
  • Minitest::Rails::SpecTests::Test__User_model_nested__test_models_user_test_rb__27_1
  • Minitest::Rails::SpecTests::Test__User_model__test_models_user_test_rb__22_1
  • Minitest::Rails::SpecTests::Test__User_model_nested__test_models_user_test_rb__27_1_1
  • Minitest::Rails::SpecTests::Test__User_model__test_models_user_test_rb__22_1_1

I can confirm both bits of code work. I've also tried it on another project with a lot more tests and it seems fine there too.

Thanks

Thank you for checking. I'll merge the PR then.