chrono_model 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ # CREATE ROLE chronomodel LOGIN ENCRYPTED PASSWORD 'chronomodel' CREATEDB;
2
+ #
3
+ hostname: localhost
4
+ username: chronomodel
5
+ password: chronomodel
6
+ database: chronomodel
7
+ #
@@ -0,0 +1,22 @@
1
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
2
+ #
3
+ require 'chrono_model'
4
+
5
+ require 'support/connection'
6
+ require 'support/matchers/schema'
7
+ require 'support/matchers/table'
8
+ require 'support/matchers/column'
9
+ require 'support/matchers/index'
10
+
11
+ RSpec.configure do |config|
12
+ config.treat_symbols_as_metadata_keys_with_true_values = true
13
+
14
+ config.include(ChronoTest::Matchers::Schema)
15
+ config.include(ChronoTest::Matchers::Table)
16
+ config.include(ChronoTest::Matchers::Column)
17
+ config.include(ChronoTest::Matchers::Index)
18
+
19
+ config.before :suite do
20
+ ChronoTest.recreate_database!
21
+ end
22
+ end
@@ -0,0 +1,74 @@
1
+ require 'pathname'
2
+ require 'active_record'
3
+ require 'active_support/core_ext/logger'
4
+
5
+ module ChronoTest
6
+ extend self
7
+
8
+ AR = ActiveRecord::Base
9
+ log = ENV['VERBOSE'].present? ? $stderr : 'spec/debug.log'.tap{|f| File.truncate(f, 0)}
10
+ AR.logger = ::Logger.new(log).tap do |l|
11
+ l.level = 0
12
+ end
13
+
14
+ def connect!(spec = self.config)
15
+ unless ENV['VERBOSE'].present?
16
+ spec = spec.merge(:min_messages => 'WARNING')
17
+ end
18
+ AR.establish_connection spec
19
+ end
20
+
21
+ def logger
22
+ AR.logger
23
+ end
24
+
25
+ def connection
26
+ AR.connection
27
+ end
28
+
29
+ def recreate_database!
30
+ database = config.fetch(:database)
31
+ connect! config.merge(:database => 'postgres')
32
+
33
+ unless AR.supports_chrono?
34
+ raise 'Your postgresql version is not supported. >= 9.0 is required.'
35
+ end
36
+
37
+ connection.drop_database database
38
+ connection.create_database database
39
+
40
+ ensure
41
+ connect!
42
+ logger.info "Connected to #{config}"
43
+ end
44
+
45
+ def config
46
+ @config ||= YAML.load(config_file.read).tap do |conf|
47
+ conf.symbolize_keys!
48
+ conf.update(:adapter => 'postgresql')
49
+
50
+ def conf.to_s
51
+ 'pgsql://%s:%s@%s/%s' % [
52
+ self[:username], self[:password], self[:hostname], self[:database]
53
+ ]
54
+ end
55
+ end
56
+
57
+ rescue Errno::ENOENT
58
+ $stderr.puts <<EOM
59
+
60
+ Please define your AR database configuration
61
+ in spec/config.yml or reference your own configuration
62
+ file using the TEST_CONFIG environment variable
63
+ EOM
64
+
65
+ abort
66
+ end
67
+
68
+ def config_file
69
+ Pathname(ENV['TEST_CONFIG'] ||
70
+ File.join(File.dirname(__FILE__), '..', 'config.yml')
71
+ )
72
+ end
73
+
74
+ end
@@ -0,0 +1,123 @@
1
+ module ChronoTest::Helpers
2
+
3
+ module Adapter
4
+ def self.included(base)
5
+ base.let!(:adapter) { ChronoTest.connection }
6
+ base.extend DSL
7
+ end
8
+
9
+ def columns
10
+ DSL.columns
11
+ end
12
+
13
+ module DSL
14
+ def with_temporal_table(&block)
15
+ context ':temporal => true' do
16
+ before(:all) { adapter.create_table(table, :temporal => true, &columns) }
17
+ after(:all) { adapter.drop_table table }
18
+
19
+ instance_eval(&block)
20
+ end
21
+ end
22
+
23
+ def with_plain_table(&block)
24
+ context ':temporal => false' do
25
+ before(:all) { adapter.create_table(table, :temporal => false, &columns) }
26
+ after(:all) { adapter.drop_table table }
27
+
28
+ instance_eval(&block)
29
+ end
30
+ end
31
+
32
+ def columns(&block)
33
+ DSL.columns = block.call
34
+ end
35
+
36
+ class << self
37
+ attr_accessor :columns
38
+ end
39
+ end
40
+ end
41
+
42
+ module TimeMachine
43
+ def self.included(base)
44
+ base.let!(:adapter) { ChronoTest.connection }
45
+ base.extend(DSL)
46
+ end
47
+
48
+ module DSL
49
+ def setup_schema!
50
+ # Set up database structure
51
+ #
52
+ before(:all) do
53
+ adapter.create_table 'foos', :temporal => true do |t|
54
+ t.string :name
55
+ t.integer :fooity
56
+ end
57
+
58
+ adapter.create_table 'bars', :temporal => true do |t|
59
+ t.string :name
60
+ t.references :foo
61
+ end
62
+
63
+ adapter.create_table 'bazs' do |t|
64
+ t.string :name
65
+ t.references :bar
66
+ end
67
+ end
68
+
69
+ after(:all) do
70
+ adapter.drop_table 'foos'
71
+ adapter.drop_table 'bars'
72
+ adapter.drop_table 'bazs'
73
+ end
74
+ end
75
+
76
+ Models = lambda {
77
+ class ::Foo < ActiveRecord::Base
78
+ include ChronoModel::TimeMachine
79
+
80
+ has_many :bars
81
+ end
82
+
83
+ class ::Bar < ActiveRecord::Base
84
+ include ChronoModel::TimeMachine
85
+
86
+ belongs_to :foo
87
+ has_one :baz
88
+ end
89
+
90
+ class ::Baz < ActiveRecord::Base
91
+ belongs_to :baz
92
+ end
93
+ }
94
+
95
+ def define_models!
96
+ before(:all) { Models.call }
97
+ end
98
+
99
+ end
100
+
101
+ # If a context object is given, evaluates the given
102
+ # block in its instance context, then defines a `ts`
103
+ # on it, backed by an Array, and adds the current
104
+ # database timestamp to it.
105
+ #
106
+ # If a context object is not given, the block is
107
+ # evaluated in the current context and the above
108
+ # mangling is done on the blocks' return value.
109
+ #
110
+ def ts_eval(ctx = nil, &block)
111
+ ret = (ctx || self).instance_eval(&block)
112
+ (ctx || ret).tap do |obj|
113
+ obj.singleton_class.instance_eval do
114
+ define_method(:ts) { @_ts ||= [] }
115
+ end unless obj.methods.include?(:ts)
116
+
117
+ now = adapter.select_value('select now()::timestamp')
118
+ obj.ts.push(Time.parse(now))
119
+ end
120
+ end
121
+ end
122
+
123
+ end
@@ -0,0 +1,59 @@
1
+ require 'ostruct'
2
+
3
+ module ChronoTest::Matchers
4
+ class Base
5
+ attr_reader :table
6
+
7
+ def matches?(table)
8
+ table = table.table_name if table.respond_to?(:table_name)
9
+ @table = table
10
+ end
11
+ private :matches? # This is an abstract class
12
+
13
+ def failure_message_for_should_not
14
+ failure_message_for_should.gsub(/to /, 'to not ')
15
+ end
16
+
17
+ protected
18
+ def connection
19
+ ChronoTest.connection
20
+ end
21
+
22
+ def temporal_schema
23
+ ChronoModel::Adapter::TEMPORAL_SCHEMA
24
+ end
25
+
26
+ def history_schema
27
+ ChronoModel::Adapter::HISTORY_SCHEMA
28
+ end
29
+
30
+ def public_schema
31
+ 'public'
32
+ end
33
+
34
+ def select_value(sql, binds, name = nil)
35
+ result = exec_query(sql, binds, name || 'select_value')
36
+ result.rows.first.try(:[], 0)
37
+ end
38
+
39
+ def select_values(sql, binds, name = nil)
40
+ result = exec_query(sql, binds, name || 'select_values')
41
+ result.rows.map(&:first)
42
+ end
43
+
44
+ def select_rows(sql, binds, name = nil)
45
+ exec_query(sql, binds, name || 'select_rows').rows
46
+ end
47
+
48
+ private
49
+ FooColumn = OpenStruct.new(:name => '')
50
+
51
+ def exec_query(sql, binds, name)
52
+ binds.map! do |col, val|
53
+ val ? [col, val] : [FooColumn, col]
54
+ end
55
+ connection.exec_query(sql, name, binds)
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,83 @@
1
+ module ChronoTest::Matchers
2
+
3
+ module Column
4
+ class HaveColumns < ChronoTest::Matchers::Base
5
+ def initialize(columns, schema = 'public')
6
+ @columns = columns
7
+ @schema = schema
8
+ end
9
+
10
+ def matches?(table)
11
+ super(table)
12
+
13
+ @matches = @columns.inject({}) do |h, (name, type)|
14
+ h.update([name, type] => has_column?(name, type))
15
+ end
16
+
17
+ @matches.values.all?
18
+ end
19
+
20
+ def failure_message_for_should
21
+ "expected #{@schema}.#{table} to have ".tap do |message|
22
+ message << @matches.map do |(name, type), match|
23
+ "a #{name}(#{type}) column" unless match
24
+ end.compact.to_sentence
25
+ end
26
+ end
27
+
28
+ protected
29
+ def has_column?(name, type)
30
+ table = "#{@schema}.#{self.table}"
31
+
32
+ select_rows(<<-SQL, [table, name], 'Check column').first == [name, type]
33
+ SELECT attname, FORMAT_TYPE(atttypid, atttypmod)
34
+ FROM pg_attribute
35
+ WHERE attrelid = $1::regclass::oid
36
+ AND attname = $2
37
+ SQL
38
+ end
39
+ end
40
+
41
+ def have_columns(*args)
42
+ HaveColumns.new(*args)
43
+ end
44
+
45
+
46
+ class HaveTemporalColumns < HaveColumns
47
+ def initialize(columns)
48
+ super(columns, temporal_schema)
49
+ end
50
+ end
51
+
52
+ def have_temporal_columns(*args)
53
+ HaveTemporalColumns.new(*args)
54
+ end
55
+
56
+
57
+ class HaveHistoryColumns < HaveColumns
58
+ def initialize(columns)
59
+ super(columns, history_schema)
60
+ end
61
+ end
62
+
63
+ def have_history_columns(*args)
64
+ HaveHistoryColumns.new(*args)
65
+ end
66
+
67
+
68
+ class HaveHistoryExtraColumns < HaveColumns
69
+ def initialize
70
+ super([
71
+ ['valid_from', 'timestamp without time zone'],
72
+ ['valid_to', 'timestamp without time zone'],
73
+ ['recorded_at', 'timestamp without time zone'],
74
+ ['hid', 'integer']
75
+ ], history_schema)
76
+ end
77
+ end
78
+
79
+ def have_history_extra_columns
80
+ HaveHistoryExtraColumns.new
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,61 @@
1
+ module ChronoTest::Matchers
2
+
3
+ module Index
4
+ class HaveIndex < ChronoTest::Matchers::Base
5
+ attr_reader :name, :columns, :schema
6
+
7
+ def initialize(name, columns, schema = 'public')
8
+ @name = name
9
+ @columns = columns.sort
10
+ @schema = schema
11
+ end
12
+
13
+ def matches?(table)
14
+ super(table)
15
+
16
+ select_values(<<-SQL, [ table, name, schema ], 'Check index') == columns
17
+ SELECT a.attname
18
+ FROM pg_class t
19
+ JOIN pg_index d ON t.oid = d.indrelid
20
+ JOIN pg_class i ON i.oid = d.indexrelid
21
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(d.indkey)
22
+ WHERE i.relkind = 'i'
23
+ AND t.relname = $1
24
+ AND i.relname = $2
25
+ AND i.relnamespace = (
26
+ SELECT oid FROM pg_namespace WHERE nspname = $3
27
+ )
28
+ ORDER BY a.attname
29
+ SQL
30
+ end
31
+
32
+ def failure_message_for_should
33
+ "expected #{schema}.#{table} to have a #{name} index on #{columns}"
34
+ end
35
+ end
36
+
37
+ def have_index(*args)
38
+ HaveIndex.new(*args)
39
+ end
40
+
41
+ class HaveTemporalIndex < HaveIndex
42
+ def initialize(name, columns)
43
+ super(name, columns, temporal_schema)
44
+ end
45
+ end
46
+
47
+ def have_temporal_index(*args)
48
+ HaveTemporalIndex.new(*args)
49
+ end
50
+
51
+ class HaveHistoryIndex < HaveIndex
52
+ def initialize(name, columns)
53
+ super(name, columns, history_schema)
54
+ end
55
+ end
56
+
57
+ def have_history_index(*args)
58
+ HaveHistoryIndex.new(*args)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,31 @@
1
+ require 'support/matchers/base'
2
+
3
+ module ChronoTest::Matchers
4
+
5
+ module Schema
6
+ class BeInSchema < ChronoTest::Matchers::Base
7
+
8
+ def initialize(expected)
9
+ @expected = expected
10
+ @expected = '"$user", public' if @expected == :default
11
+ end
12
+
13
+ def failure_message_for_should
14
+ "expected to be in schema #@expected, but was in #@current"
15
+ end
16
+
17
+ def matches?(*)
18
+ @current = select_value(<<-SQL, [], 'Current schema')
19
+ SHOW search_path
20
+ SQL
21
+
22
+ @current == @expected
23
+ end
24
+ end
25
+
26
+ def be_in_schema(schema)
27
+ BeInSchema.new(schema)
28
+ end
29
+ end
30
+
31
+ end