stepford 0.13.0 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +27 -12
- data/lib/stepford/circular_ref_checker.rb +0 -1
- data/lib/stepford/cli.rb +4 -4
- data/lib/stepford/column_representation.rb +28 -0
- data/lib/stepford/factory_girl.rb +1 -213
- data/lib/stepford/factory_girl/config.rb +54 -0
- data/lib/stepford/factory_girl/generator.rb +198 -0
- data/lib/stepford/factory_girl/methods.rb +205 -0
- data/lib/stepford/factory_girl/rspec_helpers.rb +19 -0
- data/lib/stepford/version.rb +1 -1
- metadata +7 -4
- data/lib/stepford/factory_girl_generator.rb +0 -184
- data/lib/stepford/factory_girl_rspec_helpers.rb +0 -17
data/README.md
CHANGED
@@ -92,6 +92,30 @@ Then run:
|
|
92
92
|
|
93
93
|
bundle install
|
94
94
|
|
95
|
+
### Configuration
|
96
|
+
|
97
|
+
You don't have to use a `config/stepford.rb`, but if you have one, it will load it as needed both in CLI and via helpers, etc.
|
98
|
+
|
99
|
+
If you don't use the CLI, you can just put it into your `test/test_helper.rb`, `spec/spec_helper.rb`, or similar for the deep_* methods, and with the CLI you could just put it into a Rails environment file.
|
100
|
+
|
101
|
+
Debug option:
|
102
|
+
|
103
|
+
Stepford::FactoryGirl.debug = true
|
104
|
+
|
105
|
+
Make Stepford think that the schema looks different than it does to allow virtual attributes, etc.:
|
106
|
+
|
107
|
+
Stepford::FactoryGirl.column_overrides = {
|
108
|
+
[:bartender, :experience] => {null: false},
|
109
|
+
[:patron, :time_entered_bar] => {null: false},
|
110
|
+
[:patron, :some_virtual_attribute] => {null: false, virtual: true, type: :string, limit: 5} # if you specify a virtual attribute, be sure to include virtual: true and a valid type
|
111
|
+
}
|
112
|
+
|
113
|
+
Override options are: `:virtual`, `:type`, `:limit`, `:default`, `:null`, `:precision`, and `:scale`. Each is equivalent to their [ActiveRecord column equivalents][column_meta].
|
114
|
+
|
115
|
+
You can reconfigure it at runtime during tests if you'd like, and you can just call this if you want, but it doesn't have to be loaded this way. No magic involved, but it caches a little, so faster than just doing a `load`:
|
116
|
+
|
117
|
+
Stepford::FactoryGirl.load_config(pathname)
|
118
|
+
|
95
119
|
### Usage
|
96
120
|
|
97
121
|
#### Require
|
@@ -123,22 +147,12 @@ But, you might want to specify traits, and certain attributes or associations or
|
|
123
147
|
|
124
148
|
Put this in your `spec/spec_helper.rb`:
|
125
149
|
|
126
|
-
require 'stepford/
|
150
|
+
require 'stepford/factory_girl/rspec_helpers'
|
127
151
|
|
128
152
|
Then you can just use `deep_create`, `deep_create_list`, `deep_build`, `deep_build_list`, or `deep_build_stubbed` in your rspec tests (`deep_create` becomes a shortcut for `::Stepford::FactoryGirl.create`, etc.), e.g.:
|
129
153
|
|
130
154
|
deep_create(:foo)
|
131
155
|
|
132
|
-
##### Forcing Attributes and Associations
|
133
|
-
|
134
|
-
If you want to force attributes and associations to be set, use the not_null configuration setting, or hand-edit the factories.rb:
|
135
|
-
|
136
|
-
# each entry looks like [:model_name, :association_or_column_name]
|
137
|
-
Stepford::FactoryGirl.not_null = [
|
138
|
-
[:bartender, :experience],
|
139
|
-
[:patron, :time_entered_bar],
|
140
|
-
]
|
141
|
-
|
142
156
|
##### Cleaning Up
|
143
157
|
|
144
158
|
If you just want to run rspec at command-line, want to be able to create in before hooks, and don't want to mess with database cleaner, here is some code that you can add to your spec_helper to remove all model instances.
|
@@ -328,7 +342,7 @@ See [Testing all Factories (with RSpec)][test_factories] in the FactoryGirl wiki
|
|
328
342
|
Here is a version that tests the FactoryGirl factories and the Stepford deep_creates:
|
329
343
|
|
330
344
|
require 'spec_helper'
|
331
|
-
require 'stepford/
|
345
|
+
require 'stepford/factory_girl/rspec_helpers'
|
332
346
|
|
333
347
|
describe 'validate factories build' do
|
334
348
|
FactoryGirl.factories.each do |factory|
|
@@ -407,6 +421,7 @@ or referring to created objects through associations, though he said multiple ne
|
|
407
421
|
|
408
422
|
Copyright (c) 2012 Gary S. Weaver, released under the [MIT license][lic].
|
409
423
|
|
424
|
+
[column_meta]: http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html#method-i-column
|
410
425
|
[composite_primary_keys]: https://github.com/drnic/composite_primary_keys
|
411
426
|
[test_factories]: https://github.com/thoughtbot/factory_girl/wiki/Testing-all-Factories-%28with-RSpec%29
|
412
427
|
[factory_girl]: https://github.com/thoughtbot/factory_girl/
|
@@ -76,7 +76,6 @@ module Stepford
|
|
76
76
|
model_class.reflections.collect {|association_name, reflection|
|
77
77
|
puts "warning: #{model_class}'s association #{reflection.name}'s foreign_key was nil. can't check." unless reflection.foreign_key
|
78
78
|
assc_sym = reflection.name.to_sym
|
79
|
-
clas_sym = reflection.class_name.underscore.to_sym
|
80
79
|
|
81
80
|
begin
|
82
81
|
next_class = reflection.class_name.constantize
|
data/lib/stepford/cli.rb
CHANGED
@@ -17,8 +17,8 @@ module Stepford
|
|
17
17
|
# load Rails environment
|
18
18
|
require './config/environment'
|
19
19
|
# load FactoryGirl and generate factories
|
20
|
-
require 'stepford/
|
21
|
-
exit Stepford::
|
20
|
+
require 'stepford/factory_girl/generator'
|
21
|
+
exit ::Stepford::FactoryGirl::Generator.generate_factories(options) ? 0 : 1
|
22
22
|
end
|
23
23
|
|
24
24
|
desc "circular", "check for circular refs"
|
@@ -28,9 +28,9 @@ module Stepford
|
|
28
28
|
require './config/environment'
|
29
29
|
# load FactoryGirl and generate factories
|
30
30
|
require 'stepford/circular_ref_checker'
|
31
|
-
exit Stepford::CircularRefChecker.check_refs(options) ? 0 : 1
|
31
|
+
exit ::Stepford::CircularRefChecker.check_refs(options) ? 0 : 1
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
Stepford::CLI.start
|
36
|
+
::Stepford::CLI.start
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Stepford
|
2
|
+
# Needed a column representation that would allow user to specify attributes that are used for sample data choice for virtual attributes
|
3
|
+
# e.g. if you have an object_id column in the schema, but in model you have virtual proxy attribute methods to set it like my_object_id/my_object_id=
|
4
|
+
# then you need a way to specify that it should set my_object_id= with a string vs. number, etc.
|
5
|
+
class ColumnRepresentation
|
6
|
+
attr_accessor :name, :type, :limit, :default, :null, :precision, :scale, :virtual
|
7
|
+
|
8
|
+
def initialize(args)
|
9
|
+
if args.is_a?(Symbol)
|
10
|
+
@name = args.to_sym
|
11
|
+
elsif !(args.nil?)
|
12
|
+
# assume initializing with column
|
13
|
+
# see: http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html#method-i-column
|
14
|
+
@name = args.name
|
15
|
+
@type = args.type
|
16
|
+
@limit = args.limit
|
17
|
+
@default = args.default
|
18
|
+
@null = args.null # should be called nullable, but using what Rails/AR calls it to be easier to work with as if were a "real" AR column
|
19
|
+
@precision = args.precision
|
20
|
+
@scale = args.scale
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def merge_options(options)
|
25
|
+
options.each {|k,v|instance_variable_set("@#{k}", v)}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -1,213 +1 @@
|
|
1
|
-
require 'factory_girl'
|
2
|
-
require 'bigdecimal'
|
3
|
-
|
4
|
-
module Stepford
|
5
|
-
# A proxy for FactoryGirl that automatically recursively creates/builds/stubbed factories for null=false and/or presence validated associations.
|
6
|
-
#
|
7
|
-
# Lets you specify method name and arguments/options to factory girl for associations.
|
8
|
-
#
|
9
|
-
# e.g. if the following is required:
|
10
|
-
# * Bar has a required association called house_special which uses the :beer factory, and we have a block we want to send into it
|
11
|
-
# * Beer has specials that you want to build as a list of 3 using the :tuesday_special_offer factory
|
12
|
-
# then you could set that up like this:
|
13
|
-
# Stepford::FactoryGirl.create_list(:bar, with_factory_options: {
|
14
|
-
# house_special: [:create, :beer, {blk: ->(beer) do; beer.bubbles.create(attributes_for(:bubbles)); end}],
|
15
|
-
# specials: [:build_list, :tuesday_special_offer, 3]
|
16
|
-
# }) do
|
17
|
-
# # the block you would send to FactoryGirl.create_list(:foo) would go here
|
18
|
-
# end
|
19
|
-
module FactoryGirl
|
20
|
-
OPTIONS = [
|
21
|
-
:debug,
|
22
|
-
:not_null
|
23
|
-
]
|
24
|
-
|
25
|
-
class << self
|
26
|
-
OPTIONS.each{|o|attr_accessor o; define_method("#{o}?".to_sym){!!send("#{o}")}}
|
27
|
-
def configure(&blk); class_eval(&blk); end
|
28
|
-
|
29
|
-
def handle_factory_girl_method(m, *args, &block)
|
30
|
-
|
31
|
-
if args && args.size > 0
|
32
|
-
# call Stepford::FactoryGirl.* on any not null associations recursively
|
33
|
-
model_name = args[0]
|
34
|
-
begin
|
35
|
-
model_class = model_name.to_s.camelize.constantize
|
36
|
-
rescue => e
|
37
|
-
puts "Problem in #{model_name.to_s.camelize}" if model_name
|
38
|
-
raise e
|
39
|
-
end
|
40
|
-
|
41
|
-
args = args.dup # need local version because we'll be dup'ing the options hash to add things to set prior to create/build
|
42
|
-
options = args.last
|
43
|
-
if options.is_a?(Hash)
|
44
|
-
# keep them separate
|
45
|
-
orig_options = options
|
46
|
-
options = deep_dup(options)
|
47
|
-
args[(args.size - 1)] = options # need to set the dup'd options
|
48
|
-
else
|
49
|
-
# keep them separate
|
50
|
-
orig_options = {}
|
51
|
-
options = {}
|
52
|
-
args << options # need to add options to set associations
|
53
|
-
end
|
54
|
-
|
55
|
-
options[:with_factory_options] = {} unless options[:with_factory_options]
|
56
|
-
with_factory_options = options[:with_factory_options]
|
57
|
-
|
58
|
-
orig_options[:nesting_breadcrumbs] = [] unless orig_options[:nesting_breadcrumbs]
|
59
|
-
breadcrumbs = orig_options[:nesting_breadcrumbs]
|
60
|
-
breadcrumbs << [args[0]]
|
61
|
-
|
62
|
-
orig_options[:to_reload] = [] unless orig_options[:to_reload]
|
63
|
-
to_reload = orig_options[:to_reload]
|
64
|
-
|
65
|
-
if ::Stepford::FactoryGirl.debug?
|
66
|
-
puts "#{breadcrumbs.join('>')} start. args=#{debugargs(args)}"
|
67
|
-
end
|
68
|
-
|
69
|
-
model_class.reflections.each do |association_name, reflection|
|
70
|
-
assc_sym = reflection.name.to_sym
|
71
|
-
next if options[assc_sym] || options[reflection.foreign_key.to_sym] # || reflection.macro != :belongs_to
|
72
|
-
|
73
|
-
clas_sym = reflection.class_name.underscore.to_sym
|
74
|
-
has_presence_validator = model_class.validators_on(assc_sym).collect{|v|v.class}.include?(::ActiveModel::Validations::PresenceValidator)
|
75
|
-
required = reflection.foreign_key ? (has_presence_validator || model_class.columns.any?{|c| !c.null && c.name.to_sym == reflection.foreign_key.to_sym}) : false
|
76
|
-
orig_method_args_and_options = with_factory_options ? (with_factory_options[[clas_sym, assc_sym]] || with_factory_options[clas_sym]) : nil
|
77
|
-
|
78
|
-
has_presence_validator = model_class.validators_on(assc_sym).collect{|v|v.class}.include?(ActiveModel::Validations::PresenceValidator)
|
79
|
-
required = false
|
80
|
-
if reflection.macro == :belongs_to
|
81
|
-
# note: supports composite_primary_keys gem which stores primary_key as an array
|
82
|
-
foreign_key_is_also_primary_key = Array.wrap(model_class.primary_key).collect{|pk|pk.to_sym}.include?(reflection.foreign_key.to_sym)
|
83
|
-
is_not_null_fkey_that_is_not_primary_key = model_class.columns.any?{|c| !c.null && c.name.to_sym == reflection.foreign_key.to_sym && !foreign_key_is_also_primary_key}
|
84
|
-
required = is_not_null_fkey_that_is_not_primary_key || has_presence_validator
|
85
|
-
else
|
86
|
-
# no nullable metadata on column if no foreign key in this table. we'd figure out the null requirement on the column if inspecting the child model
|
87
|
-
required = has_presence_validator
|
88
|
-
end
|
89
|
-
|
90
|
-
if required || Array.wrap(::Stepford::FactoryGirl.not_null).compact.include?([model_name.to_sym, assc_sym])
|
91
|
-
breadcrumbs << ["a:#{assc_sym}"]
|
92
|
-
if orig_method_args_and_options
|
93
|
-
method_args_and_options = orig_method_args_and_options.dup
|
94
|
-
method_options = args.last
|
95
|
-
blk = method_options.is_a?(Hash) ? method_args_and_options.delete(:blk) : nil
|
96
|
-
begin
|
97
|
-
if blk
|
98
|
-
options[assc_sym] = ::FactoryGirl.__send__(*method_args_and_options, &blk)
|
99
|
-
else
|
100
|
-
options[assc_sym] = ::FactoryGirl.__send__(*method_args_and_options)
|
101
|
-
end
|
102
|
-
to_reload << options[assc_sym]
|
103
|
-
rescue => e
|
104
|
-
puts "#{breadcrumbs.join('>')}: FactoryGirl.__send__(#{method_args_and_options.inspect}): #{e}#{::Stepford::FactoryGirl.debug? ? "\n#{e.backtrace.join("\n")}" : ''}"
|
105
|
-
raise e
|
106
|
-
end
|
107
|
-
else
|
108
|
-
if reflection.macro == :has_many
|
109
|
-
options[assc_sym] = ::Stepford::FactoryGirl.create_list(clas_sym, 2, orig_options)
|
110
|
-
else
|
111
|
-
options[assc_sym] = ::Stepford::FactoryGirl.create(clas_sym, orig_options)
|
112
|
-
end
|
113
|
-
end
|
114
|
-
breadcrumbs.pop
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
if defined?(breadcrumbs)
|
120
|
-
if ::Stepford::FactoryGirl.debug?
|
121
|
-
puts "#{breadcrumbs.join('>')} end"
|
122
|
-
puts "#{breadcrumbs.join('>')} FactoryGirl.#{m}(#{debugargs(args)})"
|
123
|
-
end
|
124
|
-
breadcrumbs.pop
|
125
|
-
end
|
126
|
-
|
127
|
-
# clean-up before sending to FactoryGirl
|
128
|
-
if args.last.is_a?(Hash)
|
129
|
-
(args.last).delete(:with_factory_options)
|
130
|
-
(args.last).delete(:nesting_breadcrumbs)
|
131
|
-
(args.last).delete(:to_reload)
|
132
|
-
end
|
133
|
-
|
134
|
-
begin
|
135
|
-
raise "#{breadcrumbs.join('>')} - Huh? args[0] was #{args[0]}. m=#{m.inspect}, args=#{args.inspect}" if args && args.size > 1 && !(args[0].is_a?(String) || args[0].is_a?(Symbol))
|
136
|
-
result = ::FactoryGirl.__send__(m, *args, &block)
|
137
|
-
rescue => e
|
138
|
-
puts "#{breadcrumbs.join('>')}: FactoryGirl.#{m}(#{args.inspect}): #{e}#{::Stepford::FactoryGirl.debug? ? "\n#{e.backtrace.join("\n")}" : ''}" if defined?(breadcrumbs)
|
139
|
-
raise e
|
140
|
-
end
|
141
|
-
|
142
|
-
if args.last.is_a?(Hash) && defined?(breadcrumbs) && breadcrumbs.size > 0
|
143
|
-
# still handling association/subassociation
|
144
|
-
args.last[:nesting_breadcrumbs] = breadcrumbs
|
145
|
-
args.last[:to_reload] = to_reload
|
146
|
-
orig_options[:to_reload] << result
|
147
|
-
else
|
148
|
-
# ready to return the initially requested instances, so reload children with their parents, in reverse order added
|
149
|
-
orig_options[:to_reload].each do |i|
|
150
|
-
begin
|
151
|
-
i.reload
|
152
|
-
rescue => e
|
153
|
-
puts "#{i} reload failed: #{e}\n#{e.backtrace.join("\n")}" if ::Stepford::FactoryGirl.debug?
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
result
|
159
|
-
end
|
160
|
-
|
161
|
-
# switched to this from method_missing to avoid method trying to handle mistaken calls
|
162
|
-
def create(*args, &block); handle_factory_girl_method(:create, *args, &block); end
|
163
|
-
def create_list(*args, &block); handle_factory_girl_method(:create_list, *args, &block); end
|
164
|
-
def build(*args, &block); handle_factory_girl_method(:build, *args, &block); end
|
165
|
-
def build_list(*args, &block); handle_factory_girl_method(:build_list, *args, &block); end
|
166
|
-
def build_stubbed(*args, &block); handle_factory_girl_method(:build_stubbed, *args, &block); end
|
167
|
-
# pass everything else to FactoryGirl to try to handle (can't reflect in current version to find what it handles)
|
168
|
-
def method_missing(m, *args, &block); ::FactoryGirl.__send__(m, *args, &block); end
|
169
|
-
|
170
|
-
def deep_dup(o)
|
171
|
-
result = nil
|
172
|
-
if o.is_a?(Hash)
|
173
|
-
result = {}
|
174
|
-
o.keys.each do |key|
|
175
|
-
result[deep_dup(key)] = deep_dup(o[key])
|
176
|
-
end
|
177
|
-
elsif o.is_a?(Array)
|
178
|
-
result = []
|
179
|
-
o.each do |value|
|
180
|
-
result << deep_dup(value)
|
181
|
-
end
|
182
|
-
elsif [NilClass,FalseClass,TrueClass,Symbol,Numeric,Class,Module].any?{|c|o.is_a?(c)}
|
183
|
-
result = o
|
184
|
-
elsif o.is_a?(BigDecimal)
|
185
|
-
# ActiveSupport v3.2.8 checks duplicable? for BigDecimal by testing it, so we'll just try to dup the value itself
|
186
|
-
result = o
|
187
|
-
begin
|
188
|
-
result = o.dup
|
189
|
-
rescue TypeError
|
190
|
-
# can't dup
|
191
|
-
end
|
192
|
-
elsif o.is_a?(Object)
|
193
|
-
result = o.dup
|
194
|
-
else
|
195
|
-
result = o
|
196
|
-
end
|
197
|
-
result
|
198
|
-
end
|
199
|
-
|
200
|
-
def debugargs(args)
|
201
|
-
result = []
|
202
|
-
args.each do |arg|
|
203
|
-
if arg.is_a?(Hash)
|
204
|
-
result << "{#{arg.keys.collect{|key|"#{key} = (#{arg[key].class.name})"}.join(', ')}}"
|
205
|
-
else
|
206
|
-
result << "#{arg.inspect},"
|
207
|
-
end
|
208
|
-
end
|
209
|
-
result.join('')
|
210
|
-
end
|
211
|
-
end
|
212
|
-
end
|
213
|
-
end
|
1
|
+
require 'stepford/factory_girl/methods'
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Stepford
|
2
|
+
module FactoryGirl
|
3
|
+
OPTIONS = [:debug, :column_overrides, :config_loaded, :column_overrides_tree]
|
4
|
+
|
5
|
+
class << self
|
6
|
+
OPTIONS.each{|o|attr_accessor o; define_method("#{o}?".to_sym){!!send("#{o}")}}
|
7
|
+
def configure(&blk); class_eval(&blk); end
|
8
|
+
|
9
|
+
# Loads the configuration from config/stepford.rb or a specified pathname unless has already been loaded.
|
10
|
+
def load_config(pathname = nil)
|
11
|
+
if !(pathname || ::Stepford::FactoryGirl.config_loaded) || (pathname && ::Stepford::FactoryGirl.config_loaded.to_sym != pathname.to_sym)
|
12
|
+
begin
|
13
|
+
if pathname
|
14
|
+
# load without checking if exists to raise error if user-specified file is missing
|
15
|
+
force_configure(pathname)
|
16
|
+
else
|
17
|
+
pathname = Rails.root.join('config', 'stepford.rb').to_s
|
18
|
+
if File.exist?(pathname)
|
19
|
+
force_configure(pathname)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
rescue => e
|
23
|
+
puts "Failed to load #{pathname}:\n#{e.message}#{e.backtrace.join("\n")}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def force_configure(pathname)
|
29
|
+
load pathname
|
30
|
+
puts "Loaded #{pathname}"
|
31
|
+
::Stepford::FactoryGirl.config_loaded = pathname
|
32
|
+
end
|
33
|
+
|
34
|
+
def column_overrides=(args)
|
35
|
+
# to avoid a lot of processing overhead, we preprocess the arrays into a hash that would look ugly to the user, e.g.
|
36
|
+
# {:model_name => {:attribute_name => {options or just empty}}}
|
37
|
+
result = {}
|
38
|
+
args.each do |k,v|
|
39
|
+
if k.is_a?(Array) && k.size == 2 && v.is_a?(Hash)
|
40
|
+
table_columns = (result[k[0].to_sym] ||= {})
|
41
|
+
table_column_options = (table_columns[k[1].to_sym] ||= {})
|
42
|
+
table_column_options.merge(v)
|
43
|
+
else
|
44
|
+
puts "Ignoring bad Stepford::FactoryGirl.column_overrides array value: #{a.inspect}. Should look like [:model_name, :attribute_name, {}]. See documentation for information on defining options hash."
|
45
|
+
end
|
46
|
+
end if args
|
47
|
+
@column_overrides = args
|
48
|
+
@column_overrides_tree = result
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
::Stepford::FactoryGirl.load_config
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'stepford/column_representation'
|
2
|
+
require 'stepford/common'
|
3
|
+
require 'stepford/factory_girl/config'
|
4
|
+
|
5
|
+
module Stepford
|
6
|
+
module FactoryGirl
|
7
|
+
class Generator
|
8
|
+
def self.generate_factories(options={})
|
9
|
+
factories = {}
|
10
|
+
expected = {}
|
11
|
+
included_model_syms = options[:models] ? options[:models].split(',').collect{|s|s.strip.to_sym}.compact : nil
|
12
|
+
puts "Rails.root=#{Rails.root}"
|
13
|
+
Dir[Rails.root.join('app','models','*.rb').to_s].each do |filename|
|
14
|
+
model_name = File.basename(filename).sub(/.rb$/, '')
|
15
|
+
model_name_sym = model_name.to_sym
|
16
|
+
next if included_model_syms && !included_model_syms.include?(model_name_sym)
|
17
|
+
load filename
|
18
|
+
|
19
|
+
begin
|
20
|
+
model_class = model_name.camelize.constantize
|
21
|
+
rescue => e
|
22
|
+
puts "Problem in #{model_name.camelize}"
|
23
|
+
raise e
|
24
|
+
end
|
25
|
+
|
26
|
+
next unless model_class.ancestors.include?(ActiveRecord::Base)
|
27
|
+
factory = (factories[model_name_sym] ||= [])
|
28
|
+
pkey_syms = Array.wrap(model_class.primary_key).collect{|pk|pk.to_sym}
|
29
|
+
excluded_attribute_syms = [:updated_at, :created_at, :object_id]
|
30
|
+
excluded_attribute_syms_and_pkeys = pkey_syms + [:updated_at, :created_at, :object_id]
|
31
|
+
model_class.reflections.collect {|association_name, reflection|
|
32
|
+
(expected[reflection.class_name.underscore.to_sym] ||= []) << model_name_sym
|
33
|
+
fkey_sym = reflection.foreign_key.try(:to_sym)
|
34
|
+
excluded_attribute_syms_and_pkeys << fkey_sym if reflection.foreign_key && !(excluded_attribute_syms_and_pkeys.include?(fkey_sym))
|
35
|
+
assc_sym = reflection.name.to_sym
|
36
|
+
clas_sym = reflection.class_name.underscore.to_sym
|
37
|
+
# if has a foreign key, then if NOT NULL or is a presence validate, the association is required and should be output. unfortunately this could mean a circular reference that will have to be manually fixed
|
38
|
+
has_presence_validator = model_class.validators_on(assc_sym).collect{|v|v.class}.include?(ActiveModel::Validations::PresenceValidator)
|
39
|
+
required = reflection.foreign_key ? (has_presence_validator || model_class.columns.any?{|c| !c.null && c.name.to_sym == fkey_sym}) : false
|
40
|
+
should_be_trait = !(options[:associations] || (options[:include_required_associations] && required)) && options[:association_traits]
|
41
|
+
if options[:associations] || (options[:include_required_associations] && required) || should_be_trait
|
42
|
+
if reflection.macro == :has_many
|
43
|
+
# In factory girl v4.1.0:
|
44
|
+
# create_list must be done in an after(:create) or you get Trait not registered or Factory not registered errors.
|
45
|
+
# this means that validators that verify presence or size > 0 in a association list will not work with this method, and you'll need to
|
46
|
+
# use build, not create: http://stackoverflow.com/questions/11209347/has-many-with-at-least-two-entries
|
47
|
+
"#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}#{should_be_trait || has_presence_validator ? '' : '#'}after(:create) do |user, evaluator|; FactoryGirl.create_list #{clas_sym.inspect}, 2; end#{should_be_trait ? '; end' : ''}#{should_be_trait ? '' : ' # commented to avoid circular reference'}"
|
48
|
+
elsif assc_sym != clas_sym
|
49
|
+
"#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}#{should_be_trait || reflection.macro == :belongs_to || has_presence_validator ? '' : '#'}association #{assc_sym.inspect}#{assc_sym != clas_sym ? ", factory: #{clas_sym.inspect}" : ''}#{should_be_trait ? '; end' : ''}#{should_be_trait || reflection.macro == :belongs_to ? '' : ' # commented to avoid circular reference'}"
|
50
|
+
else
|
51
|
+
"#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}#{should_be_trait || reflection.macro == :belongs_to || has_presence_validator ? '' : '#'}#{is_reserved?(assc_sym) ? 'self.' : ''}#{assc_sym}#{should_be_trait ? '; end' : ''}#{should_be_trait || reflection.macro == :belongs_to ? '' : ' # commented to avoid circular reference'}"
|
52
|
+
end
|
53
|
+
else
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
}.compact.sort.each {|l|factory << l}
|
57
|
+
|
58
|
+
sequenceless_table = false
|
59
|
+
begin
|
60
|
+
sequenceless_table = true unless m.sequence_name
|
61
|
+
rescue => e
|
62
|
+
# bug in Rails 3.2.8, at least: undefined method `split' for nil:NilClass in activerecord-3.2.8/lib/active_record/connection_adapters/postgresql_adapter.rb:911:in `default_sequence_name'
|
63
|
+
sequenceless_table = true
|
64
|
+
end
|
65
|
+
|
66
|
+
model_column_name_sym_to_column_representation = {}
|
67
|
+
model_class.columns.each {|c|model_column_name_sym_to_column_representation[c.name.to_sym] = ::Stepford::ColumnRepresentation.new(c)}
|
68
|
+
::Stepford::FactoryGirl.column_overrides_tree[model_name_sym].each do |column_override_attr_sym, column_override_options|
|
69
|
+
(model_column_name_sym_to_column_representation[column_override_attr_sym] ||= ::Stepford::ColumnRepresentation.new(column_override_attr_sym)).merge_options(column_override_options)
|
70
|
+
end if ::Stepford::FactoryGirl.column_overrides_tree[model_name_sym]
|
71
|
+
|
72
|
+
model_column_name_sym_to_column_representation.values {|c|[c.name]}.each {|c|
|
73
|
+
# without a sequence, FactoryGirl will need to have sequences for pkeys
|
74
|
+
if sequenceless_table && excluded_attribute_syms.include?(c.name.to_sym)
|
75
|
+
factory << ::Stepford::Common.sequence_for(c)
|
76
|
+
elsif !excluded_attribute_syms_and_pkeys.include?(c.name.to_sym) && !(c.name.to_s.downcase.end_with?('_id') && options[:exclude_all_ids]) && (options[:attributes] || !c.null)
|
77
|
+
has_uniqueness_validator = model_class.validators_on(c.name.to_sym).collect{|v|v.class}.include?(ActiveRecord::Validations::UniquenessValidator)
|
78
|
+
if has_uniqueness_validator
|
79
|
+
#TODO: specialize for different data types
|
80
|
+
factory << ::Stepford::Common.sequence_for(c)
|
81
|
+
else
|
82
|
+
factory << "#{is_reserved?(c.name) ? 'self.' : ''}#{c.name} #{Stepford::Common.value_for(c)}"
|
83
|
+
end
|
84
|
+
elsif options[:attribute_traits]
|
85
|
+
if c.type == :boolean
|
86
|
+
factory << "trait #{c.name.underscore.to_sym.inspect} do; #{is_reserved?(c.name) ? 'self.' : ''}#{c.name} true; end"
|
87
|
+
factory << "trait #{"not_#{c.name.underscore}".to_sym.inspect} do; #{is_reserved?(c.name) ? 'self.' : ''}#{c.name} false; end"
|
88
|
+
else
|
89
|
+
factory << "trait #{"with_#{c.name.underscore}".to_sym.inspect} do; #{is_reserved?(c.name) ? 'self.' : ''}#{c.name} #{Stepford::Common.value_for(c)}; end"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
if options[:associations] || options[:validate_associations]
|
96
|
+
failed = false
|
97
|
+
model_to_fixes_required = {}
|
98
|
+
expected.keys.sort.each do |factory_name_sym|
|
99
|
+
unless factories[factory_name_sym]
|
100
|
+
puts "#{Rails.root.join('app','models',"#{factory_name_sym}.rb")} missing. Model(s) with associations to it: #{expected[factory_name_sym].sort.join(', ')}"
|
101
|
+
expected[factory_name_sym].each do |model_name|
|
102
|
+
(model_to_fixes_required[model_name_sym] ||= []) << factory_name_sym
|
103
|
+
end
|
104
|
+
failed = true
|
105
|
+
end
|
106
|
+
end
|
107
|
+
model_to_fixes_required.keys.each do |model_to_fix|
|
108
|
+
puts ""
|
109
|
+
puts "In #{model_to_fix}:"
|
110
|
+
model_to_fixes_required[model_to_fix].each do |fix|
|
111
|
+
puts "- comment/remove/fix broken association to #{fix}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
return false if failed
|
115
|
+
end
|
116
|
+
|
117
|
+
path = get_factories_rb_pathname(options)
|
118
|
+
|
119
|
+
if path.end_with?('.rb')
|
120
|
+
dirpath = File.dirname(path)
|
121
|
+
unless File.directory?(dirpath)
|
122
|
+
puts "Please create this directory first: #{dirpath}"
|
123
|
+
return false
|
124
|
+
end
|
125
|
+
|
126
|
+
File.open(path, "w") do |f|
|
127
|
+
write_header(f, options)
|
128
|
+
factories.keys.sort.each do |factory_name_sym|
|
129
|
+
factory = factories[factory_name_sym]
|
130
|
+
write_factory(factory_name_sym, factory, f)
|
131
|
+
end
|
132
|
+
write_footer(f)
|
133
|
+
end
|
134
|
+
else
|
135
|
+
unless File.directory?(path)
|
136
|
+
puts "Please create this directory first: #{path}"
|
137
|
+
return false
|
138
|
+
end
|
139
|
+
|
140
|
+
factories.keys.sort.each do |factory_name_sym|
|
141
|
+
factory = factories[factory_name_sym]
|
142
|
+
File.open(File.join(path,"#{factory_name_sym}.rb"), "w") do |f|
|
143
|
+
write_header(f, options)
|
144
|
+
write_factory(factory_name_sym, factory, f)
|
145
|
+
write_footer(f)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
return true
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def self.is_reserved?(s)
|
156
|
+
# modified from http://stackoverflow.com/questions/6461303/built-in-way-to-determine-whether-a-string-is-a-ruby-reserved-word/6461673#6461673
|
157
|
+
%w{__FILE__ __LINE__ alias and begin BEGIN break case class def defined? do else elsif end END ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield}.include? s.to_s
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.get_factories_rb_pathname(options)
|
161
|
+
path = Rails.root.join('test','factories.rb')
|
162
|
+
if options[:path]
|
163
|
+
if options[:path].end_with?('.rb')
|
164
|
+
path = options[:path]
|
165
|
+
else
|
166
|
+
if options[:multiple]
|
167
|
+
path = options[:path]
|
168
|
+
else
|
169
|
+
path = Rails.root.join(options[:path],'factories.rb')
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
# convert Pathname to string
|
174
|
+
path.to_s
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.write_header(f, options)
|
178
|
+
f.puts '# original version autogenerated by Stepford: https://github.com/garysweaver/stepford'
|
179
|
+
f.puts ''
|
180
|
+
f.puts 'FactoryGirl.define do'
|
181
|
+
f.puts ' '
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.write_factory(factory_name, factory, f)
|
185
|
+
f.puts " factory #{factory_name.inspect} do"
|
186
|
+
factory.each do |line|
|
187
|
+
f.puts " #{line}"
|
188
|
+
end
|
189
|
+
f.puts ' end'
|
190
|
+
f.puts ' '
|
191
|
+
end
|
192
|
+
|
193
|
+
def self.write_footer(f)
|
194
|
+
f.puts 'end'
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require 'factory_girl'
|
2
|
+
require 'bigdecimal'
|
3
|
+
require 'stepford/factory_girl/config'
|
4
|
+
|
5
|
+
module Stepford
|
6
|
+
# Automatically recursively creates/builds/stubbed associations using FactoryGirl.
|
7
|
+
#
|
8
|
+
# You can specify the method name and arguments/options to factory girl for the associations. e.g. if the following is required:
|
9
|
+
# * Bar has a required association called house_special which uses the :beer factory, and we have a block we want to send into it
|
10
|
+
# * Beer has specials that you want to build as a list of 3 using the :tuesday_special_offer factory
|
11
|
+
# you could do that with:
|
12
|
+
# Stepford::FactoryGirl.create_list(:bar, with_factory_options: {
|
13
|
+
# house_special: [:create, :beer, {blk: ->(beer) do; beer.bubbles.create(attributes_for(:bubbles)); end}],
|
14
|
+
# specials: [:build_list, :tuesday_special_offer, 3]
|
15
|
+
# }) do
|
16
|
+
# # the block you would send to FactoryGirl.create_list(:foo) would go here
|
17
|
+
# end
|
18
|
+
module FactoryGirl
|
19
|
+
|
20
|
+
class << self
|
21
|
+
|
22
|
+
def handle_factory_girl_method(m, *args, &block)
|
23
|
+
if args && args.size > 0
|
24
|
+
# call Stepford::FactoryGirl.* on any not null associations recursively
|
25
|
+
model_name = args[0]
|
26
|
+
begin
|
27
|
+
model_class = model_name.to_s.camelize.constantize
|
28
|
+
rescue => e
|
29
|
+
puts "Problem in #{model_name.to_s.camelize}" if model_name
|
30
|
+
raise e
|
31
|
+
end
|
32
|
+
|
33
|
+
args = args.dup # need local version because we'll be dup'ing the options hash to add things to set prior to create/build
|
34
|
+
options = args.last
|
35
|
+
if options.is_a?(Hash)
|
36
|
+
# keep them separate
|
37
|
+
orig_options = options
|
38
|
+
options = deep_dup(options)
|
39
|
+
args[(args.size - 1)] = options # need to set the dup'd options
|
40
|
+
else
|
41
|
+
# keep them separate
|
42
|
+
orig_options = {}
|
43
|
+
options = {}
|
44
|
+
args << options # need to add options to set associations
|
45
|
+
end
|
46
|
+
|
47
|
+
options[:with_factory_options] = {} unless options[:with_factory_options]
|
48
|
+
with_factory_options = options[:with_factory_options]
|
49
|
+
|
50
|
+
orig_options[:nesting_breadcrumbs] = [] unless orig_options[:nesting_breadcrumbs]
|
51
|
+
breadcrumbs = orig_options[:nesting_breadcrumbs]
|
52
|
+
breadcrumbs << [args[0]]
|
53
|
+
|
54
|
+
orig_options[:to_reload] = [] unless orig_options[:to_reload]
|
55
|
+
to_reload = orig_options[:to_reload]
|
56
|
+
|
57
|
+
if ::Stepford::FactoryGirl.debug?
|
58
|
+
puts "#{breadcrumbs.join('>')} start. args=#{debugargs(args)}"
|
59
|
+
end
|
60
|
+
|
61
|
+
model_class.reflections.each do |association_name, reflection|
|
62
|
+
assc_sym = reflection.name.to_sym
|
63
|
+
next if options[assc_sym] || options[reflection.foreign_key.to_sym] # || reflection.macro != :belongs_to
|
64
|
+
|
65
|
+
clas_sym = reflection.class_name.underscore.to_sym
|
66
|
+
has_presence_validator = model_class.validators_on(assc_sym).collect{|v|v.class}.include?(::ActiveModel::Validations::PresenceValidator)
|
67
|
+
required = reflection.foreign_key ? (has_presence_validator || model_class.columns.any?{|c| !c.null && c.name.to_sym == reflection.foreign_key.to_sym}) : false
|
68
|
+
orig_method_args_and_options = with_factory_options ? (with_factory_options[[clas_sym, assc_sym]] || with_factory_options[clas_sym]) : nil
|
69
|
+
|
70
|
+
has_presence_validator = model_class.validators_on(assc_sym).collect{|v|v.class}.include?(ActiveModel::Validations::PresenceValidator)
|
71
|
+
required = false
|
72
|
+
if reflection.macro == :belongs_to
|
73
|
+
# note: supports composite_primary_keys gem which stores primary_key as an array
|
74
|
+
foreign_key_is_also_primary_key = Array.wrap(model_class.primary_key).collect{|pk|pk.to_sym}.include?(reflection.foreign_key.to_sym)
|
75
|
+
is_not_null_fkey_that_is_not_primary_key = model_class.columns.any?{|c| !c.null && c.name.to_sym == reflection.foreign_key.to_sym && !foreign_key_is_also_primary_key}
|
76
|
+
required = is_not_null_fkey_that_is_not_primary_key || has_presence_validator
|
77
|
+
else
|
78
|
+
# no nullable metadata on column if no foreign key in this table. we'd figure out the null requirement on the column if inspecting the child model
|
79
|
+
required = has_presence_validator
|
80
|
+
end
|
81
|
+
|
82
|
+
if required || Array.wrap(::Stepford::FactoryGirl.column_overrides).compact.include?([model_name.to_sym, assc_sym])
|
83
|
+
breadcrumbs << ["a:#{assc_sym}"]
|
84
|
+
if orig_method_args_and_options
|
85
|
+
method_args_and_options = orig_method_args_and_options.dup
|
86
|
+
method_options = args.last
|
87
|
+
blk = method_options.is_a?(Hash) ? method_args_and_options.delete(:blk) : nil
|
88
|
+
begin
|
89
|
+
if blk
|
90
|
+
options[assc_sym] = ::FactoryGirl.__send__(*method_args_and_options, &blk)
|
91
|
+
else
|
92
|
+
options[assc_sym] = ::FactoryGirl.__send__(*method_args_and_options)
|
93
|
+
end
|
94
|
+
to_reload << options[assc_sym]
|
95
|
+
rescue => e
|
96
|
+
puts "#{breadcrumbs.join('>')}: FactoryGirl.__send__(#{method_args_and_options.inspect}): #{e}#{::Stepford::FactoryGirl.debug? ? "\n#{e.backtrace.join("\n")}" : ''}"
|
97
|
+
raise e
|
98
|
+
end
|
99
|
+
else
|
100
|
+
if reflection.macro == :has_many
|
101
|
+
options[assc_sym] = ::Stepford::FactoryGirl.create_list(clas_sym, 2, orig_options)
|
102
|
+
else
|
103
|
+
options[assc_sym] = ::Stepford::FactoryGirl.create(clas_sym, orig_options)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
breadcrumbs.pop
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
if defined?(breadcrumbs)
|
111
|
+
if ::Stepford::FactoryGirl.debug?
|
112
|
+
puts "#{breadcrumbs.join('>')} end"
|
113
|
+
puts "#{breadcrumbs.join('>')} FactoryGirl.#{m}(#{debugargs(args)})"
|
114
|
+
end
|
115
|
+
breadcrumbs.pop
|
116
|
+
end
|
117
|
+
|
118
|
+
# clean-up before sending to FactoryGirl
|
119
|
+
if args.last.is_a?(Hash)
|
120
|
+
(args.last).delete(:with_factory_options)
|
121
|
+
(args.last).delete(:nesting_breadcrumbs)
|
122
|
+
(args.last).delete(:to_reload)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
begin
|
127
|
+
raise "#{breadcrumbs.join('>')} - Huh? args[0] was #{args[0]}. m=#{m.inspect}, args=#{args.inspect}" if args && args.size > 1 && !(args[0].is_a?(String) || args[0].is_a?(Symbol))
|
128
|
+
result = ::FactoryGirl.__send__(m, *args, &block)
|
129
|
+
rescue => e
|
130
|
+
puts "#{breadcrumbs.join('>')}: FactoryGirl.#{m}(#{args.inspect}): #{e}#{::Stepford::FactoryGirl.debug? ? "\n#{e.backtrace.join("\n")}" : ''}" if defined?(breadcrumbs)
|
131
|
+
raise e
|
132
|
+
end
|
133
|
+
|
134
|
+
if args.last.is_a?(Hash) && defined?(breadcrumbs) && breadcrumbs.size > 0
|
135
|
+
# still handling association/subassociation
|
136
|
+
args.last[:nesting_breadcrumbs] = breadcrumbs
|
137
|
+
args.last[:to_reload] = to_reload
|
138
|
+
orig_options[:to_reload] << result
|
139
|
+
else
|
140
|
+
# ready to return the initially requested instances, so reload children with their parents, in reverse order added
|
141
|
+
orig_options[:to_reload].each do |i|
|
142
|
+
begin
|
143
|
+
i.reload
|
144
|
+
rescue => e
|
145
|
+
puts "#{i} reload failed: #{e}\n#{e.backtrace.join("\n")}" if ::Stepford::FactoryGirl.debug?
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
result
|
151
|
+
end
|
152
|
+
|
153
|
+
# switched to this from method_missing to avoid method trying to handle mistaken calls
|
154
|
+
def create(*args, &block); handle_factory_girl_method(:create, *args, &block); end
|
155
|
+
def create_list(*args, &block); handle_factory_girl_method(:create_list, *args, &block); end
|
156
|
+
def build(*args, &block); handle_factory_girl_method(:build, *args, &block); end
|
157
|
+
def build_list(*args, &block); handle_factory_girl_method(:build_list, *args, &block); end
|
158
|
+
def build_stubbed(*args, &block); handle_factory_girl_method(:build_stubbed, *args, &block); end
|
159
|
+
# pass everything else to FactoryGirl to try to handle (can't reflect in current version to find what it handles)
|
160
|
+
def method_missing(m, *args, &block); ::FactoryGirl.__send__(m, *args, &block); end
|
161
|
+
|
162
|
+
def deep_dup(o)
|
163
|
+
result = nil
|
164
|
+
if o.is_a?(Hash)
|
165
|
+
result = {}
|
166
|
+
o.keys.each do |key|
|
167
|
+
result[deep_dup(key)] = deep_dup(o[key])
|
168
|
+
end
|
169
|
+
elsif o.is_a?(Array)
|
170
|
+
result = []
|
171
|
+
o.each do |value|
|
172
|
+
result << deep_dup(value)
|
173
|
+
end
|
174
|
+
elsif [NilClass,FalseClass,TrueClass,Symbol,Numeric,Class,Module].any?{|c|o.is_a?(c)}
|
175
|
+
result = o
|
176
|
+
elsif o.is_a?(BigDecimal)
|
177
|
+
# ActiveSupport v3.2.8 checks duplicable? for BigDecimal by testing it, so we'll just try to dup the value itself
|
178
|
+
result = o
|
179
|
+
begin
|
180
|
+
result = o.dup
|
181
|
+
rescue TypeError
|
182
|
+
# can't dup
|
183
|
+
end
|
184
|
+
elsif o.is_a?(Object)
|
185
|
+
result = o.dup
|
186
|
+
else
|
187
|
+
result = o
|
188
|
+
end
|
189
|
+
result
|
190
|
+
end
|
191
|
+
|
192
|
+
def debugargs(args)
|
193
|
+
result = []
|
194
|
+
args.each do |arg|
|
195
|
+
if arg.is_a?(Hash)
|
196
|
+
result << "{#{arg.keys.collect{|key|"#{key} = (#{arg[key].class.name})"}.join(', ')}}"
|
197
|
+
else
|
198
|
+
result << "#{arg.inspect},"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
result.join('')
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'stepford/factory_girl'
|
2
|
+
|
3
|
+
module Stepford
|
4
|
+
module FactoryGirl
|
5
|
+
module RspecHelpers
|
6
|
+
[:create, :create_list, :build, :build_list, :build_stubbed].each do |s|
|
7
|
+
class_eval %Q"
|
8
|
+
def deep_#{s}(*args, &block)
|
9
|
+
::Stepford::FactoryGirl.#{s}(*args, &block)
|
10
|
+
end
|
11
|
+
"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
::RSpec.configure do |c|
|
18
|
+
c.include ::Stepford::FactoryGirl::RspecHelpers
|
19
|
+
end
|
data/lib/stepford/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stepford
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.14.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-11-
|
12
|
+
date: 2012-11-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: thor
|
@@ -55,10 +55,13 @@ extra_rdoc_files: []
|
|
55
55
|
files:
|
56
56
|
- lib/stepford/circular_ref_checker.rb
|
57
57
|
- lib/stepford/cli.rb
|
58
|
+
- lib/stepford/column_representation.rb
|
58
59
|
- lib/stepford/common.rb
|
60
|
+
- lib/stepford/factory_girl/config.rb
|
61
|
+
- lib/stepford/factory_girl/generator.rb
|
62
|
+
- lib/stepford/factory_girl/methods.rb
|
63
|
+
- lib/stepford/factory_girl/rspec_helpers.rb
|
59
64
|
- lib/stepford/factory_girl.rb
|
60
|
-
- lib/stepford/factory_girl_generator.rb
|
61
|
-
- lib/stepford/factory_girl_rspec_helpers.rb
|
62
65
|
- lib/stepford/version.rb
|
63
66
|
- lib/stepford.rb
|
64
67
|
- Rakefile
|
@@ -1,184 +0,0 @@
|
|
1
|
-
require 'stepford/common'
|
2
|
-
|
3
|
-
module Stepford
|
4
|
-
class FactoryGirlGenerator
|
5
|
-
def self.generate_factories(options={})
|
6
|
-
factories = {}
|
7
|
-
expected = {}
|
8
|
-
included_models = options[:models] ? options[:models].split(',').collect{|s|s.strip}.compact : nil
|
9
|
-
Dir[File.join('app','models','*.rb').to_s].each do |filename|
|
10
|
-
model_name = File.basename(filename).sub(/.rb$/, '')
|
11
|
-
next if included_models && !included_models.include?(model_name)
|
12
|
-
load File.join('app','models',"#{model_name}.rb")
|
13
|
-
|
14
|
-
begin
|
15
|
-
model_class = model_name.camelize.constantize
|
16
|
-
rescue => e
|
17
|
-
puts "Problem in #{model_name.camelize}"
|
18
|
-
raise e
|
19
|
-
end
|
20
|
-
|
21
|
-
next unless model_class.ancestors.include?(ActiveRecord::Base)
|
22
|
-
factory = (factories[model_name.to_sym] ||= [])
|
23
|
-
pk_syms = Array.wrap(model_class.primary_key).collect{|pk|pk.to_sym}
|
24
|
-
excluded_attributes = pk_syms + [:updated_at, :created_at, :object_id]
|
25
|
-
model_class.reflections.collect {|association_name, reflection|
|
26
|
-
(expected[reflection.class_name.underscore.to_sym] ||= []) << model_name
|
27
|
-
fkey_sym = reflection.foreign_key.try(:to_sym)
|
28
|
-
excluded_attributes << fkey_sym if reflection.foreign_key && !(excluded_attributes.include?(fkey_sym))
|
29
|
-
assc_sym = reflection.name.to_sym
|
30
|
-
clas_sym = reflection.class_name.underscore.to_sym
|
31
|
-
# if has a foreign key, then if NOT NULL or is a presence validate, the association is required and should be output. unfortunately this could mean a circular reference that will have to be manually fixed
|
32
|
-
has_presence_validator = model_class.validators_on(assc_sym).collect{|v|v.class}.include?(ActiveModel::Validations::PresenceValidator)
|
33
|
-
required = reflection.foreign_key ? (has_presence_validator || model_class.columns.any?{|c| !c.null && c.name.to_sym == fkey_sym}) : false
|
34
|
-
should_be_trait = !(options[:associations] || (options[:include_required_associations] && required)) && options[:association_traits]
|
35
|
-
if options[:associations] || (options[:include_required_associations] && required) || should_be_trait
|
36
|
-
if reflection.macro == :has_many
|
37
|
-
# In factory girl v4.1.0:
|
38
|
-
# create_list must be done in an after(:create) or you get Trait not registered or Factory not registered errors.
|
39
|
-
# this means that validators that verify presence or size > 0 in a association list will not work with this method, and you'll need to
|
40
|
-
# use build, not create: http://stackoverflow.com/questions/11209347/has-many-with-at-least-two-entries
|
41
|
-
"#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}#{should_be_trait || has_presence_validator ? '' : '#'}after(:create) do |user, evaluator|; FactoryGirl.create_list #{clas_sym.inspect}, 2; end#{should_be_trait ? '; end' : ''}#{should_be_trait ? '' : ' # commented to avoid circular reference'}"
|
42
|
-
elsif assc_sym != clas_sym
|
43
|
-
"#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}#{should_be_trait || reflection.macro == :belongs_to || has_presence_validator ? '' : '#'}association #{assc_sym.inspect}#{assc_sym != clas_sym ? ", factory: #{clas_sym.inspect}" : ''}#{should_be_trait ? '; end' : ''}#{should_be_trait || reflection.macro == :belongs_to ? '' : ' # commented to avoid circular reference'}"
|
44
|
-
else
|
45
|
-
"#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}#{should_be_trait || reflection.macro == :belongs_to || has_presence_validator ? '' : '#'}#{is_reserved?(assc_sym) ? 'self.' : ''}#{assc_sym}#{should_be_trait ? '; end' : ''}#{should_be_trait || reflection.macro == :belongs_to ? '' : ' # commented to avoid circular reference'}"
|
46
|
-
end
|
47
|
-
else
|
48
|
-
nil
|
49
|
-
end
|
50
|
-
}.compact.sort.each {|l|factory << l}
|
51
|
-
|
52
|
-
sequenceless_table = false
|
53
|
-
begin
|
54
|
-
sequenceless_table = true unless m.sequence_name
|
55
|
-
rescue => e
|
56
|
-
# bug in Rails 3.2.8, at least: undefined method `split' for nil:NilClass in activerecord-3.2.8/lib/active_record/connection_adapters/postgresql_adapter.rb:911:in `default_sequence_name'
|
57
|
-
sequenceless_table = true
|
58
|
-
end
|
59
|
-
|
60
|
-
model_class.columns.sort_by {|c|[c.name]}.each {|c|
|
61
|
-
# intentional not checking excluded_attributes/exclude_all_ids when sequenceless. it needs these for create to work.
|
62
|
-
if sequenceless_table && pk_syms.include?(c.name.to_sym)
|
63
|
-
factory << Stepford::Common.sequence_for(c)
|
64
|
-
elsif !excluded_attributes.include?(c.name.to_sym) && !(c.name.to_s.downcase.end_with?('_id') && options[:exclude_all_ids]) && (options[:attributes] || !c.null)
|
65
|
-
has_uniqueness_validator = model_class.validators_on(c.name.to_sym).collect{|v|v.class}.include?(ActiveRecord::Validations::UniquenessValidator)
|
66
|
-
if has_uniqueness_validator
|
67
|
-
#TODO: specialize for different data types
|
68
|
-
factory << Stepford::Common.sequence_for(c)
|
69
|
-
else
|
70
|
-
factory << "#{is_reserved?(c.name) ? 'self.' : ''}#{c.name} #{Stepford::Common.value_for(c)}"
|
71
|
-
end
|
72
|
-
elsif options[:attribute_traits]
|
73
|
-
if c.type == :boolean
|
74
|
-
factory << "trait #{c.name.underscore.to_sym.inspect} do; #{is_reserved?(c.name) ? 'self.' : ''}#{c.name} true; end"
|
75
|
-
factory << "trait #{"not_#{c.name.underscore}".to_sym.inspect} do; #{is_reserved?(c.name) ? 'self.' : ''}#{c.name} false; end"
|
76
|
-
else
|
77
|
-
factory << "trait #{"with_#{c.name.underscore}".to_sym.inspect} do; #{is_reserved?(c.name) ? 'self.' : ''}#{c.name} #{Stepford::Common.value_for(c)}; end"
|
78
|
-
end
|
79
|
-
end
|
80
|
-
}
|
81
|
-
end
|
82
|
-
|
83
|
-
if options[:associations] || options[:validate_associations]
|
84
|
-
failed = false
|
85
|
-
model_to_fixes_required = {}
|
86
|
-
expected.keys.sort.each do |factory_name|
|
87
|
-
unless factories[factory_name.to_sym]
|
88
|
-
puts "#{File.join('app','models',"#{factory_name}.rb")} missing. Model(s) with associations to it: #{expected[factory_name].sort.join(', ')}"
|
89
|
-
expected[factory_name].each do |model_name|
|
90
|
-
(model_to_fixes_required[model_name.to_sym] ||= []) << factory_name.to_sym
|
91
|
-
end
|
92
|
-
failed = true
|
93
|
-
end
|
94
|
-
end
|
95
|
-
model_to_fixes_required.keys.each do |model_to_fix|
|
96
|
-
puts ""
|
97
|
-
puts "In #{model_to_fix}:"
|
98
|
-
model_to_fixes_required[model_to_fix].each do |fix|
|
99
|
-
puts "- comment/remove/fix broken association to #{fix}"
|
100
|
-
end
|
101
|
-
end
|
102
|
-
return false if failed
|
103
|
-
end
|
104
|
-
|
105
|
-
path = get_factories_rb_pathname(options)
|
106
|
-
|
107
|
-
if path.end_with?('.rb')
|
108
|
-
dirpath = File.dirname(path)
|
109
|
-
unless File.directory?(dirpath)
|
110
|
-
puts "Please create this directory first: #{dirpath}"
|
111
|
-
return false
|
112
|
-
end
|
113
|
-
|
114
|
-
File.open(path, "w") do |f|
|
115
|
-
write_header(f, options)
|
116
|
-
factories.keys.sort.each do |factory_name|
|
117
|
-
factory = factories[factory_name]
|
118
|
-
write_factory(factory_name, factory, f)
|
119
|
-
end
|
120
|
-
write_footer(f)
|
121
|
-
end
|
122
|
-
else
|
123
|
-
unless File.directory?(path)
|
124
|
-
puts "Please create this directory first: #{path}"
|
125
|
-
return false
|
126
|
-
end
|
127
|
-
|
128
|
-
factories.keys.sort.each do |factory_name|
|
129
|
-
factory = factories[factory_name]
|
130
|
-
File.open(File.join(path,"#{factory_name}.rb"), "w") do |f|
|
131
|
-
write_header(f, options)
|
132
|
-
write_factory(factory_name, factory, f)
|
133
|
-
write_footer(f)
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
return true
|
139
|
-
end
|
140
|
-
|
141
|
-
private
|
142
|
-
|
143
|
-
def self.is_reserved?(s)
|
144
|
-
# modified from http://stackoverflow.com/questions/6461303/built-in-way-to-determine-whether-a-string-is-a-ruby-reserved-word/6461673#6461673
|
145
|
-
%w{__FILE__ __LINE__ alias and begin BEGIN break case class def defined? do else elsif end END ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield}.include? s.to_s
|
146
|
-
end
|
147
|
-
|
148
|
-
def self.get_factories_rb_pathname(options)
|
149
|
-
path = File.join('test','factories.rb')
|
150
|
-
if options[:path]
|
151
|
-
if options[:path].end_with?('.rb')
|
152
|
-
path = options[:path]
|
153
|
-
else
|
154
|
-
if options[:multiple]
|
155
|
-
path = options[:path]
|
156
|
-
else
|
157
|
-
path = File.join(options[:path],'factories.rb')
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
|
-
path
|
162
|
-
end
|
163
|
-
|
164
|
-
def self.write_header(f, options)
|
165
|
-
f.puts '# original version autogenerated by Stepford: https://github.com/garysweaver/stepford'
|
166
|
-
f.puts ''
|
167
|
-
f.puts 'FactoryGirl.define do'
|
168
|
-
f.puts ' '
|
169
|
-
end
|
170
|
-
|
171
|
-
def self.write_factory(factory_name, factory, f)
|
172
|
-
f.puts " factory #{factory_name.inspect} do"
|
173
|
-
factory.each do |line|
|
174
|
-
f.puts " #{line}"
|
175
|
-
end
|
176
|
-
f.puts ' end'
|
177
|
-
f.puts ' '
|
178
|
-
end
|
179
|
-
|
180
|
-
def self.write_footer(f)
|
181
|
-
f.puts 'end'
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
require 'stepford/factory_girl'
|
2
|
-
|
3
|
-
module Stepford
|
4
|
-
module FactoryGirlRspecHelpers
|
5
|
-
[:create, :create_list, :build, :build_list, :build_stubbed].each do |s|
|
6
|
-
class_eval %Q"
|
7
|
-
def deep_#{s}(*args, &block)
|
8
|
-
::Stepford::FactoryGirl.#{s}(*args, &block)
|
9
|
-
end
|
10
|
-
"
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
RSpec.configure do |c|
|
16
|
-
c.include ::Stepford::FactoryGirlRspecHelpers
|
17
|
-
end
|