active_sanity 0.0.1

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.
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