consistency_fail 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in consistency_fail.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2011 Colin Jones
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Consistency Fail
2
+
3
+ ## Description
4
+ consistency\_fail is a tool to detect missing unique indexes in Rails projects.
5
+
6
+ With more than one application server, `validates_uniqueness_of` becomes a lie.
7
+ Two app servers -> two requests -> two near-simultaneous uniqueness checks ->
8
+ two processes that commit to the database independently, violating this faux
9
+ constraint. You'll need a database-level constraint for cases like these.
10
+
11
+ consistency\_fail will find your missing unique indexes, so you can add them and
12
+ stop ignoring the C in ACID.
13
+
14
+ Similar problems arise with `has_one`, so consistency\_fail finds places where
15
+ database-level enforcement is lacking there as well.
16
+
17
+
18
+ ## Installation
19
+
20
+ gem install consistency_fail
21
+
22
+ ## Limitations
23
+
24
+ Currently only Rails 2.x is supported. Rails 3 support is coming soon.
25
+
26
+ consistency\_fail depends on being able to find all your `ActiveRecord::Base`
27
+ subclasses with some `$LOAD_PATH` trickery. If any models are in a path either
28
+ not on your project's load path or in a path that doesn't include the word
29
+ "models", consistency\_fail won't be able to find or analyze them.
30
+
31
+ ## Usage
32
+
33
+ The only run mode for now is to generate a report of the problematic spots in
34
+ your application. From your Rails project directory, run:
35
+
36
+ consistency_fail
37
+
38
+ from your terminal / shell. This will spit a report to standard output, which
39
+ you can view directly, redirect to a file as evidence to embarrass a teammate,
40
+ or simply beam in happiness at your application's perfect record for
41
+ `validates_uniqueness_of` and `has_one` usage.
42
+
43
+ ## Coming Soon
44
+
45
+ * Rails 3 support
46
+ * Super-fail mode that monkey-patches explosions into your naughty models
47
+
48
+ ## License
49
+
50
+ Released under the MIT License. See the LICENSE file for further details.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "consistency_fail"
4
+
5
+ def problems(models, introspector)
6
+ problems = models.map do |m|
7
+ [m, introspector.missing_indexes(m)]
8
+ end.reject do |m, indexes|
9
+ indexes.empty?
10
+ end
11
+ end
12
+
13
+ models = ConsistencyFail::Models.new($LOAD_PATH)
14
+ models.preload_all
15
+
16
+ reporter = ConsistencyFail::Reporter.new
17
+
18
+ introspector = ConsistencyFail::Introspectors::ValidatesUniquenessOf.new
19
+ problems = problems(models.all, introspector)
20
+ reporter.report_validates_uniqueness_problems(problems)
21
+
22
+ introspector = ConsistencyFail::Introspectors::HasOne.new
23
+ problems = problems(models.all, introspector)
24
+ reporter.report_has_one_problems(problems)
25
+
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "consistency_fail/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "consistency_fail"
7
+ s.version = ConsistencyFail::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Colin Jones"]
10
+ s.email = ["colin@8thlight.com"]
11
+ s.homepage = "http://github.com/trptcolin/consistency_fail"
12
+ s.summary = %q{A tool to detect missing unique indexes}
13
+ s.description = <<-EOF
14
+ With more than one application server, validates_uniqueness_of becomes a lie.
15
+ Two app servers -> two requests -> two near-simultaneous uniqueness checks ->
16
+ two processes that commit to the database independently, violating this faux
17
+ constraint. You'll need a database-level constraint for cases like these.
18
+
19
+ consistency_fail will find your missing unique indexes, so you can add them and
20
+ stop ignoring the C in ACID.
21
+ EOF
22
+
23
+ s.add_dependency "validation_reflection", "~>0.3.8"
24
+ s.add_development_dependency "activerecord", "~>2.3"
25
+ s.add_development_dependency "rspec"
26
+
27
+ s.files = `git ls-files`.split("\n")
28
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
29
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
30
+ s.require_paths = ["lib"]
31
+ end
@@ -0,0 +1,16 @@
1
+ begin
2
+ require File.join(Dir.pwd, "config", "boot")
3
+ rescue LoadError => e
4
+ puts "\nUh-oh! You must be in the root directory of a Rails project.\n"
5
+ raise
6
+ end
7
+
8
+ require 'active_record'
9
+ require 'validation_reflection'
10
+ require File.join(Dir.pwd, "config", "environment")
11
+
12
+ require 'consistency_fail/models'
13
+ require 'consistency_fail/introspectors/table_data'
14
+ require 'consistency_fail/introspectors/validates_uniqueness_of'
15
+ require 'consistency_fail/introspectors/has_one'
16
+ require 'consistency_fail/reporter'
@@ -0,0 +1,13 @@
1
+ module ConsistencyFail
2
+ class Index
3
+ attr_reader :table_name, :columns
4
+ def initialize(table_name, columns)
5
+ @table_name = table_name
6
+ @columns = columns.map(&:to_s)
7
+ end
8
+
9
+ def ==(other)
10
+ self.table_name == other.table_name && self.columns.sort == other.columns.sort
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ require 'consistency_fail/index'
2
+
3
+ module ConsistencyFail
4
+ module Introspectors
5
+ class HasOne
6
+ def instances(model)
7
+ model.reflect_on_all_associations.select do |a|
8
+ a.macro == :has_one
9
+ end
10
+ end
11
+
12
+ # TODO: handle has_one :through cases (multicolumn index on the join table?)
13
+ def desired_indexes(model)
14
+ instances(model).map do |a|
15
+ ConsistencyFail::Index.new(a.table_name.to_s, [a.primary_key_name]) rescue nil # TODO: why?
16
+ end.compact
17
+ end
18
+ private :desired_indexes
19
+
20
+ def missing_indexes(model)
21
+ existing_indexes = TableData.new.unique_indexes(model)
22
+
23
+ desired_indexes(model).reject do |index|
24
+ existing_indexes.include?(index)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ require 'consistency_fail/index'
2
+
3
+ module ConsistencyFail
4
+ module Introspectors
5
+ class TableData
6
+ def unique_indexes(model)
7
+ return [] if !model.table_exists?
8
+
9
+ ar_indexes = model.connection.indexes(model.table_name).select(&:unique)
10
+ ar_indexes.map do |index|
11
+ ConsistencyFail::Index.new(model.table_name, index.columns)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ require 'consistency_fail/index'
2
+
3
+ module ConsistencyFail
4
+ module Introspectors
5
+ class ValidatesUniquenessOf
6
+ def instances(model)
7
+ model.reflect_on_all_validations.select do |v|
8
+ v.macro == :validates_uniqueness_of
9
+ end
10
+ end
11
+
12
+ def desired_indexes(model)
13
+ instances(model).map do |v|
14
+ scoped_columns = v.options[:scope] || []
15
+ ConsistencyFail::Index.new(model.table_name, [v.name, *scoped_columns])
16
+ end
17
+ end
18
+ private :desired_indexes
19
+
20
+ def missing_indexes(model)
21
+ existing_indexes = TableData.new.unique_indexes(model)
22
+
23
+ desired_indexes(model).reject do |index|
24
+ existing_indexes.include?(index)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ require 'active_record'
2
+ require 'consistency_fail/index'
3
+
4
+ module ConsistencyFail
5
+ class Models
6
+ MODEL_DIRECTORY_REGEXP = /models/
7
+
8
+ attr_reader :load_path
9
+
10
+ def initialize(load_path)
11
+ @load_path = load_path
12
+ end
13
+
14
+ def dirs
15
+ load_path.select { |lp| MODEL_DIRECTORY_REGEXP =~ lp }
16
+ end
17
+
18
+ def preload_all
19
+ self.dirs.each do |d|
20
+ Dir.glob(File.join(d, "**", "*.rb")).each do |model_filename|
21
+ Kernel.require_dependency model_filename
22
+ end
23
+ end
24
+ end
25
+
26
+ def all
27
+ models = []
28
+ ObjectSpace.each_object do |o|
29
+ models << o if o.class == Class &&
30
+ o.ancestors.include?(ActiveRecord::Base) &&
31
+ o != ActiveRecord::Base
32
+ end
33
+ models.sort_by(&:name)
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ require 'consistency_fail/reporters/validates_uniqueness_of'
2
+ require 'consistency_fail/reporters/has_one'
3
+
4
+ module ConsistencyFail
5
+ class Reporter
6
+ def report_validates_uniqueness_problems(indexes_by_model)
7
+ ConsistencyFail::Reporters::ValidatesUniquenessOf.new.report(indexes_by_model)
8
+ end
9
+
10
+ def report_has_one_problems(indexes_by_model)
11
+ ConsistencyFail::Reporters::HasOne.new.report(indexes_by_model)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,81 @@
1
+ module ConsistencyFail
2
+ module Reporters
3
+ class Base
4
+ TERMINAL_WIDTH = 80
5
+
6
+ RED = 31
7
+ GREEN = 32
8
+
9
+ def use_color(code)
10
+ print "\e[#{code}m"
11
+ end
12
+
13
+ def use_default_color
14
+ use_color(0)
15
+ end
16
+
17
+ def report_success(macro)
18
+ use_color(GREEN)
19
+ puts "Hooray! All calls to #{macro} are correctly backed by a unique index."
20
+ use_default_color
21
+ end
22
+
23
+ def divider(pad_to = TERMINAL_WIDTH)
24
+ puts "-" * [pad_to, TERMINAL_WIDTH].max
25
+ end
26
+
27
+ def report_failure_header(macro, longest_model_length)
28
+ puts
29
+ use_color(RED)
30
+ puts "There are calls to #{macro} that aren't backed by unique indexes."
31
+ use_default_color
32
+ divider(longest_model_length * 2)
33
+
34
+ column_1_header, column_2_header = column_headers
35
+ print column_1_header.ljust(longest_model_length + 2)
36
+ puts column_2_header
37
+
38
+ divider(longest_model_length * 2)
39
+ end
40
+
41
+ def report_index(model, index, column_1_length)
42
+ print model.name.ljust(column_1_length + 2)
43
+ puts "#{index.table_name} (#{index.columns.join(", ")})"
44
+ end
45
+
46
+ def column_1(model)
47
+ model.name
48
+ end
49
+
50
+ def column_headers
51
+ ["Model", "Table Columns"]
52
+ end
53
+
54
+ def report(indexes_by_model)
55
+ if indexes_by_model.empty?
56
+ report_success(macro)
57
+ else
58
+ indexes_by_table_name = indexes_by_model.map do |model, indexes|
59
+ [column_1(model), model, indexes]
60
+ end.sort_by(&:first)
61
+ longest_model_length = indexes_by_table_name.map(&:first).
62
+ sort_by(&:length).
63
+ last.
64
+ length
65
+ column_1_header_length = column_headers.first.length
66
+ longest_model_length = [longest_model_length, column_1_header_length].max
67
+
68
+ report_failure_header(macro, longest_model_length)
69
+
70
+ indexes_by_table_name.each do |table_name, model, indexes|
71
+ indexes.each do |index|
72
+ report_index(model, index, longest_model_length)
73
+ end
74
+ end
75
+ divider(longest_model_length * 2)
76
+ end
77
+ puts
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,13 @@
1
+ require 'consistency_fail/reporters/base'
2
+
3
+ module ConsistencyFail
4
+ module Reporters
5
+ class HasOne < Base
6
+ attr_reader :macro
7
+
8
+ def initialize
9
+ @macro = :has_one
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'consistency_fail/reporters/base'
2
+
3
+ module ConsistencyFail
4
+ module Reporters
5
+ class ValidatesUniquenessOf < Base
6
+ attr_reader :macro
7
+
8
+ def initialize
9
+ @macro = :validates_uniqueness_of
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module ConsistencyFail
2
+ VERSION = "0.1.0"
3
+ end
File without changes
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'consistency_fail/index'
3
+
4
+ describe ConsistencyFail::Index do
5
+
6
+ describe "value objectiness" do
7
+ it "holds onto table name and columns" do
8
+ index = ConsistencyFail::Index.new("addresses", ["city", "state"])
9
+ index.table_name.should == "addresses"
10
+ index.columns.should == ["city", "state"]
11
+ end
12
+
13
+ it "leaves columns in the initial order (since we only care about presence, not performance)" do
14
+ index = ConsistencyFail::Index.new("addresses", ["state", "city"])
15
+ index.table_name.should == "addresses"
16
+ index.columns.should == ["state", "city"]
17
+ end
18
+ end
19
+
20
+ describe "equality test" do
21
+ it "passes when everything matches" do
22
+ ConsistencyFail::Index.new("addresses", ["city", "state"]).should ==
23
+ ConsistencyFail::Index.new("addresses", ["city", "state"])
24
+ end
25
+
26
+ it "fails when tables are different" do
27
+ ConsistencyFail::Index.new("locations", ["city", "state"]).should_not ==
28
+ ConsistencyFail::Index.new("addresses", ["city", "state"])
29
+ end
30
+
31
+ it "fails when columns are different" do
32
+ ConsistencyFail::Index.new("addresses", ["city", "state"]).should_not ==
33
+ ConsistencyFail::Index.new("addresses", ["state", "zip"])
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+ require 'consistency_fail/introspectors/table_data'
3
+ require 'consistency_fail/introspectors/has_one'
4
+
5
+ describe ConsistencyFail::Introspectors::HasOne do
6
+ def introspector(model)
7
+ ConsistencyFail::Introspectors::HasOne.new(model)
8
+ end
9
+
10
+ describe "instances of has_one" do
11
+ it "finds none" do
12
+ model = fake_ar_model("User")
13
+ model.stub(:reflect_on_all_associations).and_return([])
14
+
15
+ subject.instances(model).should == []
16
+ end
17
+
18
+ it "finds one" do
19
+ model = fake_ar_model("User")
20
+ association = double("association", :macro => :has_one)
21
+ model.stub!(:reflect_on_all_associations).and_return([association])
22
+
23
+ subject.instances(model).should == [association]
24
+ end
25
+
26
+ it "finds other associations, but not has_one" do
27
+ model = fake_ar_model("User")
28
+ validation = double("validation", :macro => :has_many)
29
+ model.stub!(:reflect_on_all_associations).and_return([validation])
30
+
31
+ subject.instances(model).should == []
32
+ end
33
+ end
34
+
35
+ describe "finding missing indexes" do
36
+ before do
37
+ @association = double("association", :macro => :has_one)
38
+ @model = fake_ar_model("User", :table_exists? => true,
39
+ :table_name => "users",
40
+ :reflect_on_all_associations => [@association])
41
+ end
42
+
43
+ it "finds one" do
44
+ @association.stub!(:table_name => :addresses, :primary_key_name => "user_id")
45
+ @model.stub_chain(:connection, :indexes).with("users").and_return([])
46
+
47
+ indexes = subject.missing_indexes(@model)
48
+ indexes.should == [ConsistencyFail::Index.new("addresses", ["user_id"])]
49
+ end
50
+
51
+ it "finds none when they're already in place" do
52
+ @association.stub!(:table_name => :addresses, :primary_key_name => "user_id")
53
+ index = ConsistencyFail::Index.new("addresses", ["user_id"])
54
+ ConsistencyFail::Introspectors::TableData.stub_chain(:new, :unique_indexes).
55
+ and_return([index])
56
+
57
+ subject.missing_indexes(@model).should == []
58
+ end
59
+
60
+ end
61
+ end
62
+
63
+
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+ require 'consistency_fail/introspectors/table_data'
3
+
4
+ describe ConsistencyFail::Introspectors::TableData do
5
+ describe "finding unique indexes" do
6
+ it "finds none when the table does not exist" do
7
+ model = fake_ar_model("User", :table_exists? => false)
8
+
9
+ subject.unique_indexes(model).should == []
10
+ end
11
+
12
+ it "gets one" do
13
+ model = fake_ar_model("User", :table_exists? => true,
14
+ :table_name => "users")
15
+
16
+ model.stub_chain(:connection, :indexes).
17
+ with("users").
18
+ and_return([fake_index_on(["a"], :unique => true)])
19
+
20
+ indexes = subject.unique_indexes(model)
21
+ indexes.should == [ConsistencyFail::Index.new("users", ["a"])]
22
+ end
23
+
24
+ it "doesn't get non-unique indexes" do
25
+ model = fake_ar_model("User", :table_exists? => true,
26
+ :table_name => "users")
27
+
28
+ model.stub_chain(:connection, :indexes).
29
+ with("users").
30
+ and_return([fake_index_on(["a"], :unique => false)])
31
+
32
+ subject.unique_indexes(model).should == []
33
+ end
34
+
35
+ it "gets multiple unique indexes" do
36
+ model = fake_ar_model("User", :table_exists? => true,
37
+ :table_name => "users")
38
+
39
+ model.stub_chain(:connection, :indexes).
40
+ with("users").
41
+ and_return([fake_index_on(["a"], :unique => true),
42
+ fake_index_on(["b", "c"], :unique => true)])
43
+
44
+ indexes = subject.unique_indexes(model)
45
+ indexes.size.should == 2
46
+ indexes.should == [ConsistencyFail::Index.new("users", ["a"]),
47
+ ConsistencyFail::Index.new("users", ["b", "c"])]
48
+ end
49
+ end
50
+
51
+ end
52
+
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+ require 'consistency_fail/introspectors/validates_uniqueness_of'
3
+
4
+ describe ConsistencyFail::Introspectors::ValidatesUniquenessOf do
5
+ def introspector(model)
6
+ ConsistencyFail::Introspectors::ValidatesUniquenessOf.new(model)
7
+ end
8
+
9
+ describe "instances of validates_uniqueness_of" do
10
+ it "finds none" do
11
+ model = fake_ar_model("User")
12
+ model.stub!(:reflect_on_all_validations).and_return([])
13
+
14
+ subject.instances(model).should == []
15
+ end
16
+
17
+ it "finds one" do
18
+ model = fake_ar_model("User")
19
+ validation = double("validation", :macro => :validates_uniqueness_of)
20
+ model.stub!(:reflect_on_all_validations).and_return([validation])
21
+
22
+ subject.instances(model).should == [validation]
23
+ end
24
+
25
+ it "finds other validations, but not uniqueness" do
26
+ model = fake_ar_model("User")
27
+ validation = double("validation", :macro => :validates_format_of)
28
+ model.stub!(:reflect_on_all_validations).and_return([validation])
29
+
30
+ subject.instances(model).should == []
31
+ end
32
+ end
33
+
34
+ describe "finding missing indexes" do
35
+ before do
36
+ @validation = double("validation", :macro => :validates_uniqueness_of)
37
+ @model = fake_ar_model("User", :table_exists? => true,
38
+ :table_name => "users",
39
+ :reflect_on_all_validations => [@validation])
40
+ end
41
+
42
+ it "finds one" do
43
+ @validation.stub!(:name => :email, :options => {})
44
+ @model.stub_chain(:connection, :indexes).with("users").and_return([])
45
+
46
+ indexes = subject.missing_indexes(@model)
47
+ indexes.should == [ConsistencyFail::Index.new("users", ["email"])]
48
+ end
49
+
50
+ it "finds one where the validation has scoped columns" do
51
+ @validation.stub!(:name => :city, :options => {:scope => [:email, :state]})
52
+ @model.stub_chain(:connection, :indexes).with("users").and_return([])
53
+
54
+ indexes = subject.missing_indexes(@model)
55
+ indexes.should == [ConsistencyFail::Index.new("users", ["city", "email", "state"])]
56
+ end
57
+
58
+ it "sorts the scoped columns" do
59
+ @validation.stub!(:name => :email, :options => {:scope => [:city, :state]})
60
+ @model.stub_chain(:connection, :indexes).with("users").and_return([])
61
+
62
+ indexes = subject.missing_indexes(@model)
63
+ indexes.should == [ConsistencyFail::Index.new("users", ["email", "city", "state"])]
64
+ end
65
+
66
+ it "finds none when they're already in place" do
67
+ @validation.stub!(:name => :email, :options => {})
68
+ index = fake_index_on(["email"], :unique => true)
69
+ @model.stub_chain(:connection, :indexes).with("users").
70
+ and_return([index])
71
+
72
+ subject.missing_indexes(@model).should == []
73
+ end
74
+
75
+ it "finds none when indexes are there but in a different order" do
76
+ @validation.stub!(:name => :email, :options => {:scope => [:city, :state]})
77
+ index = fake_index_on(["state", "email", "city"], :unique => true)
78
+ @model.stub_chain(:connection, :indexes).with("users").
79
+ and_return([index])
80
+
81
+ subject.missing_indexes(@model).should == []
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+ require 'consistency_fail/models'
3
+
4
+ describe ConsistencyFail::Models do
5
+ def models(load_path)
6
+ ConsistencyFail::Models.new(load_path)
7
+ end
8
+
9
+ it "gets the load path" do
10
+ models([:a, :b, :c]).load_path.should == [:a, :b, :c]
11
+ end
12
+
13
+ it "gets the directories matching /models/" do
14
+ models = models(["foo/bar/baz", "app/models", "some/other/models"])
15
+ models.dirs.should == ["app/models", "some/other/models"]
16
+ end
17
+
18
+ it "preloads models by calling require_dependency" do
19
+ models = models(["foo/bar/baz", "app/models", "some/other/models"])
20
+ Dir.stub(:glob).
21
+ with(File.join("app/models", "**", "*.rb")).
22
+ and_return(["app/models/user.rb", "app/models/address.rb"])
23
+ Dir.stub(:glob).
24
+ with(File.join("some/other/models", "**", "*.rb")).
25
+ and_return(["some/other/models/foo.rb"])
26
+
27
+ Kernel.should_receive(:require_dependency).with("app/models/user.rb")
28
+ Kernel.should_receive(:require_dependency).with("app/models/address.rb")
29
+ Kernel.should_receive(:require_dependency).with("some/other/models/foo.rb")
30
+
31
+ models.preload_all
32
+ end
33
+
34
+ it "gets all models" do
35
+ model_a = double("model_a",
36
+ :ancestors => [ActiveRecord::Base],
37
+ :class => Class,
38
+ :name => "ModelA")
39
+ model_b = double("model_b",
40
+ :ancestors => [ActiveRecord::Base],
41
+ :class => Class,
42
+ :name => "ModelB")
43
+ model_c = double("model_c",
44
+ :ancestors => [ActiveRecord::Base],
45
+ :class => ActiveRecord::Base,
46
+ :name => "ModelC")
47
+
48
+ ObjectSpace.stub(:each_object).
49
+ and_yield(model_c).
50
+ and_yield(model_b).
51
+ and_yield(model_a)
52
+
53
+ models([]).all.should == [model_a, model_b]
54
+ end
55
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+ require 'consistency_fail/reporter'
3
+ require 'consistency_fail/index'
4
+
5
+ describe ConsistencyFail::Reporter do
6
+ before(:each) do
7
+ @real_out = $stdout
8
+ @fake_out = StringIO.new
9
+ $stdout = @fake_out
10
+ end
11
+ after(:each) do
12
+ $stdout = @real_out
13
+ end
14
+
15
+ context "validates_uniqueness_of" do
16
+ it "says everything's good" do
17
+ subject.report_validates_uniqueness_problems([])
18
+
19
+ @fake_out.string.should =~ /Hooray!/
20
+ end
21
+
22
+ it "shows a missing single-column index on a single model" do
23
+ missing_indexes = [ConsistencyFail::Index.new("users", ["email"])]
24
+
25
+ subject.report_validates_uniqueness_problems(fake_ar_model("User", :table_name => "users") => missing_indexes)
26
+
27
+ @fake_out.string.should =~ /users\s+\(email\)/
28
+ end
29
+
30
+ it "shows a missing multiple-column index on a single model" do
31
+ missing_indexes = [ConsistencyFail::Index.new("addresses", ["number", "street", "zip"])]
32
+
33
+ subject.report_validates_uniqueness_problems(fake_ar_model("Address", :table_name => "addresses") => missing_indexes)
34
+
35
+ @fake_out.string.should =~ /addresses\s+\(number, street, zip\)/
36
+ end
37
+
38
+ context "with problems on multiple models" do
39
+ before(:each) do
40
+ subject.report_validates_uniqueness_problems(
41
+ fake_ar_model("User", :table_name => "users") =>
42
+ [ConsistencyFail::Index.new("users", ["email"])],
43
+ fake_ar_model("Citizen", :table_name => "citizens") =>
44
+ [ConsistencyFail::Index.new("citizens", ["ssn"])]
45
+ )
46
+ end
47
+
48
+ it "shows all problems" do
49
+ @fake_out.string.should =~ /users\s+\(email\)/m
50
+ @fake_out.string.should =~ /citizens\s+\(ssn\)/m
51
+ end
52
+
53
+ it "orders the models alphabetically" do
54
+ @fake_out.string.should =~ /citizens\s+\(ssn\).*users\s+\(email\)/m
55
+ end
56
+ end
57
+ end
58
+
59
+ context "has_one" do
60
+ it "says everything's good" do
61
+ subject.report_has_one_problems([])
62
+
63
+ @fake_out.string.should =~ /Hooray!/
64
+ end
65
+
66
+ it "shows a missing single-column index on a single model" do
67
+ missing_indexes = [ConsistencyFail::Index.new("users", ["email"])]
68
+
69
+ subject.report_has_one_problems(fake_ar_model("Friend", :table_name => "users") => missing_indexes)
70
+
71
+ @fake_out.string.should =~ /Friend\s+users\s+\(email\)/m
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,13 @@
1
+ $:<< File.expand_path('..', __FILE__)
2
+ $:<< File.expand_path('../../lib', __FILE__)
3
+
4
+ require 'rubygems'
5
+
6
+ def fake_ar_model(name, options = {})
7
+ double("AR model: #{name}", options.merge(:name => name))
8
+ end
9
+
10
+ def fake_index_on(columns, options = {})
11
+ double("index on #{columns.inspect}", options.merge(:columns => columns))
12
+ end
13
+
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: consistency_fail
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Colin Jones
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-06-10 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: validation_reflection
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ - 3
33
+ - 8
34
+ version: 0.3.8
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: activerecord
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 5
46
+ segments:
47
+ - 2
48
+ - 3
49
+ version: "2.3"
50
+ type: :development
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: rspec
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
+ description: |
67
+ With more than one application server, validates_uniqueness_of becomes a lie.
68
+ Two app servers -> two requests -> two near-simultaneous uniqueness checks ->
69
+ two processes that commit to the database independently, violating this faux
70
+ constraint. You'll need a database-level constraint for cases like these.
71
+
72
+ consistency_fail will find your missing unique indexes, so you can add them and
73
+ stop ignoring the C in ACID.
74
+
75
+ email:
76
+ - colin@8thlight.com
77
+ executables:
78
+ - consistency_fail
79
+ extensions: []
80
+
81
+ extra_rdoc_files: []
82
+
83
+ files:
84
+ - .gitignore
85
+ - Gemfile
86
+ - LICENSE
87
+ - README.md
88
+ - Rakefile
89
+ - bin/consistency_fail
90
+ - consistency_fail.gemspec
91
+ - lib/consistency_fail.rb
92
+ - lib/consistency_fail/index.rb
93
+ - lib/consistency_fail/introspectors/has_one.rb
94
+ - lib/consistency_fail/introspectors/table_data.rb
95
+ - lib/consistency_fail/introspectors/validates_uniqueness_of.rb
96
+ - lib/consistency_fail/models.rb
97
+ - lib/consistency_fail/reporter.rb
98
+ - lib/consistency_fail/reporters/base.rb
99
+ - lib/consistency_fail/reporters/has_one.rb
100
+ - lib/consistency_fail/reporters/validates_uniqueness_of.rb
101
+ - lib/consistency_fail/version.rb
102
+ - spec/consistency_fail_spec.rb
103
+ - spec/index_spec.rb
104
+ - spec/introspectors/has_one_spec.rb
105
+ - spec/introspectors/table_data_spec.rb
106
+ - spec/introspectors/validates_uniqueness_of_spec.rb
107
+ - spec/models_spec.rb
108
+ - spec/reporter_spec.rb
109
+ - spec/spec_helper.rb
110
+ has_rdoc: true
111
+ homepage: http://github.com/trptcolin/consistency_fail
112
+ licenses: []
113
+
114
+ post_install_message:
115
+ rdoc_options: []
116
+
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ hash: 3
125
+ segments:
126
+ - 0
127
+ version: "0"
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ hash: 3
134
+ segments:
135
+ - 0
136
+ version: "0"
137
+ requirements: []
138
+
139
+ rubyforge_project:
140
+ rubygems_version: 1.5.2
141
+ signing_key:
142
+ specification_version: 3
143
+ summary: A tool to detect missing unique indexes
144
+ test_files: []
145
+