consistency_fail 0.1.0

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,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
+