active_sanity 0.1.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '0558af4db68be1d5ad7994453fee0a5abdfd3a79fd0e01417fa25afa7dc6ad98'
4
+ data.tar.gz: 7422349844512998be28691efc24d2211554a819f395cc39b0540d4edeedf398
5
+ SHA512:
6
+ metadata.gz: 4f825526ec0ee8c0a52ad40beef09c86cb5f64f7a8e61f2ed4beb01e1f22e56a97a77dfc4b7cf3368e147181f33b3829ad631bf9631d6b16fab12805ae11e493
7
+ data.tar.gz: bee1b677fd834b29b3b8edc1c573372b6de0a3db0b8edf2cf86a88c0ecdc7b89fbb0d576c17525c04fea54de13f8671e009c9f16472b846b1b6b6aa3e403fa1d
@@ -0,0 +1,18 @@
1
+ version: 2.1
2
+ orbs:
3
+ ruby: circleci/ruby@0.1.2
4
+
5
+ jobs:
6
+ build:
7
+ docker:
8
+ - image: circleci/ruby:2.6.3-stretch-node
9
+ executor: ruby/default
10
+ steps:
11
+ - checkout
12
+ - run:
13
+ name: Which bundler?
14
+ command: bundle -v
15
+ - ruby/bundle-install
16
+ - run:
17
+ name: cucumber
18
+ command: RAILS_ENV=test bundle exec cucumber
data/.gitignore CHANGED
@@ -1,5 +1,8 @@
1
1
  *.gem
2
2
  .bundle
3
+ .ruby-gemset
4
+ .ruby-version
3
5
  Gemfile.lock
4
6
  pkg/*
5
7
  test/rails_app
8
+ active_sanity.ignore.yml
@@ -0,0 +1,33 @@
1
+ AllCops:
2
+ Include:
3
+ - 'Gemfile'
4
+ - 'Rakefile'
5
+ - '*.gemspec'
6
+ - 'lib/**/*'
7
+ - 'test/**/*'
8
+ Exclude:
9
+ - 'features/**/*'
10
+ - 'test/rails_app/**/*'
11
+ RunRailsCops: false
12
+ Metrics/LineLength:
13
+ Enabled: false
14
+ Style/AlignHash:
15
+ EnforcedHashRocketStyle: table
16
+ EnforcedColonStyle: table
17
+ Style/Documentation:
18
+ Enabled: false
19
+ Style/RegexpLiteral:
20
+ Enabled: false
21
+ Style/CollectionMethods:
22
+ # Mapping from undesired method to desired_method
23
+ # e.g. to use `detect` over `find`:
24
+ #
25
+ # CollectionMethods:
26
+ # PreferredMethods:
27
+ # find: detect
28
+ PreferredMethods:
29
+ map: 'collect'
30
+ map!: 'collect!'
31
+ reduce: 'inject'
32
+ detect: 'find'
33
+ select: 'find_all'
@@ -0,0 +1,24 @@
1
+ ## 0.3.0
2
+
3
+ * Add Rails ~4 compatibility.
4
+ * Does not rerun validations for the 'InvalidRecord' model on the second pass.
5
+ * batch size for fetched records is now configurable by setting ActiveSanity::Checker.batch_size after the gem has been loaded ie in 'config\application.rb' (or 'config\environments\test.rb') inside an 'after_initialize' block
6
+
7
+ ## 0.2.0
8
+
9
+ * Add Rails ~3.1 compatibility. Pull Request [#6][] by [@skateinmars][].
10
+
11
+ ## 0.1.1
12
+
13
+ * Fix bug where records stored with STI would be duplicated in the
14
+ database. Issue [#1][]. [@vraravam][]
15
+
16
+ ## 0.1.0
17
+
18
+ Initial release.
19
+
20
+ <!--- The following link definition list is generated by PimpMyChangelog --->
21
+ [#1]: https://github.com/versapay/active_sanity/issues/1
22
+ [#6]: https://github.com/versapay/active_sanity/issues/6
23
+ [@skateinmars]: https://github.com/skateinmars
24
+ [@vraravam]: https://github.com/vraravam
data/Gemfile CHANGED
@@ -1,4 +1,7 @@
1
- source "http://rubygems.org"
1
+ source 'http://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in active_sanity.gemspec
4
4
  gemspec
5
+
6
+ gem 'sqlite3'
7
+ gem 'bootsnap', require: false
data/README.md CHANGED
@@ -1,8 +1,17 @@
1
1
  # Active Sanity
2
2
 
3
+ [![Build Status](https://circleci.com/gh/pcreux/active_sanity.svg?style=svg)](https://circleci.com/gh/pcreux/active_sanity)
4
+
3
5
  Perform a sanity check on your database through active record
4
6
  validation.
5
7
 
8
+ ## Requirements
9
+
10
+ * ActiveSanity ~0.5 Rails ~5.0 or ~6.0
11
+ * ActiveSanity ~0.3 and ~0.4 requires Rails ~4.0
12
+ * ActiveSanity ~0.2 requires Rails ~3.1
13
+ * ActiveSanity ~0.1 requires Rails ~3.0
14
+
6
15
  ## Install
7
16
 
8
17
  Add the following line to your Gemfile
@@ -11,17 +20,17 @@ Add the following line to your Gemfile
11
20
 
12
21
  If you wish to store invalid records in your database run:
13
22
 
14
- $ rails generate active_sanity
15
- $ rake db:migrate
23
+ $ bin/rails generate active_sanity
24
+ $ bin/rails db:migrate
16
25
 
17
26
  ## Usage
18
27
 
19
28
  Just run:
20
29
 
21
- rake db:check_sanity
30
+ bin/rails db:check_sanity
22
31
 
23
32
  ActiveSanity will iterate over every records of all your models to check
24
- weither they're valid or not. It will save invalid records in the table
33
+ whether they're valid or not. It will save invalid records in the table
25
34
  invalid_records if it exists and output all invalid records.
26
35
 
27
36
  The output might look like the following:
@@ -31,13 +40,20 @@ The output might look like the following:
31
40
  Flight | 123 | { "arrival_time" => ["can't be nil"], "departure_time" => ["is invalid"] }
32
41
  Flight | 323 | { "arrival_time" => ["can't be nil"] }
33
42
 
34
- ## Known issues
43
+ By default, the number of records fetched from the database for validation is set to 500. If this causes any issues in your domain/codebase, you can configure it this way in `config\application.rb` (or `config\environments\test.rb`):
44
+
45
+ class Application < Rails::Application
46
+ config.after_initialize do
47
+ ActiveSanity::Checker.batch_size = 439
48
+ end
49
+ end
50
+
51
+ If you want to ignore certain models from being verified, you can create a file named `active_sanity.ignore.yml` at the root of your project with the following structure
35
52
 
36
- There is a bug in Rails 3.0.5 with the unserialization of OrderedHash
37
- storing arrays: only the last error of a given attribute gets retrieved
38
- from the database.
53
+ models:
54
+ - '<name of class to ignore>'
55
+ - '<name of class to ignore>'
39
56
 
40
- See https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/6646-orderedhash-serialization-does-not-work-when-storing-arrays
41
57
 
42
58
  ## Contribute & Dev environment
43
59
 
@@ -47,7 +63,7 @@ This gem is quite simple so I experiment using features only. To run the
47
63
  acceptance test suite, just run:
48
64
 
49
65
  bundle install
50
- cucumber features
66
+ RAILS_ENV=test bundle exec cucumber features
51
67
 
52
68
  Using features only was kinda handsome until I had to deal with two
53
69
  different database schema (with / without the table invalid_records) in
data/Rakefile CHANGED
@@ -1,2 +1,14 @@
1
1
  require 'bundler'
2
2
  Bundler::GemHelper.install_tasks
3
+
4
+ task default: :features
5
+
6
+ desc 'Run features'
7
+ task :features do
8
+ fail 'Failed!' unless system('export RAILS_ENV=test && bundle exec cucumber features')
9
+ end
10
+
11
+ desc 'Clean test rails app'
12
+ task :clean do
13
+ puts 'test/rails_app deleted successfully' if system('rm -r test/rails_app')
14
+ end
@@ -1,26 +1,26 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "active_sanity/version"
2
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
3
+ require 'active_sanity/version'
4
4
 
5
5
  Gem::Specification.new do |s|
6
- s.name = "active_sanity"
6
+ s.name = 'active_sanity'
7
7
  s.version = ActiveSanity::VERSION
8
8
  s.platform = Gem::Platform::RUBY
9
- s.authors = ["VersaPay", "Philippe Creux"]
10
- s.email = ["philippe.creux@versapay.com"]
11
- s.homepage = ""
12
- s.summary = %q{Checks Sanity of Active Record records}
13
- s.description = %q{Performs a Sanity Check of your database by logging all invalid Active Records}
14
- s.homepage = "https://github.com/versapay/active_sanity"
9
+ s.authors = ['Philippe Creux']
10
+ s.email = ['pcreux@gmail.com']
11
+ s.summary = 'Checks Sanity of Active Record records'
12
+ s.description = 'Performs a Sanity Check of your database by logging all invalid Active Records'
13
+ s.homepage = 'https://github.com/pcreux/active_sanity'
15
14
 
16
- s.add_dependency "rails", ">=3.0.0"
15
+ s.add_dependency 'rails', '>=5.0'
17
16
 
18
- s.add_development_dependency "rspec"
19
- s.add_development_dependency "cucumber"
20
- s.add_development_dependency "sqlite3"
17
+ s.add_development_dependency 'rails', '~>6.0'
18
+ s.add_development_dependency 'rspec', '~>3.1'
19
+ s.add_development_dependency 'cucumber', '~>1.3'
20
+ s.add_development_dependency 'sqlite3', '~>1.3'
21
21
 
22
22
  s.files = `git ls-files`.split("\n")
23
23
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
- s.require_paths = ["lib"]
24
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
25
+ s.require_paths = ['lib']
26
26
  end
File without changes
@@ -9,7 +9,7 @@ Feature: Check sanity
9
9
 
10
10
  Scenario: Check sanity on empty database
11
11
  When I run "rake db:check_sanity"
12
- Then I should see "Checking the following models: InvalidRecord, Category, Post, User"
12
+ Then I should see "Checking the following models: Category, Post, User"
13
13
  Then I should not see any invalid records
14
14
 
15
15
  Scenario: Check sanity on database with valid records
@@ -19,8 +19,15 @@ Feature: Check sanity
19
19
 
20
20
  Scenario: Check sanity on database with invalid records
21
21
  Given the database contains a few valid records
22
- And the first user's username is empty and the first post category_id is nil
22
+ And the first author's username is empty and the first post category_id is nil
23
23
  When I run "rake db:check_sanity"
24
24
  Then I should see the following invalid records:
25
25
  | User | 1 | {:username=>["can't be blank", "is too short (minimum is 3 characters)"]} |
26
- | Post | 1 | {:category=>["can't be blank"]} |
26
+ | Post | 1 | {:category=>["must exist", "can't be blank"]} |
27
+
28
+ Scenario: Check sanity on database with invalid records with ignored classes
29
+ Given the database contains a few valid records
30
+ And the first author's username is empty and the first post category_id is nil
31
+ And the User class is ignored
32
+ When I run "rake db:check_sanity"
33
+ Then I should see "Checking the following models: Category, Post"
@@ -9,7 +9,7 @@ Feature: Check sanity with db storage
9
9
 
10
10
  Scenario: Check sanity on empty database
11
11
  When I run "rake db:check_sanity"
12
- Then I should see "Checking the following models: InvalidRecord, Category, Post, User"
12
+ Then I should see "Checking the following models: Category, Post, User"
13
13
  Then the table "invalid_records" should be empty
14
14
 
15
15
  Scenario: Check sanity on database with valid records
@@ -19,37 +19,37 @@ Feature: Check sanity with db storage
19
19
 
20
20
  Scenario: Check sanity on database with invalid records
21
21
  Given the database contains a few valid records
22
- And the first user's username is empty and the first post category_id is nil
22
+ And the first author's username is empty and the first post category_id is nil
23
23
  When I run "rake db:check_sanity"
24
24
  Then the table "invalid_records" should contain:
25
- | User | 1 | {:username=>["is too short (minimum is 3 characters)"]} |
26
- | Post | 1 | {:category=>["can't be blank"]} |
25
+ | User | 1 | {:username=>["can't be blank", "is too short (minimum is 3 characters)"]} |
26
+ | Post | 1 | {:category=>["must exist", "can't be blank"]} |
27
27
 
28
28
  Scenario: Check sanity on database with invalid records now valid
29
29
  Given the database contains a few valid records
30
- And the first user's username is empty and the first post category_id is nil
30
+ And the first author's username is empty and the first post category_id is nil
31
31
  When I run "rake db:check_sanity"
32
32
  Then the table "invalid_records" should contain:
33
- | User | 1 | {:username=>["is too short (minimum is 3 characters)"]} |
34
- | Post | 1 | {:category=>["can't be blank"]} |
33
+ | User | 1 | {:username=>["can't be blank", "is too short (minimum is 3 characters)"]} |
34
+ | Post | 1 | {:category=>["must exist", "can't be blank"]} |
35
35
 
36
- Given the first user's username is "Greg"
36
+ Given the first author's username is "Greg"
37
37
  When I run "rake db:check_sanity"
38
38
  Then the table "invalid_records" should contain:
39
- | Post | 1 | {:category=>["can't be blank"]} |
39
+ | Post | 1 | {:category=>["must exist", "can't be blank"]} |
40
40
  Then the table "invalid_records" should not contain errors for "User" "1"
41
41
 
42
42
  Scenario: Check sanity on database with invalid records that were invalid for different reasons earlier
43
43
  Given the database contains a few valid records
44
- And the first user's username is empty and the first post category_id is nil
44
+ And the first author's username is empty and the first post category_id is nil
45
45
  When I run "rake db:check_sanity"
46
46
  Then the table "invalid_records" should contain:
47
- | User | 1 | {:username=>["is too short (minimum is 3 characters)"]} |
48
- | Post | 1 | {:category=>["can't be blank"]} |
47
+ | User | 1 | {:username=>["can't be blank", "is too short (minimum is 3 characters)"]} |
48
+ | Post | 1 | {:category=>["must exist", "can't be blank"]} |
49
49
 
50
50
  Given the first post category is set
51
51
  And the first post title is empty
52
52
  When I run "rake db:check_sanity"
53
53
  Then the table "invalid_records" should contain:
54
- | User | 1 | {:username=>["is too short (minimum is 3 characters)"]} |
54
+ | User | 1 | {:username=>["can't be blank", "is too short (minimum is 3 characters)"]} |
55
55
  | Post | 1 | {:title=>["can't be blank"]} |
@@ -1,50 +1,66 @@
1
- def setup_rails_app
2
- return if File.directory?("test/rails_app")
1
+ SystemCommandFailed = Class.new(StandardError)
3
2
 
4
- unless system "bundle exec rails new test/rails_app -m test/rails_template.rb"
5
- system("rm -fr test/rails_app")
6
- raise "Failed to generate test/rails_app"
7
- end
3
+ def system!(cmd)
4
+ system(cmd) || raise(SystemCommandFailed, cmd)
5
+ end
6
+
7
+ def setup_rails_app
8
+ return if File.directory?('test/rails_app')
9
+
10
+ system! 'bundle exec rails new test/rails_app --skip-sprockets --skip-spring --skip-javascript --skip-turbolinks'
11
+ system! 'cd ./test/rails_app && bundle'
12
+ system! 'cd ./test/rails_app && bin/rails app:template LOCATION=../rails_template.rb'
13
+ system!' cd ./test/rails_app && RAILS_ENV=test bin/rails db:migrate'
14
+ rescue SystemCommandFailed => e
15
+ system!('rm -fr test/rails_app')
16
+ raise e
8
17
  end
9
18
 
10
19
  Given /^I have a rails app using 'active_sanity'$/ do
11
- Dir["./test/rails_app/db/migrate/*create_invalid_records.rb"].each do |migration|
12
- raise unless system("rm #{migration}")
20
+ Dir['./test/rails_app/db/migrate/*create_invalid_records.rb'].each do |migration|
21
+ fail unless system("rm #{migration}")
13
22
  end
14
23
 
15
24
  setup_rails_app
16
25
 
17
26
  require './test/rails_app/config/environment'
27
+
28
+ # Reset connection
29
+ ActiveRecord::Base.connection.reconnect!
18
30
  end
19
31
 
20
32
  Given /^I have a rails app using 'active_sanity' with db storage$/ do
21
33
  setup_rails_app
22
34
 
23
- raise unless system("cd ./test/rails_app && rails generate active_sanity && rake db:migrate")
35
+ fail unless system('cd ./test/rails_app && bundle exec rails generate active_sanity && RAILS_ENV=test bundle exec rake db:migrate')
24
36
 
25
37
  require './test/rails_app/config/environment'
26
38
 
27
39
  # Reset connection
28
40
  ActiveRecord::Base.connection.reconnect!
29
- InvalidRecord.table_exists?
41
+ InvalidRecord.table_exists?.should be true # Looks up if table exists.
30
42
  end
31
43
 
32
44
  Given /^the database contains a few valid records$/ do
33
- User.create!(:first_name => "Greg", :last_name => "Bell", :username => "gregbell")
34
- User.create!(:first_name => "Sam", :last_name => "Vincent", :username => "samvincent")
35
- Category.create!(:name => "Uncategorized")
36
- Post.create!(:author => User.first, :category => Category.first,
37
- :title => "How ActiveAdmin changed the world", :body => "Only gods knows how...",
38
- :published_at => 4.years.from_now)
45
+ Author.create!(first_name: 'Greg', last_name: 'Bell', username: 'gregbell')
46
+ Publisher.create!(first_name: 'Sam', last_name: 'Vincent', username: 'samvincent')
47
+ Category.create!(name: 'Uncategorized')
48
+ Post.create!(author: Author.first!, category: Category.first!,
49
+ title: 'How ActiveAdmin changed the world', body: 'Lot of love.',
50
+ published_at: 4.years.from_now)
39
51
  end
40
52
 
41
- Given /^the first user's username is empty and the first post category_id is nil$/ do
42
- User.first.update_attribute(:username, "")
53
+ Given /^the first author's username is empty and the first post category_id is nil$/ do
54
+ Author.first.update_attribute(:username, '')
43
55
  Post.first.update_attribute(:category_id, nil)
44
56
  end
45
57
 
46
- Given /^the first user's username is "([^"]*)"$/ do |username|
47
- User.first.update_attribute('username', username)
58
+ Given /^the User class is ignored$/ do
59
+ system("echo 'models: User' > active_sanity.ignore.yml")
60
+ end
61
+
62
+ Given /^the first author's username is "([^"]*)"$/ do |username|
63
+ Author.first.update_attribute('username', username)
48
64
  end
49
65
 
50
66
  Given /^the first post category is set$/ do
@@ -55,13 +71,11 @@ Given /^the first post title is empty$/ do
55
71
  Post.first.update_attribute('title', '')
56
72
  end
57
73
 
58
-
59
74
  When /^I run "([^"]*)"$/ do |command|
60
- puts @output = `cd ./test/rails_app && #{command}; echo "RETURN:$?"`
61
- raise unless @output['RETURN:0']
75
+ puts @output = `cd ./test/rails_app && RAILS_ENV=test bundle exec #{command} --trace; echo "RETURN:$?"`
76
+ fail unless @output['RETURN:0']
62
77
  end
63
78
 
64
-
65
79
  Then /^I should see the following invalid records:$/ do |table|
66
80
  table.raw.each do |model, id, errors|
67
81
  @output.should =~ /#{model}\s+\|\s+#{id}\s+\|\s+#{Regexp.escape errors}/
@@ -82,30 +96,15 @@ end
82
96
 
83
97
  Then /^the table "([^"]*)" should contain:$/ do |_, table|
84
98
  table.raw.each do |model, id, errors|
85
- invalid_record = InvalidRecord.where(:record_type => model, :record_id => id).first
99
+ invalid_record = InvalidRecord.where(record_type: model, record_id: id).first
86
100
  invalid_record.should be_an_instance_of(InvalidRecord)
87
101
  errors = eval(errors)
88
102
  errors.each do |k, v|
89
- begin
90
- invalid_record.validation_errors[k].should == v
91
- rescue RSpec::Expectations::ExpectationNotMetError => e
92
- # A bug in serialization of ordered hash get rid of the array of
93
- # errors. The following is stored in the db:
94
- # --- !omap
95
- # - :username: can't be blank
96
- # - :username: is too short (minimum is 3 characters)
97
- #
98
- # https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/6646-orderedhash-serialization-does-not-work-when-storing-arrays
99
- #
100
- # You actually get the last error on an attribute only
101
- #
102
- invalid_record.validation_errors[k].should == v.last
103
- end
103
+ invalid_record.validation_errors[k].should == v
104
104
  end
105
105
  end
106
106
  end
107
107
 
108
108
  Then /^the table "([^"]*)" should not contain errors for "([^"]*)" "([^"]*)"$/ do |_, model, id|
109
- InvalidRecord.where(:record_type => model, :record_id => id).first.should be_nil
109
+ InvalidRecord.where(record_type: model, record_id: id).first.should be_nil
110
110
  end
111
-
@@ -1,20 +1,23 @@
1
1
  ENV['BUNDLE_GEMFILE'] = File.expand_path('../../../Gemfile', __FILE__)
2
2
 
3
3
  require 'rubygems'
4
- require "bundler"
4
+ require 'bundler'
5
5
  Bundler.setup
6
6
 
7
- system("rm ./test/rails_app/db/migrate/*create_invalid_records.rb")
8
-
9
- raise unless File.directory?("test/rails_app") && system("cd test/rails_app && rake db:drop db:create db:migrate")
7
+ if File.directory?('test/rails_app')
8
+ Dir.chdir('test/rails_app') do
9
+ fail unless system('rm -f db/migrate/*create_invalid_records.rb && RAILS_ENV=test bin/rails db:drop db:create db:migrate')
10
+ end
11
+ end
10
12
 
11
13
  After do
12
14
  # Reset DB!
13
- User.delete_all
14
- Category.delete_all
15
- Post.delete_all
16
- InvalidRecord.delete_all if InvalidRecord.table_exists?
17
- %w(users categories posts invalid_records).each do |table|
18
- ActiveRecord::Base.connection.execute("DELETE FROM sqlite_sequence WHERE name='#{table}'")
15
+ tables = %w(categories invalid_records posts users)
16
+ conn = ActiveRecord::Base.connection
17
+ tables.each do |table|
18
+ if conn.table_exists?(table)
19
+ conn.execute("DELETE FROM '#{table}'")
20
+ conn.execute("DELETE FROM sqlite_sequence WHERE name='#{table}'")
21
+ end
19
22
  end
20
23
  end
@@ -1,4 +1,3 @@
1
1
  require File.dirname(__FILE__) + '/active_sanity/checker'
2
2
  require File.dirname(__FILE__) + '/active_sanity/invalid_record'
3
3
  require File.dirname(__FILE__) + '/active_sanity/railtie'
4
-
@@ -1,44 +1,91 @@
1
1
  module ActiveSanity
2
2
  class Checker
3
+ class << self
4
+ attr_accessor :batch_size
5
+ end
6
+
7
+ def initialize
8
+ Checker.batch_size ||= 500
9
+ end
10
+
3
11
  def self.check!
4
12
  new.check!
5
13
  end
6
14
 
7
15
  def check!
8
- puts "Sanity Check"
16
+ puts 'Sanity Check'
9
17
  puts "Checking the following models: #{models.join(', ')}"
10
18
 
19
+ # TODO: Wouldnt this list already be checked by the next all records call if those records do exist?
20
+ # This will validate and destroy the records that either dont exist currently, or are now valid. But the ones are continue to be invalid - these will
21
+ # have been run through the validation process twice
11
22
  check_previously_invalid_records
12
23
  check_all_records
13
24
  end
14
25
 
26
+ # @return [Array] of [ActiveRecord::Base] direct descendants
15
27
  def models
16
- # Ensure ActiveRecord::Base is aware of all models under
17
- # app/models
18
- Dir["#{Rails.root}/app/models/**/*.rb"].each do |file_path|
19
- require file_path rescue nil
28
+ return @models if @models
29
+
30
+ load_all_models
31
+
32
+ @models ||= direct_active_record_base_descendants
33
+ @models -= [InvalidRecord]
34
+ if File.exist?('active_sanity.ignore.yml')
35
+ yaml_contents = YAML.load_file('active_sanity.ignore.yml')
36
+ model_names_to_ignore = Array(yaml_contents['models'] || []).uniq
37
+ @models = @models.reject { |model| model_names_to_ignore.include?(model.name) }
20
38
  end
21
-
22
- @models ||= ActiveRecord::Base.subclasses
39
+ @models
23
40
  end
24
41
 
25
42
  protected
26
43
 
44
+ # Require all files under /app/models.
45
+ # All models under /lib are required when the rails app loads.
46
+ def load_all_models
47
+ Dir["#{Rails.root}/app/models/**/*.rb"].each { |file_path| require file_path rescue nil }
48
+ end
49
+
50
+ # @return [Array] of direct ActiveRecord::Base descendants.
51
+ # Example:
52
+ # The following tree:
53
+ # ActiveRecord::Base
54
+ # |
55
+ # |- User
56
+ # |- Account
57
+ # | |
58
+ # | |- PersonalAccount
59
+ # | |- BusinessAccount
60
+ #
61
+ # Should return: [Account, User]
62
+ def direct_active_record_base_descendants
63
+ parent_class = defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base
64
+ parent_class.descendants.select(&:descends_from_active_record?).sort_by(&:name)
65
+ end
66
+
67
+ # Remove records that are now valid from the list of invalid records.
27
68
  def check_previously_invalid_records
28
69
  return unless InvalidRecord.table_exists?
29
70
 
30
- InvalidRecord.find_each do |invalid_record|
31
- invalid_record.destroy if invalid_record.record.valid?
71
+ InvalidRecord.find_each(batch_size: Checker.batch_size) do |invalid_record|
72
+ begin
73
+ invalid_record.destroy if invalid_record.record.valid?
74
+ rescue
75
+ # Record does not exists.
76
+ invalid_record.delete
77
+ end
32
78
  end
33
79
  end
34
80
 
81
+ # Go over every single record. When the record is not valid
82
+ # log it to STDOUT and into the invalid_records table if it exists.
35
83
  def check_all_records
36
84
  models.each do |model|
37
85
  begin
38
- model.find_each do |record|
39
- unless record.valid?
40
- invalid_record!(record)
41
- end
86
+ # TODO: Can we filter based on those records that are already present in the 'invalid_records' table - especially since they have been re-verified in the method before?
87
+ model.find_each(batch_size: Checker.batch_size) do |record|
88
+ invalid_record!(record) unless record.valid?
42
89
  end
43
90
  rescue => e
44
91
  # Rescue from exceptions (table does not exists,
@@ -54,22 +101,30 @@ module ActiveSanity
54
101
  store_invalid_record(record)
55
102
  end
56
103
 
104
+ # Say that the record is invalid. Example:
105
+ #
106
+ # Account | 10 | :name => "Can't be blank"
57
107
  def log_invalid_record(record)
58
- puts record.class.to_s + " | " + record.id.to_s + " | " + pretty_errors(record)
108
+ puts "#{type_of(record)} | #{record.id} | #{pretty_errors(record)}"
59
109
  end
60
-
110
+
111
+ # Store invalid record in InvalidRecord table if it exists
61
112
  def store_invalid_record(record)
62
113
  return unless InvalidRecord.table_exists?
63
114
 
64
- invalid_record = InvalidRecord.where(:record_type => record.type, :record_id => record.id).first
115
+ invalid_record = InvalidRecord.where(record: record).first
65
116
  invalid_record ||= InvalidRecord.new
66
117
  invalid_record.record = record
67
- invalid_record.validation_errors = record.errors
118
+ invalid_record.validation_errors = record.errors.messages
68
119
  invalid_record.save!
69
120
  end
70
121
 
122
+ def type_of(record)
123
+ record.class.base_class
124
+ end
125
+
71
126
  def pretty_errors(record)
72
- record.errors.inspect.sub(/^#<OrderedHash /, '').sub(/>$/, '')
127
+ record.errors.messages.inspect.sub(/^#<OrderedHash (.*)>$/, '\1')
73
128
  end
74
129
  end
75
130
  end
@@ -1,6 +1,7 @@
1
1
  class InvalidRecord < ActiveRecord::Base
2
- belongs_to :record, :polymorphic => true
2
+ belongs_to :record, polymorphic: true
3
3
  serialize :validation_errors
4
4
 
5
- validates_presence_of :record, :validation_errors
5
+ validates :record, :validation_errors, presence: true
6
+ validates :record_id, presence: true, uniqueness: { scope: :record_type }
6
7
  end
@@ -8,4 +8,3 @@ module ActiveSanity
8
8
  end
9
9
  end
10
10
  end
11
-
@@ -1,7 +1,6 @@
1
1
  namespace :db do
2
- desc "Check records sanity"
3
- task :check_sanity => :environment do
2
+ desc 'Check records sanity'
3
+ task check_sanity: :environment do
4
4
  ActiveSanity::Checker.check!
5
5
  end
6
6
  end
7
-
@@ -1,3 +1,3 @@
1
1
  module ActiveSanity
2
- VERSION = "0.1.0"
2
+ VERSION = '0.5.0'
3
3
  end
@@ -6,21 +6,31 @@ module ActiveSanity
6
6
  include Rails::Generators::Migration
7
7
 
8
8
  def self.source_root
9
- @source_root ||= File.join(File.dirname(__FILE__), 'templates')
9
+ @source_root ||= File.join(File.dirname(__FILE__), 'templates')
10
10
  end
11
11
 
12
12
  def self.next_migration_number(dirname) #:nodoc:
13
13
  next_migration_number = current_migration_number(dirname) + 1
14
14
  if ActiveRecord::Base.timestamped_migrations
15
- [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
15
+ [Time.now.utc.strftime('%Y%m%d%H%M%S'), '%.14d' % next_migration_number].max
16
16
  else
17
- "%.3d" % next_migration_number
17
+ '%.3d' % next_migration_number
18
18
  end
19
19
  end
20
20
 
21
21
  def create_migration_file
22
- migration_template 'create_invalid_records.rb', 'db/migrate/create_invalid_records.rb'
22
+ migration_template 'create_invalid_records.rb.erb', 'db/migrate/create_invalid_records.rb'
23
23
  end
24
+
25
+ # Used by migration file
26
+ def rails_version
27
+ if Rails.version >= "5"
28
+ "[#{Rails.version.gsub(/\.\d+$/, '')}]" # 5.0
29
+ else
30
+ nil
31
+ end
32
+ end
33
+
24
34
  end
25
35
  end
26
36
  end
@@ -0,0 +1,13 @@
1
+ class CreateInvalidRecords < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def up
3
+ create_table :invalid_records do |t|
4
+ t.references :record, polymorphic: true, null: false
5
+ t.text :validation_errors
6
+ t.timestamps
7
+ end
8
+ end
9
+
10
+ def down
11
+ drop_table :invalid_records
12
+ end
13
+ end
@@ -1,37 +1,56 @@
1
-
2
1
  # Generate some test models
3
- generate :model, "post title:string body:text published_at:datetime author_id:integer category_id:integer"
2
+
3
+ BASE_MODEL_CLASS = Rails.version >= "5." ? "ApplicationRecord" : "ActiveRecord::Base"
4
+
5
+ # Post
6
+ generate :model, 'post title:string body:text published_at:datetime author_id:integer category_id:integer'
4
7
  post_code = <<-CODE
5
8
  belongs_to :author, :class_name => 'User'
6
9
  belongs_to :category
7
10
  accepts_nested_attributes_for :author
8
11
 
9
- validates_presence_of :author, :category, :title, :published_at
10
- CODE
11
- inject_into_file 'app/models/post.rb', post_code, :after => "class Post < ActiveRecord::Base\n"
12
-
13
- generate :model, "user first_name:string last_name:string username:string"
14
- user_code = <<-CODE
15
- has_many :posts, :foreign_key => 'author_id'
16
-
17
- validates_presence_of :first_name, :last_name, :username
18
- validates_length_of :username, :minimum => 3
12
+ validates :author, :category, :title, :published_at, presence: true
19
13
  CODE
20
- inject_into_file 'app/models/user.rb', user_code, :after => "class User < ActiveRecord::Base\n"
14
+ inject_into_file 'app/models/post.rb', post_code, after: "class Post < #{BASE_MODEL_CLASS}\n"
21
15
 
16
+ # Category
22
17
  generate :model, 'category name:string description:text'
23
18
  category_code = <<-CODE
24
19
  has_many :posts
25
20
 
26
- validates_presence_of :name
21
+ validates :name, presence: true
27
22
  CODE
28
- inject_into_file 'app/models/category.rb', category_code, :after => "class Category < ActiveRecord::Base\n"
23
+ inject_into_file 'app/models/category.rb', category_code, after: "class Category < #{BASE_MODEL_CLASS}\n"
29
24
 
30
- create_file 'app/models/not_a_model.rb', "class NotAModel; end"
25
+ # User
26
+ generate :model, 'user first_name:string last_name:string username:string type:string'
27
+ user_code = <<-CODE
28
+ has_many :posts, :foreign_key => 'author_id'
29
+
30
+ validates :first_name, :last_name, :username, presence: true
31
+ validates :username, length: { minimum: 3 }
32
+ CODE
33
+ inject_into_file 'app/models/user.rb', user_code, after: "class User < #{BASE_MODEL_CLASS}\n"
34
+
35
+ # Author < User
36
+ create_file 'app/models/author.rb', 'class Author < User; end'
37
+ # Publisher < User
38
+ create_file 'app/models/publisher.rb', 'class Publisher < User; end'
39
+
40
+ # NotAModel
41
+ create_file 'app/models/not_a_model.rb', 'class NotAModel; end'
31
42
 
32
43
  # Add active_sanity
33
44
  append_file 'Gemfile', "gem 'active_sanity', :path => '../../'"
34
45
 
35
- run "bundle"
36
- rake "db:migrate"
46
+ # Configure for custom batch_size
47
+ custom_batch_size_code = <<-CODE
48
+ config.after_initialize do
49
+ ActiveSanity::Checker.batch_size = 439
50
+ end
51
+ CODE
52
+
53
+ inject_into_file 'config/application.rb', custom_batch_size_code, after: " < Rails::Application\n"
37
54
 
55
+ run 'bundle'
56
+ rake 'db:drop db:create db:migrate'
metadata CHANGED
@@ -1,97 +1,102 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: active_sanity
3
- version: !ruby/object:Gem::Version
4
- hash: 27
5
- prerelease: false
6
- segments:
7
- - 0
8
- - 1
9
- - 0
10
- version: 0.1.0
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
11
5
  platform: ruby
12
- authors:
13
- - VersaPay
6
+ authors:
14
7
  - Philippe Creux
15
8
  autorequire:
16
9
  bindir: bin
17
10
  cert_chain: []
18
-
19
- date: 2011-03-30 00:00:00 -07:00
20
- default_executable:
21
- dependencies:
22
- - !ruby/object:Gem::Dependency
11
+ date: 2020-06-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
23
14
  name: rails
24
- prerelease: false
25
- requirement: &id001 !ruby/object:Gem::Requirement
26
- none: false
27
- requirements:
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
28
17
  - - ">="
29
- - !ruby/object:Gem::Version
30
- hash: 7
31
- segments:
32
- - 3
33
- - 0
34
- - 0
35
- version: 3.0.0
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
36
20
  type: :runtime
37
- version_requirements: *id001
38
- - !ruby/object:Gem::Dependency
39
- name: rspec
40
21
  prerelease: false
41
- requirement: &id002 !ruby/object:Gem::Requirement
42
- none: false
43
- requirements:
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
44
24
  - - ">="
45
- - !ruby/object:Gem::Version
46
- hash: 3
47
- segments:
48
- - 0
49
- version: "0"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
50
34
  type: :development
51
- version_requirements: *id002
52
- - !ruby/object:Gem::Dependency
53
- name: cucumber
54
35
  prerelease: false
55
- requirement: &id003 !ruby/object:Gem::Requirement
56
- none: false
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- hash: 3
61
- segments:
62
- - 0
63
- version: "0"
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '6.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.1'
64
48
  type: :development
65
- version_requirements: *id003
66
- - !ruby/object:Gem::Dependency
67
- name: sqlite3
68
49
  prerelease: false
69
- requirement: &id004 !ruby/object:Gem::Requirement
70
- none: false
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- hash: 3
75
- segments:
76
- - 0
77
- version: "0"
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: cucumber
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
78
62
  type: :development
79
- version_requirements: *id004
80
- description: Performs a Sanity Check of your database by logging all invalid Active Records
81
- email:
82
- - philippe.creux@versapay.com
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.3'
83
+ description: Performs a Sanity Check of your database by logging all invalid Active
84
+ Records
85
+ email:
86
+ - pcreux@gmail.com
83
87
  executables: []
84
-
85
88
  extensions: []
86
-
87
89
  extra_rdoc_files: []
88
-
89
- files:
90
- - .gitignore
90
+ files:
91
+ - ".circleci/config.yml"
92
+ - ".gitignore"
93
+ - ".rubocop.yml"
94
+ - CHANGELOG.md
91
95
  - Gemfile
92
96
  - README.md
93
97
  - Rakefile
94
98
  - active_sanity.gemspec
99
+ - commits.csv
95
100
  - features/check_sanity.feature
96
101
  - features/check_sanity_with_db_storage.feature
97
102
  - features/step_definitions/rails_app.rb
@@ -103,43 +108,31 @@ files:
103
108
  - lib/active_sanity/tasks.rb
104
109
  - lib/active_sanity/version.rb
105
110
  - lib/generators/active_sanity/active_sanity_generator.rb
106
- - lib/generators/active_sanity/templates/create_invalid_records.rb
111
+ - lib/generators/active_sanity/templates/create_invalid_records.rb.erb
107
112
  - test/rails_template.rb
108
- has_rdoc: true
109
- homepage: https://github.com/versapay/active_sanity
113
+ homepage: https://github.com/pcreux/active_sanity
110
114
  licenses: []
111
-
115
+ metadata: {}
112
116
  post_install_message:
113
117
  rdoc_options: []
114
-
115
- require_paths:
118
+ require_paths:
116
119
  - lib
117
- required_ruby_version: !ruby/object:Gem::Requirement
118
- none: false
119
- requirements:
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
120
122
  - - ">="
121
- - !ruby/object:Gem::Version
122
- hash: 3
123
- segments:
124
- - 0
125
- version: "0"
126
- required_rubygems_version: !ruby/object:Gem::Requirement
127
- none: false
128
- requirements:
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
129
127
  - - ">="
130
- - !ruby/object:Gem::Version
131
- hash: 3
132
- segments:
133
- - 0
134
- version: "0"
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
135
130
  requirements: []
136
-
137
- rubyforge_project:
138
- rubygems_version: 1.3.7
131
+ rubygems_version: 3.0.3
139
132
  signing_key:
140
- specification_version: 3
133
+ specification_version: 4
141
134
  summary: Checks Sanity of Active Record records
142
- test_files:
135
+ test_files:
143
136
  - features/check_sanity.feature
144
137
  - features/check_sanity_with_db_storage.feature
145
138
  - features/step_definitions/rails_app.rb
@@ -1,15 +0,0 @@
1
- class CreateInvalidRecords < ActiveRecord::Migration
2
- def self.up
3
- create_table :invalid_records do |t|
4
- t.references :record, :polymorphic => true, :null => false
5
- t.text :validation_errors
6
- t.timestamps
7
- end
8
- add_index :invalid_records, [:record_type, :record_id]
9
- end
10
-
11
- def self.down
12
- drop_table :invalid_records
13
- end
14
- end
15
-