Skip to content

Fold definition hierarchy into Factory - #1847

Draft
neilvcarvalho wants to merge 1 commit into
mainfrom
nc/definition-hierarchy-fold
Draft

neilvcarvalho wants to merge 1 commit into
mainfrom
nc/definition-hierarchy-fold

Conversation

@neilvcarvalho

Copy link
Copy Markdown
Member

This is the first of a sequence of refactors toward a compiled, non-cloning core. Each step is meant to leave the acceptance suite green and delete machinery that later steps no longer need.

What changed

Resolving a factory's callbacks, constructor and to_create through its parent chain used to build one anonymous subclass of DefinitionHierarchy per factory and rely on Ruby method lookup and super() to perform the inheritance. This replaces that with three methods on Factory that fold over the parent:

def callbacks
  parent.callbacks + definition.callbacks
end

def compiled_to_create
  definition.to_create || parent.compiled_to_create
end

def compiled_constructor
  definition.constructor || parent.compiled_constructor
end

NullFactory ends the recursion by answering with Internal.callbacks, Internal.constructor and Internal.to_create, which is exactly what the root DefinitionHierarchy delegated to before. Callback order is unchanged: global callbacks first, then each ancestor's in parent-to-child order, then the factory's own. definition.callbacks on a compiled definition already includes callbacks aggregated from traits, so no trait handling moves.

Deleted

  • lib/factory_bot/definition_hierarchy.rb and its require in lib/factory_bot.rb.
  • Factory#hierarchy_class, Factory#hierarchy_instance, Factory#build_hierarchy, the build_hierarchy call in Factory#compile, and the @hierarchy_class / @hierarchy_instance resets in Factory#initialize_copy.
  • NullFactory#hierarchy_class and the callbacks, constructor and to_create delegations on NullFactory. The spec for NullFactory is updated to match.

Public behavior is unchanged. No acceptance spec or feature was edited.

Verification

All runs on Ruby 4.0.1.

  • bundle exec standardrb: no offenses.

  • bundle exec rake all_specs (default Gemfile): 334 unit examples, 445 acceptance examples, 4 Cucumber scenarios, all passing.

  • BUNDLE_GEMFILE=gemfiles/main.gemfile bundle exec rake all_specs: same counts, all passing.

  • BUNDLE_GEMFILE=gemfiles/7.0.gemfile bundle exec rake all_specs: unit and Cucumber passing. The acceptance suite passed in 12 of 14 runs; the two failures were both this timing assertion:

    rspec ./spec/acceptance/activesupport_instrumentation_spec.rb:186 # using ActiveSupport::Instrumentation to track compile_factory interaction tracks proper time of compiling the factory
         Failure/Error: expect(time_to_execute[:user]).to be > 0
    

    That example asserts a strictly positive duration for the factory_bot.compile_factory notification, which Definition#compile emits with no block, so the measured span contains no factory code at all. On ActiveSupport 7.0 the subscriber uses wall-clock Time.now, and a direct measurement of that span in a warm process reports zero about half the time on both main and this branch (1562 of 3000 samples on main, 1702 of 3000 here). It passes in a fresh process because the first notification is slower (0 zero durations in 60 fresh processes on either checkout). main happened to pass 15 of 15 full acceptance runs under the same gemfile, so the flake rate is low in practice, but it is a property of the spec rather than of this change. I did not loosen the spec.

Resolving a factory's callbacks, constructor and to_create through
its parent chain built one anonymous subclass of DefinitionHierarchy
per factory and relied on Ruby method lookup and super() to perform
the inheritance. The chain was hard to follow, had to be rebuilt on
every clone, and NullFactory existed partly to terminate it.

Replace the class chain with three methods on Factory that fold over
the parent: callbacks are the parent's callbacks followed by the
definition's own, and the constructor and to_create are the
definition's own or, failing that, the parent's. NullFactory ends
the recursion with the global values from Internal, which is exactly
what the root DefinitionHierarchy delegated to before.

This deletes DefinitionHierarchy, the hierarchy_class,
hierarchy_instance and build_hierarchy methods on Factory, and the
hierarchy-related delegations on NullFactory. Behavior is unchanged:
callbacks still run in global, ancestor, then own order, and
to_create and initialize_with still resolve from the nearest
definition outward. This is the first step of a sequence of
refactors toward a compiled, non-cloning core.
@neilvcarvalho
neilvcarvalho force-pushed the nc/definition-hierarchy-fold branch from 9bf728a to 3d5cecb Compare October 13, 2026 13:15

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant