factory_bot 6.4.4 → 6.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/GETTING_STARTED.md +130 -87
- data/NEWS.md +465 -300
- data/README.md +22 -29
- data/lib/factory_bot/attribute_assigner.rb +75 -19
- data/lib/factory_bot/callbacks_observer.rb +19 -1
- data/lib/factory_bot/decorator.rb +5 -17
- data/lib/factory_bot/definition.rb +7 -2
- data/lib/factory_bot/definition_proxy.rb +19 -6
- data/lib/factory_bot/evaluator.rb +12 -7
- data/lib/factory_bot/factory.rb +22 -7
- data/lib/factory_bot/factory_runner.rb +2 -0
- data/lib/factory_bot/find_definitions.rb +2 -2
- data/lib/factory_bot/internal.rb +33 -0
- data/lib/factory_bot/linter.rb +13 -2
- data/lib/factory_bot/registry.rb +1 -1
- data/lib/factory_bot/sequence.rb +137 -10
- data/lib/factory_bot/strategy/build.rb +2 -0
- data/lib/factory_bot/strategy/create.rb +2 -0
- data/lib/factory_bot/strategy/stub.rb +4 -2
- data/lib/factory_bot/strategy.rb +15 -0
- data/lib/factory_bot/syntax/default.rb +2 -2
- data/lib/factory_bot/syntax/methods.rb +62 -15
- data/lib/factory_bot/trait.rb +11 -5
- data/lib/factory_bot/uri_manager.rb +63 -0
- data/lib/factory_bot/version.rb +1 -1
- data/lib/factory_bot.rb +35 -9
- metadata +24 -11
- data/lib/factory_bot/strategy_calculator.rb +0 -26
data/README.md
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
# factory_bot
|
|
1
|
+
# factory_bot
|
|
2
|
+
|
|
3
|
+
[![Build Status][ci-image]][ci] [![Gem Version][version-image]][version]
|
|
2
4
|
|
|
3
5
|
factory_bot is a fixtures replacement with a straightforward definition syntax, support for multiple build strategies (saved instances, unsaved instances, attribute hashes, and stubbed objects), and support for multiple factories for the same class (user, admin_user, and so on), including factory inheritance.
|
|
4
6
|
|
|
5
7
|
If you want to use factory_bot with Rails, see
|
|
6
8
|
[factory_bot_rails](https://github.com/thoughtbot/factory_bot_rails).
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
### Transitioning from factory\_girl?
|
|
12
|
-
|
|
13
|
-
Check out the [guide](https://github.com/thoughtbot/factory_bot/blob/4-9-0-stable/UPGRADE_FROM_FACTORY_GIRL.md).
|
|
10
|
+
Interested in the history of the project name? You can find the history [here](https://github.com/thoughtbot/factory_bot/blob/main/NAME.md)
|
|
14
11
|
|
|
12
|
+
Transitioning from factory\_girl? Check out the [upgrade guide](https://github.com/thoughtbot/factory_bot/blob/v4.9.0/UPGRADE_FROM_FACTORY_GIRL.md).
|
|
15
13
|
|
|
16
|
-
Documentation
|
|
17
|
-
-------------
|
|
14
|
+
## Documentation
|
|
18
15
|
|
|
19
16
|
See our extensive reference, guides, and cookbook in [the factory_bot book][].
|
|
20
17
|
|
|
@@ -27,8 +24,7 @@ Rails, see [the factory_bot wiki][].
|
|
|
27
24
|
[the factory_bot book]: https://thoughtbot.github.io/factory_bot
|
|
28
25
|
[the factory_bot wiki]: https://github.com/thoughtbot/factory_bot/wiki
|
|
29
26
|
|
|
30
|
-
Install
|
|
31
|
-
--------
|
|
27
|
+
## Install
|
|
32
28
|
|
|
33
29
|
Run:
|
|
34
30
|
|
|
@@ -42,13 +38,11 @@ To install the gem manually from your shell, run:
|
|
|
42
38
|
gem install factory_bot
|
|
43
39
|
```
|
|
44
40
|
|
|
45
|
-
Supported Ruby versions
|
|
46
|
-
-----------------------
|
|
41
|
+
## Supported Ruby versions
|
|
47
42
|
|
|
48
|
-
Supported Ruby versions are listed in
|
|
43
|
+
Supported Ruby versions are listed in `.github/workflows/build.yml` ([source](https://github.com/thoughtbot/factory_bot/blob/main/.github/workflows/build.yml))
|
|
49
44
|
|
|
50
|
-
More Information
|
|
51
|
-
----------------
|
|
45
|
+
## More Information
|
|
52
46
|
|
|
53
47
|
* [Rubygems](https://rubygems.org/gems/factory_bot)
|
|
54
48
|
* [Stack Overflow](https://stackoverflow.com/questions/tagged/factory-bot)
|
|
@@ -56,12 +50,11 @@ More Information
|
|
|
56
50
|
* [GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS](https://robots.thoughtbot.com/)
|
|
57
51
|
|
|
58
52
|
[GETTING_STARTED]: https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md
|
|
59
|
-
[NAME]: https://github.com/thoughtbot/factory_bot/blob/main/NAME.md
|
|
60
53
|
|
|
61
|
-
Useful Tools
|
|
62
|
-
------------
|
|
54
|
+
## Useful Tools
|
|
63
55
|
|
|
64
56
|
* [FactoryTrace](https://github.com/djezzzl/factory_trace) - helps to find unused factories and traits.
|
|
57
|
+
* [ruby-lsp-factory_bot](https://github.com/donny741/ruby-lsp-factory_bot) / [ruby-lsp-rails-factory-bot](https://github.com/johansenja/ruby-lsp-rails-factory-bot) - integration with [ruby-lsp](https://github.com/Shopify/ruby-lsp) to provide intellisense
|
|
65
58
|
|
|
66
59
|
Contributing
|
|
67
60
|
------------
|
|
@@ -75,31 +68,31 @@ community](https://github.com/thoughtbot/factory_bot/graphs/contributors).
|
|
|
75
68
|
License
|
|
76
69
|
-------
|
|
77
70
|
|
|
78
|
-
factory_bot is Copyright © 2008
|
|
71
|
+
factory_bot is Copyright © 2008 Joe Ferris and thoughtbot. It is free
|
|
79
72
|
software, and may be redistributed under the terms specified in the
|
|
80
73
|
[LICENSE] file.
|
|
81
74
|
|
|
82
75
|
[LICENSE]: https://github.com/thoughtbot/factory_bot/blob/main/LICENSE
|
|
83
76
|
|
|
77
|
+
<!-- START /templates/footer.md -->
|
|
78
|
+
## About thoughtbot
|
|
84
79
|
|
|
85
|
-
|
|
86
|
-
----------------
|
|
87
|
-
|
|
88
|
-

|
|
80
|
+

|
|
89
81
|
|
|
90
|
-
|
|
82
|
+
This repo is maintained and funded by thoughtbot, inc.
|
|
91
83
|
The names and logos for thoughtbot are trademarks of thoughtbot, inc.
|
|
92
84
|
|
|
93
85
|
We love open source software!
|
|
94
|
-
See [our other projects][community]
|
|
95
|
-
[hire
|
|
86
|
+
See [our other projects][community].
|
|
87
|
+
We are [available for hire][hire].
|
|
96
88
|
|
|
97
89
|
[community]: https://thoughtbot.com/community?utm_source=github
|
|
98
90
|
[hire]: https://thoughtbot.com/hire-us?utm_source=github
|
|
91
|
+
|
|
92
|
+
<!-- END /templates/footer.md -->
|
|
93
|
+
|
|
99
94
|
[ci-image]: https://github.com/thoughtbot/factory_bot/actions/workflows/build.yml/badge.svg?branch=main
|
|
100
95
|
[ci]: https://github.com/thoughtbot/factory_bot/actions?query=workflow%3ABuild+branch%3Amain
|
|
101
|
-
[grade-image]: https://codeclimate.com/github/thoughtbot/factory_bot/badges/gpa.svg
|
|
102
|
-
[grade]: https://codeclimate.com/github/thoughtbot/factory_bot
|
|
103
96
|
[version-image]: https://badge.fury.io/rb/factory_bot.svg
|
|
104
97
|
[version]: https://badge.fury.io/rb/factory_bot
|
|
105
98
|
[hound-badge-image]: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg
|
|
@@ -9,16 +9,18 @@ module FactoryBot
|
|
|
9
9
|
@attribute_names_assigned = []
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
# constructs an object-based factory product
|
|
12
13
|
def object
|
|
13
14
|
@evaluator.instance = build_class_instance
|
|
14
15
|
build_class_instance.tap do |instance|
|
|
15
16
|
attributes_to_set_on_instance.each do |attribute|
|
|
16
|
-
instance.public_send("#{attribute}=", get(attribute))
|
|
17
|
+
instance.public_send(:"#{attribute}=", get(attribute))
|
|
17
18
|
@attribute_names_assigned << attribute
|
|
18
19
|
end
|
|
19
20
|
end
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
# constructs a Hash-based factory product
|
|
22
24
|
def hash
|
|
23
25
|
@evaluator.instance = build_hash
|
|
24
26
|
|
|
@@ -29,6 +31,8 @@ module FactoryBot
|
|
|
29
31
|
|
|
30
32
|
private
|
|
31
33
|
|
|
34
|
+
# Track evaluation of methods on the evaluator to prevent the duplicate
|
|
35
|
+
# assignment of attributes accessed and via `initialize_with` syntax
|
|
32
36
|
def method_tracking_evaluator
|
|
33
37
|
@method_tracking_evaluator ||= Decorator::AttributeHash.new(
|
|
34
38
|
decorated_evaluator,
|
|
@@ -67,12 +71,15 @@ module FactoryBot
|
|
|
67
71
|
attribute_names_to_assign - association_names
|
|
68
72
|
end
|
|
69
73
|
|
|
74
|
+
# Builds a list of attributes names that should be assigned to the factory product
|
|
70
75
|
def attribute_names_to_assign
|
|
71
|
-
@attribute_names_to_assign ||=
|
|
72
|
-
|
|
73
|
-
override_names
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
@attribute_names_to_assign ||= begin
|
|
77
|
+
# start a list of candidates containing non-transient attributes and overrides
|
|
78
|
+
assignment_candidates = non_ignored_attribute_names + override_names
|
|
79
|
+
# then remove any transient attributes (potentially reintroduced by the overrides),
|
|
80
|
+
# and remove ignorable aliased attributes from the candidate list
|
|
81
|
+
assignment_candidates - ignored_attribute_names - attribute_names_overriden_by_alias
|
|
82
|
+
end
|
|
76
83
|
end
|
|
77
84
|
|
|
78
85
|
def non_ignored_attribute_names
|
|
@@ -91,22 +98,71 @@ module FactoryBot
|
|
|
91
98
|
@evaluator.__override_names__
|
|
92
99
|
end
|
|
93
100
|
|
|
94
|
-
def
|
|
95
|
-
@attribute_list.names
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def alias_names_to_ignore
|
|
99
|
-
@attribute_list.non_ignored.flat_map { |attribute|
|
|
100
|
-
override_names.map do |override|
|
|
101
|
-
attribute.name if ignorable_alias?(attribute, override)
|
|
102
|
-
end
|
|
103
|
-
}.compact
|
|
101
|
+
def attribute_names
|
|
102
|
+
@attribute_list.names
|
|
104
103
|
end
|
|
105
104
|
|
|
105
|
+
def hash_instance_methods_to_respond_to
|
|
106
|
+
attribute_names + override_names + @build_class.instance_methods
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Builds a list of attribute names which are slated to be interrupted by an override.
|
|
110
|
+
def attribute_names_overriden_by_alias
|
|
111
|
+
@attribute_list
|
|
112
|
+
.non_ignored
|
|
113
|
+
.flat_map { |attribute|
|
|
114
|
+
override_names.map do |override|
|
|
115
|
+
attribute.name if ignorable_alias?(attribute, override)
|
|
116
|
+
end
|
|
117
|
+
}
|
|
118
|
+
.compact
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Is the attribute an ignorable alias of the override?
|
|
122
|
+
# An attribute is ignorable when it is an alias of the override AND it is
|
|
123
|
+
# either interrupting an assocciation OR is not the name of another attribute
|
|
124
|
+
#
|
|
125
|
+
# @note An "alias" is currently an overloaded term for two distinct cases:
|
|
126
|
+
# (1) attributes which are aliases and reference the same value
|
|
127
|
+
# (2) a logical grouping of a foreign key and an associated object
|
|
106
128
|
def ignorable_alias?(attribute, override)
|
|
107
|
-
attribute.alias_for?(override)
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
return false unless attribute.alias_for?(override)
|
|
130
|
+
|
|
131
|
+
# The attribute alias should be ignored when the override interrupts an association
|
|
132
|
+
return true if override_interrupts_association?(attribute, override)
|
|
133
|
+
|
|
134
|
+
# Remaining aliases should be ignored when the override does not match a declared attribute.
|
|
135
|
+
# An override which is an alias to a declared attribute should not interrupt the aliased
|
|
136
|
+
# attribute and interrupt only the attribute with a matching name. This workaround allows a
|
|
137
|
+
# factory to declare both <attribute> and <attribute>_id as separate and distinct attributes.
|
|
138
|
+
!override_matches_declared_attribute?(override)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Does this override interrupt an association?
|
|
142
|
+
# When true, this indicates the aliased attribute is related to a declared association and the
|
|
143
|
+
# override does not match the attribute name.
|
|
144
|
+
#
|
|
145
|
+
# @note Association overrides should take precedence over a declared foreign key attribute.
|
|
146
|
+
#
|
|
147
|
+
# @note An override may interrupt an association by providing the associated object or
|
|
148
|
+
# by providing the foreign key.
|
|
149
|
+
#
|
|
150
|
+
# @param [FactoryBot::Attribute] aliased_attribute
|
|
151
|
+
# @param [Symbol] override name of an override which is an alias to the attribute name
|
|
152
|
+
def override_interrupts_association?(aliased_attribute, override)
|
|
153
|
+
(aliased_attribute.association? || association_names.include?(override)) &&
|
|
154
|
+
aliased_attribute.name != override
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Does this override match the name of any declared attribute?
|
|
158
|
+
#
|
|
159
|
+
# @note Checking against the names of all attributes, resolves any issues with having both
|
|
160
|
+
# <attribute> and <attribute>_id in the same factory. This also takes into account ignored
|
|
161
|
+
# attributes that should not be assigned (aka transient attributes)
|
|
162
|
+
#
|
|
163
|
+
# @param [Symbol] override the name of an override
|
|
164
|
+
def override_matches_declared_attribute?(override)
|
|
165
|
+
attribute_names.include?(override)
|
|
110
166
|
end
|
|
111
167
|
end
|
|
112
168
|
end
|
|
@@ -4,11 +4,15 @@ module FactoryBot
|
|
|
4
4
|
def initialize(callbacks, evaluator)
|
|
5
5
|
@callbacks = callbacks
|
|
6
6
|
@evaluator = evaluator
|
|
7
|
+
@completed = []
|
|
7
8
|
end
|
|
8
9
|
|
|
9
10
|
def update(name, result_instance)
|
|
10
11
|
callbacks_by_name(name).each do |callback|
|
|
11
|
-
|
|
12
|
+
if !completed?(result_instance, callback)
|
|
13
|
+
callback.run(result_instance, @evaluator)
|
|
14
|
+
record_completion!(result_instance, callback)
|
|
15
|
+
end
|
|
12
16
|
end
|
|
13
17
|
end
|
|
14
18
|
|
|
@@ -17,5 +21,19 @@ module FactoryBot
|
|
|
17
21
|
def callbacks_by_name(name)
|
|
18
22
|
@callbacks.select { |callback| callback.name == name }
|
|
19
23
|
end
|
|
24
|
+
|
|
25
|
+
def completed?(instance, callback)
|
|
26
|
+
key = completion_key_for(instance, callback)
|
|
27
|
+
@completed.include?(key)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def record_completion!(instance, callback)
|
|
31
|
+
key = completion_key_for(instance, callback)
|
|
32
|
+
@completed << key
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def completion_key_for(instance, callback)
|
|
36
|
+
"#{instance.object_id}-#{callback.object_id}"
|
|
37
|
+
end
|
|
20
38
|
end
|
|
21
39
|
end
|
|
@@ -6,24 +6,12 @@ module FactoryBot
|
|
|
6
6
|
@component = component
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@component.send(...)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def send(...)
|
|
16
|
-
__send__(...)
|
|
17
|
-
end
|
|
18
|
-
RUBY
|
|
19
|
-
else
|
|
20
|
-
def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing
|
|
21
|
-
@component.send(name, *args, &block)
|
|
22
|
-
end
|
|
9
|
+
def method_missing(...) # rubocop:disable Style/MethodMissingSuper
|
|
10
|
+
@component.send(...)
|
|
11
|
+
end
|
|
23
12
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
13
|
+
def send(...)
|
|
14
|
+
__send__(...)
|
|
27
15
|
end
|
|
28
16
|
|
|
29
17
|
def respond_to_missing?(name, include_private = false)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
module FactoryBot
|
|
2
2
|
# @api private
|
|
3
3
|
class Definition
|
|
4
|
-
attr_reader :defined_traits, :declarations, :name, :registered_enums
|
|
4
|
+
attr_reader :defined_traits, :declarations, :name, :registered_enums, :uri_manager
|
|
5
5
|
attr_accessor :klass
|
|
6
6
|
|
|
7
|
-
def initialize(name, base_traits = [])
|
|
7
|
+
def initialize(name, base_traits = [], **opts)
|
|
8
8
|
@name = name
|
|
9
|
+
@uri_manager = opts[:uri_manager]
|
|
9
10
|
@declarations = DeclarationList.new(name)
|
|
10
11
|
@callbacks = []
|
|
11
12
|
@defined_traits = Set.new
|
|
@@ -95,6 +96,10 @@ module FactoryBot
|
|
|
95
96
|
@defined_traits.add(trait)
|
|
96
97
|
end
|
|
97
98
|
|
|
99
|
+
def defined_traits_names
|
|
100
|
+
@defined_traits.map(&:name)
|
|
101
|
+
end
|
|
102
|
+
|
|
98
103
|
def register_enum(enum)
|
|
99
104
|
@registered_enums << enum
|
|
100
105
|
end
|
|
@@ -94,7 +94,7 @@ module FactoryBot
|
|
|
94
94
|
if association_options.nil?
|
|
95
95
|
__declare_attribute__(name, block)
|
|
96
96
|
elsif __valid_association_options?(association_options)
|
|
97
|
-
association(name, association_options)
|
|
97
|
+
association(name, association_options, &block)
|
|
98
98
|
else
|
|
99
99
|
raise NoMethodError.new(<<~MSG)
|
|
100
100
|
undefined method '#{name}' in '#{@definition.name}' factory
|
|
@@ -120,9 +120,13 @@ module FactoryBot
|
|
|
120
120
|
#
|
|
121
121
|
# Except that no globally available sequence will be defined.
|
|
122
122
|
def sequence(name, *args, &block)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
options = args.extract_options!
|
|
124
|
+
options[:uri_paths] = @definition.uri_manager.to_a
|
|
125
|
+
args << options
|
|
126
|
+
|
|
127
|
+
new_sequence = Sequence.new(name, *args, &block)
|
|
128
|
+
registered_sequence = __fetch_or_register_sequence(new_sequence)
|
|
129
|
+
add_attribute(name) { increment_sequence(registered_sequence) }
|
|
126
130
|
end
|
|
127
131
|
|
|
128
132
|
# Adds an attribute that builds an association. The associated instance will
|
|
@@ -169,11 +173,11 @@ module FactoryBot
|
|
|
169
173
|
end
|
|
170
174
|
|
|
171
175
|
def factory(name, options = {}, &block)
|
|
172
|
-
|
|
176
|
+
child_factories << [name, options, block]
|
|
173
177
|
end
|
|
174
178
|
|
|
175
179
|
def trait(name, &block)
|
|
176
|
-
@definition.define_trait(Trait.new(name, &block))
|
|
180
|
+
@definition.define_trait(Trait.new(name, uri_paths: @definition.uri_manager.to_a, &block))
|
|
177
181
|
end
|
|
178
182
|
|
|
179
183
|
# Creates traits for enumerable values.
|
|
@@ -252,5 +256,14 @@ module FactoryBot
|
|
|
252
256
|
def __valid_association_options?(options)
|
|
253
257
|
options.respond_to?(:has_key?) && options.has_key?(:factory)
|
|
254
258
|
end
|
|
259
|
+
|
|
260
|
+
##
|
|
261
|
+
# If the inline sequence has already been registered by a parent,
|
|
262
|
+
# return that one, otherwise register and return the given sequence
|
|
263
|
+
#
|
|
264
|
+
def __fetch_or_register_sequence(sequence)
|
|
265
|
+
FactoryBot::Sequence.find_by_uri(sequence.uri_manager.first) ||
|
|
266
|
+
FactoryBot::Internal.register_inline_sequence(sequence)
|
|
267
|
+
end
|
|
255
268
|
end
|
|
256
269
|
end
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
require "active_support/core_ext/hash/except"
|
|
2
1
|
require "active_support/core_ext/class/attribute"
|
|
3
2
|
|
|
4
3
|
module FactoryBot
|
|
@@ -35,14 +34,13 @@ module FactoryBot
|
|
|
35
34
|
|
|
36
35
|
attr_accessor :instance
|
|
37
36
|
|
|
38
|
-
def method_missing(method_name,
|
|
37
|
+
def method_missing(method_name, ...)
|
|
39
38
|
if @instance.respond_to?(method_name)
|
|
40
|
-
@instance.send(method_name,
|
|
39
|
+
@instance.send(method_name, ...)
|
|
41
40
|
else
|
|
42
|
-
SyntaxRunner.new.send(method_name,
|
|
41
|
+
SyntaxRunner.new.send(method_name, ...)
|
|
43
42
|
end
|
|
44
43
|
end
|
|
45
|
-
ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
|
|
46
44
|
|
|
47
45
|
def respond_to_missing?(method_name, _include_private = false)
|
|
48
46
|
@instance.respond_to?(method_name) || SyntaxRunner.new.respond_to?(method_name)
|
|
@@ -52,8 +50,15 @@ module FactoryBot
|
|
|
52
50
|
@overrides.keys
|
|
53
51
|
end
|
|
54
52
|
|
|
55
|
-
def increment_sequence(sequence)
|
|
56
|
-
sequence.next(
|
|
53
|
+
def increment_sequence(sequence, scope: self)
|
|
54
|
+
value = sequence.next(scope)
|
|
55
|
+
|
|
56
|
+
raise if value.respond_to?(:start_with?) && value.start_with?("#<FactoryBot::Declaration")
|
|
57
|
+
|
|
58
|
+
value
|
|
59
|
+
rescue
|
|
60
|
+
raise ArgumentError, "Sequence '#{sequence.uri_manager.first}' failed to " \
|
|
61
|
+
"return a value. Perhaps it needs a scope to operate? (scope: <object>)"
|
|
57
62
|
end
|
|
58
63
|
|
|
59
64
|
def self.attribute_list
|
data/lib/factory_bot/factory.rb
CHANGED
|
@@ -12,16 +12,20 @@ module FactoryBot
|
|
|
12
12
|
@parent = options[:parent]
|
|
13
13
|
@aliases = options[:aliases] || []
|
|
14
14
|
@class_name = options[:class]
|
|
15
|
-
@
|
|
15
|
+
@uri_manager = FactoryBot::UriManager.new(names)
|
|
16
|
+
@definition = Definition.new(@name, options[:traits] || [], uri_manager: @uri_manager)
|
|
16
17
|
@compiled = false
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
delegate :add_callback, :declare_attribute, :to_create, :define_trait, :constructor,
|
|
20
|
-
:defined_traits, :inherit_traits, :append_traits,
|
|
21
|
+
:defined_traits, :defined_traits_names, :inherit_traits, :append_traits,
|
|
22
|
+
to: :@definition
|
|
21
23
|
|
|
22
24
|
def build_class
|
|
23
25
|
@build_class ||= if class_name.is_a? Class
|
|
24
26
|
class_name
|
|
27
|
+
elsif class_name.to_s.safe_constantize
|
|
28
|
+
class_name.to_s.safe_constantize
|
|
25
29
|
else
|
|
26
30
|
class_name.to_s.camelize.constantize
|
|
27
31
|
end
|
|
@@ -29,18 +33,22 @@ module FactoryBot
|
|
|
29
33
|
|
|
30
34
|
def run(build_strategy, overrides, &block)
|
|
31
35
|
block ||= ->(result) { result }
|
|
36
|
+
|
|
32
37
|
compile
|
|
33
38
|
|
|
34
|
-
strategy =
|
|
39
|
+
strategy = Strategy.lookup_strategy(build_strategy).new
|
|
35
40
|
|
|
36
41
|
evaluator = evaluator_class.new(strategy, overrides.symbolize_keys)
|
|
37
42
|
attribute_assigner = AttributeAssigner.new(evaluator, build_class, &compiled_constructor)
|
|
38
43
|
|
|
39
44
|
observer = CallbacksObserver.new(callbacks, evaluator)
|
|
40
|
-
evaluation =
|
|
41
|
-
|
|
45
|
+
evaluation = Evaluation.new(evaluator, attribute_assigner, compiled_to_create, observer)
|
|
46
|
+
|
|
47
|
+
evaluation.notify(:before_all, nil)
|
|
48
|
+
instance = strategy.result(evaluation).tap(&block)
|
|
49
|
+
evaluation.notify(:after_all, instance)
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
instance
|
|
44
52
|
end
|
|
45
53
|
|
|
46
54
|
def human_names
|
|
@@ -83,7 +91,7 @@ module FactoryBot
|
|
|
83
91
|
def compile
|
|
84
92
|
unless @compiled
|
|
85
93
|
parent.compile
|
|
86
|
-
|
|
94
|
+
inherit_parent_traits
|
|
87
95
|
@definition.compile(build_class)
|
|
88
96
|
build_hierarchy
|
|
89
97
|
@compiled = true
|
|
@@ -151,6 +159,13 @@ module FactoryBot
|
|
|
151
159
|
end
|
|
152
160
|
end
|
|
153
161
|
|
|
162
|
+
def inherit_parent_traits
|
|
163
|
+
parent.defined_traits.each do |trait|
|
|
164
|
+
next if defined_traits_names.include?(trait.name)
|
|
165
|
+
define_trait(trait.clone)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
154
169
|
def initialize_copy(source)
|
|
155
170
|
super
|
|
156
171
|
@definition = @definition.clone
|
|
@@ -25,6 +25,8 @@ module FactoryBot
|
|
|
25
25
|
factory: factory
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
ActiveSupport::Notifications.instrument("factory_bot.before_run_factory", instrumentation_payload)
|
|
29
|
+
|
|
28
30
|
ActiveSupport::Notifications.instrument("factory_bot.run_factory", instrumentation_payload) do
|
|
29
31
|
factory.run(runner_strategy, @overrides, &block)
|
|
30
32
|
end
|
|
@@ -2,8 +2,8 @@ module FactoryBot
|
|
|
2
2
|
class << self
|
|
3
3
|
# An Array of strings specifying locations that should be searched for
|
|
4
4
|
# factory definitions. By default, factory_bot will attempt to require
|
|
5
|
-
# "factories", "
|
|
6
|
-
#
|
|
5
|
+
# "factories.rb", "factories/**/*.rb", "test/factories.rb",
|
|
6
|
+
# "test/factories/**.rb", "spec/factories.rb", and "spec/factories/**.rb".
|
|
7
7
|
attr_accessor :definition_file_paths
|
|
8
8
|
end
|
|
9
9
|
|
data/lib/factory_bot/internal.rb
CHANGED
|
@@ -26,6 +26,7 @@ module FactoryBot
|
|
|
26
26
|
|
|
27
27
|
def register_inline_sequence(sequence)
|
|
28
28
|
inline_sequences.push(sequence)
|
|
29
|
+
sequence
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def rewind_inline_sequences
|
|
@@ -59,6 +60,22 @@ module FactoryBot
|
|
|
59
60
|
rewind_inline_sequences
|
|
60
61
|
end
|
|
61
62
|
|
|
63
|
+
def rewind_sequence(*uri_parts)
|
|
64
|
+
fail_argument_count(0, "1+") if uri_parts.empty?
|
|
65
|
+
|
|
66
|
+
uri = build_uri(uri_parts)
|
|
67
|
+
sequence = Sequence.find_by_uri(uri) || fail_unregistered_sequence(uri)
|
|
68
|
+
|
|
69
|
+
sequence.rewind
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def set_sequence(*uri_parts, value)
|
|
73
|
+
uri = build_uri(uri_parts) || fail_argument_count(uri_parts.size, "2+")
|
|
74
|
+
sequence = Sequence.find(*uri) || fail_unregistered_sequence(uri)
|
|
75
|
+
|
|
76
|
+
sequence.set_value(value)
|
|
77
|
+
end
|
|
78
|
+
|
|
62
79
|
def register_factory(factory)
|
|
63
80
|
factory.names.each do |name|
|
|
64
81
|
factories.register(name, factory)
|
|
@@ -86,6 +103,22 @@ module FactoryBot
|
|
|
86
103
|
register_strategy(:build_stubbed, FactoryBot::Strategy::Stub)
|
|
87
104
|
register_strategy(:null, FactoryBot::Strategy::Null)
|
|
88
105
|
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def build_uri(...)
|
|
110
|
+
FactoryBot::UriManager.build_uri(...)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def fail_argument_count(received, expected)
|
|
114
|
+
fail ArgumentError,
|
|
115
|
+
"wrong number of arguments (given #{received}, expected #{expected})"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def fail_unregistered_sequence(uri)
|
|
119
|
+
fail KeyError,
|
|
120
|
+
"Sequence not registered: '#{uri}'."
|
|
121
|
+
end
|
|
89
122
|
end
|
|
90
123
|
end
|
|
91
124
|
end
|
data/lib/factory_bot/linter.rb
CHANGED
|
@@ -70,7 +70,7 @@ module FactoryBot
|
|
|
70
70
|
def lint_factory(factory)
|
|
71
71
|
result = []
|
|
72
72
|
begin
|
|
73
|
-
FactoryBot.public_send(factory_strategy, factory.name)
|
|
73
|
+
in_transaction { FactoryBot.public_send(factory_strategy, factory.name) }
|
|
74
74
|
rescue => e
|
|
75
75
|
result |= [FactoryError.new(e, factory)]
|
|
76
76
|
end
|
|
@@ -80,7 +80,7 @@ module FactoryBot
|
|
|
80
80
|
def lint_traits(factory)
|
|
81
81
|
result = []
|
|
82
82
|
factory.definition.defined_traits.map(&:name).each do |trait_name|
|
|
83
|
-
FactoryBot.public_send(factory_strategy, factory.name, trait_name)
|
|
83
|
+
in_transaction { FactoryBot.public_send(factory_strategy, factory.name, trait_name) }
|
|
84
84
|
rescue => e
|
|
85
85
|
result |= [FactoryTraitError.new(e, factory, trait_name)]
|
|
86
86
|
end
|
|
@@ -106,5 +106,16 @@ module FactoryBot
|
|
|
106
106
|
:message
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
|
+
|
|
110
|
+
def in_transaction
|
|
111
|
+
if defined?(ActiveRecord::Base)
|
|
112
|
+
ActiveRecord::Base.transaction(joinable: false) do
|
|
113
|
+
yield
|
|
114
|
+
raise ActiveRecord::Rollback
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
yield
|
|
118
|
+
end
|
|
119
|
+
end
|
|
109
120
|
end
|
|
110
121
|
end
|
data/lib/factory_bot/registry.rb
CHANGED