factory_bot 6.5.1 → 6.5.5

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.
data/README.md CHANGED
@@ -10,7 +10,7 @@ _[Interested in the history of the project name?][NAME]_
10
10
 
11
11
  ### Transitioning from factory\_girl?
12
12
 
13
- Check out the [guide](https://github.com/thoughtbot/factory_bot/blob/4-9-0-stable/UPGRADE_FROM_FACTORY_GIRL.md).
13
+ Check out the [guide](https://github.com/thoughtbot/factory_bot/blob/v4.9.0/UPGRADE_FROM_FACTORY_GIRL.md).
14
14
 
15
15
 
16
16
  Documentation
@@ -62,6 +62,7 @@ Useful Tools
62
62
  ------------
63
63
 
64
64
  * [FactoryTrace](https://github.com/djezzzl/factory_trace) - helps to find unused factories and traits.
65
+ * [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
66
 
66
67
  Contributing
67
68
  ------------
@@ -96,7 +97,6 @@ We are [available for hire][hire].
96
97
  [community]: https://thoughtbot.com/community?utm_source=github
97
98
  [hire]: https://thoughtbot.com/hire-us?utm_source=github
98
99
 
99
-
100
100
  <!-- END /templates/footer.md -->
101
101
 
102
102
  [ci-image]: https://github.com/thoughtbot/factory_bot/actions/workflows/build.yml/badge.svg?branch=main
@@ -72,7 +72,7 @@ module FactoryBot
72
72
  non_ignored_attribute_names +
73
73
  override_names -
74
74
  ignored_attribute_names -
75
- alias_names_to_ignore
75
+ aliased_attribute_names_to_ignore
76
76
  end
77
77
 
78
78
  def non_ignored_attribute_names
@@ -91,22 +91,39 @@ module FactoryBot
91
91
  @evaluator.__override_names__
92
92
  end
93
93
 
94
+ def attribute_names
95
+ @attribute_list.names
96
+ end
97
+
94
98
  def hash_instance_methods_to_respond_to
95
- @attribute_list.names + override_names + @build_class.instance_methods
99
+ attribute_names + override_names + @build_class.instance_methods
96
100
  end
97
101
 
98
- def alias_names_to_ignore
102
+ ##
103
+ # Creat a list of attribute names that will be
104
+ # overridden by an alias, so any defaults can
105
+ # ignored.
106
+ #
107
+ def aliased_attribute_names_to_ignore
99
108
  @attribute_list.non_ignored.flat_map { |attribute|
100
109
  override_names.map do |override|
101
- attribute.name if ignorable_alias?(attribute, override)
110
+ attribute.name if aliased_attribute?(attribute, override)
102
111
  end
103
112
  }.compact
104
113
  end
105
114
 
106
- def ignorable_alias?(attribute, override)
107
- attribute.alias_for?(override) &&
108
- attribute.name != override &&
109
- !ignored_attribute_names.include?(override)
115
+ ##
116
+ # Is the override an alias for the attribute and not the
117
+ # actual name of another attribute?
118
+ #
119
+ # Note: Checking against the names of all attributes, resolves any
120
+ # issues with having both <attribute> and <attribute>_id
121
+ # in the same factory.
122
+ #
123
+ def aliased_attribute?(attribute, override)
124
+ return false if attribute_names.include?(override)
125
+
126
+ attribute.alias_for?(override)
110
127
  end
111
128
  end
112
129
  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
- callback.run(result_instance, @evaluator)
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
- sequence = Sequence.new(name, ...)
124
- FactoryBot::Internal.register_inline_sequence(sequence)
125
- add_attribute(name) { increment_sequence(sequence) }
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
- @child_factories << [name, options, block]
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(self)
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
@@ -12,7 +12,8 @@ module FactoryBot
12
12
  @parent = options[:parent]
13
13
  @aliases = options[:aliases] || []
14
14
  @class_name = options[:class]
15
- @definition = Definition.new(@name, options[:traits] || [])
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,6 +33,7 @@ module FactoryBot
32
33
 
33
34
  def run(build_strategy, overrides, &block)
34
35
  block ||= ->(result) { result }
36
+
35
37
  compile
36
38
 
37
39
  strategy = StrategyCalculator.new(build_strategy).strategy.new
@@ -43,7 +45,11 @@ module FactoryBot
43
45
  evaluation =
44
46
  Evaluation.new(evaluator, attribute_assigner, compiled_to_create, observer)
45
47
 
46
- strategy.result(evaluation).tap(&block)
48
+ evaluation.notify(:before_all, nil)
49
+ instance = strategy.result(evaluation).tap(&block)
50
+ evaluation.notify(:after_all, instance)
51
+
52
+ instance
47
53
  end
48
54
 
49
55
  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", "test/factories" and "spec/factories". Only the first
6
- # existing file will be loaded.
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
 
@@ -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
@@ -1,4 +1,4 @@
1
- require "active_support/core_ext/hash/indifferent_access"
1
+ require "active_support/hash_with_indifferent_access"
2
2
 
3
3
  module FactoryBot
4
4
  class Registry
@@ -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
- options = args.extract_options!
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(value)
53
- @first_value = value
54
- @value = value
153
+ def initialize(initial_value)
154
+ @initial_value = initial_value
55
155
  end
56
156
 
57
157
  def peek
58
- @value
158
+ value
59
159
  end
60
160
 
61
161
  def next
62
- @value = @value.next
162
+ @value = value.next
63
163
  end
64
164
 
65
165
  def rewind
66
- @value = @first_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
@@ -6,6 +6,8 @@ module FactoryBot
6
6
  end
7
7
 
8
8
  def result(evaluation)
9
+ evaluation.notify(:before_build, nil)
10
+
9
11
  evaluation.object.tap do |instance|
10
12
  evaluation.notify(:after_build, instance)
11
13
  end
@@ -6,6 +6,8 @@ module FactoryBot
6
6
  end
7
7
 
8
8
  def result(evaluation)
9
+ evaluation.notify(:before_build, nil)
10
+
9
11
  evaluation.object.tap do |instance|
10
12
  evaluation.notify(:after_build, instance)
11
13
  evaluation.notify(:before_create, instance)
@@ -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 = Time.current
108
+ result_instance.created_at = timestamp
107
109
  end
108
110
 
109
111
  if missing_updated_at?(result_instance)
110
- result_instance.updated_at = Time.current
112
+ result_instance.updated_at = timestamp
111
113
  end
112
114
  end
113
115
 
@@ -102,33 +102,80 @@ module FactoryBot
102
102
  # @param [Array<Symbol, Symbol, Hash>] traits_and_overrides splat args traits and a hash of overrides
103
103
  # @param [Proc] block block to be executed
104
104
 
105
- # Generates and returns the next value in a sequence.
105
+ # Generates and returns the next value in a global or factory sequence.
106
106
  #
107
107
  # Arguments:
108
- # name: (Symbol)
109
- # The name of the sequence that a value should be generated for.
108
+ # context: (Array of Symbols)
109
+ # The definition context of the sequence, with the sequence name
110
+ # as the final entry
111
+ # scope: (object)(optional)
112
+ # The object the sequence should be evaluated within
110
113
  #
111
114
  # Returns:
112
115
  # The next value in the sequence. (Object)
113
- def generate(name)
114
- Internal.sequence_by_name(name).next
116
+ #
117
+ # Example:
118
+ # generate(:my_factory, :my_trair, :my_sequence)
119
+ #
120
+ def generate(*uri_parts, scope: nil)
121
+ uri = FactoryBot::UriManager.build_uri(uri_parts)
122
+ sequence = Sequence.find_by_uri(uri) ||
123
+ raise(KeyError,
124
+ "Sequence not registered: #{FactoryBot::UriManager.build_uri(uri_parts)}")
125
+
126
+ increment_sequence(sequence, scope: scope)
115
127
  end
116
128
 
117
- # Generates and returns the list of values in a sequence.
129
+ # Generates and returns the list of values in a global or factory sequence.
118
130
  #
119
131
  # Arguments:
120
- # name: (Symbol)
121
- # The name of the sequence that a value should be generated for.
122
- # count: (Fixnum)
123
- # Count of values
132
+ # uri_parts: (Array of Symbols)
133
+ # The definition context of the sequence, with the sequence name
134
+ # as the final entry
135
+ # scope: (object)(optional)
136
+ # The object the sequence should be evaluated within
124
137
  #
125
138
  # Returns:
126
139
  # The next value in the sequence. (Object)
127
- def generate_list(name, count)
140
+ #
141
+ # Example:
142
+ # generate_list(:my_factory, :my_trair, :my_sequence, 5)
143
+ #
144
+ def generate_list(*uri_parts, count, scope: nil)
145
+ uri = FactoryBot::UriManager.build_uri(uri_parts)
146
+ sequence = Sequence.find_by_uri(uri) ||
147
+ raise(KeyError, "Sequence not registered: '#{uri}'")
148
+
128
149
  (1..count).map do
129
- Internal.sequence_by_name(name).next
150
+ increment_sequence(sequence, scope: scope)
130
151
  end
131
152
  end
153
+
154
+ # ======================================================================
155
+ # = PRIVATE
156
+ # ======================================================================
157
+ #
158
+ private
159
+
160
+ ##
161
+ # Increments the given sequence and returns the value.
162
+ #
163
+ # Arguments:
164
+ # sequence:
165
+ # The sequence instance
166
+ # scope: (object)(optional)
167
+ # The object the sequence should be evaluated within
168
+ #
169
+ def increment_sequence(sequence, scope: nil)
170
+ value = sequence.next(scope)
171
+
172
+ raise if value.respond_to?(:start_with?) && value.start_with?("#<FactoryBot::Declaration")
173
+
174
+ value
175
+ rescue
176
+ raise ArgumentError, "Sequence '#{sequence.uri_manager.first}' failed to " \
177
+ "return a value. Perhaps it needs a scope to operate? (scope: <object>)"
178
+ end
132
179
  end
133
180
  end
134
181
  end
@@ -1,12 +1,17 @@
1
1
  module FactoryBot
2
2
  # @api private
3
3
  class Trait
4
- attr_reader :name, :definition
4
+ attr_reader :name, :uid, :definition
5
5
 
6
- def initialize(name, &block)
6
+ delegate :add_callback, :declare_attribute, :to_create, :define_trait, :constructor,
7
+ :callbacks, :attributes, :klass, :klass=, to: :@definition
8
+
9
+ def initialize(name, **options, &block)
7
10
  @name = name.to_s
8
11
  @block = block
9
- @definition = Definition.new(@name)
12
+ @uri_manager = FactoryBot::UriManager.new(names, paths: options[:uri_paths])
13
+
14
+ @definition = Definition.new(@name, uri_manager: @uri_manager)
10
15
  proxy = FactoryBot::DefinitionProxy.new(@definition)
11
16
 
12
17
  if block
@@ -15,12 +20,9 @@ module FactoryBot
15
20
  end
16
21
 
17
22
  def clone
18
- Trait.new(name, &block)
23
+ Trait.new(name, uri_paths: definition.uri_manager.paths, &block)
19
24
  end
20
25
 
21
- delegate :add_callback, :declare_attribute, :to_create, :define_trait, :constructor,
22
- :callbacks, :attributes, :klass, :klass=, to: :@definition
23
-
24
26
  def names
25
27
  [@name]
26
28
  end