broken_record 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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