2017-10-20 15:20:28 -04:00
|
|
|
module FactoryBot
|
2012-05-05 02:31:31 -04:00
|
|
|
# @api private
|
2011-10-28 17:01:27 -04:00
|
|
|
class Definition
|
Add functionality for enum traits (#1380)
## Enum traits
Given a Rails model with an enum attribute:
```rb
class Task < ActiveRecord::Base
enum status: {queued: 0, started: 1, finished: 2}
end
```
It is common for people to define traits for each possible value of the enum:
```rb
FactoryBot.define do
factory :task do
trait :queued do
status { :queued }
end
trait :started do
status { :started }
end
trait :finished do
status { :finished }
end
end
end
```
With this commit, those trait definitions are no longer necessary—they are defined automatically by factory_bot.
If automatically defining traits for enum attributes on every factory is not desired, it is possible to disable the feature by setting `FactoryBot.automatically_define_enum_traits = false` (see commit: [Allow opting out of automatically defining traits](https://github.com/thoughtbot/factory_bot/pull/1380/commits/5a20017351b08ce2ec9918d799e187e9eaa3ec32)).
In that case, it is still possible to explicitly define traits for an enum attribute in a particular factory:
```rb
FactoryBot.automatically_define_enum_traits = false
FactoryBot.define do
factory :task do
traits_for_enum(:status)
end
end
```
It is also possible to use this feature for other enumerable values, not specifically tied to ActiveRecord enum attributes:
```rb
class Task
attr_accessor :status
end
FactoryBot.define do
factory :task do
traits_for_enum(:status, ["queued", "started", "finished"])
end
end
```
The second argument here can be an enumerable object, including a Hash or Array.
Closes #1049
Co-authored-by: Lance Johnson <lancejjohnson@gmail.com>
Co-authored-by: PoTa <pota@mosfet.hu>
Co-authored-by: Frida Casas <fridacasas.fc@gmail.com>
Co-authored-by: Daniel Colson <danieljamescolson@gmail.com>
2020-05-01 17:50:51 -04:00
|
|
|
attr_reader :defined_traits, :declarations, :name, :registered_enums
|
2011-10-28 17:01:27 -04:00
|
|
|
|
2018-08-04 10:06:57 -04:00
|
|
|
def initialize(name, base_traits = [])
|
2020-06-05 15:15:18 -04:00
|
|
|
@name = name
|
|
|
|
@declarations = DeclarationList.new(name)
|
|
|
|
@callbacks = []
|
|
|
|
@defined_traits = Set.new
|
|
|
|
@registered_enums = []
|
|
|
|
@to_create = nil
|
|
|
|
@base_traits = base_traits
|
2012-01-17 23:15:41 -05:00
|
|
|
@additional_traits = []
|
2020-06-05 15:15:18 -04:00
|
|
|
@constructor = nil
|
|
|
|
@attributes = nil
|
|
|
|
@compiled = false
|
Ensure enum traits only get expanded once
Fixes #1411
Before this commit, anybody with Active Record enum attributes using
factory_bot 6.0 would have seen an increase in memory every time they
used any trait.
This behavior can be seen with the following benchmark:
```rb
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "factory_bot", "~> 6.0.1"
gem "activerecord"
gem "sqlite3"
end
require "active_record"
require "factory_bot"
require "logger"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :posts, force: true do |t|
t.integer :status
end
end
class Post < ActiveRecord::Base
enum status: ("a".."z").to_a
end
FactoryBot.define do
factory :post do
trait :x
end
end
require "benchmark"
TIMES = 50
FactoryBot.build(:post)
Benchmark.bm do |bm|
bm.report("false, false") do
FactoryBot.automatically_define_enum_traits = false
TIMES.times do
FactoryBot.build(:post)
end
end
bm.report("true, false") do
FactoryBot.automatically_define_enum_traits = true
TIMES.times do
FactoryBot.build(:post)
end
end
bm.report("false, true") do
FactoryBot.automatically_define_enum_traits = false
TIMES.times do
FactoryBot.build(:post, :x)
end
end
bm.report("true, true") do
FactoryBot.automatically_define_enum_traits = true
TIMES.times do
FactoryBot.build(:post, :x)
end
end
end
```
The first three cases work as expected, but in the last case, with
` FactoryBot.automatically_define_enum_traits = true` and the `:x` trait
passed in at run time, the behavior before this commit was as follows:
1. When passing in traits, the [factory runner] calls
[factory.with_traits], which clones the factory and then applies the
traits to the clone.
2. Cloning the factory also clones the factory's definition
3. The cloned definition retains some of the state of the original
definition, most notably [`@defined_traits` and
`@registered_enums`][shared state], but it sets `@compiled` back to false.
4. When the [definition recompiles], it [re-registers enums] into
`@register_enums`, duplicating what is already there. For each
registered enum, it [adds the relevant traits] to @defined_traits,
again duplicating what was already there.
5. This gets worse and worse every time a trait gets used, increasing
the size of `@defined_traits` exponentially until the program grids
to a halt.
With this commit, we keep an additional piece of state to ensure we only
register and apply enum traits in the original definition. When any
cloned definitions recompile, they will skip these steps.
[factory runner]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/factory_runner.rb#L16-L18
[factory.with_traits]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/factory.rb#L93-L97
[shared state]: https://github.com/thoughtbot/factory_bot/blob/master/lib/factory_bot/definition.rb#L47-L4://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/definition.rb#L10-L11
[definition recompiles]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/definition.rb#L47-L49
[re-registers enums]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/definition.rb#L146-L148
[adds the relevant traits]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/definition.rb#L150-L153
2020-06-19 23:34:21 -04:00
|
|
|
@expanded_enum_traits = false
|
2011-10-28 17:01:27 -04:00
|
|
|
end
|
|
|
|
|
2012-03-09 17:20:38 -05:00
|
|
|
delegate :declare_attribute, to: :declarations
|
2011-10-30 15:45:00 -04:00
|
|
|
|
|
|
|
def attributes
|
2012-06-12 23:11:55 -04:00
|
|
|
@attributes ||= AttributeList.new.tap do |attribute_list|
|
|
|
|
attribute_lists = aggregate_from_traits_and_self(:attributes) { declarations.attributes }
|
|
|
|
attribute_lists.each do |attributes|
|
|
|
|
attribute_list.apply_attributes attributes
|
|
|
|
end
|
|
|
|
end
|
2011-10-30 15:45:00 -04:00
|
|
|
end
|
|
|
|
|
2012-06-12 23:11:55 -04:00
|
|
|
def to_create(&block)
|
|
|
|
if block_given?
|
|
|
|
@to_create = block
|
|
|
|
else
|
|
|
|
aggregate_from_traits_and_self(:to_create) { @to_create }.last
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def constructor
|
|
|
|
aggregate_from_traits_and_self(:constructor) { @constructor }.last
|
|
|
|
end
|
|
|
|
|
|
|
|
def callbacks
|
|
|
|
aggregate_from_traits_and_self(:callbacks) { @callbacks }
|
2011-10-30 15:45:00 -04:00
|
|
|
end
|
|
|
|
|
Add functionality for enum traits (#1380)
## Enum traits
Given a Rails model with an enum attribute:
```rb
class Task < ActiveRecord::Base
enum status: {queued: 0, started: 1, finished: 2}
end
```
It is common for people to define traits for each possible value of the enum:
```rb
FactoryBot.define do
factory :task do
trait :queued do
status { :queued }
end
trait :started do
status { :started }
end
trait :finished do
status { :finished }
end
end
end
```
With this commit, those trait definitions are no longer necessary—they are defined automatically by factory_bot.
If automatically defining traits for enum attributes on every factory is not desired, it is possible to disable the feature by setting `FactoryBot.automatically_define_enum_traits = false` (see commit: [Allow opting out of automatically defining traits](https://github.com/thoughtbot/factory_bot/pull/1380/commits/5a20017351b08ce2ec9918d799e187e9eaa3ec32)).
In that case, it is still possible to explicitly define traits for an enum attribute in a particular factory:
```rb
FactoryBot.automatically_define_enum_traits = false
FactoryBot.define do
factory :task do
traits_for_enum(:status)
end
end
```
It is also possible to use this feature for other enumerable values, not specifically tied to ActiveRecord enum attributes:
```rb
class Task
attr_accessor :status
end
FactoryBot.define do
factory :task do
traits_for_enum(:status, ["queued", "started", "finished"])
end
end
```
The second argument here can be an enumerable object, including a Hash or Array.
Closes #1049
Co-authored-by: Lance Johnson <lancejjohnson@gmail.com>
Co-authored-by: PoTa <pota@mosfet.hu>
Co-authored-by: Frida Casas <fridacasas.fc@gmail.com>
Co-authored-by: Daniel Colson <danieljamescolson@gmail.com>
2020-05-01 17:50:51 -04:00
|
|
|
def compile(klass = nil)
|
2012-06-12 23:11:55 -04:00
|
|
|
unless @compiled
|
Add functionality for enum traits (#1380)
## Enum traits
Given a Rails model with an enum attribute:
```rb
class Task < ActiveRecord::Base
enum status: {queued: 0, started: 1, finished: 2}
end
```
It is common for people to define traits for each possible value of the enum:
```rb
FactoryBot.define do
factory :task do
trait :queued do
status { :queued }
end
trait :started do
status { :started }
end
trait :finished do
status { :finished }
end
end
end
```
With this commit, those trait definitions are no longer necessary—they are defined automatically by factory_bot.
If automatically defining traits for enum attributes on every factory is not desired, it is possible to disable the feature by setting `FactoryBot.automatically_define_enum_traits = false` (see commit: [Allow opting out of automatically defining traits](https://github.com/thoughtbot/factory_bot/pull/1380/commits/5a20017351b08ce2ec9918d799e187e9eaa3ec32)).
In that case, it is still possible to explicitly define traits for an enum attribute in a particular factory:
```rb
FactoryBot.automatically_define_enum_traits = false
FactoryBot.define do
factory :task do
traits_for_enum(:status)
end
end
```
It is also possible to use this feature for other enumerable values, not specifically tied to ActiveRecord enum attributes:
```rb
class Task
attr_accessor :status
end
FactoryBot.define do
factory :task do
traits_for_enum(:status, ["queued", "started", "finished"])
end
end
```
The second argument here can be an enumerable object, including a Hash or Array.
Closes #1049
Co-authored-by: Lance Johnson <lancejjohnson@gmail.com>
Co-authored-by: PoTa <pota@mosfet.hu>
Co-authored-by: Frida Casas <fridacasas.fc@gmail.com>
Co-authored-by: Daniel Colson <danieljamescolson@gmail.com>
2020-05-01 17:50:51 -04:00
|
|
|
expand_enum_traits(klass) unless klass.nil?
|
|
|
|
|
2012-06-12 23:11:55 -04:00
|
|
|
declarations.attributes
|
|
|
|
|
|
|
|
defined_traits.each do |defined_trait|
|
2020-06-05 15:15:18 -04:00
|
|
|
base_traits.each { |bt| bt.define_trait defined_trait }
|
2019-10-25 13:03:09 -04:00
|
|
|
additional_traits.each { |at| at.define_trait defined_trait }
|
2012-06-12 23:11:55 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
@compiled = true
|
|
|
|
end
|
2012-01-17 23:15:41 -05:00
|
|
|
end
|
|
|
|
|
2011-10-30 15:45:00 -04:00
|
|
|
def overridable
|
|
|
|
declarations.overridable
|
|
|
|
self
|
|
|
|
end
|
2011-10-28 17:01:27 -04:00
|
|
|
|
2011-10-28 23:01:50 -04:00
|
|
|
def inherit_traits(new_traits)
|
2012-05-06 16:56:37 -04:00
|
|
|
@base_traits += new_traits
|
|
|
|
end
|
|
|
|
|
|
|
|
def append_traits(new_traits)
|
2012-01-17 23:15:41 -05:00
|
|
|
@additional_traits += new_traits
|
2011-10-28 23:01:50 -04:00
|
|
|
end
|
|
|
|
|
2011-10-28 17:01:27 -04:00
|
|
|
def add_callback(callback)
|
|
|
|
@callbacks << callback
|
|
|
|
end
|
|
|
|
|
2012-05-12 00:42:44 -04:00
|
|
|
def skip_create
|
2018-09-27 21:35:05 -04:00
|
|
|
@to_create = ->(instance) {}
|
2012-05-12 00:42:44 -04:00
|
|
|
end
|
|
|
|
|
2011-10-28 17:01:27 -04:00
|
|
|
def define_trait(trait)
|
2013-12-11 08:53:05 -05:00
|
|
|
@defined_traits.add(trait)
|
2011-10-28 17:01:27 -04:00
|
|
|
end
|
|
|
|
|
Add functionality for enum traits (#1380)
## Enum traits
Given a Rails model with an enum attribute:
```rb
class Task < ActiveRecord::Base
enum status: {queued: 0, started: 1, finished: 2}
end
```
It is common for people to define traits for each possible value of the enum:
```rb
FactoryBot.define do
factory :task do
trait :queued do
status { :queued }
end
trait :started do
status { :started }
end
trait :finished do
status { :finished }
end
end
end
```
With this commit, those trait definitions are no longer necessary—they are defined automatically by factory_bot.
If automatically defining traits for enum attributes on every factory is not desired, it is possible to disable the feature by setting `FactoryBot.automatically_define_enum_traits = false` (see commit: [Allow opting out of automatically defining traits](https://github.com/thoughtbot/factory_bot/pull/1380/commits/5a20017351b08ce2ec9918d799e187e9eaa3ec32)).
In that case, it is still possible to explicitly define traits for an enum attribute in a particular factory:
```rb
FactoryBot.automatically_define_enum_traits = false
FactoryBot.define do
factory :task do
traits_for_enum(:status)
end
end
```
It is also possible to use this feature for other enumerable values, not specifically tied to ActiveRecord enum attributes:
```rb
class Task
attr_accessor :status
end
FactoryBot.define do
factory :task do
traits_for_enum(:status, ["queued", "started", "finished"])
end
end
```
The second argument here can be an enumerable object, including a Hash or Array.
Closes #1049
Co-authored-by: Lance Johnson <lancejjohnson@gmail.com>
Co-authored-by: PoTa <pota@mosfet.hu>
Co-authored-by: Frida Casas <fridacasas.fc@gmail.com>
Co-authored-by: Daniel Colson <danieljamescolson@gmail.com>
2020-05-01 17:50:51 -04:00
|
|
|
def register_enum(enum)
|
|
|
|
@registered_enums << enum
|
|
|
|
end
|
|
|
|
|
2012-01-20 13:04:48 -05:00
|
|
|
def define_constructor(&block)
|
|
|
|
@constructor = block
|
|
|
|
end
|
|
|
|
|
2013-02-08 11:00:22 -05:00
|
|
|
def before(*names, &block)
|
2013-12-14 22:33:15 -05:00
|
|
|
callback(*names.map { |name| "before_#{name}" }, &block)
|
2013-02-08 11:00:22 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def after(*names, &block)
|
2013-12-14 22:33:15 -05:00
|
|
|
callback(*names.map { |name| "after_#{name}" }, &block)
|
2013-02-08 11:00:22 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def callback(*names, &block)
|
|
|
|
names.each do |name|
|
|
|
|
add_callback(Callback.new(name, block))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-10-28 23:01:50 -04:00
|
|
|
private
|
|
|
|
|
2012-01-17 23:15:41 -05:00
|
|
|
def base_traits
|
|
|
|
@base_traits.map { |name| trait_by_name(name) }
|
2020-07-10 11:53:20 -04:00
|
|
|
rescue KeyError => error
|
|
|
|
raise error_with_definition_name(error)
|
|
|
|
end
|
|
|
|
|
|
|
|
def error_with_definition_name(error)
|
|
|
|
message = error.message
|
|
|
|
message.insert(
|
|
|
|
message.index("\nDid you mean?") || message.length,
|
|
|
|
" referenced within \"#{name}\" definition"
|
|
|
|
)
|
|
|
|
|
|
|
|
error.class.new(message).tap do |new_error|
|
|
|
|
new_error.set_backtrace(error.backtrace)
|
|
|
|
end
|
2012-01-17 23:15:41 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def additional_traits
|
|
|
|
@additional_traits.map { |name| trait_by_name(name) }
|
|
|
|
end
|
|
|
|
|
2011-10-28 17:01:27 -04:00
|
|
|
def trait_by_name(name)
|
2019-04-19 16:01:32 -04:00
|
|
|
trait_for(name) || Internal.trait_by_name(name)
|
2011-10-28 17:01:27 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def trait_for(name)
|
2019-09-30 23:53:04 -04:00
|
|
|
@defined_traits_by_name ||= defined_traits.each_with_object({}) { |t, memo| memo[t.name] ||= t }
|
|
|
|
@defined_traits_by_name[name.to_s]
|
2011-10-28 17:01:27 -04:00
|
|
|
end
|
2012-06-12 23:11:55 -04:00
|
|
|
|
|
|
|
def initialize_copy(source)
|
|
|
|
super
|
|
|
|
@attributes = nil
|
2020-06-05 15:15:18 -04:00
|
|
|
@compiled = false
|
2019-09-30 23:53:04 -04:00
|
|
|
@defined_traits_by_name = nil
|
2012-06-12 23:11:55 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def aggregate_from_traits_and_self(method_name, &block)
|
|
|
|
compile
|
|
|
|
|
2016-04-02 15:42:40 -04:00
|
|
|
[
|
|
|
|
base_traits.map(&method_name),
|
|
|
|
instance_exec(&block),
|
2020-06-05 15:15:18 -04:00
|
|
|
additional_traits.map(&method_name)
|
2016-04-02 15:42:40 -04:00
|
|
|
].flatten.compact
|
2012-06-12 23:11:55 -04:00
|
|
|
end
|
Add functionality for enum traits (#1380)
## Enum traits
Given a Rails model with an enum attribute:
```rb
class Task < ActiveRecord::Base
enum status: {queued: 0, started: 1, finished: 2}
end
```
It is common for people to define traits for each possible value of the enum:
```rb
FactoryBot.define do
factory :task do
trait :queued do
status { :queued }
end
trait :started do
status { :started }
end
trait :finished do
status { :finished }
end
end
end
```
With this commit, those trait definitions are no longer necessary—they are defined automatically by factory_bot.
If automatically defining traits for enum attributes on every factory is not desired, it is possible to disable the feature by setting `FactoryBot.automatically_define_enum_traits = false` (see commit: [Allow opting out of automatically defining traits](https://github.com/thoughtbot/factory_bot/pull/1380/commits/5a20017351b08ce2ec9918d799e187e9eaa3ec32)).
In that case, it is still possible to explicitly define traits for an enum attribute in a particular factory:
```rb
FactoryBot.automatically_define_enum_traits = false
FactoryBot.define do
factory :task do
traits_for_enum(:status)
end
end
```
It is also possible to use this feature for other enumerable values, not specifically tied to ActiveRecord enum attributes:
```rb
class Task
attr_accessor :status
end
FactoryBot.define do
factory :task do
traits_for_enum(:status, ["queued", "started", "finished"])
end
end
```
The second argument here can be an enumerable object, including a Hash or Array.
Closes #1049
Co-authored-by: Lance Johnson <lancejjohnson@gmail.com>
Co-authored-by: PoTa <pota@mosfet.hu>
Co-authored-by: Frida Casas <fridacasas.fc@gmail.com>
Co-authored-by: Daniel Colson <danieljamescolson@gmail.com>
2020-05-01 17:50:51 -04:00
|
|
|
|
|
|
|
def expand_enum_traits(klass)
|
Ensure enum traits only get expanded once
Fixes #1411
Before this commit, anybody with Active Record enum attributes using
factory_bot 6.0 would have seen an increase in memory every time they
used any trait.
This behavior can be seen with the following benchmark:
```rb
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "factory_bot", "~> 6.0.1"
gem "activerecord"
gem "sqlite3"
end
require "active_record"
require "factory_bot"
require "logger"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :posts, force: true do |t|
t.integer :status
end
end
class Post < ActiveRecord::Base
enum status: ("a".."z").to_a
end
FactoryBot.define do
factory :post do
trait :x
end
end
require "benchmark"
TIMES = 50
FactoryBot.build(:post)
Benchmark.bm do |bm|
bm.report("false, false") do
FactoryBot.automatically_define_enum_traits = false
TIMES.times do
FactoryBot.build(:post)
end
end
bm.report("true, false") do
FactoryBot.automatically_define_enum_traits = true
TIMES.times do
FactoryBot.build(:post)
end
end
bm.report("false, true") do
FactoryBot.automatically_define_enum_traits = false
TIMES.times do
FactoryBot.build(:post, :x)
end
end
bm.report("true, true") do
FactoryBot.automatically_define_enum_traits = true
TIMES.times do
FactoryBot.build(:post, :x)
end
end
end
```
The first three cases work as expected, but in the last case, with
` FactoryBot.automatically_define_enum_traits = true` and the `:x` trait
passed in at run time, the behavior before this commit was as follows:
1. When passing in traits, the [factory runner] calls
[factory.with_traits], which clones the factory and then applies the
traits to the clone.
2. Cloning the factory also clones the factory's definition
3. The cloned definition retains some of the state of the original
definition, most notably [`@defined_traits` and
`@registered_enums`][shared state], but it sets `@compiled` back to false.
4. When the [definition recompiles], it [re-registers enums] into
`@register_enums`, duplicating what is already there. For each
registered enum, it [adds the relevant traits] to @defined_traits,
again duplicating what was already there.
5. This gets worse and worse every time a trait gets used, increasing
the size of `@defined_traits` exponentially until the program grids
to a halt.
With this commit, we keep an additional piece of state to ensure we only
register and apply enum traits in the original definition. When any
cloned definitions recompile, they will skip these steps.
[factory runner]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/factory_runner.rb#L16-L18
[factory.with_traits]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/factory.rb#L93-L97
[shared state]: https://github.com/thoughtbot/factory_bot/blob/master/lib/factory_bot/definition.rb#L47-L4://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/definition.rb#L10-L11
[definition recompiles]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/definition.rb#L47-L49
[re-registers enums]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/definition.rb#L146-L148
[adds the relevant traits]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/definition.rb#L150-L153
2020-06-19 23:34:21 -04:00
|
|
|
return if @expanded_enum_traits
|
|
|
|
|
Add functionality for enum traits (#1380)
## Enum traits
Given a Rails model with an enum attribute:
```rb
class Task < ActiveRecord::Base
enum status: {queued: 0, started: 1, finished: 2}
end
```
It is common for people to define traits for each possible value of the enum:
```rb
FactoryBot.define do
factory :task do
trait :queued do
status { :queued }
end
trait :started do
status { :started }
end
trait :finished do
status { :finished }
end
end
end
```
With this commit, those trait definitions are no longer necessary—they are defined automatically by factory_bot.
If automatically defining traits for enum attributes on every factory is not desired, it is possible to disable the feature by setting `FactoryBot.automatically_define_enum_traits = false` (see commit: [Allow opting out of automatically defining traits](https://github.com/thoughtbot/factory_bot/pull/1380/commits/5a20017351b08ce2ec9918d799e187e9eaa3ec32)).
In that case, it is still possible to explicitly define traits for an enum attribute in a particular factory:
```rb
FactoryBot.automatically_define_enum_traits = false
FactoryBot.define do
factory :task do
traits_for_enum(:status)
end
end
```
It is also possible to use this feature for other enumerable values, not specifically tied to ActiveRecord enum attributes:
```rb
class Task
attr_accessor :status
end
FactoryBot.define do
factory :task do
traits_for_enum(:status, ["queued", "started", "finished"])
end
end
```
The second argument here can be an enumerable object, including a Hash or Array.
Closes #1049
Co-authored-by: Lance Johnson <lancejjohnson@gmail.com>
Co-authored-by: PoTa <pota@mosfet.hu>
Co-authored-by: Frida Casas <fridacasas.fc@gmail.com>
Co-authored-by: Daniel Colson <danieljamescolson@gmail.com>
2020-05-01 17:50:51 -04:00
|
|
|
if automatically_register_defined_enums?(klass)
|
|
|
|
automatically_register_defined_enums(klass)
|
|
|
|
end
|
|
|
|
|
|
|
|
registered_enums.each do |enum|
|
|
|
|
traits = enum.build_traits(klass)
|
|
|
|
traits.each { |trait| define_trait(trait) }
|
|
|
|
end
|
Ensure enum traits only get expanded once
Fixes #1411
Before this commit, anybody with Active Record enum attributes using
factory_bot 6.0 would have seen an increase in memory every time they
used any trait.
This behavior can be seen with the following benchmark:
```rb
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "factory_bot", "~> 6.0.1"
gem "activerecord"
gem "sqlite3"
end
require "active_record"
require "factory_bot"
require "logger"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :posts, force: true do |t|
t.integer :status
end
end
class Post < ActiveRecord::Base
enum status: ("a".."z").to_a
end
FactoryBot.define do
factory :post do
trait :x
end
end
require "benchmark"
TIMES = 50
FactoryBot.build(:post)
Benchmark.bm do |bm|
bm.report("false, false") do
FactoryBot.automatically_define_enum_traits = false
TIMES.times do
FactoryBot.build(:post)
end
end
bm.report("true, false") do
FactoryBot.automatically_define_enum_traits = true
TIMES.times do
FactoryBot.build(:post)
end
end
bm.report("false, true") do
FactoryBot.automatically_define_enum_traits = false
TIMES.times do
FactoryBot.build(:post, :x)
end
end
bm.report("true, true") do
FactoryBot.automatically_define_enum_traits = true
TIMES.times do
FactoryBot.build(:post, :x)
end
end
end
```
The first three cases work as expected, but in the last case, with
` FactoryBot.automatically_define_enum_traits = true` and the `:x` trait
passed in at run time, the behavior before this commit was as follows:
1. When passing in traits, the [factory runner] calls
[factory.with_traits], which clones the factory and then applies the
traits to the clone.
2. Cloning the factory also clones the factory's definition
3. The cloned definition retains some of the state of the original
definition, most notably [`@defined_traits` and
`@registered_enums`][shared state], but it sets `@compiled` back to false.
4. When the [definition recompiles], it [re-registers enums] into
`@register_enums`, duplicating what is already there. For each
registered enum, it [adds the relevant traits] to @defined_traits,
again duplicating what was already there.
5. This gets worse and worse every time a trait gets used, increasing
the size of `@defined_traits` exponentially until the program grids
to a halt.
With this commit, we keep an additional piece of state to ensure we only
register and apply enum traits in the original definition. When any
cloned definitions recompile, they will skip these steps.
[factory runner]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/factory_runner.rb#L16-L18
[factory.with_traits]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/factory.rb#L93-L97
[shared state]: https://github.com/thoughtbot/factory_bot/blob/master/lib/factory_bot/definition.rb#L47-L4://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/definition.rb#L10-L11
[definition recompiles]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/definition.rb#L47-L49
[re-registers enums]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/definition.rb#L146-L148
[adds the relevant traits]: https://github.com/thoughtbot/factory_bot/blob/0785796f0823528fd36d1c3e4a3df4511b9876e4/lib/factory_bot/definition.rb#L150-L153
2020-06-19 23:34:21 -04:00
|
|
|
|
|
|
|
@expanded_enum_traits = true
|
Add functionality for enum traits (#1380)
## Enum traits
Given a Rails model with an enum attribute:
```rb
class Task < ActiveRecord::Base
enum status: {queued: 0, started: 1, finished: 2}
end
```
It is common for people to define traits for each possible value of the enum:
```rb
FactoryBot.define do
factory :task do
trait :queued do
status { :queued }
end
trait :started do
status { :started }
end
trait :finished do
status { :finished }
end
end
end
```
With this commit, those trait definitions are no longer necessary—they are defined automatically by factory_bot.
If automatically defining traits for enum attributes on every factory is not desired, it is possible to disable the feature by setting `FactoryBot.automatically_define_enum_traits = false` (see commit: [Allow opting out of automatically defining traits](https://github.com/thoughtbot/factory_bot/pull/1380/commits/5a20017351b08ce2ec9918d799e187e9eaa3ec32)).
In that case, it is still possible to explicitly define traits for an enum attribute in a particular factory:
```rb
FactoryBot.automatically_define_enum_traits = false
FactoryBot.define do
factory :task do
traits_for_enum(:status)
end
end
```
It is also possible to use this feature for other enumerable values, not specifically tied to ActiveRecord enum attributes:
```rb
class Task
attr_accessor :status
end
FactoryBot.define do
factory :task do
traits_for_enum(:status, ["queued", "started", "finished"])
end
end
```
The second argument here can be an enumerable object, including a Hash or Array.
Closes #1049
Co-authored-by: Lance Johnson <lancejjohnson@gmail.com>
Co-authored-by: PoTa <pota@mosfet.hu>
Co-authored-by: Frida Casas <fridacasas.fc@gmail.com>
Co-authored-by: Daniel Colson <danieljamescolson@gmail.com>
2020-05-01 17:50:51 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def automatically_register_defined_enums(klass)
|
|
|
|
klass.defined_enums.each_key { |name| register_enum(Enum.new(name)) }
|
|
|
|
end
|
|
|
|
|
|
|
|
def automatically_register_defined_enums?(klass)
|
|
|
|
FactoryBot.automatically_define_enum_traits &&
|
|
|
|
klass.respond_to?(:defined_enums)
|
|
|
|
end
|
2011-10-28 17:01:27 -04:00
|
|
|
end
|
|
|
|
end
|