diff --git a/lib/fixture_kit/adapters/minitest_adapter.rb b/lib/fixture_kit/adapters/minitest_adapter.rb index 4f3039f..a6c90df 100644 --- a/lib/fixture_kit/adapters/minitest_adapter.rb +++ b/lib/fixture_kit/adapters/minitest_adapter.rb @@ -7,15 +7,18 @@ module FixtureKit class MinitestAdapter < FixtureKit::Adapter TEST_NAME = "fixture kit cache pregeneration" + TEST_METHOD = :"test_#{TEST_NAME.tr(" ", "_")}" def execute(&block) - test_class = build_test_class - test_method = test_class.test(TEST_NAME) do + test = test_class.new(TEST_METHOD) + # Minitest runs a test by sending its name to the instance, so defining it + # here leaves the reused class untouched and the block dies with the test. + test.define_singleton_method(TEST_METHOD) do block.call(self) pass end - result = test_class.new(test_method).run + result = test.run return if result.passed? raise result.failures.first.error @@ -27,8 +30,11 @@ def identifier_for(identifier) private - def build_test_class - Class.new(ActiveSupport::TestCase) do + # Reused rather than built per generation: including + # ActiveRecord::TestFixtures appends the class to ActiveSupport's + # :active_record_fixtures load hooks, and that never shrinks. + def test_class + @test_class ||= Class.new(ActiveSupport::TestCase) do ::Minitest::Runnable.runnables.delete(self) include(::ActiveRecord::TestFixtures) end diff --git a/lib/fixture_kit/adapters/rspec_adapter.rb b/lib/fixture_kit/adapters/rspec_adapter.rb index e1dbe7b..5e8d3a7 100644 --- a/lib/fixture_kit/adapters/rspec_adapter.rb +++ b/lib/fixture_kit/adapters/rspec_adapter.rb @@ -7,12 +7,14 @@ class RSpecAdapter < FixtureKit::Adapter def execute(&block) previous_example = ::RSpec.current_example previous_scope = ::RSpec.current_scope - example_group = build_example_group - example = example_group.example { block.call(self) } + group = example_group + example = group.example { block.call(self) } succeeded = begin - example.run(example_group.new, ::RSpec::Core::NullReporter) + example.run(group.new, ::RSpec::Core::NullReporter) ensure + # The group is reused, and the example it holds retains this block. + group.examples.clear ::RSpec.current_example = previous_example ::RSpec.current_scope = previous_scope end @@ -27,8 +29,11 @@ def identifier_for(identifier) private - def build_example_group - ::RSpec::Core::ExampleGroup.subclass( + # Reused rather than built per generation: rspec-rails includes + # ActiveRecord::TestFixtures into every group, which appends it to + # ActiveSupport's :active_record_fixtures load hooks, and that never shrinks. + def example_group + @example_group ||= ::RSpec::Core::ExampleGroup.subclass( ::RSpec::Core::ExampleGroup, "FixtureKit", [], diff --git a/spec/unit/minitest_adapter_spec.rb b/spec/unit/minitest_adapter_spec.rb index 6c5cb92..081c057 100644 --- a/spec/unit/minitest_adapter_spec.rb +++ b/spec/unit/minitest_adapter_spec.rb @@ -16,24 +16,48 @@ expect(captured_test_case_class).to be < ActiveSupport::TestCase expect(captured_test_case_class.included_modules).to include(ActiveRecord::TestFixtures) - expect(captured_test_case_class.public_instance_methods(false).map(&:to_s)) - .to include("test_fixture_kit_cache_pregeneration") end - it "builds a fresh harness class for each execute on the same adapter instance" do - captured_test_case_classes = [] + it "defines the pregeneration test method on the instance, not the harness class" do + harness_class = nil + defined_on_instance = nil - allow(Minitest::Runnable.runnables).to receive(:delete).and_wrap_original do |original, test_case_class| - captured_test_case_classes << test_case_class - original.call(test_case_class) + described_class.new.execute do |context| + harness_class = context.class + defined_on_instance = context.singleton_class + .instance_methods(false) + .include?(described_class::TEST_METHOD) end + expect(defined_on_instance).to be(true) + expect(harness_class.method_defined?(described_class::TEST_METHOD)).to be(false) + end + + it "reuses one harness class across executes on the same adapter instance" do + harness_classes = [] adapter = described_class.new + + adapter.execute { |context| harness_classes << context.class } + adapter.execute { |context| harness_classes << context.class } + + expect(harness_classes.size).to eq(2) + expect(harness_classes.first).to equal(harness_classes.last) + end + + it "leaves no per-generation state on the reused harness class" do + adapter = described_class.new + harness_class = nil + adapter.execute { |context| harness_class = context.class } + snapshot = lambda do + [harness_class.instance_variables.sort, harness_class.public_instance_methods(false).sort] + end + baseline = snapshot.call + adapter.execute { nil } - adapter.execute { nil } + expect { adapter.execute { raise "harness exploded" } } + .to raise_error(RuntimeError, "harness exploded") - expect(captured_test_case_classes.size).to be >= 2 - expect(captured_test_case_classes[-2]).not_to equal(captured_test_case_classes[-1]) + expect(snapshot.call).to eq(baseline) end it "does not leak harness test cases into minitest runnables" do diff --git a/spec/unit/rspec_adapter_spec.rb b/spec/unit/rspec_adapter_spec.rb index 4ee6d38..1e0fd9a 100644 --- a/spec/unit/rspec_adapter_spec.rb +++ b/spec/unit/rspec_adapter_spec.rb @@ -58,6 +58,36 @@ end.to raise_error(RuntimeError, "harness exploded") end + it "reuses one harness example group across executes" do + harness_groups = [] + adapter = described_class.new + + adapter.execute { |context| harness_groups << context.class } + adapter.execute { |context| harness_groups << context.class } + + expect(harness_groups.size).to eq(2) + expect(harness_groups.first).to equal(harness_groups.last) + end + + it "assigns only one RSpec::ExampleGroups constant no matter how many times it runs" do + adapter = described_class.new + adapter.execute { nil } + constants_after_first = RSpec::ExampleGroups.constants.grep(/\AFixtureKit/).size + + 3.times { adapter.execute { nil } } + + expect(RSpec::ExampleGroups.constants.grep(/\AFixtureKit/).size) + .to eq(constants_after_first) + end + + it "does not retain the harness example after running" do + harness_group = nil + + described_class.new.execute { |context| harness_group = context.class } + + expect(harness_group.examples).to be_empty + end + it "re-raises example.exception when execute returns false" do example_group = double("example_group") example = instance_double(RSpec::Core::Example) @@ -66,6 +96,7 @@ allow(::RSpec::Core::ExampleGroup).to receive(:subclass).and_return(example_group) allow(example_group).to receive(:example).and_return(example) + allow(example_group).to receive(:examples).and_return([]) allow(example_group).to receive(:new).and_return(instance) allow(example).to receive(:run).with(instance, RSpec::Core::NullReporter).and_return(false) allow(example).to receive(:exception).and_return(failure)