activerecord-sqlserver-adapter 2.2.18
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/CHANGELOG +175 -0
- data/MIT-LICENSE +20 -0
- data/Manifest +36 -0
- data/README.rdoc +175 -0
- data/RUNNING_UNIT_TESTS +60 -0
- data/Rakefile +18 -0
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +1126 -0
- data/lib/activerecord-sqlserver-adapter.rb +1 -0
- data/lib/core_ext/active_record.rb +133 -0
- data/lib/core_ext/dbi.rb +85 -0
- data/tasks/sqlserver.rake +31 -0
- data/test/cases/aaaa_create_tables_test_sqlserver.rb +19 -0
- data/test/cases/adapter_test_sqlserver.rb +707 -0
- data/test/cases/attribute_methods_test_sqlserver.rb +33 -0
- data/test/cases/basics_test_sqlserver.rb +21 -0
- data/test/cases/calculations_test_sqlserver.rb +20 -0
- data/test/cases/column_test_sqlserver.rb +264 -0
- data/test/cases/connection_test_sqlserver.rb +142 -0
- data/test/cases/eager_association_test_sqlserver.rb +42 -0
- data/test/cases/execute_procedure_test_sqlserver.rb +33 -0
- data/test/cases/inheritance_test_sqlserver.rb +28 -0
- data/test/cases/method_scoping_test_sqlserver.rb +28 -0
- data/test/cases/migration_test_sqlserver.rb +93 -0
- data/test/cases/offset_and_limit_test_sqlserver.rb +108 -0
- data/test/cases/pessimistic_locking_test_sqlserver.rb +125 -0
- data/test/cases/query_cache_test_sqlserver.rb +24 -0
- data/test/cases/schema_dumper_test_sqlserver.rb +72 -0
- data/test/cases/specific_schema_test_sqlserver.rb +57 -0
- data/test/cases/sqlserver_helper.rb +123 -0
- data/test/cases/table_name_test_sqlserver.rb +22 -0
- data/test/cases/transaction_test_sqlserver.rb +93 -0
- data/test/cases/unicode_test_sqlserver.rb +50 -0
- data/test/connections/native_sqlserver/connection.rb +23 -0
- data/test/connections/native_sqlserver_odbc/connection.rb +25 -0
- data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
- data/test/schema/sqlserver_specific_schema.rb +91 -0
- metadata +120 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'cases/sqlserver_helper'
|
2
|
+
require 'models/author'
|
3
|
+
require 'models/post'
|
4
|
+
require 'models/comment'
|
5
|
+
|
6
|
+
class EagerAssociationTestSqlserver < ActiveRecord::TestCase
|
7
|
+
end
|
8
|
+
|
9
|
+
class EagerAssociationTest < ActiveRecord::TestCase
|
10
|
+
|
11
|
+
COERCED_TESTS = [
|
12
|
+
:test_count_with_include,
|
13
|
+
:test_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_conditions,
|
14
|
+
:test_eager_with_has_many_and_limit_and_high_offset_and_multiple_hash_conditions
|
15
|
+
]
|
16
|
+
|
17
|
+
include SqlserverCoercedTest
|
18
|
+
|
19
|
+
fixtures :authors, :posts, :comments
|
20
|
+
|
21
|
+
def test_coerced_test_count_with_include
|
22
|
+
assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "len(comments.body) > 15")
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_coerced_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_conditions
|
26
|
+
assert_queries(2) do
|
27
|
+
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10,
|
28
|
+
:conditions => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ])
|
29
|
+
assert_equal 0, posts.size
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_coerced_eager_with_has_many_and_limit_and_high_offset_and_multiple_hash_conditions
|
34
|
+
assert_queries(2) do
|
35
|
+
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10,
|
36
|
+
:conditions => { 'authors.name' => 'David', 'comments.body' => 'go crazy' })
|
37
|
+
assert_equal 0, posts.size
|
38
|
+
end
|
39
|
+
end unless active_record_2_point_2?
|
40
|
+
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'cases/sqlserver_helper'
|
2
|
+
|
3
|
+
class ExecuteProcedureTestSqlserver < ActiveRecord::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@klass = ActiveRecord::Base
|
7
|
+
end
|
8
|
+
|
9
|
+
should 'execute a simple procedure' do
|
10
|
+
tables = @klass.execute_procedure :sp_tables
|
11
|
+
assert_instance_of Array, tables
|
12
|
+
assert_instance_of HashWithIndifferentAccess, tables.first
|
13
|
+
end
|
14
|
+
|
15
|
+
should 'take parameter arguments' do
|
16
|
+
tables = @klass.execute_procedure :sp_tables, 'sql_server_chronics'
|
17
|
+
table_info = tables.first
|
18
|
+
assert_equal 1, tables.size
|
19
|
+
assert_equal 'activerecord_unittest', table_info[:TABLE_QUALIFIER], "Table Info: #{table_info.inspect}"
|
20
|
+
assert_equal 'TABLE', table_info[:TABLE_TYPE], "Table Info: #{table_info.inspect}"
|
21
|
+
end
|
22
|
+
|
23
|
+
should 'quote bind vars correctly' do
|
24
|
+
assert_sql(/EXEC sp_tables '%sql_server%', NULL, NULL, NULL, 1/) do
|
25
|
+
@klass.execute_procedure :sp_tables, '%sql_server%', nil, nil, nil, true
|
26
|
+
end if sqlserver_2005? || sqlserver_2008?
|
27
|
+
assert_sql(/EXEC sp_tables '%sql_server%', NULL, NULL, NULL/) do
|
28
|
+
@klass.execute_procedure :sp_tables, '%sql_server%', nil, nil, nil
|
29
|
+
end if sqlserver_2000?
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'cases/sqlserver_helper'
|
2
|
+
require 'models/company'
|
3
|
+
|
4
|
+
class InheritanceTestSqlserver < ActiveRecord::TestCase
|
5
|
+
end
|
6
|
+
|
7
|
+
class InheritanceTest < ActiveRecord::TestCase
|
8
|
+
|
9
|
+
COERCED_TESTS = [
|
10
|
+
:test_eager_load_belongs_to_primary_key_quoting,
|
11
|
+
:test_a_bad_type_column
|
12
|
+
]
|
13
|
+
|
14
|
+
include SqlserverCoercedTest
|
15
|
+
|
16
|
+
def test_coerced_test_eager_load_belongs_to_primary_key_quoting
|
17
|
+
assert_sql(/\(\[companies\].\[id\] = 1\)/) do
|
18
|
+
Account.find(1, :include => :firm)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_coerced_test_a_bad_type_column
|
23
|
+
Company.connection.insert "INSERT INTO [companies] ([id], #{QUOTED_TYPE}, [name]) VALUES(100, 'bad_class!', 'Not happening')"
|
24
|
+
assert_raises(ActiveRecord::SubclassNotFound) { Company.find(100) }
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'cases/sqlserver_helper'
|
2
|
+
require 'models/developer'
|
3
|
+
|
4
|
+
class MethodScopingTestSqlServer < ActiveRecord::TestCase
|
5
|
+
end
|
6
|
+
|
7
|
+
class NestedScopingTest < ActiveRecord::TestCase
|
8
|
+
|
9
|
+
COERCED_TESTS = [:test_merged_scoped_find]
|
10
|
+
|
11
|
+
include SqlserverCoercedTest
|
12
|
+
|
13
|
+
fixtures :developers
|
14
|
+
|
15
|
+
def test_coerced_test_merged_scoped_find
|
16
|
+
poor_jamis = developers(:poor_jamis)
|
17
|
+
Developer.with_scope(:find => { :conditions => "salary < 100000" }) do
|
18
|
+
Developer.with_scope(:find => { :offset => 1, :order => 'id asc' }) do
|
19
|
+
assert_sql /ORDER BY id ASC/ do
|
20
|
+
assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc'))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'cases/sqlserver_helper'
|
2
|
+
require 'models/person'
|
3
|
+
|
4
|
+
class MigrationTestSqlserver < ActiveRecord::TestCase
|
5
|
+
|
6
|
+
context 'For transactions' do
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@connection = ActiveRecord::Base.connection
|
10
|
+
@trans_test_table1 = 'sqlserver_trans_table1'
|
11
|
+
@trans_test_table2 = 'sqlserver_trans_table2'
|
12
|
+
@trans_tables = [@trans_test_table1,@trans_test_table2]
|
13
|
+
end
|
14
|
+
|
15
|
+
teardown do
|
16
|
+
@trans_tables.each do |table_name|
|
17
|
+
ActiveRecord::Migration.drop_table(table_name) if @connection.tables.include?(table_name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
should 'not create a tables if error in migrations' do
|
22
|
+
begin
|
23
|
+
ActiveRecord::Migrator.up(SQLSERVER_MIGRATIONS_ROOT+'/transaction_table')
|
24
|
+
rescue Exception => e
|
25
|
+
assert_match %r|this and all later migrations canceled|, e.message
|
26
|
+
end
|
27
|
+
assert_does_not_contain @trans_test_table1, @connection.tables
|
28
|
+
assert_does_not_contain @trans_test_table2, @connection.tables
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
class MigrationTest < ActiveRecord::TestCase
|
38
|
+
|
39
|
+
COERCED_TESTS = [:test_add_column_not_null_without_default]
|
40
|
+
|
41
|
+
include SqlserverCoercedTest
|
42
|
+
|
43
|
+
def test_coerced_test_add_column_not_null_without_default
|
44
|
+
Person.connection.create_table :testings do |t|
|
45
|
+
t.column :foo, :string
|
46
|
+
t.column :bar, :string, :null => false
|
47
|
+
end
|
48
|
+
assert_raises(ActiveRecord::StatementInvalid) do
|
49
|
+
Person.connection.execute "INSERT INTO [testings] ([foo], [bar]) VALUES ('hello', NULL)"
|
50
|
+
end
|
51
|
+
ensure
|
52
|
+
Person.connection.drop_table :testings rescue nil
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
class ChangeTableMigrationsTest < ActiveRecord::TestCase
|
58
|
+
|
59
|
+
COERCED_TESTS = [:test_string_creates_string_column]
|
60
|
+
|
61
|
+
include SqlserverCoercedTest
|
62
|
+
|
63
|
+
def setup
|
64
|
+
@connection = Person.connection
|
65
|
+
@connection.create_table :delete_me, :force => true do |t|
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def teardown
|
70
|
+
@connection.drop_table :delete_me rescue nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_coerced_string_creates_string_column
|
74
|
+
with_sqlserver_change_table do |t|
|
75
|
+
@connection.expects(:add_column).with(:delete_me, :foo, sqlserver_string_column, {})
|
76
|
+
@connection.expects(:add_column).with(:delete_me, :bar, sqlserver_string_column, {})
|
77
|
+
t.string :foo, :bar
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
def with_sqlserver_change_table
|
84
|
+
@connection.change_table :delete_me do |t|
|
85
|
+
yield t
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def sqlserver_string_column
|
90
|
+
"#{@connection.native_string_database_type}(255)"
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'cases/sqlserver_helper'
|
2
|
+
require 'models/book'
|
3
|
+
|
4
|
+
class OffsetAndLimitTestSqlserver < ActiveRecord::TestCase
|
5
|
+
|
6
|
+
class Account < ActiveRecord::Base; end
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@connection = ActiveRecord::Base.connection
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'When selecting with limit' do
|
13
|
+
|
14
|
+
setup do
|
15
|
+
@select_sql = 'SELECT * FROM schema'
|
16
|
+
end
|
17
|
+
|
18
|
+
should 'alter SQL to limit number of records returned' do
|
19
|
+
options = { :limit => 10 }
|
20
|
+
assert_equal('SELECT TOP 10 * FROM schema', @connection.add_limit_offset!(@select_sql, options))
|
21
|
+
end
|
22
|
+
|
23
|
+
should 'only allow integers for limit' do
|
24
|
+
options = { :limit => 'ten' }
|
25
|
+
assert_raise(ArgumentError) {@connection.add_limit_offset!(@select_sql, options) }
|
26
|
+
end
|
27
|
+
|
28
|
+
should 'convert strings which look like integers to integers' do
|
29
|
+
options = { :limit => '42' }
|
30
|
+
assert_nothing_raised(ArgumentError) {@connection.add_limit_offset!(@select_sql, options)}
|
31
|
+
end
|
32
|
+
|
33
|
+
should 'not allow sql injection via limit' do
|
34
|
+
options = { :limit => '1 * FROM schema; DELETE * FROM table; SELECT TOP 10 *'}
|
35
|
+
assert_raise(ArgumentError) { @connection.add_limit_offset!(@select_sql, options) }
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'When selecting with limit and offset' do
|
41
|
+
|
42
|
+
setup do
|
43
|
+
@select_sql = 'SELECT * FROM books'
|
44
|
+
@subquery_select_sql = 'SELECT *, (SELECT TOP 1 id FROM books) AS book_id FROM books'
|
45
|
+
@books = (1..10).map {|i| Book.create!}
|
46
|
+
end
|
47
|
+
|
48
|
+
teardown do
|
49
|
+
@books.each {|b| b.destroy}
|
50
|
+
end
|
51
|
+
|
52
|
+
should 'have limit if offset is passed' do
|
53
|
+
options = { :offset => 1 }
|
54
|
+
assert_raise(ArgumentError) { @connection.add_limit_offset!(@select_sql, options) }
|
55
|
+
end
|
56
|
+
|
57
|
+
should 'only allow integers for offset' do
|
58
|
+
options = { :limit => 10, :offset => 'five' }
|
59
|
+
assert_raise(ArgumentError) { @connection.add_limit_offset!(@select_sql, options)}
|
60
|
+
end
|
61
|
+
|
62
|
+
should 'convert strings which look like integers to integers' do
|
63
|
+
options = { :limit => 10, :offset => '5' }
|
64
|
+
assert_nothing_raised(ArgumentError) {@connection.add_limit_offset!(@select_sql, options)}
|
65
|
+
end
|
66
|
+
|
67
|
+
should 'alter SQL to limit number of records returned offset by specified amount' do
|
68
|
+
options = { :limit => 3, :offset => 5 }
|
69
|
+
expected_sql = "SELECT * FROM (SELECT TOP 3 * FROM (SELECT TOP 8 * FROM books) AS tmp1) AS tmp2"
|
70
|
+
assert_equal(expected_sql, @connection.add_limit_offset!(@select_sql, options))
|
71
|
+
end
|
72
|
+
|
73
|
+
should 'add locks to deepest sub select in limit offset sql that has a limited tally' do
|
74
|
+
options = { :limit => 3, :offset => 5, :lock => 'WITH (NOLOCK)' }
|
75
|
+
expected_sql = "SELECT * FROM (SELECT TOP 3 * FROM (SELECT TOP 8 * FROM books WITH (NOLOCK)) AS tmp1) AS tmp2"
|
76
|
+
@connection.add_limit_offset! @select_sql, options
|
77
|
+
assert_equal expected_sql, @connection.add_lock!(@select_sql,options)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Not really sure what an offset sql injection might look like
|
81
|
+
should 'not allow sql injection via offset' do
|
82
|
+
options = { :limit => 10, :offset => '1 * FROM schema; DELETE * FROM table; SELECT TOP 10 *'}
|
83
|
+
assert_raise(ArgumentError) { @connection.add_limit_offset!(@select_sql, options) }
|
84
|
+
end
|
85
|
+
|
86
|
+
should 'not create invalid SQL with subquery SELECTs with TOP' do
|
87
|
+
options = { :limit => 5, :offset => 1 }
|
88
|
+
expected_sql = "SELECT * FROM (SELECT TOP 5 * FROM (SELECT TOP 6 *, (SELECT TOP 1 id FROM books) AS book_id FROM books) AS tmp1) AS tmp2"
|
89
|
+
assert_equal expected_sql, @connection.add_limit_offset!(@subquery_select_sql,options)
|
90
|
+
end
|
91
|
+
|
92
|
+
should 'add lock hints to tally sql if :lock option is present' do
|
93
|
+
assert_sql %r|SELECT TOP 1000000000 \* FROM \[people\] WITH \(NOLOCK\)| do
|
94
|
+
Person.all :limit => 5, :offset => 1, :lock => 'WITH (NOLOCK)'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
should 'not add lock hints to tally sql if there is no :lock option' do
|
99
|
+
assert_sql %r|\(SELECT TOP 1000000000 \* FROM \[people\] \)| do
|
100
|
+
Person.all :limit => 5, :offset => 1
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
end
|
108
|
+
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'cases/sqlserver_helper'
|
2
|
+
require 'models/person'
|
3
|
+
require 'models/reader'
|
4
|
+
|
5
|
+
class PessimisticLockingTestSqlserver < ActiveRecord::TestCase
|
6
|
+
|
7
|
+
self.use_transactional_fixtures = false
|
8
|
+
fixtures :people, :readers
|
9
|
+
|
10
|
+
def setup
|
11
|
+
Person.columns; Reader.columns # Avoid introspection queries during tests.
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'For simple finds with default lock option' do
|
15
|
+
|
16
|
+
should 'lock with simple find' do
|
17
|
+
assert_nothing_raised do
|
18
|
+
Person.transaction do
|
19
|
+
Person.find 1, :lock => true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
should 'lock with scoped find' do
|
25
|
+
assert_nothing_raised do
|
26
|
+
Person.transaction do
|
27
|
+
Person.with_scope(:find => { :lock => true }) do
|
28
|
+
Person.find 1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
should 'lock with eager find' do
|
35
|
+
assert_nothing_raised do
|
36
|
+
Person.transaction do
|
37
|
+
Person.find 1, :include => :readers, :lock => true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
should 'reload with lock when #lock! called' do
|
43
|
+
assert_nothing_raised do
|
44
|
+
Person.transaction do
|
45
|
+
person = Person.find 1
|
46
|
+
old, person.first_name = person.first_name, 'fooman'
|
47
|
+
person.lock!
|
48
|
+
assert_equal old, person.first_name
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
should 'simply add lock to find all' do
|
54
|
+
assert_sql %r|SELECT \* FROM \[people\] WITH \(NOLOCK\)| do
|
55
|
+
Person.all(:lock => 'WITH (NOLOCK)')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'For paginated finds' do
|
62
|
+
|
63
|
+
setup do
|
64
|
+
20.times { |n| Person.create!(:first_name => "Thing_#{n}") }
|
65
|
+
end
|
66
|
+
|
67
|
+
should 'cope with un-locked paginated results' do
|
68
|
+
tally_not_locked = %r|SELECT count\(\*\) as TotalRows from \(SELECT TOP 1000000000 \* FROM \[people\]\s+WITH \(NOLOCK\) \) tally|
|
69
|
+
inner_tmp_not_locked = %r|SELECT TOP 15 \* FROM \[people\] WITH \(NOLOCK\)|
|
70
|
+
# Currently association limiting is not locked like the parent.
|
71
|
+
association_limiting_not_locked = %r|SELECT \[readers\]\.\* FROM \[readers\] WITH \(NOLOCK\) WHERE \(\[readers\]\.person_id IN \(1,2,3,4,5\)\)|
|
72
|
+
assert_sql(tally_not_locked,inner_tmp_not_locked) do
|
73
|
+
Person.all(:include => :readers, :lock => 'WITH (NOLOCK)', :limit => 5, :offset => 10)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
context 'For dueling concurrent connections' do
|
81
|
+
|
82
|
+
use_concurrent_connections
|
83
|
+
|
84
|
+
should 'no locks does not wait' do
|
85
|
+
first, second = duel { Person.find 1 }
|
86
|
+
assert first.end > second.end
|
87
|
+
end
|
88
|
+
|
89
|
+
should 'that second lock waits' do
|
90
|
+
assert [0.2, 1, 5].any? { |zzz|
|
91
|
+
first, second = duel(zzz) { Person.find 1, :lock => true }
|
92
|
+
second.end > first.end
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def duel(zzz = 5)
|
102
|
+
t0, t1, t2, t3 = nil, nil, nil, nil
|
103
|
+
a = Thread.new do
|
104
|
+
t0 = Time.now
|
105
|
+
Person.transaction do
|
106
|
+
yield
|
107
|
+
sleep zzz # block thread 2 for zzz seconds
|
108
|
+
end
|
109
|
+
t1 = Time.now
|
110
|
+
end
|
111
|
+
b = Thread.new do
|
112
|
+
sleep zzz / 2.0 # ensure thread 1 tx starts first
|
113
|
+
t2 = Time.now
|
114
|
+
Person.transaction { yield }
|
115
|
+
t3 = Time.now
|
116
|
+
end
|
117
|
+
a.join
|
118
|
+
b.join
|
119
|
+
assert t1 > t0 + zzz
|
120
|
+
assert t2 > t0
|
121
|
+
assert t3 > t2
|
122
|
+
[t0.to_f..t1.to_f, t2.to_f..t3.to_f]
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|