active_sanity 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ test/rails_app
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in active_sanity.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # Active Sanity
2
+
3
+ Perform a sanity check on your database through active record
4
+ validation.
5
+
6
+ ## Install
7
+
8
+ Add the following line to your Gemfile
9
+
10
+ gem 'active_sanity'
11
+
12
+ If you wish to store invalid records in your database run:
13
+
14
+ $ rails generate active_sanity
15
+ $ rake db:migrate
16
+
17
+ ## Usage
18
+
19
+ Just run:
20
+
21
+ rake db:sanity_check
22
+
23
+ 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
25
+ invalid_records if it exists and output all invalid records.
26
+
27
+ The output might look like the following:
28
+
29
+ model | id | errors
30
+ User | 1 | { "email" => ["is invalid"] }
31
+ Flight | 123 | { "arrival_time" => ["can't be nil"], "departure_time" => ["is invalid"] }
32
+ Flight | 323 | { "arrival_time" => ["can't be nil"] }
33
+
34
+ ## Known issues
35
+
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.
39
+
40
+ See https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/6646-orderedhash-serialization-does-not-work-when-storing-arrays
41
+
42
+ ## Contribute & Dev environment
43
+
44
+ Usual fork & pull request.
45
+
46
+ This gem is quite simple so I experiment using features only. To run the
47
+ acceptance test suite, just run:
48
+
49
+ bundle install
50
+ cucumber features
51
+
52
+ Using features only was kinda handsome until I had to deal with two
53
+ different database schema (with / without the table invalid_records) in
54
+ the same test suite. I guess that the same complexity would arise using
55
+ any other testing framework.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "active_sanity/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "active_sanity"
7
+ s.version = ActiveSanity::VERSION
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{Active Record databases Sanity Check }
13
+ s.description = %q{Performs a sanity check on your database through active record validations.}
14
+
15
+ s.add_dependency "rails", ">=3.0.0"
16
+
17
+ s.add_development_dependency "rspec"
18
+ s.add_development_dependency "cucumber"
19
+ s.add_development_dependency "sqlite3"
20
+
21
+ s.rubyforge_project = "active_sanity"
22
+
23
+ s.files = `git ls-files`.split("\n")
24
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
26
+ s.require_paths = ["lib"]
27
+ end
@@ -0,0 +1,26 @@
1
+ Feature: Check sanity
2
+
3
+ As a developer
4
+ In order to ensure that existing records are valid
5
+ I want to run 'rake db:check_sanity' and see which records are invalid
6
+
7
+ Background:
8
+ Given I have a rails app using 'active_sanity'
9
+
10
+ Scenario: Check sanity on empty database
11
+ When I run "rake db:check_sanity"
12
+ Then I should see "Checking the following models: Category, Post, User"
13
+ Then I should not see any invalid records
14
+
15
+ Scenario: Check sanity on database with valid records
16
+ Given the database contains a few valid records
17
+ When I run "rake db:check_sanity"
18
+ Then I should not see any invalid records
19
+
20
+ Scenario: Check sanity on database with invalid records
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
23
+ When I run "rake db:check_sanity --trace"
24
+ Then I should see the following invalid records:
25
+ | User | 1 | {:username=>["can't be blank", "is too short (minimum is 3 characters)"]} |
26
+ | Post | 1 | {:category=>["can't be blank"]} |
@@ -0,0 +1,55 @@
1
+ Feature: Check sanity with db storage
2
+
3
+ As a developer
4
+ In order to ensure that existing records are valid
5
+ I want to run 'rake db:check_sanity' to log invalid records in the db
6
+
7
+ Background:
8
+ Given I have a rails app using 'active_sanity' with db storage
9
+
10
+ Scenario: Check sanity on empty database
11
+ When I run "rake db:check_sanity"
12
+ Then I should see "Checking the following models: Category, Post, User"
13
+ Then the table "invalid_records" should be empty
14
+
15
+ Scenario: Check sanity on database with valid records
16
+ Given the database contains a few valid records
17
+ When I run "rake db:check_sanity"
18
+ Then the table "invalid_records" should be empty
19
+
20
+ Scenario: Check sanity on database with invalid records
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
23
+ When I run "rake db:check_sanity"
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"]} |
27
+
28
+ Scenario: Check sanity on database with invalid records now valid
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
31
+ When I run "rake db:check_sanity"
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"]} |
35
+
36
+ Given the first user's username is "Greg"
37
+ When I run "rake db:check_sanity"
38
+ Then the table "invalid_records" should contain:
39
+ | Post | 1 | {:category=>["can't be blank"]} |
40
+ Then the table "invalid_records" should not contain errors for "User" "1"
41
+
42
+ Scenario: Check sanity on database with invalid records that were invalid for different reasons earlier
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
45
+ When I run "rake db:check_sanity"
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"]} |
49
+
50
+ Given the first post category is set
51
+ And the first post title is empty
52
+ When I run "rake db:check_sanity"
53
+ Then the table "invalid_records" should contain:
54
+ | User | 1 | {:username=>["is too short (minimum is 3 characters)"]} |
55
+ | Post | 1 | {:title=>["can't be blank"]} |
@@ -0,0 +1,111 @@
1
+ def setup_rails_app
2
+ return if File.directory?("test/rails_app")
3
+
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
8
+ end
9
+
10
+ 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}")
13
+ end
14
+
15
+ setup_rails_app
16
+
17
+ require './test/rails_app/config/environment'
18
+ end
19
+
20
+ Given /^I have a rails app using 'active_sanity' with db storage$/ do
21
+ setup_rails_app
22
+
23
+ raise unless system("cd ./test/rails_app && rails generate active_sanity && rake db:migrate")
24
+
25
+ require './test/rails_app/config/environment'
26
+
27
+ # Reset connection
28
+ ActiveRecord::Base.connection.reconnect!
29
+ InvalidRecord.table_exists?
30
+ end
31
+
32
+ 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)
39
+ end
40
+
41
+ Given /^the first user's username is empty and the first post category_id is nil$/ do
42
+ User.first.update_attribute(:username, "")
43
+ Post.first.update_attribute(:category_id, nil)
44
+ end
45
+
46
+ Given /^the first user's username is "([^"]*)"$/ do |username|
47
+ User.first.update_attribute('username', username)
48
+ end
49
+
50
+ Given /^the first post category is set$/ do
51
+ Post.first.update_attribute('category_id', Category.first.id)
52
+ end
53
+
54
+ Given /^the first post title is empty$/ do
55
+ Post.first.update_attribute('title', '')
56
+ end
57
+
58
+
59
+ When /^I run "([^"]*)"$/ do |command|
60
+ puts @output = `cd ./test/rails_app && #{command}; echo "RETURN:$?"`
61
+ raise unless @output['RETURN:0']
62
+ end
63
+
64
+
65
+ Then /^I should see the following invalid records:$/ do |table|
66
+ table.raw.each do |model, id, errors|
67
+ @output.should =~ /#{model}\s+\|\s+#{id}\s+\|\s+#{Regexp.escape errors}/
68
+ end
69
+ end
70
+
71
+ Then /^I should see "([^"]*)"$/ do |output|
72
+ @output.should include(output)
73
+ end
74
+
75
+ Then /^I should not see any invalid records$/ do
76
+ @output.should_not include('|')
77
+ end
78
+
79
+ Then /^the table "([^"]*)" should be empty$/ do |_|
80
+ InvalidRecord.count.should == 0
81
+ end
82
+
83
+ Then /^the table "([^"]*)" should contain:$/ do |_, table|
84
+ table.raw.each do |model, id, errors|
85
+ invalid_record = InvalidRecord.where(:record_type => model, :record_id => id).first
86
+ invalid_record.should be_an_instance_of(InvalidRecord)
87
+ errors = eval(errors)
88
+ 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
104
+ end
105
+ end
106
+ end
107
+
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
110
+ end
111
+
@@ -0,0 +1,20 @@
1
+ ENV['BUNDLE_GEMFILE'] = File.expand_path('../../../Gemfile', __FILE__)
2
+
3
+ require 'rubygems'
4
+ require "bundler"
5
+ Bundler.setup
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")
10
+
11
+ After do
12
+ # 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}'")
19
+ end
20
+ end
@@ -0,0 +1,4 @@
1
+ require File.dirname(__FILE__) + '/active_sanity/checker'
2
+ require File.dirname(__FILE__) + '/active_sanity/invalid_record'
3
+ require File.dirname(__FILE__) + '/active_sanity/railtie'
4
+
@@ -0,0 +1,66 @@
1
+ module ActiveSanity
2
+ class Checker
3
+ def self.check!
4
+ new.check!
5
+ end
6
+
7
+ def check!
8
+ puts "Sanity Check"
9
+ puts "Checking the following models: #{models.join(', ')}"
10
+
11
+ check_previously_invalid_records
12
+ check_all_records
13
+ end
14
+
15
+ def models
16
+ @models ||= Dir["#{Rails.root}/app/models/**/*.rb"].map do |file_path|
17
+ basename = File.basename(file_path, File.extname(file_path))
18
+ klass = basename.camelize.constantize
19
+ klass.new.is_a?(ActiveRecord::Base) ? klass : nil
20
+ end.compact
21
+ end
22
+
23
+ protected
24
+
25
+ def check_previously_invalid_records
26
+ return unless InvalidRecord.table_exists?
27
+
28
+ InvalidRecord.find_each do |invalid_record|
29
+ invalid_record.destroy if invalid_record.record.valid?
30
+ end
31
+ end
32
+
33
+ def check_all_records
34
+ models.each do |model|
35
+ model.find_each do |record|
36
+ unless record.valid?
37
+ invalid_record!(record)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def invalid_record!(record)
44
+ log_invalid_record(record)
45
+ store_invalid_record(record)
46
+ end
47
+
48
+ def log_invalid_record(record)
49
+ puts record.class.to_s + " | " + record.id.to_s + " | " + pretty_errors(record)
50
+ end
51
+
52
+ def store_invalid_record(record)
53
+ return unless InvalidRecord.table_exists?
54
+
55
+ invalid_record = InvalidRecord.where(:record_type => record.type, :record_id => record.id).first
56
+ invalid_record ||= InvalidRecord.new
57
+ invalid_record.record = record
58
+ invalid_record.validation_errors = record.errors
59
+ invalid_record.save!
60
+ end
61
+
62
+ def pretty_errors(record)
63
+ record.errors.inspect.sub(/^#<OrderedHash /, '').sub(/>$/, '')
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,6 @@
1
+ class InvalidRecord < ActiveRecord::Base
2
+ belongs_to :record, :polymorphic => true
3
+ serialize :validation_errors
4
+
5
+ validates_presence_of :record, :validation_errors
6
+ end
@@ -0,0 +1,11 @@
1
+ require 'active_sanity'
2
+ require 'rails'
3
+
4
+ module ActiveSanity
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ load 'active_sanity/tasks.rb'
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,7 @@
1
+ namespace :db do
2
+ desc "Check records sanity"
3
+ task :check_sanity => :environment do
4
+ ActiveSanity::Checker.check!
5
+ end
6
+ end
7
+
@@ -0,0 +1,3 @@
1
+ module ActiveSanity
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,26 @@
1
+ module ActiveSanity
2
+ module Generators
3
+ class ActiveSanityGenerator < Rails::Generators::Base
4
+ desc "Generate migration to create table 'invalid_records'"
5
+
6
+ include Rails::Generators::Migration
7
+
8
+ def self.source_root
9
+ @source_root ||= File.join(File.dirname(__FILE__), 'templates')
10
+ end
11
+
12
+ def self.next_migration_number(dirname) #:nodoc:
13
+ next_migration_number = current_migration_number(dirname) + 1
14
+ if ActiveRecord::Base.timestamped_migrations
15
+ [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
16
+ else
17
+ "%.3d" % next_migration_number
18
+ end
19
+ end
20
+
21
+ def create_migration_file
22
+ migration_template 'create_invalid_records.rb', 'db/migrate/create_invalid_records.rb'
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
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
+
@@ -0,0 +1,37 @@
1
+
2
+ # Generate some test models
3
+ generate :model, "post title:string body:text published_at:datetime author_id:integer category_id:integer"
4
+ post_code = <<-CODE
5
+ belongs_to :author, :class_name => 'User'
6
+ belongs_to :category
7
+ accepts_nested_attributes_for :author
8
+
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
19
+ CODE
20
+ inject_into_file 'app/models/user.rb', user_code, :after => "class User < ActiveRecord::Base\n"
21
+
22
+ generate :model, 'category name:string description:text'
23
+ category_code = <<-CODE
24
+ has_many :posts
25
+
26
+ validates_presence_of :name
27
+ CODE
28
+ inject_into_file 'app/models/category.rb', category_code, :after => "class Category < ActiveRecord::Base\n"
29
+
30
+ create_file 'app/models/not_a_model.rb', "class NotAModel; end"
31
+
32
+ # Add active_sanity
33
+ append_file 'Gemfile', "gem 'active_sanity', :path => '../../'"
34
+
35
+ run "bundle"
36
+ rake "db:migrate"
37
+
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_sanity
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - VersaPay
14
+ - Philippe Creux
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2011-03-29 00:00:00 -07:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: rails
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 7
31
+ segments:
32
+ - 3
33
+ - 0
34
+ - 0
35
+ version: 3.0.0
36
+ type: :runtime
37
+ version_requirements: *id001
38
+ - !ruby/object:Gem::Dependency
39
+ name: rspec
40
+ prerelease: false
41
+ requirement: &id002 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ hash: 3
47
+ segments:
48
+ - 0
49
+ version: "0"
50
+ type: :development
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: cucumber
54
+ 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"
64
+ type: :development
65
+ version_requirements: *id003
66
+ - !ruby/object:Gem::Dependency
67
+ name: sqlite3
68
+ 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"
78
+ type: :development
79
+ version_requirements: *id004
80
+ description: Performs a sanity check on your database through active record validations.
81
+ email:
82
+ - philippe.creux@versapay.com
83
+ executables: []
84
+
85
+ extensions: []
86
+
87
+ extra_rdoc_files: []
88
+
89
+ files:
90
+ - .gitignore
91
+ - Gemfile
92
+ - README.md
93
+ - Rakefile
94
+ - active_sanity.gemspec
95
+ - features/check_sanity.feature
96
+ - features/check_sanity_with_db_storage.feature
97
+ - features/step_definitions/rails_app.rb
98
+ - features/support/env.rb
99
+ - lib/active_sanity.rb
100
+ - lib/active_sanity/checker.rb
101
+ - lib/active_sanity/invalid_record.rb
102
+ - lib/active_sanity/railtie.rb
103
+ - lib/active_sanity/tasks.rb
104
+ - lib/active_sanity/version.rb
105
+ - lib/generators/active_sanity/active_sanity_generator.rb
106
+ - lib/generators/active_sanity/templates/create_invalid_records.rb
107
+ - test/rails_template.rb
108
+ has_rdoc: true
109
+ homepage: ""
110
+ licenses: []
111
+
112
+ post_install_message:
113
+ rdoc_options: []
114
+
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ">="
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:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ hash: 3
132
+ segments:
133
+ - 0
134
+ version: "0"
135
+ requirements: []
136
+
137
+ rubyforge_project: active_sanity
138
+ rubygems_version: 1.3.7
139
+ signing_key:
140
+ specification_version: 3
141
+ summary: Active Record databases Sanity Check
142
+ test_files:
143
+ - features/check_sanity.feature
144
+ - features/check_sanity_with_db_storage.feature
145
+ - features/step_definitions/rails_app.rb
146
+ - features/support/env.rb
147
+ - test/rails_template.rb