modelist 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +105 -0
- data/Rakefile +11 -0
- data/bin/modelist +3 -0
- data/lib/modelist/analyst.rb +97 -0
- data/lib/modelist/circular_ref_checker.rb +169 -0
- data/lib/modelist/cli.rb +44 -0
- data/lib/modelist/config.rb +11 -0
- data/lib/modelist/tester.rb +184 -0
- data/lib/modelist/version.rb +3 -0
- data/lib/modelist.rb +5 -0
- metadata +91 -0
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
Modelist
|
2
|
+
=====
|
3
|
+
|
4
|
+
CLI and API to test and analyze ActiveRecord models.
|
5
|
+
|
6
|
+
### Setup
|
7
|
+
|
8
|
+
In your Rails 3+ project, add this to your Gemfile:
|
9
|
+
|
10
|
+
gem 'modelist'
|
11
|
+
|
12
|
+
Then run:
|
13
|
+
|
14
|
+
bundle install
|
15
|
+
|
16
|
+
### API Configuration
|
17
|
+
|
18
|
+
Not needed for CLI. Just used if you are using as an API:
|
19
|
+
|
20
|
+
Modelist::quiet = true
|
21
|
+
|
22
|
+
### Usage
|
23
|
+
|
24
|
+
#### CLI
|
25
|
+
|
26
|
+
Modelist has a command-line interface with options to test for required circular dependencies, models that the specified models require due to nullable or validation constraints, or to test attributes and associations.
|
27
|
+
|
28
|
+
##### Circular
|
29
|
+
|
30
|
+
Check ActiveRecord circular dependencies. Find circular chains of dependencies where foreign keys that are not primary keys of the models are all not nullable in the schema or not nullable because of ActiveRecord presence validation with:
|
31
|
+
|
32
|
+
bundle exec modelist circular
|
33
|
+
|
34
|
+
or:
|
35
|
+
|
36
|
+
bundle exec modelist circular my_model_1 my_model_2 --output-file=/path/to/errors.log
|
37
|
+
|
38
|
+
Example output:
|
39
|
+
|
40
|
+
The following non-nullable foreign keys used in ActiveRecord model associations are involved in circular dependencies:
|
41
|
+
|
42
|
+
beers.waitress_id -> waitresses.bartender_id -> bartenders.beer_id -> beers.waitress_id
|
43
|
+
|
44
|
+
beers.waitress_id -> waitresses.bartender_id -> bartenders.order_id -> order.beer_id -> beers.waitress_id
|
45
|
+
|
46
|
+
|
47
|
+
Distinct foreign keys involved in a circular dependency:
|
48
|
+
|
49
|
+
beers.waitress_id
|
50
|
+
order.beer_id
|
51
|
+
bartenders.beer_id
|
52
|
+
bartenders.order_id
|
53
|
+
waitresses.bartender_id
|
54
|
+
|
55
|
+
|
56
|
+
Foreign keys by number of circular dependency chains involved with:
|
57
|
+
|
58
|
+
2 (out of 2): beers.waitress_id -> waitresses
|
59
|
+
2 (out of 2): waitresses.bartender_id -> bartenders
|
60
|
+
1 (out of 2): order.beer_id -> beers
|
61
|
+
1 (out of 2): bartenders.order_id -> order
|
62
|
+
1 (out of 2): bartenders.beer_id -> beers
|
63
|
+
|
64
|
+
Specify '--output-file' to provide an pathname of an errors file.
|
65
|
+
|
66
|
+
##### Required
|
67
|
+
|
68
|
+
Find the models that the specified models have non-nullable or presence validations for directly and indirectly. You can use this to determine which models are really required with:
|
69
|
+
|
70
|
+
bundle exec modelist required my_model_1 my_model_2
|
71
|
+
|
72
|
+
Example output:
|
73
|
+
|
74
|
+
Required models:
|
75
|
+
|
76
|
+
Bartender
|
77
|
+
Beer
|
78
|
+
Order
|
79
|
+
Waitress
|
80
|
+
|
81
|
+
##### Test
|
82
|
+
|
83
|
+
Test ActiveRecord models, their attributes, and associations with:
|
84
|
+
|
85
|
+
bundle exec modelist test
|
86
|
+
|
87
|
+
or:
|
88
|
+
|
89
|
+
bundle exec modelist test my_model_1 my_model_2 --output-file=/path/to/errors.log
|
90
|
+
|
91
|
+
Example output:
|
92
|
+
|
93
|
+
Specify '--output-file' to provide an pathname of an errors file.
|
94
|
+
|
95
|
+
### API
|
96
|
+
|
97
|
+
Modelist::Analyst.find_required_models(:model1, :model2)
|
98
|
+
Modelist::CircularRefChecker.test_models(:model1, :model2, output_file: true)
|
99
|
+
Modelist::Tester.test_models(:model1, :model2, output_file: true)
|
100
|
+
|
101
|
+
### License
|
102
|
+
|
103
|
+
Copyright (c) 2012 Gary S. Weaver, released under the [MIT license][lic].
|
104
|
+
|
105
|
+
[lic]: http://github.com/garysweaver/modelist/blob/master/LICENSE
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
#http://nicksda.apotomo.de/2010/10/testing-your-rails-3-engine-sitting-in-a-gem/
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << 'test'
|
6
|
+
t.test_files = FileList['test/**/*_test.rb']
|
7
|
+
t.verbose = true
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Run tests"
|
11
|
+
task :default => :test
|
data/bin/modelist
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
module Modelist
|
2
|
+
class Analyst
|
3
|
+
|
4
|
+
# Check refs on all models or models specified, e.g.
|
5
|
+
# Modelist::Analyst.find_model_requirements
|
6
|
+
# or
|
7
|
+
# Modelist::Analyst.find_model_requirements(:my_model, :some_other_model)
|
8
|
+
def self.find_required_models(*args)
|
9
|
+
# less-dependent extract_options!
|
10
|
+
#options = args.last.is_a?(Hash) ? args.pop : {}
|
11
|
+
raise ArgumentError.new("Please supply one or more models") unless args.size > 0
|
12
|
+
results = {}
|
13
|
+
models = []
|
14
|
+
included_models = args.compact.collect{|m|m.to_sym}
|
15
|
+
puts "Checking models: #{included_models.collect{|m|m.inspect}.join(', ')}" if !Modelist.quiet? && included_models.size > 0
|
16
|
+
Dir[File.join('app','models','*.rb').to_s].each do |filename|
|
17
|
+
model_name = File.basename(filename).sub(/.rb$/, '')
|
18
|
+
next if included_models.size > 0 && !included_models.include?(model_name.to_sym)
|
19
|
+
load File.join('app','models',"#{model_name}.rb")
|
20
|
+
|
21
|
+
begin
|
22
|
+
model_class = model_name.camelize.constantize
|
23
|
+
rescue => e
|
24
|
+
puts "Problem in #{model_name.camelize}" unless Modelist.quiet?
|
25
|
+
raise e
|
26
|
+
end
|
27
|
+
|
28
|
+
next unless model_class.ancestors.include?(ActiveRecord::Base)
|
29
|
+
models << model_class
|
30
|
+
end
|
31
|
+
|
32
|
+
models.each do |model_class|
|
33
|
+
test_model(model_class, results)
|
34
|
+
end
|
35
|
+
|
36
|
+
unless Modelist.quiet? || !results[:required_models]
|
37
|
+
puts "Required models:"
|
38
|
+
puts
|
39
|
+
results[:required_models].collect{|c|c.name}.sort.each do |c|
|
40
|
+
puts "#{c}"
|
41
|
+
end
|
42
|
+
puts
|
43
|
+
end
|
44
|
+
|
45
|
+
return results[:required_models] ? results[:required_models] : []
|
46
|
+
end
|
47
|
+
|
48
|
+
# Get hash of required model information including non-nullable or validation presence associations throughout associations tree for model specified, e.g.
|
49
|
+
# Modelist::Analyst.test_model(:my_model)
|
50
|
+
# Also can take model class:
|
51
|
+
# Modelist::Analyst.test_model(MyModel)
|
52
|
+
def self.test_model(model_class, results = nil, model_and_association_names = [])
|
53
|
+
model_class = model_class.to_s.camelize.constantize unless model_class.is_a?(Class)
|
54
|
+
|
55
|
+
results ||= {}
|
56
|
+
results[:required_models] ||= []
|
57
|
+
results[:required_models] << model_class unless results[:required_models].include?(model_class)
|
58
|
+
|
59
|
+
model_class.reflections.collect {|association_name, reflection|
|
60
|
+
puts "warning: #{model_class}'s association #{reflection.name}'s foreign_key was nil. can't check." unless reflection.foreign_key || Modelist.quiet?
|
61
|
+
assc_sym = reflection.name.to_sym
|
62
|
+
|
63
|
+
begin
|
64
|
+
next_class = reflection.class_name.constantize
|
65
|
+
rescue => e
|
66
|
+
puts "Problem in #{model_class.name} with association: #{reflection.macro} #{assc_sym.inspect} which refers to class #{reflection.class_name}" unless Modelist.quiet?
|
67
|
+
raise e
|
68
|
+
end
|
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
|
+
puts "#{model_class.name}.#{association_name} #{required ? 'required' : 'not required'}"
|
83
|
+
|
84
|
+
if required
|
85
|
+
key = [model_class.table_name.to_sym, reflection.foreign_key.to_sym, next_class.table_name.to_sym]
|
86
|
+
unless model_and_association_names.include?(key)
|
87
|
+
model_and_association_names << key
|
88
|
+
test_model(next_class, results, model_and_association_names)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
}
|
92
|
+
|
93
|
+
model_and_association_names.pop
|
94
|
+
results
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module Modelist
|
2
|
+
class CircularRefChecker
|
3
|
+
|
4
|
+
# Check refs on all models or models specified, e.g.
|
5
|
+
# Modelist::CircularRefChecker.test_models
|
6
|
+
# or
|
7
|
+
# Modelist::CircularRefChecker.test_models(:my_model, :some_other_model)
|
8
|
+
def self.test_models(*args)
|
9
|
+
# less-dependent extract_options!
|
10
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
11
|
+
results = {}
|
12
|
+
models = []
|
13
|
+
included_models = args.compact.collect{|m|m.to_sym}
|
14
|
+
puts "Checking models: #{included_models.collect{|m|m.inspect}.join(', ')}" if !Modelist.quiet? && included_models.size > 0
|
15
|
+
Dir[File.join('app','models','*.rb').to_s].each do |filename|
|
16
|
+
model_name = File.basename(filename).sub(/.rb$/, '')
|
17
|
+
next if included_models.size > 0 && !included_models.include?(model_name.to_sym)
|
18
|
+
load File.join('app','models',"#{model_name}.rb")
|
19
|
+
|
20
|
+
begin
|
21
|
+
model_class = model_name.camelize.constantize
|
22
|
+
rescue => e
|
23
|
+
puts "Problem in #{model_name.camelize}" unless Modelist.quiet?
|
24
|
+
raise e
|
25
|
+
end
|
26
|
+
|
27
|
+
next unless model_class.ancestors.include?(ActiveRecord::Base)
|
28
|
+
models << model_class
|
29
|
+
end
|
30
|
+
|
31
|
+
models.each do |model_class|
|
32
|
+
test_model(model_class, results)
|
33
|
+
end
|
34
|
+
|
35
|
+
if results[:circles].nil? || results[:circles].size == 0
|
36
|
+
unless Modelist.quiet?
|
37
|
+
puts
|
38
|
+
puts "No circular dependencies."
|
39
|
+
puts
|
40
|
+
end
|
41
|
+
return true
|
42
|
+
end
|
43
|
+
|
44
|
+
if !Modelist.quiet? || options[:output_file]
|
45
|
+
totals = {}
|
46
|
+
results[:circles_sorted].each do |arr|
|
47
|
+
arr.each do |key|
|
48
|
+
totals[key] = 0 unless totals[key]
|
49
|
+
totals[key] = totals[key] + 1
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
unless Modelist.quiet?
|
54
|
+
puts "The following non-nullable foreign keys used in ActiveRecord model associations are involved in circular dependencies:"
|
55
|
+
results[:circles].sort.each do |c|
|
56
|
+
puts
|
57
|
+
puts "#{c}"
|
58
|
+
end
|
59
|
+
puts
|
60
|
+
puts
|
61
|
+
puts "Distinct foreign keys involved in a circular dependency:"
|
62
|
+
puts
|
63
|
+
results[:offenders].sort.each do |c|
|
64
|
+
puts "#{c[0]}.#{c[1]}"
|
65
|
+
end
|
66
|
+
puts
|
67
|
+
puts
|
68
|
+
puts "Foreign keys by number of circular dependency chains involved with:"
|
69
|
+
puts
|
70
|
+
totals.sort_by {|k,v| v}.reverse.each do |arr|
|
71
|
+
c = arr[0]
|
72
|
+
t = arr[1]
|
73
|
+
puts "#{t} (out of #{results[:circles_sorted].size}): #{c[0]}.#{c[1]} -> #{c[2]}"
|
74
|
+
end
|
75
|
+
puts
|
76
|
+
end
|
77
|
+
|
78
|
+
if options[:output_file]
|
79
|
+
File.open(options[:output_file], "w") do |f|
|
80
|
+
f.puts "The following non-nullable foreign keys used in ActiveRecord model associations are involved in circular dependencies:"
|
81
|
+
results[:circles].sort.each do |c|
|
82
|
+
f.puts
|
83
|
+
f.puts "#{c}"
|
84
|
+
end
|
85
|
+
f.puts
|
86
|
+
f.puts
|
87
|
+
f.puts "Distinct foreign keys involved in a circular dependency:"
|
88
|
+
f.puts
|
89
|
+
results[:offenders].sort.each do |c|
|
90
|
+
f.puts "#{c[0]}.#{c[1]}"
|
91
|
+
end
|
92
|
+
f.puts
|
93
|
+
f.puts
|
94
|
+
f.puts "Foreign keys by number of circular dependency chains involved with:"
|
95
|
+
f.puts
|
96
|
+
totals.sort_by {|k,v| v}.reverse.each do |arr|
|
97
|
+
c = arr[0]
|
98
|
+
t = arr[1]
|
99
|
+
f.puts "#{t} (out of #{results[:circles_sorted].size}): #{c[0]}.#{c[1]} -> #{c[2]}"
|
100
|
+
end
|
101
|
+
f.puts
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
return false
|
107
|
+
end
|
108
|
+
|
109
|
+
# Get hash of circular reference information about associations tree on model specified, e.g.
|
110
|
+
# Modelist::CircularRefChecker.test_model(:my_model)
|
111
|
+
# Also can take model class:
|
112
|
+
# Modelist::CircularRefChecker.test_model(MyModel)
|
113
|
+
def self.test_model(model_class, results = nil, model_and_association_names = [])
|
114
|
+
model_class = model_class.to_s.camelize.constantize unless model_class.is_a?(Class)
|
115
|
+
|
116
|
+
results ||= {}
|
117
|
+
results[:offenders] ||= []
|
118
|
+
results[:circles_sorted] ||= []
|
119
|
+
results[:circles] ||= []
|
120
|
+
results[:selected_offenders] ||= []
|
121
|
+
|
122
|
+
model_class.reflections.collect {|association_name, reflection|
|
123
|
+
puts "warning: #{model_class}'s association #{reflection.name}'s foreign_key was nil. can't check." unless reflection.foreign_key || Modelist.quiet?
|
124
|
+
assc_sym = reflection.name.to_sym
|
125
|
+
|
126
|
+
begin
|
127
|
+
next_class = reflection.class_name.constantize
|
128
|
+
rescue => e
|
129
|
+
puts "Problem in #{model_class.name} with association: #{reflection.macro} #{assc_sym.inspect} which refers to class #{reflection.class_name}" unless Modelist.quiet?
|
130
|
+
raise e
|
131
|
+
end
|
132
|
+
|
133
|
+
has_presence_validator = model_class.validators_on(assc_sym).collect{|v|v.class}.include?(ActiveModel::Validations::PresenceValidator)
|
134
|
+
required = false
|
135
|
+
if reflection.macro == :belongs_to
|
136
|
+
# note: supports composite_primary_keys gem which stores primary_key as an array
|
137
|
+
foreign_key_is_also_primary_key = Array.wrap(model_class.primary_key).collect{|pk|pk.to_sym}.include?(reflection.foreign_key.to_sym)
|
138
|
+
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}
|
139
|
+
required = is_not_null_fkey_that_is_not_primary_key || has_presence_validator
|
140
|
+
else
|
141
|
+
# 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
|
142
|
+
required = has_presence_validator
|
143
|
+
end
|
144
|
+
|
145
|
+
if required
|
146
|
+
key = [model_class.table_name.to_sym, reflection.foreign_key.to_sym, next_class.table_name.to_sym]
|
147
|
+
if model_and_association_names.include?(key)
|
148
|
+
results[:offenders] << model_and_association_names.last unless results[:offenders].include?(model_and_association_names.last)
|
149
|
+
short = model_and_association_names.dup
|
150
|
+
# drop all preceding keys that have nothing to do with the circle
|
151
|
+
(short.index(key)).times {short.delete_at(0)}
|
152
|
+
sorted = short.sort
|
153
|
+
unless results[:circles_sorted].include?(sorted)
|
154
|
+
results[:circles_sorted] << sorted
|
155
|
+
results[:circles] << "#{(short + [key]).collect{|b|"#{b[0]}.#{b[1]}"}.join(' -> ')}".to_sym
|
156
|
+
end
|
157
|
+
else
|
158
|
+
model_and_association_names << key
|
159
|
+
test_model(next_class, results, model_and_association_names)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
}
|
163
|
+
|
164
|
+
model_and_association_names.pop
|
165
|
+
|
166
|
+
results
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
data/lib/modelist/cli.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Modelist
|
4
|
+
class CLI < Thor
|
5
|
+
desc "test", "Tests specified models, their attributes, and their associations. Will test all models if no models are specified."
|
6
|
+
method_option :output_file, :desc => "Pathname of file to output to"
|
7
|
+
def test(*args)
|
8
|
+
# load Rails environment
|
9
|
+
require './config/environment'
|
10
|
+
require 'modelist/tester'
|
11
|
+
#args = options[:models] ? options.delete(:models).split(',').collect{|s|s.strip} << options : []
|
12
|
+
puts "args=#{args.inspect}"
|
13
|
+
args.each {|a| puts "Unsupported option: #{args.delete(a)}" if a.to_s.starts_with?('-')}
|
14
|
+
exit ::Modelist::Tester.test_models(*args) ? 0 : 1
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "required", "Finds model dependencies in specified models."
|
18
|
+
def required(*args)
|
19
|
+
# load Rails environment
|
20
|
+
require './config/environment'
|
21
|
+
require 'modelist/analyst'
|
22
|
+
# args are [*options[:models], options hash (minus the models)]
|
23
|
+
#args = options[:models] ? options.delete(:models).split(',').collect{|s|s.strip} << options : []
|
24
|
+
puts "args=#{args.inspect}"
|
25
|
+
args.each {|a| puts "Unsupported option: #{args.delete(a)}" if a.to_s.starts_with?('-')}
|
26
|
+
Modelist::Analyst.find_required_models(*args)
|
27
|
+
exit 0
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "circular", "Checks for required circular references in specified models. Will test all models if no models are specified."
|
31
|
+
method_option :output_file, :desc => "Pathname of file to output to"
|
32
|
+
def circular(*args)
|
33
|
+
# load Rails environment
|
34
|
+
require './config/environment'
|
35
|
+
require 'modelist/circular_ref_checker'
|
36
|
+
#args = options[:models] ? options.delete(:models).split(',').collect{|s|s.strip} << options : []
|
37
|
+
puts "args=#{args.inspect}"
|
38
|
+
args.each {|a| puts "Unsupported option: #{args.delete(a)}" if a.to_s.starts_with?('-')}
|
39
|
+
exit ::Modelist::CircularRefChecker.test_models(*args) ? 0 : 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
::Modelist::CLI.start
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# If you use Modelist in an application, you can specify:
|
2
|
+
# Modelist.quiet = true
|
3
|
+
# to limit output.
|
4
|
+
module Modelist
|
5
|
+
OPTIONS = [:quiet]
|
6
|
+
|
7
|
+
class << self
|
8
|
+
OPTIONS.each{|o|attr_accessor o; define_method("#{o}?".to_sym){!!send("#{o}")}}
|
9
|
+
def configure(&blk); class_eval(&blk); end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
module Modelist
|
2
|
+
class Tester
|
3
|
+
|
4
|
+
# Check refs on all models or models specified, e.g.
|
5
|
+
# Modelist::Tester.test_models
|
6
|
+
# or
|
7
|
+
# Modelist::Tester.test_models(:my_model, :some_other_model)
|
8
|
+
def self.test_models(*args)
|
9
|
+
# less-dependent extract_options!
|
10
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
11
|
+
results = {}
|
12
|
+
models = []
|
13
|
+
included_models = args.compact.collect{|m|m.to_sym}
|
14
|
+
puts "Checking models: #{included_models.collect{|m|m.inspect}.join(', ')}" if !Modelist.quiet? && included_models.size > 0
|
15
|
+
Dir[File.join('app','models','*.rb').to_s].each do |filename|
|
16
|
+
model_name = File.basename(filename).sub(/.rb$/, '')
|
17
|
+
next if included_models.size > 0 && !included_models.include?(model_name.to_sym)
|
18
|
+
load File.join('app','models',"#{model_name}.rb")
|
19
|
+
|
20
|
+
begin
|
21
|
+
model_class = model_name.camelize.constantize
|
22
|
+
rescue => e
|
23
|
+
puts "Problem in #{model_name.camelize}" unless Modelist.quiet?
|
24
|
+
results[:failures] << "FAILED: #{model_name}\n\n---\n\n#{filename}\n---\n#{formatted_errors.join("\n")}\n\n"
|
25
|
+
results[:failed] << model_class_name
|
26
|
+
raise e
|
27
|
+
end
|
28
|
+
|
29
|
+
next unless model_class.ancestors.include?(ActiveRecord::Base)
|
30
|
+
models << model_class
|
31
|
+
end
|
32
|
+
|
33
|
+
models.each do |model_class|
|
34
|
+
test_model(model_class, results)
|
35
|
+
end
|
36
|
+
|
37
|
+
unless Modelist.quiet?
|
38
|
+
puts "Done testing"
|
39
|
+
puts ""
|
40
|
+
puts ""
|
41
|
+
|
42
|
+
# Write warnings to file and console
|
43
|
+
if results[:failures] && results[:failures].size > 0
|
44
|
+
unless Modelist.quiet?
|
45
|
+
puts "Failures:"
|
46
|
+
puts
|
47
|
+
results[:failures].sort.each do |failure|
|
48
|
+
puts *(failure.split('\n'))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if options[:output_file]
|
53
|
+
File.open(options[:output_file], "w") do |f|
|
54
|
+
f.puts "Failures:"
|
55
|
+
f.puts
|
56
|
+
results[:failures].sort.each do |failure|
|
57
|
+
f.puts *(failure.split('\n'))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
unless Modelist.quiet?
|
62
|
+
puts ""
|
63
|
+
puts "Errors in #{ERRORS_FILE}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
unless Modelist.quiet? || !results[:passed]
|
69
|
+
puts
|
70
|
+
puts "Passed (#{results[:passed].size}):"
|
71
|
+
puts "---"
|
72
|
+
results[:passed].each do |s|
|
73
|
+
puts s
|
74
|
+
end
|
75
|
+
puts
|
76
|
+
puts "Warnings (#{results[:warnings].size}):"
|
77
|
+
puts "---"
|
78
|
+
results[:warnings].each do |s|
|
79
|
+
puts s
|
80
|
+
end
|
81
|
+
puts
|
82
|
+
puts "Failed (#{results[:failed].size}):"
|
83
|
+
puts "---"
|
84
|
+
results[:failed].each do |s|
|
85
|
+
puts s
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
return results[:failed] ? results[:failed].size > 0 : true
|
91
|
+
end
|
92
|
+
|
93
|
+
# Test and get hash of information about model specified, e.g.
|
94
|
+
# Modelist::Tester.test_associations(:my_model)
|
95
|
+
# Also can take model class:
|
96
|
+
# Modelist::Tester.test_associations(MyModel)
|
97
|
+
def self.test_model(model_class, results = nil)
|
98
|
+
model_class = model_class.to_s.camelize.constantize unless model_class.is_a?(Class)
|
99
|
+
|
100
|
+
results ||= {}
|
101
|
+
results[:passed] ||= []
|
102
|
+
results[:warnings] ||= []
|
103
|
+
results[:failed] ||= []
|
104
|
+
results[:failures] ||= []
|
105
|
+
|
106
|
+
puts "Testing #{model_class}"
|
107
|
+
method_name_to_exception = {}
|
108
|
+
if model_class
|
109
|
+
|
110
|
+
model = nil
|
111
|
+
|
112
|
+
begin
|
113
|
+
model = model_class.first
|
114
|
+
puts "#{model_class}.first = #{model.inspect}"
|
115
|
+
if model.nil?
|
116
|
+
results[:warnings] << "#{model_class.name}.first was nil. Assuming there is no data in the associated table, but please verify."
|
117
|
+
end
|
118
|
+
rescue Exception => e
|
119
|
+
method_name_to_exception["#{model_class.name}.first"] = e
|
120
|
+
end
|
121
|
+
|
122
|
+
if model
|
123
|
+
begin
|
124
|
+
model = model_class.last
|
125
|
+
puts "#{model_class}.last = #{model.inspect}"
|
126
|
+
if model.nil?
|
127
|
+
results[:warnings] << "#{model_class.name}.last was nil."
|
128
|
+
end
|
129
|
+
rescue Exception => e
|
130
|
+
method_name_to_exception["#{model_class.name}.last"] = e
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
if model
|
135
|
+
attrs = model.attributes.keys
|
136
|
+
attrs.each do |attr|
|
137
|
+
begin
|
138
|
+
result = model.read_attribute(attr)
|
139
|
+
if result.is_a? Array
|
140
|
+
size = result.size
|
141
|
+
puts "#{model_class.name.underscore}.#{attr}.size = #{size}"
|
142
|
+
else
|
143
|
+
puts "#{model_class.name.underscore}.#{attr} = #{result}"
|
144
|
+
end
|
145
|
+
rescue Exception => e
|
146
|
+
method_name_to_exception["#{model_class.name}.#{attr}"] = e
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
model_class.reflections.collect do |association_name, reflection|
|
152
|
+
begin
|
153
|
+
reflection.class_name
|
154
|
+
rescue Exception => e
|
155
|
+
method_name_to_exception["#{model_class.name}'.{association_name}'s reflection class_name method"] = e
|
156
|
+
next
|
157
|
+
end
|
158
|
+
|
159
|
+
model = model_class.new unless model
|
160
|
+
begin
|
161
|
+
value = model.send(association_name.to_sym)
|
162
|
+
if value.nil?
|
163
|
+
#results[:warnings] << "(ignore) #{model_class_name}.#{association_name} was nil"
|
164
|
+
elsif (value.is_a?(Array) && value.size == 0)
|
165
|
+
#results[:warnings] << "(ignore) #{model_class_name}.#{association_name} was empty"
|
166
|
+
end
|
167
|
+
rescue Exception => e
|
168
|
+
method_name_to_exception["#{model_class.name}.last.#{association_name}"] = e
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
if method_name_to_exception.size > 0
|
174
|
+
formatted_errors = method_name_to_exception.keys.collect{|method_name|"#{method_name}: #{method_name_to_exception[method_name].message}\n#{method_name_to_exception[method_name].backtrace.join("\n")}\n---\n"}
|
175
|
+
results[:failures] << "FAILED: #{model_class.name}\n\n---\n#{formatted_errors.join("\n")}\n\n"
|
176
|
+
results[:failed] << model_class.name
|
177
|
+
else
|
178
|
+
results[:passed] << model_class.name
|
179
|
+
end
|
180
|
+
|
181
|
+
results
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
data/lib/modelist.rb
ADDED
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: modelist
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Gary S. Weaver
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-20 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: thor
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rails
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: CLI and API to perform basic testing of all models, their attributes,
|
47
|
+
and their associations, and determine what models are depended on directly and indirectly
|
48
|
+
by a specific model.
|
49
|
+
email:
|
50
|
+
- garysweaver@gmail.com
|
51
|
+
executables:
|
52
|
+
- modelist
|
53
|
+
extensions: []
|
54
|
+
extra_rdoc_files: []
|
55
|
+
files:
|
56
|
+
- lib/modelist/analyst.rb
|
57
|
+
- lib/modelist/circular_ref_checker.rb
|
58
|
+
- lib/modelist/cli.rb
|
59
|
+
- lib/modelist/config.rb
|
60
|
+
- lib/modelist/tester.rb
|
61
|
+
- lib/modelist/version.rb
|
62
|
+
- lib/modelist.rb
|
63
|
+
- Rakefile
|
64
|
+
- README.md
|
65
|
+
- bin/modelist
|
66
|
+
homepage: https://github.com/garysweaver/modelist
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ! '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubyforge_project:
|
87
|
+
rubygems_version: 1.8.24
|
88
|
+
signing_key:
|
89
|
+
specification_version: 3
|
90
|
+
summary: Tests and analyzes requirements of your ActiveRecord models.
|
91
|
+
test_files: []
|