From e3384fa4f058d223ce2cdf5bb1fd0350f83bafbc Mon Sep 17 00:00:00 2001 From: Mark Isaac Date: Fri, 3 Jul 2026 17:00:00 +0100 Subject: [PATCH 1/2] fix: embedded factory build with multiple factories --fix: chaos mode validation for embedded attributes --fix: chaos attribute leaking to child factories --- lib/faker_maker/factory.rb | 11 +++-- spec/faker_maker/factory_spec.rb | 75 ++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/lib/faker_maker/factory.rb b/lib/faker_maker/factory.rb index f1c578e..0c877bc 100644 --- a/lib/faker_maker/factory.rb +++ b/lib/faker_maker/factory.rb @@ -237,7 +237,9 @@ def assert_only_known_attributes_for_override( attr_override_values ) def assert_only_known_and_optional_attributes_for_chaos( chaos_attr_values ) chaos_attr_values = chaos_attr_values.map(&:to_sym) - unknown_attrs = chaos_attr_values - attribute_names + unknown_attrs = chaos_attr_values - attribute_names.flat_map do |item| + item.is_a?(Hash) ? item.keys : item + end issue = "Can't build an instance of '#{class_name}' " \ "setting '#{unknown_attrs.join( ', ' )}', no such attribute(s)" raise FakerMaker::NoSuchAttributeError, issue unless unknown_attrs.empty? @@ -280,12 +282,15 @@ def manufacture_from_embedded_factory( attr, attributes = {}, chaos: false ) attributes = attr .embedded_factories .reject { |e| e == embedded_factory } - .flat_map { |f| pp f.attributes.map(&:name) } + .flat_map { |f| f.attributes(include_embeddings: false).map(&:name) } .then { |excl| attributes.delete_if { |k, _v| excl.include?(k) } } # The object that is being manufactured by the factory. # If an embedded factory name is provided, it builds the object using FakerMaker. - embedded_factory&.build(attributes:, chaos:) + # Chaos is converted to a boolean so that child factories inherit random chaos + # behaviour without receiving attribute names meant for the parent. + embedded_chaos = chaos.is_a?(Array) || chaos.is_a?(String) || chaos.is_a?(Symbol) ? true : chaos + embedded_factory&.build(attributes:, chaos: embedded_chaos) end def instantiate diff --git a/spec/faker_maker/factory_spec.rb b/spec/faker_maker/factory_spec.rb index c3da1fb..c0d488f 100644 --- a/spec/faker_maker/factory_spec.rb +++ b/spec/faker_maker/factory_spec.rb @@ -299,6 +299,81 @@ end end + describe 'embedded factories' do + it 'builds a factory with multiple embedded factories' do + first_embed = FakerMaker::Factory.new( :first_embed ) + first_embed.attach_attribute( FakerMaker::Attribute.new( :colour, proc { 'red' } ) ) + FakerMaker.register_factory( first_embed ) + + second_embed = FakerMaker::Factory.new( :second_embed ) + second_embed.attach_attribute( FakerMaker::Attribute.new( :size, proc { 'large' } ) ) + FakerMaker.register_factory( second_embed ) + + factory = FakerMaker::Factory.new( :multi_embed ) + factory.attach_attribute( FakerMaker::Attribute.new( :name, proc { 'test' }, required: true ) ) + factory.attach_attribute( FakerMaker::Attribute.new( :variant, nil, factory: %i[first_embed second_embed] ) ) + FakerMaker.register_factory( factory ) + + expect { factory.build }.not_to raise_error + + fake = factory.build + variant = fake.variant + expect( variant ).to satisfy { |v| v.respond_to?(:colour) || v.respond_to?(:size) } + end + + it 'builds with chaos mode enabled on a factory with embedded factories' do + embed = FakerMaker::Factory.new( :chaos_embed ) + embed.attach_attribute( FakerMaker::Attribute.new( :value, proc { 'embedded' } ) ) + FakerMaker.register_factory( embed ) + + factory = FakerMaker::Factory.new( :chaos_parent ) + factory.attach_attribute( FakerMaker::Attribute.new( :title, proc { 'hello' }, required: true ) ) + factory.attach_attribute( FakerMaker::Attribute.new( :child, nil, factory: :chaos_embed ) ) + factory.attach_attribute( FakerMaker::Attribute.new( :optional_field, proc { 'maybe' } ) ) + FakerMaker.register_factory( factory ) + + expect { factory.build( chaos: true ) }.not_to raise_error + end + + it 'allows chaos mode to target an embedded factory attribute' do + embed = FakerMaker::Factory.new( :target_embed ) + embed.attach_attribute( FakerMaker::Attribute.new( :value, proc { 'embedded' } ) ) + FakerMaker.register_factory( embed ) + + factory = FakerMaker::Factory.new( :target_parent ) + factory.attach_attribute( FakerMaker::Attribute.new( :title, proc { 'hello' }, required: true ) ) + factory.attach_attribute( FakerMaker::Attribute.new( :child, nil, factory: :target_embed ) ) + factory.attach_attribute( FakerMaker::Attribute.new( :optional_field, proc { 'maybe' } ) ) + FakerMaker.register_factory( factory ) + + expect { factory.build( chaos: [:child] ) }.not_to raise_error + + fakes = [] + 10.times { fakes << factory.build( chaos: [:child] ) } + + fakes.each { |fake| expect( fake.title ).to eq 'hello' } + fakes.each { |fake| expect( fake.optional_field ).to eq 'maybe' } + expect( fakes.map(&:child) ).to include nil + end + + it 'does not pass parent chaos attribute names to child factories' do + embed = FakerMaker::Factory.new( :leak_embed ) + embed.attach_attribute( FakerMaker::Attribute.new( :inner_value, proc { 'inner' }, required: true ) ) + FakerMaker.register_factory( embed ) + + factory = FakerMaker::Factory.new( :leak_parent ) + factory.attach_attribute( FakerMaker::Attribute.new( :name, proc { 'parent' }, required: true ) ) + factory.attach_attribute( FakerMaker::Attribute.new( :child, nil, factory: :leak_embed ) ) + factory.attach_attribute( FakerMaker::Attribute.new( :optional_thing, proc { 'optional' } ) ) + FakerMaker.register_factory( factory ) + + expect { factory.build( chaos: [:optional_thing] ) }.not_to raise_error + + fake = factory.build( chaos: [:optional_thing] ) + expect( fake.child.inner_value ).to eq 'inner' + end + end + describe '#instance' do it 'returns the instance' do factory = FakerMaker::Factory.new( :factory ) From c4f05d34c82f8c1ee42c19cc8a5b84d75712374b Mon Sep 17 00:00:00 2001 From: Mark Isaac Date: Fri, 3 Jul 2026 17:11:49 +0100 Subject: [PATCH 2/2] bump patch version --- lib/faker_maker/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/faker_maker/version.rb b/lib/faker_maker/version.rb index 149cf69..312220b 100644 --- a/lib/faker_maker/version.rb +++ b/lib/faker_maker/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module FakerMaker - VERSION = '5.0.0' + VERSION = '5.0.1' end