table_warnings 0.0.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  module TableWarnings
2
- VERSION = "0.0.7"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,53 +1,163 @@
1
+ require 'thread'
2
+
1
3
  require 'active_support'
2
4
  require 'active_support/version'
3
- require 'active_support/core_ext' if ::ActiveSupport::VERSION::MAJOR >= 3
5
+ require 'active_support/core_ext' if ActiveSupport::VERSION::MAJOR >= 3
4
6
  require 'active_record'
5
7
 
6
- module TableWarnings
7
- autoload :Config, 'table_warnings/config'
8
- autoload :Warning, 'table_warnings/warning'
8
+ require 'table_warnings/registry'
9
+ require 'table_warnings/exclusive'
10
+ require 'table_warnings/blank'
11
+ require 'table_warnings/size'
12
+ require 'table_warnings/arbitrary'
13
+ require 'table_warnings/null'
14
+ require 'table_warnings/column'
15
+ require 'table_warnings/scout'
16
+ require 'table_warnings/nonexistent_owner'
17
+ require 'table_warnings/range'
9
18
 
10
- def self.config #:nodoc: all
11
- Config.instance
19
+ module TableWarnings
20
+ def TableWarnings.registry
21
+ @registry || Thread.exclusive do
22
+ @registry ||= Registry.new
23
+ end
12
24
  end
13
25
 
14
- # Get current warnings on the table.
26
+ # used to resolve columns to warnings
27
+ Disposition = Struct.new(:exclusives, :covers, :matches)
28
+
29
+ # Get current warning messages on the table.
15
30
  def table_warnings
16
- ::TableWarnings.config.warnings[self].map { |w| w.messages }.flatten.compact.sort
31
+ messages = []
32
+
33
+ TableWarnings.registry.nonexclusive(self).each do |warning|
34
+ messages << warning.messages
35
+ end
36
+
37
+ pool = column_names.map do |column_name|
38
+ TableWarnings::Column.new self, column_name
39
+ end
40
+ exclusive = TableWarnings.registry.exclusive(self)
41
+
42
+ assignments = {}
43
+ # pass 1 - exclusives and covers
44
+ exclusive.each do |warning|
45
+ disposition = Disposition.new
46
+ disposition.exclusives = warning.exclusives pool
47
+ disposition.covers = warning.covers pool
48
+ assignments[warning] = disposition
49
+ pool -= disposition.exclusives
50
+ end
51
+ if ENV['TABLE_WARNINGS_DEBUG'] == 'true'
52
+ $stderr.puts "pass 1"
53
+ assignments.each do |warning, disposition|
54
+ $stderr.puts " #{warning.scout.matcher} - exclusives=#{disposition.exclusives.map(&:name)} covers=#{disposition.covers.map(&:name)}"
55
+ end
56
+ end
57
+ # pass 2 - allow regexp matching, but only if somebody else didn't cover it
58
+ exclusive.each do |warning|
59
+ disposition = assignments[warning]
60
+ disposition.matches = warning.matches(pool).select do |match|
61
+ assignments.except(warning).none? { |_, other| other.covers.include?(match) }
62
+ end
63
+ pool -= disposition.matches
64
+ end
65
+ if ENV['TABLE_WARNINGS_DEBUG'] == 'true'
66
+ $stderr.puts "pass 2"
67
+ assignments.each do |warning, disposition|
68
+ $stderr.puts " #{warning.scout.matcher} - exclusives=#{disposition.exclusives.map(&:name)} covers=#{disposition.covers.map(&:name)} matches=#{disposition.matches.map(&:name)}"
69
+ end
70
+ end
71
+ if ENV['TABLE_WARNINGS_STRICT'] == 'true'
72
+ $stderr.puts "uncovered columns"
73
+ $stderr.puts pool.join("\n")
74
+ end
75
+
76
+ # now you can generate messages
77
+ assignments.each do |warning, disposition|
78
+ messages << warning.messages(disposition.exclusives+disposition.matches)
79
+ end
80
+
81
+ messages.flatten.compact
82
+ end
83
+
84
+ def warn_unless_range(*args)
85
+ options = args.extract_options!
86
+ args.flatten.each do |matcher|
87
+ TableWarnings.registry.add_warning self, TableWarnings::Range.new(self, matcher, options)
88
+ end
17
89
  end
18
90
 
19
91
  # Warn if there are blanks in a certain column.
20
92
  #
21
93
  # Blank includes both NULL and "" (empty string)
22
- def warn_if_blanks_in(column_name)
23
- warning = ::TableWarnings::Warning::Blank.new :table => self, :column_name => column_name
24
- ::TableWarnings.config.warnings[self].add warning
94
+ def warn_if_blanks(*args)
95
+ options = args.extract_options!
96
+ args.flatten.each do |matcher|
97
+ TableWarnings.registry.add_warning self, TableWarnings::Blank.new(self, matcher, options)
98
+ end
25
99
  end
26
100
 
101
+ # Warn if there are NULLs in a certain column.
102
+ def warn_if_nulls(*args)
103
+ options = args.extract_options!
104
+ args.flatten.each do |matcher|
105
+ TableWarnings.registry.add_warning self, TableWarnings::Null.new(self, matcher, options)
106
+ end
107
+ end
108
+
27
109
  # Warn if there are blanks in ANY column.
28
- #
29
- # Blank includes both NULL and "" (empty string)
30
110
  def warn_if_any_blanks
31
- warning = ::TableWarnings::Warning::Blank.new :table => self
32
- ::TableWarnings.config.warnings[self].add warning
111
+ TableWarnings.registry.add_warning self, TableWarnings::Blank.new(self, /.*/)
112
+ end
113
+
114
+ # Warn if there are nulls in ANY column.
115
+ def warn_if_any_nulls
116
+ TableWarnings.registry.add_warning self, TableWarnings::Null.new(self, /.*/)
33
117
  end
34
118
 
35
- # Warn if the number of records falls out of an (approximate) range.
119
+ # Warn if the number of records falls out of an (approximate) size.
36
120
  #
37
121
  # Approximations: :few, :tens, :dozens, :hundreds, :thousands, :hundreds_of_thousands, :millions
38
122
  # Exact: pass a Range or a Numeric
39
- def warn_unless_size_is(approximate_size)
40
- warning = ::TableWarnings::Warning::Size.new :table => self, :approximate_size => approximate_size
41
- ::TableWarnings.config.warnings[self].add warning
123
+ def warn_unless_size(approximate_size, options = {})
124
+ TableWarnings.registry.add_warning self, TableWarnings::Size.new(self, approximate_size, options)
42
125
  end
43
126
 
44
127
  # An arbitrary warning.
45
- def warn(&blk)
46
- warning = ::TableWarnings::Warning::Arbitrary.new :table => self, :blk => blk
47
- ::TableWarnings.config.warnings[self].add warning
128
+ def warn_if(&blk)
129
+ TableWarnings.registry.add_warning self, TableWarnings::Arbitrary.new(self, blk)
130
+ end
131
+
132
+ def warn_if_nulls_except(*args)
133
+ options = args.extract_options!
134
+ args.flatten.each do |matcher|
135
+ TableWarnings.registry.add_warning self, TableWarnings::Null.new(self, matcher, options.merge(:negative => true))
136
+ end
137
+ end
138
+
139
+ def warn_if_blanks_except(*args)
140
+ options = args.extract_options!
141
+ args.flatten.each do |matcher|
142
+ TableWarnings.registry.add_warning self, TableWarnings::Blank.new(self, matcher, options.merge(:negative => true))
143
+ end
144
+ end
145
+
146
+ def warn_if_nonexistent_owner(*args)
147
+ options = args.extract_options!
148
+ args.flatten.each do |belongs_to_association_name|
149
+ TableWarnings.registry.add_warning self, TableWarnings::NonexistentOwner.new(self, belongs_to_association_name, options)
150
+ end
151
+ end
152
+
153
+ def warn_if_nonexistent_owner_except(*args)
154
+ options = args.extract_options!
155
+ args.flatten.each do |belongs_to_association_name|
156
+ TableWarnings.registry.add_warning self, TableWarnings::NonexistentOwner.new(self, belongs_to_association_name, options.merge(:negative => true))
157
+ end
48
158
  end
49
159
  end
50
160
 
51
- unless ::ActiveRecord::Base.method_defined? :table_warnings
52
- ::ActiveRecord::Base.extend ::TableWarnings
161
+ unless ActiveRecord::Base.method_defined? :table_warnings
162
+ ActiveRecord::Base.extend TableWarnings
53
163
  end
@@ -1,11 +1,9 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path('../lib', __FILE__)
3
- require 'table_warnings/version'
2
+ require File.expand_path('../lib/table_warnings/version', __FILE__)
4
3
 
5
4
  Gem::Specification.new do |s|
6
5
  s.name = 'table_warnings'
7
6
  s.version = TableWarnings::VERSION
8
- s.platform = Gem::Platform::RUBY
9
7
  s.authors = ['Seamus Abshere']
10
8
  s.email = ['seamus@abshere.net']
11
9
  s.homepage = 'https://github.com/seamusabshere/table_warnings'
@@ -23,7 +21,10 @@ Gem::Specification.new do |s|
23
21
  s.add_runtime_dependency 'activesupport'
24
22
 
25
23
  s.add_development_dependency 'fastercsv'
26
- s.add_development_dependency 'rake'
27
- s.add_development_dependency 'mini_record-compat'
24
+ s.add_development_dependency 'active_record_inline_schema'
28
25
  s.add_development_dependency 'sqlite3'
26
+ s.add_development_dependency 'minitest'
27
+ s.add_development_dependency 'minitest-reporters'
28
+ s.add_development_dependency 'yard'
29
+ # s.add_development_dependency 'debugger'
29
30
  end
data/test/helper.rb CHANGED
@@ -1,25 +1,103 @@
1
1
  require 'rubygems'
2
- require 'bundler'
3
- Bundler.setup
4
- require 'test/unit'
5
- require 'active_support/all'
6
- require 'active_record'
7
- require 'mini_record'
8
- # thanks authlogic!
9
- ActiveRecord::Schema.verbose = false
10
- begin
11
- ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
12
- rescue ArgumentError
13
- ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
2
+ require 'bundler/setup'
3
+
4
+ if ::Bundler.definition.specs['debugger'].first
5
+ require 'debugger'
6
+ elsif ::Bundler.definition.specs['ruby-debug'].first
7
+ require 'ruby-debug'
14
8
  end
15
9
 
10
+ require 'minitest/spec'
11
+ require 'minitest/autorun'
12
+ require 'minitest/reporters'
13
+ MiniTest::Unit.runner = MiniTest::SuiteRunner.new
14
+ MiniTest::Unit.runner.reporters << MiniTest::Reporters::SpecReporter.new
15
+
16
+ require 'active_record'
17
+ require 'active_record_inline_schema'
18
+
19
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
20
+
16
21
  # require 'logger'
17
22
  # logger = Logger.new $stdout
18
23
  # logger.level = Logger::DEBUG
19
24
  # ActiveRecord::Base.logger = logger
20
25
 
21
- $LOAD_PATH.unshift(File.dirname(__FILE__))
22
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
23
- require 'table_warnings'
24
- class Test::Unit::TestCase
26
+ class ActiveRecord::Base
27
+ class << self
28
+ # ignores protected attrs
29
+ def force_create!(attrs = {})
30
+ record = new
31
+ attrs.each do |k, v|
32
+ record.send "#{k}=", v
33
+ end
34
+ record.save!
35
+ record
36
+ end
37
+ end
25
38
  end
39
+
40
+ class MiniTest::Spec
41
+ # start transaction
42
+ before do
43
+ # activerecord-3.2.3/lib/active_record/fixtures.rb
44
+ @fixture_connections = ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection)
45
+ @fixture_connections.each do |connection|
46
+ connection.increment_open_transactions
47
+ connection.transaction_joinable = false
48
+ connection.begin_db_transaction
49
+ end
50
+ end
51
+
52
+ # rollback
53
+ after do
54
+ @fixture_connections.each do |connection|
55
+ if connection.open_transactions != 0
56
+ connection.rollback_db_transaction
57
+ connection.decrement_open_transactions
58
+ end
59
+ end
60
+ @fixture_connections.clear
61
+ ActiveRecord::Base.clear_active_connections!
62
+ end
63
+
64
+ def assert_warning(model, expected_warning)
65
+ hits = model.table_warnings.select { |warning| warning =~ expected_warning }
66
+ refute hits.none?, "#{model.name} unexpectedly lacked warning #{expected_warning.inspect}"
67
+ refute hits.many?, "#{model.name} had MULTIPLE warnings like #{expected_warning.inspect}: #{hits.inspect}"
68
+ end
69
+
70
+ def assert_no_warning(model, specific_unexpected_warning = nil)
71
+ warnings = model.table_warnings
72
+ if specific_unexpected_warning
73
+ refute(warnings.any? { |warning| warning =~ specific_unexpected_warning }, "#{model.name} unexpectedly had warning #{specific_unexpected_warning.inspect}")
74
+ else
75
+ refute warnings.any?, "#{model.name} unexpectedly had some warnings (#{warnings.inspect})"
76
+ end
77
+ end
78
+
79
+ def assert_causes_warning(model, expected_warnings)
80
+ expected_warnings = [expected_warnings].flatten
81
+ expected_warnings.each do |expected_warning|
82
+ assert_no_warning model, expected_warning
83
+ end
84
+ warnings_before = model.table_warnings
85
+ yield
86
+ expected_warnings.each do |expected_warning|
87
+ assert_warning model, expected_warning
88
+ end
89
+ unexpected_warnings = (model.table_warnings - warnings_before).reject do |warning|
90
+ expected_warnings.any? { |expected_warning| warning =~ expected_warning }
91
+ end
92
+ refute unexpected_warnings.any?, "#{model.name} unexpectedly ALSO got warnings #{unexpected_warnings.inspect}"
93
+ end
94
+
95
+ def assert_does_not_cause_warning(model)
96
+ assert_no_warning model
97
+ yield
98
+ assert_no_warning model
99
+ end
100
+
101
+ end
102
+
103
+ require 'table_warnings'
@@ -0,0 +1,85 @@
1
+ require 'helper'
2
+
3
+ class PetAlpha < ActiveRecord::Base
4
+ col :birthday, :type => :datetime
5
+ col :gender
6
+ col :sire
7
+ warn_if_nulls :birthday
8
+ warn_if_nulls_except :gender
9
+ end
10
+ PetAlpha.auto_upgrade!
11
+
12
+ class PetBeta < ActiveRecord::Base
13
+ col :birthday, :type => :datetime
14
+ col :gender
15
+ col :sire
16
+ warn_if_nulls :birthday
17
+ warn_if_nulls_except /ende/
18
+ end
19
+ PetBeta.auto_upgrade!
20
+
21
+ class PetGamma < ActiveRecord::Base
22
+ col :birthday, :type => :datetime
23
+ col :gender
24
+ col :sire
25
+ col :certified, :type => :boolean
26
+ warn_if_nulls /irthd/, :conditions => { :certified => true }
27
+ warn_if_nulls_except /ende/
28
+ end
29
+ PetGamma.auto_upgrade!
30
+
31
+ class PetDelta < ActiveRecord::Base
32
+ col :birthday, :type => :datetime
33
+ col :gender
34
+ col :sire
35
+ col :certified, :type => :boolean
36
+ warn_if_nulls /irthd/, :conditions => { :certified => true }
37
+ warn_if_nulls_except /ende/, :conditions => { :certified => true }
38
+ end
39
+ PetDelta.auto_upgrade!
40
+
41
+ describe TableWarnings do
42
+ describe "combinations of positive ('in') and negative ('except') rules" do
43
+ it "combines a positive column and a negative column" do
44
+ assert_causes_warning PetAlpha, [/null.*birthday/i, /null.*sire/i] do
45
+ PetAlpha.force_create!
46
+ end
47
+ end
48
+ it "combines a positive column and a negative regexp" do
49
+ assert_causes_warning PetBeta, [/null.*birthday/i, /null.*sire/i] do
50
+ PetBeta.force_create!
51
+ end
52
+ end
53
+ it "combines a positive regexp with conditions and a negative regexp" do
54
+ assert_causes_warning PetGamma, [/null.*sire/i, /null.*certified/i] do
55
+ PetGamma.force_create!
56
+ end
57
+ PetGamma.delete_all # !
58
+ assert_causes_warning PetGamma, [/null.*sire/i, /null.*birthday/i] do
59
+ PetGamma.force_create! :certified => true
60
+ end
61
+ end
62
+ it "combines a positive regexp with conditions and a negative regexp with conditions" do
63
+ assert_does_not_cause_warning PetDelta do
64
+ PetDelta.force_create!
65
+ end
66
+ PetDelta.delete_all # !
67
+ assert_causes_warning PetDelta, [/null.*sire/i, /null.*birthday/i] do
68
+ PetDelta.force_create! :certified => true
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ =begin
75
+ warn_if_nulls_except(
76
+ :alt_fuel_code,
77
+ :carline_mfr_code,
78
+ :vi_mfr_code,
79
+ :carline_code,
80
+ :carline_class_code,
81
+ :carline_class_name,
82
+ )
83
+ warn_if_nulls /alt_fuel_efficiency/, :conditions => 'alt_fuel_code IS NOT NULL'
84
+ warn_if_nulls :carline_class, :conditions => 'year < 1998'
85
+ =end
@@ -0,0 +1,60 @@
1
+ require 'helper'
2
+
3
+ class Person < ActiveRecord::Base
4
+ col :llc_name
5
+ end
6
+ Person.auto_upgrade!
7
+
8
+ class PetRed < ActiveRecord::Base
9
+ col :handler_id, :type => :integer
10
+ belongs_to :handler, :class_name => 'Person'
11
+ warn_if_nonexistent_owner :handler
12
+ end
13
+ PetRed.auto_upgrade!
14
+
15
+ class PetBlue < ActiveRecord::Base
16
+ col :trainer_id
17
+ belongs_to :trainer, :class_name => 'Person', :primary_key => :llc_name
18
+ warn_if_nonexistent_owner :trainer
19
+ end
20
+ PetBlue.auto_upgrade!
21
+
22
+ class PetGreen < ActiveRecord::Base
23
+ col :trainer_id
24
+ belongs_to :trainer, :class_name => 'Person', :primary_key => :llc_name
25
+ warn_if_nonexistent_owner :trainer, :allow_null => true
26
+ end
27
+ PetGreen.auto_upgrade!
28
+
29
+ describe TableWarnings do
30
+ describe :warn_if_nonexistent_owner do
31
+ before do
32
+ Person.force_create! :llc_name => 'My Small Business, LLC'
33
+ end
34
+ it "takes a single belongs-to association name" do
35
+ assert_causes_warning PetRed, /nonexistent.*handler/i do
36
+ PetRed.force_create!
37
+ end
38
+ end
39
+ it "checks the value of foreign keys not just their presence" do
40
+ assert_causes_warning PetRed, /nonexistent.*handler/i do
41
+ PetRed.force_create! :handler_id => 999999
42
+ end
43
+ end
44
+ it "doesn't raise false warnings" do
45
+ assert_does_not_cause_warning PetRed do
46
+ PetRed.force_create! :handler_id => Person.first.id
47
+ end
48
+ end
49
+ it "regards nulls as nonexistent even if the association primary key column contains nulls" do
50
+ assert_causes_warning PetBlue, /nonexistent.*trainer/i do
51
+ PetBlue.force_create!
52
+ end
53
+ end
54
+ it "allows nulls if explicitly requested" do
55
+ assert_does_not_cause_warning PetGreen do
56
+ PetGreen.force_create!
57
+ end
58
+ end
59
+ end
60
+ end