factory_bot 6.5.1 → 6.5.6
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 +110 -87
- data/NEWS.md +439 -323
- data/README.md +12 -20
- data/lib/factory_bot/attribute_assigner.rb +74 -18
- data/lib/factory_bot/callbacks_observer.rb +19 -1
- data/lib/factory_bot/definition.rb +3 -2
- data/lib/factory_bot/definition_proxy.rb +19 -6
- data/lib/factory_bot/evaluator.rb +9 -3
- data/lib/factory_bot/factory.rb +10 -5
- data/lib/factory_bot/find_definitions.rb +2 -2
- data/lib/factory_bot/internal.rb +33 -0
- 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/methods.rb +62 -15
- data/lib/factory_bot/trait.rb +9 -7
- data/lib/factory_bot/uri_manager.rb +63 -0
- data/lib/factory_bot/version.rb +1 -1
- data/lib/factory_bot.rb +34 -9
- metadata +7 -9
- 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] [![Code Climate][grade-image]][grade] [![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?
|
|
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)
|
|
12
11
|
|
|
13
|
-
Check out the [guide](https://github.com/thoughtbot/factory_bot/blob/
|
|
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).
|
|
14
13
|
|
|
15
|
-
|
|
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
|
------------
|
|
@@ -96,7 +89,6 @@ We are [available for hire][hire].
|
|
|
96
89
|
[community]: https://thoughtbot.com/community?utm_source=github
|
|
97
90
|
[hire]: https://thoughtbot.com/hire-us?utm_source=github
|
|
98
91
|
|
|
99
|
-
|
|
100
92
|
<!-- END /templates/footer.md -->
|
|
101
93
|
|
|
102
94
|
[ci-image]: https://github.com/thoughtbot/factory_bot/actions/workflows/build.yml/badge.svg?branch=main
|
|
@@ -9,6 +9,7 @@ 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|
|
|
@@ -19,6 +20,7 @@ module FactoryBot
|
|
|
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
|
|
@@ -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
|
|
@@ -119,10 +119,14 @@ module FactoryBot
|
|
|
119
119
|
# end
|
|
120
120
|
#
|
|
121
121
|
# Except that no globally available sequence will be defined.
|
|
122
|
-
def sequence(name,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
def sequence(name, *args, &block)
|
|
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
|
|
@@ -51,8 +50,15 @@ module FactoryBot
|
|
|
51
50
|
@overrides.keys
|
|
52
51
|
end
|
|
53
52
|
|
|
54
|
-
def increment_sequence(sequence)
|
|
55
|
-
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>)"
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
def self.attribute_list
|
data/lib/factory_bot/factory.rb
CHANGED
|
@@ -12,7 +12,8 @@ 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
|
|
|
@@ -32,18 +33,22 @@ module FactoryBot
|
|
|
32
33
|
|
|
33
34
|
def run(build_strategy, overrides, &block)
|
|
34
35
|
block ||= ->(result) { result }
|
|
36
|
+
|
|
35
37
|
compile
|
|
36
38
|
|
|
37
|
-
strategy =
|
|
39
|
+
strategy = Strategy.lookup_strategy(build_strategy).new
|
|
38
40
|
|
|
39
41
|
evaluator = evaluator_class.new(strategy, overrides.symbolize_keys)
|
|
40
42
|
attribute_assigner = AttributeAssigner.new(evaluator, build_class, &compiled_constructor)
|
|
41
43
|
|
|
42
44
|
observer = CallbacksObserver.new(callbacks, evaluator)
|
|
43
|
-
evaluation =
|
|
44
|
-
|
|
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)
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
instance
|
|
47
52
|
end
|
|
48
53
|
|
|
49
54
|
def human_names
|
|
@@ -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/registry.rb
CHANGED
data/lib/factory_bot/sequence.rb
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
|
+
require "timeout"
|
|
2
|
+
|
|
1
3
|
module FactoryBot
|
|
2
4
|
# Sequences are defined using sequence within a FactoryBot.define block.
|
|
3
5
|
# Sequence values are generated using next.
|
|
4
6
|
# @api private
|
|
5
7
|
class Sequence
|
|
6
|
-
attr_reader :name
|
|
8
|
+
attr_reader :name, :uri_manager, :aliases
|
|
9
|
+
|
|
10
|
+
def self.find(*uri_parts)
|
|
11
|
+
if uri_parts.empty?
|
|
12
|
+
fail ArgumentError, "wrong number of arguments, expected 1+)"
|
|
13
|
+
else
|
|
14
|
+
find_by_uri FactoryBot::UriManager.build_uri(*uri_parts)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.find_by_uri(uri)
|
|
19
|
+
uri = uri.to_sym
|
|
20
|
+
FactoryBot::Internal.sequences.to_a.find { |seq| seq.has_uri?(uri) } ||
|
|
21
|
+
FactoryBot::Internal.inline_sequences.find { |seq| seq.has_uri?(uri) }
|
|
22
|
+
end
|
|
7
23
|
|
|
8
24
|
def initialize(name, *args, &proc)
|
|
25
|
+
options = args.extract_options!
|
|
9
26
|
@name = name
|
|
10
27
|
@proc = proc
|
|
11
|
-
|
|
12
|
-
|
|
28
|
+
@aliases = options.fetch(:aliases, []).map(&:to_sym)
|
|
29
|
+
@uri_manager = FactoryBot::UriManager.new(names, paths: options[:uri_paths])
|
|
13
30
|
@value = args.first || 1
|
|
14
|
-
@aliases = options.fetch(:aliases) { [] }
|
|
15
31
|
|
|
16
32
|
unless @value.respond_to?(:peek)
|
|
17
33
|
@value = EnumeratorAdapter.new(@value)
|
|
@@ -34,10 +50,40 @@ module FactoryBot
|
|
|
34
50
|
[@name] + @aliases
|
|
35
51
|
end
|
|
36
52
|
|
|
53
|
+
def has_name?(test_name)
|
|
54
|
+
names.include?(test_name.to_sym)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def has_uri?(uri)
|
|
58
|
+
uri_manager.include?(uri)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def for_factory?(test_factory_name)
|
|
62
|
+
FactoryBot::Internal.factory_by_name(factory_name).names.include?(test_factory_name.to_sym)
|
|
63
|
+
end
|
|
64
|
+
|
|
37
65
|
def rewind
|
|
38
66
|
@value.rewind
|
|
39
67
|
end
|
|
40
68
|
|
|
69
|
+
##
|
|
70
|
+
# If it's an Integer based sequence, set the new value directly,
|
|
71
|
+
# else rewind and seek from the beginning until a match is found.
|
|
72
|
+
#
|
|
73
|
+
def set_value(new_value)
|
|
74
|
+
if can_set_value_directly?(new_value)
|
|
75
|
+
@value.set_value(new_value)
|
|
76
|
+
elsif can_set_value_by_index?
|
|
77
|
+
set_value_by_index(new_value)
|
|
78
|
+
else
|
|
79
|
+
seek_value(new_value)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
protected
|
|
84
|
+
|
|
85
|
+
attr_reader :proc
|
|
86
|
+
|
|
41
87
|
private
|
|
42
88
|
|
|
43
89
|
def value
|
|
@@ -48,22 +94,103 @@ module FactoryBot
|
|
|
48
94
|
@value.next
|
|
49
95
|
end
|
|
50
96
|
|
|
97
|
+
def can_set_value_by_index?
|
|
98
|
+
@value.respond_to?(:find_index)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
##
|
|
102
|
+
# Set to the given value, or fail if not found
|
|
103
|
+
#
|
|
104
|
+
def set_value_by_index(value)
|
|
105
|
+
index = @value.find_index(value) || fail_value_not_found(value)
|
|
106
|
+
@value.rewind
|
|
107
|
+
index.times { @value.next }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
##
|
|
111
|
+
# Rewind index and seek until the value is found or the max attempts
|
|
112
|
+
# have been tried. If not found, the sequence is rewound to its original value
|
|
113
|
+
#
|
|
114
|
+
def seek_value(value)
|
|
115
|
+
original_value = @value.peek
|
|
116
|
+
|
|
117
|
+
# rewind and search for the new value
|
|
118
|
+
@value.rewind
|
|
119
|
+
Timeout.timeout(FactoryBot.sequence_setting_timeout) do
|
|
120
|
+
loop do
|
|
121
|
+
return if @value.peek == value
|
|
122
|
+
increment_value
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# loop auto-recues a StopIteration error, so if we
|
|
126
|
+
# reached this point, re-raise it now
|
|
127
|
+
fail StopIteration
|
|
128
|
+
end
|
|
129
|
+
rescue Timeout::Error, StopIteration
|
|
130
|
+
reset_original_value(original_value)
|
|
131
|
+
fail_value_not_found(value)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def reset_original_value(original_value)
|
|
135
|
+
@value.rewind
|
|
136
|
+
|
|
137
|
+
until @value.peek == original_value
|
|
138
|
+
increment_value
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def can_set_value_directly?(value)
|
|
143
|
+
return false unless value.is_a?(Integer)
|
|
144
|
+
return false unless @value.is_a?(EnumeratorAdapter)
|
|
145
|
+
@value.integer_value?
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def fail_value_not_found(value)
|
|
149
|
+
fail ArgumentError, "Unable to find '#{value}' in the sequence."
|
|
150
|
+
end
|
|
151
|
+
|
|
51
152
|
class EnumeratorAdapter
|
|
52
|
-
def initialize(
|
|
53
|
-
@
|
|
54
|
-
@value = value
|
|
153
|
+
def initialize(initial_value)
|
|
154
|
+
@initial_value = initial_value
|
|
55
155
|
end
|
|
56
156
|
|
|
57
157
|
def peek
|
|
58
|
-
|
|
158
|
+
value
|
|
59
159
|
end
|
|
60
160
|
|
|
61
161
|
def next
|
|
62
|
-
@value =
|
|
162
|
+
@value = value.next
|
|
63
163
|
end
|
|
64
164
|
|
|
65
165
|
def rewind
|
|
66
|
-
@value =
|
|
166
|
+
@value = first_value
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def set_value(new_value)
|
|
170
|
+
if new_value >= first_value
|
|
171
|
+
@value = new_value
|
|
172
|
+
else
|
|
173
|
+
fail ArgumentError, "Value cannot be less than: #{@first_value}"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def integer_value?
|
|
178
|
+
first_value.is_a?(Integer)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
def first_value
|
|
184
|
+
@first_value ||= initial_value
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def value
|
|
188
|
+
@value ||= initial_value
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def initial_value
|
|
192
|
+
@value = @initial_value.respond_to?(:call) ? @initial_value.call : @initial_value
|
|
193
|
+
@first_value = @value
|
|
67
194
|
end
|
|
68
195
|
end
|
|
69
196
|
end
|
|
@@ -102,12 +102,14 @@ module FactoryBot
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
def set_timestamps(result_instance)
|
|
105
|
+
timestamp = Time.current
|
|
106
|
+
|
|
105
107
|
if missing_created_at?(result_instance)
|
|
106
|
-
result_instance.created_at =
|
|
108
|
+
result_instance.created_at = timestamp
|
|
107
109
|
end
|
|
108
110
|
|
|
109
111
|
if missing_updated_at?(result_instance)
|
|
110
|
-
result_instance.updated_at =
|
|
112
|
+
result_instance.updated_at = timestamp
|
|
111
113
|
end
|
|
112
114
|
end
|
|
113
115
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "factory_bot/strategy/build"
|
|
2
|
+
require "factory_bot/strategy/create"
|
|
3
|
+
require "factory_bot/strategy/attributes_for"
|
|
4
|
+
require "factory_bot/strategy/stub"
|
|
5
|
+
require "factory_bot/strategy/null"
|
|
6
|
+
|
|
7
|
+
module FactoryBot
|
|
8
|
+
module Strategy
|
|
9
|
+
def self.lookup_strategy(name_or_object)
|
|
10
|
+
return name_or_object if name_or_object.is_a?(Class)
|
|
11
|
+
|
|
12
|
+
FactoryBot::Internal.strategy_by_name(name_or_object)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|