broken_record 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- NGU5YWE0ODA2MWVkY2M4MTRkYjc3NTZkN2U3ZDdlYjc2OGRlNmM0YQ==
5
- data.tar.gz: !binary |-
6
- OWM4MTEwYmZjMmI3YmJhMjIzNzk3ZDMwYzg3YzE3ZDM2MGE3NTI0Zg==
2
+ SHA1:
3
+ metadata.gz: 0b5338e93e1f7a715a2f8bb0416a4a945d9ad297
4
+ data.tar.gz: 71a2a195aa089ee10b57675f6b9bd08a58fcffaf
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- Njg3OWNjMmE0NDIxMDc0YWUzNzFlMjBjNDIwNzhkMDdiODBhZmYzOGFlMmNh
10
- OGY3Yjk1Mzc4YjQyOWFlYTc0MDMzZjUwYzNlYzcyODA4ZGVhMTkyYTQzNGE1
11
- ODUxZTczNDg5ZTA1YzczM2NlY2NiNDhlOGM1Y2QwNjgzODgwNWU=
12
- data.tar.gz: !binary |-
13
- ZGYyYmI0ZDlkNjZmMzMxYmZkNGQ0YWUyZTBkMzNiMDAzYjliOWIwNTMzNDY0
14
- NTY4YmUwNmE2MmQ4ZjY1ZGIzNDI5MjgxYzBlYzdhZjllOGZlYzk5MWUzYWI0
15
- YmUzNjg4NWQ1Nzk4MGY3OTliMzJmYTA1NDY1MjIyMzY2ZjljOTc=
6
+ metadata.gz: fc9009d880eb5ad5cbae3f63d3f0b8e8dc2a8924b6a5d0a044a9b8bd9142118fa96a91f48b2b82eee1a8c44a09ec1e667c138fa9f2f5f9d67bd7628c0802eb84
7
+ data.tar.gz: 159098ba5d8b0fc69cadba33b3daf4c80da6fc34e8a62d456134bda83d33eed0247910054fea40c1e178ea0b2d8952a2008634169fd83eb9e6925c241596a853
@@ -17,4 +17,13 @@
17
17
 
18
18
  ## v0.0.6
19
19
 
20
- * Allow classes_to_skip and default_scope keys to be strings as well as classes
20
+ * Allow classes_to_skip and default_scope keys to be strings as well as classes
21
+
22
+ ## v0.0.7
23
+
24
+ * Make BrokenRecord work with colorize >= 0.5.8
25
+ * Allow BrokenRecord::Scanner to be used programmatically (i.e. from rails console)
26
+ * Remove assumption that all models are in app/models/**/*.rb
27
+ * Show backtrace when exception occurs during scan
28
+ * Change parallelization strategy to better distribute load across cores
29
+ * Add an after_fork hook
data/README.md CHANGED
@@ -6,43 +6,68 @@ Provides a rake task for scanning your ActiveRecord models and detecting validat
6
6
 
7
7
  Add this line to your application's Gemfile:
8
8
 
9
- gem 'broken_record'
9
+ ```ruby
10
+ gem 'broken_record'
11
+ ```
10
12
 
11
13
  And then execute:
12
14
 
13
- $ bundle
15
+ ```bash
16
+ bundle
17
+ ```
14
18
 
15
19
  Or install it yourself as:
16
20
 
17
- $ gem install broken_record
21
+ ```bash
22
+ gem install broken_record
23
+ ```
18
24
 
19
25
  ## Usage
20
26
 
21
27
  To scan all records of all models in your project:
22
28
 
23
- rake broken_record:scan
29
+ ```bash
30
+ rake broken_record:scan
31
+ ```
24
32
 
25
- If you want to scan all records of a specific model (e.g. the User model)
33
+ If you want to scan all records of a specific model (e.g. the User model):
26
34
 
27
- rake broken_record:scan[User]
35
+ ```bash
36
+ rake broken_record:scan[User]
37
+ ```
38
+
39
+ You can also specify a list of models to scan:
40
+
41
+ ```bash
42
+ rake broken_record:scan[Product,User]
43
+ ```
28
44
 
29
45
  ## Configuration
30
46
 
31
- BrokenRecord provides a configure method with two options. Here's an example:
47
+ BrokenRecord provides a configure method with multiple options. Here's an example:
32
48
 
33
- BrokenRecord.configure do |config|
34
- # Skip the Foo and Bar models when scanning.
35
- config.classes_to_skip = [Foo, Bar]
49
+ ```ruby
50
+ BrokenRecord.configure do |config|
51
+ # Skip the Foo and Bar models when scanning.
52
+ config.classes_to_skip = [Foo, Bar]
36
53
 
37
- # Set a scope for which models should be validated
38
- config.default_scopes = { Foo => proc { with_bars } }
54
+ # Set a scope for which models should be validated
55
+ config.default_scopes = { Foo => proc { with_bars } }
56
+
57
+ # The follow block will be called before scanning your records.
58
+ # This is useful for skipping validations you want to ignore.
59
+ config.before_scan do
60
+ User.skip_callback :validate, :before, :user_must_be_active
61
+ end
39
62
 
40
- # BrokenRecord will call the block provided in before_scan before scanning
41
- # your records. This is useful for skipping validations you want to ignore.
42
- config.before_scan do
43
- User.skip_callback :validate, :before, :user_must_be_active
44
- end
63
+ # BrokenRecord uses the parallelize gem to distribute work across
64
+ # multiple cores. The following block will be called every time
65
+ # the process is forked (useful for re-establishing connections).
66
+ config.after_fork do
67
+ Rails.cache.reconnect if Rails.cache.respond_to? :reconnect
45
68
  end
69
+ end
70
+ ```
46
71
 
47
72
  ## Contributing
48
73
 
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_development_dependency 'bundler', '~> 1.3'
22
22
 
23
- spec.add_runtime_dependency 'rake', '~> 10'
24
- spec.add_runtime_dependency 'parallel', '~> 0'
25
- spec.add_runtime_dependency 'colorize', '0.5.8'
23
+ spec.add_runtime_dependency 'rake', '~> 10.1', '>= 10.1.10'
24
+ spec.add_runtime_dependency 'parallel', '~> 1.2', '>= 1.2.3'
25
+ spec.add_runtime_dependency 'colorize', '~> 0.5', '>= 0.5.8'
26
26
  end
@@ -1,12 +1,17 @@
1
1
  module BrokenRecord
2
2
  module Config
3
3
  extend self
4
- attr_accessor :classes_to_skip, :before_scan_callbacks, :default_scopes
4
+ attr_accessor :classes_to_skip, :before_scan_callbacks, :after_fork_callbacks, :default_scopes
5
5
  self.before_scan_callbacks = []
6
+ self.after_fork_callbacks = []
6
7
  self.default_scopes = {}
7
8
 
8
9
  def before_scan(&block)
9
10
  self.before_scan_callbacks << block
10
11
  end
12
+
13
+ def after_fork(&block)
14
+ self.after_fork_callbacks << block
15
+ end
11
16
  end
12
17
  end
@@ -0,0 +1,66 @@
1
+ require 'broken_record/job_result'
2
+
3
+ module BrokenRecord
4
+ class Job
5
+ JOBS_PER_PROCESSOR = 1
6
+
7
+ attr_accessor :klass, :index
8
+
9
+ def self.jobs_per_class
10
+ JOBS_PER_PROCESSOR * Parallel.processor_count
11
+ end
12
+
13
+ def self.build_jobs(classes)
14
+ jobs = []
15
+ classes.each do |klass|
16
+ jobs_per_class.times do |index|
17
+ jobs << Job.new(:klass => klass, :index => index)
18
+ end
19
+ end
20
+ jobs
21
+ end
22
+
23
+ def initialize(options)
24
+ options.each { |k, v| send("#{k}=", v) }
25
+ end
26
+
27
+ def perform
28
+ result = BrokenRecord::JobResult.new(self)
29
+ result.start_timer
30
+
31
+ records.each do |r|
32
+ begin
33
+ if !r.valid?
34
+ message = " Invalid record in #{klass} id=#{r.id}."
35
+ r.errors.each { |attr,msg| message << "\n #{attr} - #{msg}" }
36
+ result.add_error message
37
+ end
38
+ rescue Exception => e
39
+ message = " Exception for record in #{klass} id=#{r.id} - #{e}.\n"
40
+ message << e.backtrace.map { |line| " #{line}"}.join("\n")
41
+ result.add_error message
42
+ end
43
+ end
44
+
45
+ result.stop_timer
46
+ result
47
+ end
48
+
49
+ private
50
+
51
+ def records
52
+ default_scope = BrokenRecord::Config.default_scopes[klass] || BrokenRecord::Config.default_scopes[klass.to_s]
53
+
54
+ model_scope = if default_scope
55
+ klass.instance_exec &default_scope
56
+ else
57
+ klass.unscoped
58
+ end
59
+
60
+ records_per_group = model_scope.count / self.class.jobs_per_class
61
+ scope = model_scope.offset(records_per_group * index)
62
+
63
+ index == Parallel.processor_count - 1 ? scope.load : scope.first(records_per_group)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,22 @@
1
+ module BrokenRecord
2
+ class JobResult
3
+ attr_reader :start_time, :end_time, :errors, :job
4
+
5
+ def initialize(job)
6
+ @job = job
7
+ @errors = []
8
+ end
9
+
10
+ def start_timer
11
+ @start_time = Time.now
12
+ end
13
+
14
+ def stop_timer
15
+ @end_time = Time.now
16
+ end
17
+
18
+ def add_error(error)
19
+ @errors << "#{error.red}\n"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,50 @@
1
+ module BrokenRecord
2
+ class ResultAggregator
3
+ def initialize
4
+ @total_errors = 0
5
+ @aggregated_results = {}
6
+ end
7
+
8
+ def add_result(result)
9
+ @aggregated_results[result.job.klass] ||= []
10
+ @aggregated_results[result.job.klass] << result
11
+
12
+ if klass_done?(result.job.klass)
13
+ report_results result.job.klass
14
+ end
15
+ end
16
+
17
+ def report_final_results
18
+ if @total_errors == 0
19
+ puts "\nAll models validated successfully.".green
20
+ else
21
+ puts "\n#{@total_errors} errors were found while running validations.".red
22
+ exit 1
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def klass_done?(klass)
29
+ @aggregated_results[klass].count == Job.jobs_per_class
30
+ end
31
+
32
+ def report_results(klass)
33
+ all_errors = @aggregated_results[klass].map(&:errors).flatten
34
+ start_time = @aggregated_results[klass].map(&:start_time).min
35
+ end_time = @aggregated_results[klass].map(&:end_time).max
36
+ duration = (end_time - start_time).round(3)
37
+
38
+ @total_errors += all_errors.count
39
+
40
+ print "Validating model #{klass}... ".ljust(70)
41
+ if all_errors.empty?
42
+ print '[PASS]'.green
43
+ else
44
+ print '[FAIL]'.red
45
+ end
46
+ print " (#{duration}s)\n"
47
+ print all_errors.join if all_errors.any?
48
+ end
49
+ end
50
+ end
@@ -1,78 +1,51 @@
1
- require "broken_record/logger"
1
+ require 'broken_record/job'
2
+ require 'broken_record/result_aggregator'
2
3
  require 'parallel'
3
4
 
4
5
  module BrokenRecord
5
6
  class Scanner
6
- def run(model_name = nil)
7
- models = models_to_validate(model_name)
7
+ def run(class_names)
8
+ ResultAggregator.new.tap do |aggregator|
9
+ classes = classes_to_validate(class_names)
8
10
 
9
- BrokenRecord::Config.before_scan_callbacks.each { |callback| callback.call }
11
+ BrokenRecord::Config.before_scan_callbacks.each { |callback| callback.call }
10
12
 
11
- results = BrokenRecord::Logger.parallel do |lock|
12
- Parallel.map(models) do |model|
13
- result = validate_model(model)
14
- BrokenRecord::Logger.report_output(result, lock)
15
- result
13
+ jobs = BrokenRecord::Job.build_jobs(classes)
14
+
15
+ callback = proc do |_, _, result|
16
+ aggregator.add_result result if result.is_a? BrokenRecord::JobResult
16
17
  end
17
- end
18
18
 
19
- BrokenRecord::Logger.report_results(results)
19
+ Parallel.each(jobs, :finish => callback) do |job|
20
+ ActiveRecord::Base.connection.reconnect!
21
+ BrokenRecord::Config.after_fork_callbacks.each { |callback| callback.call }
22
+ job.perform
23
+ end
24
+ end
20
25
  end
21
26
 
22
27
  private
23
28
 
24
- def models_to_validate(model_name)
25
- if model_name
26
- [ model_name.constantize ]
27
- else
29
+ def classes_to_validate(class_names)
30
+ if class_names.empty?
28
31
  load_all_active_record_classes
32
+ else
33
+ class_names.map(&:strip).map(&:constantize)
29
34
  end
30
35
  end
31
36
 
32
37
  def load_all_active_record_classes
33
- Dir.glob(Rails.root.to_s + '/app/models/**/*.rb').each { |file| require file }
38
+ Rails.application.eager_load!
34
39
  objects = Set.new
35
40
  # Classes to skip may either be constants or strings. Convert all to strings for easier lookup
36
41
  classes_to_skip = BrokenRecord::Config.classes_to_skip.map(&:to_s)
37
- ObjectSpace.each_object(Class) do |klass|
38
- if ActiveRecord::Base > klass
39
- # Use base_class so we don't try to validate abstract classes and so we don't validate
40
- # STI classes multiple times. See active_record/inheritance.rb for more details.
41
- objects.add klass.base_class unless classes_to_skip.include?(klass.to_s)
42
- end
42
+ ActiveRecord::Base.descendants.each do |klass|
43
+ # Use base_class so we don't try to validate abstract classes and so we don't validate
44
+ # STI classes multiple times. See active_record/inheritance.rb for more details.
45
+ objects.add klass.base_class unless classes_to_skip.include?(klass.to_s)
43
46
  end
44
47
 
45
48
  objects.sort_by(&:name)
46
49
  end
47
-
48
- def validate_model(model)
49
- ActiveRecord::Base.connection.reconnect!
50
-
51
- BrokenRecord::Logger.log(model) do |logger|
52
- begin
53
- default_scope = BrokenRecord::Config.default_scopes[model] || BrokenRecord::Config.default_scopes[model.to_s]
54
-
55
- if default_scope
56
- model_scope = model.instance_exec &default_scope
57
- else
58
- model_scope = model.unscoped
59
- end
60
-
61
- model_scope.find_each do |r|
62
- begin
63
- if !r.valid?
64
- message = " Invalid record in #{model} id=#{r.id}."
65
- r.errors.each { |attr,msg| message << "\n #{attr} - #{msg}" }
66
- logger.log_error message
67
- end
68
- rescue Exception => msg
69
- logger.log_error " Exception for record in #{model} id=#{r.id} - #{msg}."
70
- end
71
- end
72
- rescue Exception => msg
73
- logger.log_error " Error querying model #{model} - #{msg}."
74
- end
75
- end
76
- end
77
50
  end
78
- end
51
+ end
@@ -1,9 +1,12 @@
1
1
  require 'rake'
2
2
 
3
3
  namespace :broken_record do
4
- desc 'Scans all models for validation errors'
5
- task :scan, [:model_name] => :environment do |t, args|
4
+ desc 'Scans models for validation errors'
5
+ task :scan, [:class_name] => :environment do |t, args|
6
6
  scanner = BrokenRecord::Scanner.new
7
- scanner.run(args[:model_name])
7
+ class_names = args[:class_name] ? [args[:class_name]] : []
8
+ class_names += args.extras
9
+ aggregator = scanner.run(class_names)
10
+ aggregator.report_final_results
8
11
  end
9
- end
12
+ end
@@ -1,3 +1,3 @@
1
1
  module BrokenRecord
2
- VERSION = "0.0.6"
2
+ VERSION = '0.0.7'
3
3
  end
metadata CHANGED
@@ -1,69 +1,87 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: broken_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicholas Gervasi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-30 00:00:00.000000000 Z
11
+ date: 2015-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.3'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.3'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ~>
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10'
33
+ version: '10.1'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 10.1.10
34
37
  type: :runtime
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
37
40
  requirements:
38
- - - ~>
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '10.1'
44
+ - - ">="
39
45
  - !ruby/object:Gem::Version
40
- version: '10'
46
+ version: 10.1.10
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: parallel
43
49
  requirement: !ruby/object:Gem::Requirement
44
50
  requirements:
45
- - - ~>
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.2'
54
+ - - ">="
46
55
  - !ruby/object:Gem::Version
47
- version: '0'
56
+ version: 1.2.3
48
57
  type: :runtime
49
58
  prerelease: false
50
59
  version_requirements: !ruby/object:Gem::Requirement
51
60
  requirements:
52
- - - ~>
61
+ - - "~>"
53
62
  - !ruby/object:Gem::Version
54
- version: '0'
63
+ version: '1.2'
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 1.2.3
55
67
  - !ruby/object:Gem::Dependency
56
68
  name: colorize
57
69
  requirement: !ruby/object:Gem::Requirement
58
70
  requirements:
59
- - - '='
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '0.5'
74
+ - - ">="
60
75
  - !ruby/object:Gem::Version
61
76
  version: 0.5.8
62
77
  type: :runtime
63
78
  prerelease: false
64
79
  version_requirements: !ruby/object:Gem::Requirement
65
80
  requirements:
66
- - - '='
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0.5'
84
+ - - ">="
67
85
  - !ruby/object:Gem::Version
68
86
  version: 0.5.8
69
87
  description: Detects ActiveRecord models that are not valid.
@@ -73,7 +91,7 @@ executables: []
73
91
  extensions: []
74
92
  extra_rdoc_files: []
75
93
  files:
76
- - .gitignore
94
+ - ".gitignore"
77
95
  - CHANGELOG.md
78
96
  - Gemfile
79
97
  - LICENSE.txt
@@ -82,8 +100,10 @@ files:
82
100
  - broken_record.gemspec
83
101
  - lib/broken_record.rb
84
102
  - lib/broken_record/config.rb
85
- - lib/broken_record/logger.rb
103
+ - lib/broken_record/job.rb
104
+ - lib/broken_record/job_result.rb
86
105
  - lib/broken_record/railtie.rb
106
+ - lib/broken_record/result_aggregator.rb
87
107
  - lib/broken_record/scanner.rb
88
108
  - lib/broken_record/tasks.rb
89
109
  - lib/broken_record/version.rb
@@ -97,17 +117,17 @@ require_paths:
97
117
  - lib
98
118
  required_ruby_version: !ruby/object:Gem::Requirement
99
119
  requirements:
100
- - - ! '>='
120
+ - - ">="
101
121
  - !ruby/object:Gem::Version
102
122
  version: '0'
103
123
  required_rubygems_version: !ruby/object:Gem::Requirement
104
124
  requirements:
105
- - - ! '>='
125
+ - - ">="
106
126
  - !ruby/object:Gem::Version
107
127
  version: '0'
108
128
  requirements: []
109
129
  rubyforge_project:
110
- rubygems_version: 2.2.2
130
+ rubygems_version: 2.4.6
111
131
  signing_key:
112
132
  specification_version: 4
113
133
  summary: Provides a rake task for scanning your ActiveRecord models and detecting
@@ -1,85 +0,0 @@
1
- require 'colorize'
2
- require 'tempfile'
3
-
4
- module BrokenRecord
5
- class Logger
6
-
7
- # Static Methods
8
-
9
- def self.report_output(result, lock = nil)
10
- lock.flock File::LOCK_EX if lock
11
- $stdout.print result[:stdout]
12
- $stdout.flush
13
- ensure
14
- lock.flock File::LOCK_UN if lock
15
- end
16
-
17
- def self.report_results(test_results)
18
- total_errors = 0
19
- test_results.each { |result| total_errors += result[:error_count] }
20
- if total_errors == 0
21
- puts "\nAll models validated successfully.".green
22
- else
23
- puts "\n#{total_errors} errors were found while running validations.".red
24
- exit 1
25
- end
26
- end
27
-
28
- def self.parallel
29
- Tempfile.open 'broken_record_lock' do |lock|
30
- yield lock
31
- end
32
- end
33
-
34
- def self.log(model, &block)
35
- logger = new
36
- logger.start_log
37
- logger.log_header "Validating model #{model}... ".ljust(70)
38
-
39
- yield(logger)
40
-
41
- logger.log_result
42
- logger.result
43
- end
44
-
45
- # Instance Methods
46
-
47
- def initialize
48
- @header = ''
49
- @errors = []
50
-
51
- @stdout = ''
52
- end
53
-
54
- def log_header(header_message)
55
- @header = header_message
56
- end
57
-
58
- def start_log
59
- @start_time = Time.now
60
- end
61
-
62
- def log_error(message)
63
- @errors << "#{message.red}\n"
64
- end
65
-
66
- def log_result
67
- @stdout << @header
68
-
69
- if @errors.empty?
70
- @stdout << '[PASS]'.green
71
- else
72
- @stdout << '[FAIL]'.red
73
- end
74
-
75
- duration = (Time.now - @start_time).round(3)
76
- @stdout << " (#{duration}s)\n"
77
-
78
- @stdout << @errors.join
79
- end
80
-
81
- def result
82
- { stdout: @stdout, error_count: @errors.count}
83
- end
84
- end
85
- end