hoodoo 1.1.3 → 1.2.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.
- checksums.yaml +8 -8
- data/lib/hoodoo/active.rb +1 -0
- data/lib/hoodoo/active/active_record/base.rb +7 -0
- data/lib/hoodoo/active/active_record/creator.rb +1 -1
- data/lib/hoodoo/active/active_record/dated.rb +8 -1
- data/lib/hoodoo/active/active_record/finder.rb +37 -3
- data/lib/hoodoo/active/active_record/manually_dated.rb +710 -0
- data/lib/hoodoo/active/active_record/secure.rb +1 -1
- data/lib/hoodoo/active/active_record/support.rb +8 -2
- data/lib/hoodoo/active/active_record/translated.rb +1 -1
- data/lib/hoodoo/active/active_record/uuid.rb +27 -3
- data/lib/hoodoo/active/active_record/writer.rb +1 -1
- data/lib/hoodoo/utilities/uuid.rb +13 -9
- data/lib/hoodoo/version.rb +1 -1
- data/spec/active/active_record/dated_spec.rb +24 -6
- data/spec/active/active_record/finder_spec.rb +26 -4
- data/spec/active/active_record/manually_dated_spec.rb +776 -0
- data/spec/spec_helper.rb +2 -1
- data/spec/utilities/uuid_spec.rb +142 -10
- metadata +5 -16
@@ -64,7 +64,9 @@ module Hoodoo
|
|
64
64
|
#
|
65
65
|
# * Hoodoo::ActiveRecord::Secure#secure
|
66
66
|
# * Hoodoo::ActiveRecord::Translated#translated
|
67
|
-
# * Hoodoo::ActiveRecord::Dated#dated
|
67
|
+
# * Hoodoo::ActiveRecord::Dated#dated (if "dating_enabled?" is +true+)
|
68
|
+
# * Hoodoo::ActiveRecord::ManuallyDated#manually_dated
|
69
|
+
# (if "manual_dating_enabled?" is +true+)
|
68
70
|
#
|
69
71
|
# +klass+:: The ActiveRecord::Base subclass _class_ (not instance)
|
70
72
|
# which is making the call here. This is the entity which is
|
@@ -86,10 +88,14 @@ module Hoodoo
|
|
86
88
|
# Due to the mechanism used, dating scope must be done first or the
|
87
89
|
# rest of the query may be invalid.
|
88
90
|
#
|
89
|
-
if klass.include?( Hoodoo::ActiveRecord::Dated )
|
91
|
+
if klass.include?( Hoodoo::ActiveRecord::Dated ) && klass.dating_enabled?()
|
90
92
|
prevailing_scope = prevailing_scope.dated( context )
|
91
93
|
end
|
92
94
|
|
95
|
+
if klass.include?( Hoodoo::ActiveRecord::ManuallyDated ) && klass.manual_dating_enabled?()
|
96
|
+
prevailing_scope = prevailing_scope.manually_dated( context )
|
97
|
+
end
|
98
|
+
|
93
99
|
if klass.include?( Hoodoo::ActiveRecord::Secure )
|
94
100
|
prevailing_scope = prevailing_scope.secure( context )
|
95
101
|
end
|
@@ -29,7 +29,7 @@ module Hoodoo
|
|
29
29
|
#
|
30
30
|
module UUID
|
31
31
|
|
32
|
-
# Instantiates this module when it is included
|
32
|
+
# Instantiates this module when it is included.
|
33
33
|
#
|
34
34
|
# Example:
|
35
35
|
#
|
@@ -69,10 +69,34 @@ module Hoodoo
|
|
69
69
|
model.primary_key = 'id'
|
70
70
|
|
71
71
|
model.validate( :on => :create ) do
|
72
|
-
self.id
|
72
|
+
self.id ||= Hoodoo::UUID.generate()
|
73
73
|
end
|
74
74
|
|
75
|
-
model.validates(
|
75
|
+
model.validates(
|
76
|
+
:id,
|
77
|
+
{
|
78
|
+
:uuid => true,
|
79
|
+
:presence => true,
|
80
|
+
:uniqueness => true,
|
81
|
+
}
|
82
|
+
)
|
83
|
+
|
84
|
+
# We also have to remove ActiveRecord's default unscoped uniqueness
|
85
|
+
# check on 'id'. We've added an equivalent validator above.
|
86
|
+
#
|
87
|
+
# Sadly there is no API for this even as late as ActiveRecord 4.2,
|
88
|
+
# so we have to resort to fragile hackery.
|
89
|
+
#
|
90
|
+
model._validators.reject!() do | key, ignored |
|
91
|
+
key == :id
|
92
|
+
end
|
93
|
+
|
94
|
+
id_validation_callback = model._validate_callbacks.find do | callback |
|
95
|
+
callback.raw_filter.is_a?( ::ActiveRecord::Validations::UniquenessValidator ) &&
|
96
|
+
callback.raw_filter.attributes == [ :id ]
|
97
|
+
end
|
98
|
+
|
99
|
+
model._validate_callbacks.delete( id_validation_callback )
|
76
100
|
end
|
77
101
|
|
78
102
|
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require "uuidtools"
|
2
|
-
|
3
1
|
module Hoodoo
|
4
2
|
|
5
3
|
# Class that handles generation and validation of UUIDs. Whenever you
|
@@ -13,22 +11,30 @@ module Hoodoo
|
|
13
11
|
|
14
12
|
# A regexp which, as its name suggests, only matches a string that
|
15
13
|
# contains 16 pairs of hex digits (with upper or lower case A-F).
|
14
|
+
# Legacy value kept in case third party client code is using it.
|
16
15
|
#
|
17
16
|
# http://stackoverflow.com/questions/287684/regular-expression-to-validate-hex-string
|
18
17
|
#
|
19
18
|
MATCH_16_PAIRS_OF_HEX_DIGITS = /^([[:xdigit:]]{2}){16}$/
|
20
19
|
|
20
|
+
# A regexp which matches V4 UUIDs with hyphens removed. Note that
|
21
|
+
# this is more strict than MATCH_16_PAIRS_OF_HEX_DIGITS.
|
22
|
+
#
|
23
|
+
MATCH_V4_UUID = /^[a-fA-F0-9]{12}4[a-fA-F0-9]{3}[89aAbB][a-fA-F0-9]{15}$/
|
24
|
+
|
21
25
|
# Generate a unique identifier. Returns a 32 character string.
|
22
26
|
#
|
23
27
|
def self.generate
|
24
|
-
|
28
|
+
SecureRandom.uuid().gsub!( '-', '' )
|
25
29
|
end
|
26
30
|
|
27
31
|
# Checks if a UUID string is valid. Returns +true+ if so, else +false+.
|
28
32
|
#
|
29
|
-
# +uuid+::
|
30
|
-
#
|
31
|
-
#
|
33
|
+
# +uuid+:: Quantity to validate.
|
34
|
+
#
|
35
|
+
# The method will only return +true+ if the input parameter is a String
|
36
|
+
# containing 32 mostly random hex digits representing a valid V4 UUID
|
37
|
+
# with hyphens removed.
|
32
38
|
#
|
33
39
|
# Note that the validity of a UUID says nothing about where, if anywhere,
|
34
40
|
# it might have been used. So, just because a UUID is valid, doesn't mean
|
@@ -36,9 +42,7 @@ module Hoodoo
|
|
36
42
|
# row in a database.
|
37
43
|
#
|
38
44
|
def self.valid?( uuid )
|
39
|
-
uuid.is_a?( ::String )
|
40
|
-
( uuid =~ MATCH_16_PAIRS_OF_HEX_DIGITS ) != nil &&
|
41
|
-
UUIDTools::UUID.parse_hexdigest( uuid ).valid?()
|
45
|
+
uuid.is_a?( ::String ) && ( uuid =~ MATCH_V4_UUID ) != nil
|
42
46
|
end
|
43
47
|
end
|
44
48
|
end
|
data/lib/hoodoo/version.rb
CHANGED
@@ -53,11 +53,13 @@ describe Hoodoo::ActiveRecord::Dated do
|
|
53
53
|
|
54
54
|
before( :all ) do
|
55
55
|
|
56
|
-
# Create some
|
56
|
+
# Create some example data for finding. The data has two different UUIDs
|
57
57
|
# which I'll referer to as A and B. The following tables contain the
|
58
|
-
# historical and current records separately with their attributes
|
58
|
+
# historical and current records separately with their attributes, with
|
59
|
+
# items created in the historical or main database tables respectively.
|
59
60
|
#
|
60
61
|
# Historical:
|
62
|
+
#
|
61
63
|
# -------------------------------------------------------------------
|
62
64
|
# uuid | data | created_at | effective_end | effective_start |
|
63
65
|
# -------------------------------------------------------------------
|
@@ -67,12 +69,12 @@ describe Hoodoo::ActiveRecord::Dated do
|
|
67
69
|
# B | "four" | now - 4 hours | now | now - 2 hour |
|
68
70
|
#
|
69
71
|
# Current:
|
72
|
+
#
|
70
73
|
# --------------------------------
|
71
74
|
# uuid | data | created_at |
|
72
75
|
# --------------------------------
|
73
76
|
# B | "five" | now - 4 hours |
|
74
77
|
# A | "six" | now - 5 hours |
|
75
|
-
#
|
76
78
|
|
77
79
|
@uuid_a = Hoodoo::UUID.generate
|
78
80
|
@uuid_b = Hoodoo::UUID.generate
|
@@ -111,7 +113,23 @@ describe Hoodoo::ActiveRecord::Dated do
|
|
111
113
|
|
112
114
|
end
|
113
115
|
|
114
|
-
context '
|
116
|
+
context 'unscoped' do
|
117
|
+
it 'counts only the current records in the main database table' do
|
118
|
+
expect( model_klass.count ).to be 2
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'finds only the current records in the main database table' do
|
122
|
+
expect( model_klass.pluck( :data ) ).to match_array( [ 'five', 'six' ] )
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
context '#dating_enabled?' do
|
127
|
+
it 'says it is automatically dated' do
|
128
|
+
expect( model_klass.dating_enabled? ).to eq( true )
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context '#dated_at' do
|
115
133
|
it 'returns counts correctly' do
|
116
134
|
expect( model_klass.dated_at( @now - 10.hours ).count ).to be 0
|
117
135
|
expect( model_klass.dated_at( @now ).count ).to be 2
|
@@ -143,7 +161,7 @@ describe Hoodoo::ActiveRecord::Dated do
|
|
143
161
|
|
144
162
|
end
|
145
163
|
|
146
|
-
context '
|
164
|
+
context '#dated' do
|
147
165
|
it 'returns counts correctly' do
|
148
166
|
# The contents of the Context are irrelevant aside from the fact that it
|
149
167
|
# needs a request to store the dated_at value.
|
@@ -207,7 +225,7 @@ describe Hoodoo::ActiveRecord::Dated do
|
|
207
225
|
|
208
226
|
end
|
209
227
|
|
210
|
-
context '
|
228
|
+
context '#dated_historical_and_current' do
|
211
229
|
it 'returns counts correctly' do
|
212
230
|
expect( model_klass.dated_historical_and_current.count ).to be 6
|
213
231
|
end
|
@@ -350,19 +350,41 @@ describe Hoodoo::ActiveRecord::Finder do
|
|
350
350
|
|
351
351
|
# ==========================================================================
|
352
352
|
|
353
|
-
context '
|
354
|
-
|
355
|
-
sql = RSpecModelFinderTest.acquisition_scope( @id ).to_sql()
|
353
|
+
context 'acquisition scope and overrides' do
|
354
|
+
def expect_sql( sql, id_attr_name )
|
356
355
|
expect( sql ).to eq( "SELECT \"r_spec_model_finder_tests\".* "<<
|
357
356
|
"FROM \"r_spec_model_finder_tests\" " <<
|
358
357
|
"WHERE (" <<
|
359
358
|
"(" <<
|
360
|
-
"\"r_spec_model_finder_tests\".\"
|
359
|
+
"\"r_spec_model_finder_tests\".\"#{ id_attr_name }\" = '#{ @id }' OR " <<
|
361
360
|
"\"r_spec_model_finder_tests\".\"uuid\" = '#{ @id }'" <<
|
362
361
|
") OR " <<
|
363
362
|
"\"r_spec_model_finder_tests\".\"code\" = '#{ @id }'" <<
|
364
363
|
")" )
|
365
364
|
end
|
365
|
+
|
366
|
+
context 'acquisition_scope' do
|
367
|
+
it 'SQL generation is as expected' do
|
368
|
+
sql = RSpecModelFinderTest.acquisition_scope( @id ).to_sql()
|
369
|
+
expect_sql( sql, 'id' )
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
context 'acquire_with_id_substitute' do
|
374
|
+
before :each do
|
375
|
+
@alt_attr_name = 'foo'
|
376
|
+
RSpecModelFinderTest.acquire_with_id_substitute( @alt_attr_name )
|
377
|
+
end
|
378
|
+
|
379
|
+
after :each do
|
380
|
+
RSpecModelFinderTest.acquire_with_id_substitute( 'id' )
|
381
|
+
end
|
382
|
+
|
383
|
+
it 'SQL generation is as expected' do
|
384
|
+
sql = RSpecModelFinderTest.acquisition_scope( @id ).to_sql()
|
385
|
+
expect_sql( sql, @alt_attr_name )
|
386
|
+
end
|
387
|
+
end
|
366
388
|
end
|
367
389
|
|
368
390
|
# ==========================================================================
|
@@ -0,0 +1,776 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
describe Hoodoo::ActiveRecord::ManuallyDated do
|
5
|
+
|
6
|
+
# ==========================================================================
|
7
|
+
# Data setup
|
8
|
+
# ==========================================================================
|
9
|
+
|
10
|
+
BAD_DATA_FOR_VALIDATIONS = 'bad_data'
|
11
|
+
|
12
|
+
before :all do
|
13
|
+
spec_helper_silence_stdout() do
|
14
|
+
ActiveRecord::Migration.create_table( :r_spec_model_manual_date_tests, :id => false ) do | t |
|
15
|
+
t.string :uuid, :null => false, :length => 32
|
16
|
+
t.string :id, :null => false, :length => 32
|
17
|
+
|
18
|
+
t.text :data
|
19
|
+
|
20
|
+
t.timestamps
|
21
|
+
t.datetime :effective_start, :null => false
|
22
|
+
t.datetime :effective_end, :null => false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class RSpecModelManualDateTest < ActiveRecord::Base
|
27
|
+
include Hoodoo::ActiveRecord::ManuallyDated
|
28
|
+
manual_dating_enabled()
|
29
|
+
|
30
|
+
validates_each :data do | record, attribute, value |
|
31
|
+
if value == BAD_DATA_FOR_VALIDATIONS
|
32
|
+
record.errors.add( attribute, 'contains bad text' )
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create some example data for finding. The data has two different UUIDs
|
39
|
+
# which I'll referer to as A and B. The following tables list the
|
40
|
+
# historical and current records with their attributes. All are created
|
41
|
+
# as rows within the main test model class's one database table.
|
42
|
+
#
|
43
|
+
# The data is also seeded for any other tests, so that there's a known
|
44
|
+
# set of rows which can be examined for changes, or lack thereof.
|
45
|
+
#
|
46
|
+
# Historical:
|
47
|
+
#
|
48
|
+
# -------------------------------------------------------------------
|
49
|
+
# uuid | data | created_at | effective_end | effective_start |
|
50
|
+
# -------------------------------------------------------------------
|
51
|
+
# A | 'one' | now - 5 hours | now - 3 hours | now - 5 hours |
|
52
|
+
# B | 'two' | now - 4 hours | now - 2 hours | now - 4 hours |
|
53
|
+
# A | 'three' | now - 5 hours | now - 1 hour | now - 3 hours |
|
54
|
+
# B | 'four' | now - 4 hours | now | now - 2 hour |
|
55
|
+
#
|
56
|
+
# Current:
|
57
|
+
#
|
58
|
+
# -------------------------------------------------------------------
|
59
|
+
# uuid | data | created_at | effective_end | effective_start |
|
60
|
+
# -------------------------------------------------------------------
|
61
|
+
# B | 'five' | now - 4 hours | nil | now - 5 hours |
|
62
|
+
# A | 'six' | now - 5 hours | nil | now - 4 hours |
|
63
|
+
#
|
64
|
+
before :each do
|
65
|
+
|
66
|
+
@now = Time.now.utc.round( Hoodoo::ActiveRecord::ManuallyDated::SECONDS_DECIMAL_PLACES )
|
67
|
+
@uuid_a = Hoodoo::UUID.generate
|
68
|
+
@uuid_b = Hoodoo::UUID.generate
|
69
|
+
@eot = Hoodoo::ActiveRecord::ManuallyDated::DATE_MAXIMUM
|
70
|
+
|
71
|
+
# uuid, data, created_at, effective_end, effective_start
|
72
|
+
[
|
73
|
+
[ @uuid_a, 'one', @now - 5.hours, @now - 5.hours, @now - 3.hours ],
|
74
|
+
[ @uuid_b, 'two', @now - 4.hours, @now - 4.hours, @now - 2.hours ],
|
75
|
+
[ @uuid_a, 'three', @now - 5.hours, @now - 3.hours, @now - 1.hour ],
|
76
|
+
[ @uuid_b, 'four', @now - 4.hours, @now - 2.hours, @now ],
|
77
|
+
[ @uuid_b, 'five', @now - 4.hours, @now, @eot ],
|
78
|
+
[ @uuid_a, 'six', @now - 5.hours, @now - 1.hour, @eot ]
|
79
|
+
].each do | row_data |
|
80
|
+
RSpecModelManualDateTest.new( {
|
81
|
+
:id => row_data[ 0 ],
|
82
|
+
:data => row_data[ 1 ],
|
83
|
+
:created_at => row_data[ 2 ],
|
84
|
+
:updated_at => row_data[ 2 ],
|
85
|
+
:effective_start => row_data[ 3 ],
|
86
|
+
:effective_end => row_data[ 4 ]
|
87
|
+
} ).save!
|
88
|
+
end
|
89
|
+
|
90
|
+
# This is a useful thing to have around! Just the bare minimum for the
|
91
|
+
# API under test. At the time of writing you can actually pass a "nil"
|
92
|
+
# context if all other attribute values are given, but that's not
|
93
|
+
# documented and besides, we want to test a mixture of context-based
|
94
|
+
# and explicitly specified parameters.
|
95
|
+
#
|
96
|
+
@context = Hoodoo::Services::Context.new( nil,
|
97
|
+
Hoodoo::Services::Request.new,
|
98
|
+
nil,
|
99
|
+
nil )
|
100
|
+
end
|
101
|
+
|
102
|
+
# ==========================================================================
|
103
|
+
# Reading tests
|
104
|
+
# ==========================================================================
|
105
|
+
|
106
|
+
context 'reading data' do
|
107
|
+
context 'unscoped' do
|
108
|
+
it 'counts all historical and current records in one database table' do
|
109
|
+
expect( RSpecModelManualDateTest.count ).to be 6
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'finds all historical and current records in one database table' do
|
113
|
+
expect( RSpecModelManualDateTest.pluck( :data ) ).to match_array( [ 'one', 'two', 'three', 'four', 'five', 'six' ] )
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context '#manual_dating_enabled?' do
|
118
|
+
it 'says it is manually dated' do
|
119
|
+
expect( RSpecModelManualDateTest.manual_dating_enabled? ).to eq( true )
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context '#manually_dated_at' do
|
124
|
+
it 'returns counts correctly' do
|
125
|
+
expect( RSpecModelManualDateTest.manually_dated_at( @now - 10.hours ).count ).to be 0
|
126
|
+
expect( RSpecModelManualDateTest.manually_dated_at( @now ).count ).to be 2
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_expectation( time, expected_data )
|
130
|
+
expect( RSpecModelManualDateTest.manually_dated_at( time ).pluck( :data ) ).to match_array( expected_data )
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'returns no records before any were effective' do
|
134
|
+
test_expectation( @now - 10.hours, [] )
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'returns records that used to be effective starting at past time' do
|
138
|
+
test_expectation( @now - 5.hours, [ 'one' ] )
|
139
|
+
test_expectation( @now - 4.hours, [ 'one', 'two' ] )
|
140
|
+
test_expectation( @now - 3.hours, [ 'two', 'three' ] )
|
141
|
+
test_expectation( @now - 2.hours, [ 'three', 'four' ] )
|
142
|
+
test_expectation( @now - 1.hour, [ 'four', 'six' ] )
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'returns records that are effective now' do
|
146
|
+
test_expectation( @now, [ 'five', 'six' ] )
|
147
|
+
end
|
148
|
+
|
149
|
+
# Given the test above, if this was ignoring timezone or otherwise
|
150
|
+
# being confused it would take "now", subtract an hour, then add it
|
151
|
+
# back again; we'd see "five" and "six" instead of "four" and "six".
|
152
|
+
#
|
153
|
+
it 'converts inbound date/times to UTC' do
|
154
|
+
local = ( @now - 1.hour ).localtime( '+01:00' )
|
155
|
+
test_expectation( local, [ 'four', 'six' ] )
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'works with further filtering' do
|
159
|
+
expect( RSpecModelManualDateTest.manually_dated_at( @now ).where( :uuid => @uuid_a ).pluck( :data ) ).to eq( [ 'six' ] )
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context '#manually_dated' do
|
164
|
+
it 'returns counts correctly' do
|
165
|
+
# The contents of the Context are irrelevant aside from the fact that it
|
166
|
+
# needs a request to store the dated_at value.
|
167
|
+
request = Hoodoo::Services::Request.new
|
168
|
+
context = Hoodoo::Services::Context.new( nil, request, nil, nil )
|
169
|
+
|
170
|
+
context.request.dated_at = @now - 10.hours
|
171
|
+
expect( RSpecModelManualDateTest.manually_dated( context ).count ).to be 0
|
172
|
+
|
173
|
+
context.request.dated_at = @now
|
174
|
+
expect( RSpecModelManualDateTest.manually_dated( context ).count ).to be 2
|
175
|
+
end
|
176
|
+
|
177
|
+
def test_expectation( time, expected_data )
|
178
|
+
# The contents of the Context are irrelevant aside from the fact that it
|
179
|
+
# needs a request to store the dated_at value.
|
180
|
+
request = Hoodoo::Services::Request.new
|
181
|
+
context = Hoodoo::Services::Context.new( nil, request, nil, nil )
|
182
|
+
context.request.dated_at = time
|
183
|
+
|
184
|
+
expect( RSpecModelManualDateTest.manually_dated( context ).pluck( :data ) ).to match_array( expected_data )
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'returns no records before any were effective' do
|
188
|
+
test_expectation( @now - 10.hours, [] )
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'returns records that used to be effective starting at past time' do
|
192
|
+
test_expectation( @now - 5.hours, [ 'one' ] )
|
193
|
+
test_expectation( @now - 4.hours, [ 'one', 'two' ] )
|
194
|
+
test_expectation( @now - 3.hours, [ 'two', 'three' ] )
|
195
|
+
test_expectation( @now - 2.hours, [ 'three', 'four' ] )
|
196
|
+
test_expectation( @now - 1.hour, [ 'four', 'six' ] )
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'returns records that are effective now' do
|
200
|
+
test_expectation( @now, [ 'five', 'six' ] )
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'converts inbound date/times to UTC' do
|
204
|
+
local = ( @now - 1.hour ).localtime( '+01:00' )
|
205
|
+
test_expectation( local, [ 'four', 'six' ] )
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'works with further filtering' do
|
209
|
+
|
210
|
+
# The contents of the Context are irrelevant aside from the fact that it
|
211
|
+
# needs a request to store the dated_at value.
|
212
|
+
request = Hoodoo::Services::Request.new
|
213
|
+
context = Hoodoo::Services::Context.new( nil, request, nil, nil )
|
214
|
+
context.request.dated_at = @now
|
215
|
+
|
216
|
+
expect( RSpecModelManualDateTest.manually_dated( context ).where( :uuid => @uuid_a ).pluck( :data ) ).to eq( [ 'six' ] )
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'works with dating last' do
|
220
|
+
|
221
|
+
# The contents of the Context are irrelevant aside from the fact that it
|
222
|
+
# needs a request to store the dated_at value.
|
223
|
+
request = Hoodoo::Services::Request.new
|
224
|
+
context = Hoodoo::Services::Context.new( nil, request, nil, nil )
|
225
|
+
context.request.dated_at = @now
|
226
|
+
|
227
|
+
expect( RSpecModelManualDateTest.where( :uuid => @uuid_a ).manually_dated( context ).pluck( :data ) ).to eq( [ 'six' ] )
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
context '#manually_dated_historic' do
|
232
|
+
it 'counts only historic entries' do
|
233
|
+
expect( RSpecModelManualDateTest.manually_dated_historic.count ).to eq( 4 )
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'finds only historic entries' do
|
237
|
+
expect( RSpecModelManualDateTest.manually_dated_historic.pluck( :data ) ).to match_array( [ 'one', 'two', 'three', 'four' ] )
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
context '#manually_dated_contemporary' do
|
242
|
+
it 'counts only contemporary entries' do
|
243
|
+
expect( RSpecModelManualDateTest.manually_dated_contemporary.count ).to eq( 2 )
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'finds only contemporary entries' do
|
247
|
+
expect( RSpecModelManualDateTest.manually_dated_contemporary.pluck( :data ) ).to match_array( [ 'five', 'six' ] )
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# ==========================================================================
|
253
|
+
# Writing tests
|
254
|
+
# ==========================================================================
|
255
|
+
|
256
|
+
context 'writing data' do
|
257
|
+
context '#manually_dated_update_in' do
|
258
|
+
before :each do
|
259
|
+
@change_data_from = Hoodoo::UUID.generate()
|
260
|
+
@change_data_to = Hoodoo::UUID.generate()
|
261
|
+
|
262
|
+
@record = RSpecModelManualDateTest.new( {
|
263
|
+
:data => @change_data_from,
|
264
|
+
:created_at => @now,
|
265
|
+
:updated_at => @now
|
266
|
+
} )
|
267
|
+
|
268
|
+
@record.save!
|
269
|
+
sleep( ( 0.1 ** Hoodoo::ActiveRecord::ManuallyDated::SECONDS_DECIMAL_PLACES ) * 2 )
|
270
|
+
|
271
|
+
@context.request.instance_variable_set( '@ident', @record.uuid )
|
272
|
+
@context.request.body = { 'data' => @change_data_to }
|
273
|
+
end
|
274
|
+
|
275
|
+
# Call only for 'successful' update cases. Pass the result of a call to
|
276
|
+
# #manually_dated_update_in. Expects:
|
277
|
+
#
|
278
|
+
# * No new 'current' items
|
279
|
+
# * One new 'historic' item
|
280
|
+
# * Two unscoped things can now be found by @record's UUID
|
281
|
+
# * They should have the correct bounding dates and data.
|
282
|
+
#
|
283
|
+
# Remember that the very top of this file seeds in 2 contemporary and
|
284
|
+
# 4 historical entries, so counts are relative to that baseline.
|
285
|
+
#
|
286
|
+
def run_expectations( result )
|
287
|
+
expect( RSpecModelManualDateTest.manually_dated_contemporary.count ).to eq( 3 )
|
288
|
+
expect( RSpecModelManualDateTest.manually_dated_historic.count ).to eq( 5 )
|
289
|
+
expect( RSpecModelManualDateTest.manually_dated_contemporary.where( :uuid => @record.uuid ).count ).to eq( 1 )
|
290
|
+
expect( RSpecModelManualDateTest.manually_dated_historic.where( :uuid => @record.uuid ).count ).to eq( 1 )
|
291
|
+
|
292
|
+
# Current record is now at 'no'/nil time - i.e. actually Time.now. The
|
293
|
+
# time frozen into "@now" at the top of this file is by this point the
|
294
|
+
# historic time of the old record.
|
295
|
+
|
296
|
+
historic = RSpecModelManualDateTest.manually_dated_at( @now ).find_by_uuid( @record.uuid )
|
297
|
+
current = RSpecModelManualDateTest.manually_dated_at().find_by_uuid( @record.uuid )
|
298
|
+
|
299
|
+
expect( result.uuid ).to eq( current.uuid )
|
300
|
+
|
301
|
+
expect( historic.data ).to eq( @change_data_from )
|
302
|
+
expect( current.data ).to eq( @change_data_to )
|
303
|
+
|
304
|
+
expect( historic.effective_end ).to eq( current.effective_start )
|
305
|
+
expect( historic.effective_end ).to eq( current.updated_at )
|
306
|
+
expect( current.effective_end ).to eq( @eot )
|
307
|
+
end
|
308
|
+
|
309
|
+
it 'via context alone' do
|
310
|
+
result = RSpecModelManualDateTest.manually_dated_update_in(
|
311
|
+
@context
|
312
|
+
)
|
313
|
+
|
314
|
+
run_expectations( result )
|
315
|
+
end
|
316
|
+
|
317
|
+
# Generate a random => invalid UUID in the request data to prove that
|
318
|
+
# the valid one given in the input parameter is used as an override.
|
319
|
+
#
|
320
|
+
it 'specifying "ident"' do
|
321
|
+
@context.request.instance_variable_set( '@ident', Hoodoo::UUID.generate() )
|
322
|
+
|
323
|
+
result = RSpecModelManualDateTest.manually_dated_update_in(
|
324
|
+
@context,
|
325
|
+
ident: @record.uuid
|
326
|
+
)
|
327
|
+
|
328
|
+
run_expectations( result )
|
329
|
+
end
|
330
|
+
|
331
|
+
# Generate a random => invalid payload in the request data to prove that
|
332
|
+
# the valid one given in the input parameter is used as an override.
|
333
|
+
#
|
334
|
+
it 'specifying "attributes"' do
|
335
|
+
@context.request.body = { Hoodoo::UUID.generate() => 42 }
|
336
|
+
|
337
|
+
result = RSpecModelManualDateTest.manually_dated_update_in(
|
338
|
+
@context,
|
339
|
+
attributes: { 'data' => @change_data_to }
|
340
|
+
)
|
341
|
+
|
342
|
+
run_expectations( result )
|
343
|
+
end
|
344
|
+
|
345
|
+
it 'uses a given scope' do
|
346
|
+
|
347
|
+
# We expect the custom scope to be customised to find an
|
348
|
+
# acquisition scope for locking, if it's being used OK.
|
349
|
+
# This is fragile; depends heavily on implementation.
|
350
|
+
|
351
|
+
custom_scope = RSpecModelManualDateTest.where( :data => [ 'one', 'two', 'three' ] + [ @change_data_from ] )
|
352
|
+
expect( custom_scope ).to receive( :acquisition_scope ).and_call_original
|
353
|
+
|
354
|
+
result = RSpecModelManualDateTest.manually_dated_update_in(
|
355
|
+
@context,
|
356
|
+
scope: custom_scope
|
357
|
+
)
|
358
|
+
|
359
|
+
run_expectations( result )
|
360
|
+
end
|
361
|
+
|
362
|
+
context 'handles not-found' do
|
363
|
+
it 'because of a bad identifier' do
|
364
|
+
result = RSpecModelManualDateTest.manually_dated_update_in(
|
365
|
+
@context,
|
366
|
+
ident: Hoodoo::UUID.generate() # Random => invalid
|
367
|
+
)
|
368
|
+
|
369
|
+
expect( result ).to be_nil
|
370
|
+
end
|
371
|
+
|
372
|
+
it 'because of a scope' do
|
373
|
+
result = RSpecModelManualDateTest.manually_dated_update_in(
|
374
|
+
@context,
|
375
|
+
scope: RSpecModelManualDateTest.where( :data => [ Hoodoo::UUID.generate() ] ) # Random => invalid
|
376
|
+
)
|
377
|
+
|
378
|
+
expect( result ).to be_nil
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
context 'exceptions' do
|
383
|
+
def expect_correct_rollback( &block )
|
384
|
+
starting_original = RSpecModelManualDateTest.manually_dated_contemporary.acquire_in( @context )
|
385
|
+
expect( starting_original ).to_not be_nil # Self-check this test
|
386
|
+
|
387
|
+
yield( block )
|
388
|
+
|
389
|
+
ending_original = RSpecModelManualDateTest.manually_dated_contemporary.acquire_in( @context )
|
390
|
+
|
391
|
+
expect( ending_original ).to_not be_nil
|
392
|
+
expect( starting_original.attributes ).to eq( ending_original.attributes )
|
393
|
+
end
|
394
|
+
|
395
|
+
it 'handles validation errors and does not change the contemporary record' do
|
396
|
+
expect_correct_rollback do
|
397
|
+
result = nil
|
398
|
+
|
399
|
+
expect {
|
400
|
+
result = RSpecModelManualDateTest.manually_dated_update_in(
|
401
|
+
@context,
|
402
|
+
attributes: { 'data' => BAD_DATA_FOR_VALIDATIONS }
|
403
|
+
)
|
404
|
+
}.to_not change( RSpecModelManualDateTest, :count )
|
405
|
+
|
406
|
+
expect( result ).to_not be_nil
|
407
|
+
expect( result.persisted? ).to eq( false )
|
408
|
+
expect( result.errors.messages ).to eq( { :data => [ 'contains bad text' ] } )
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
it 'correctly rolls back in the face of unexpected exceptions' do
|
413
|
+
expect_correct_rollback do
|
414
|
+
expect_any_instance_of( RSpecModelManualDateTest ).to receive( :save ) {
|
415
|
+
raise 'stop'
|
416
|
+
}
|
417
|
+
|
418
|
+
expect {
|
419
|
+
expect {
|
420
|
+
RSpecModelManualDateTest.manually_dated_update_in(
|
421
|
+
@context,
|
422
|
+
attributes: { 'data' => Hoodoo::UUID.generate() }
|
423
|
+
)
|
424
|
+
}.to raise_exception( RuntimeError, 'stop' )
|
425
|
+
}.to_not change( RSpecModelManualDateTest, :count )
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
it 'retries after one deadlock' do
|
430
|
+
raised = false
|
431
|
+
|
432
|
+
allow_any_instance_of( RSpecModelManualDateTest ).to receive( :update_column ) do | instance, name, value |
|
433
|
+
if raised == false
|
434
|
+
raised = true
|
435
|
+
raise ::ActiveRecord::StatementInvalid.new( 'MOCK DEADLOCK EXCEPTION' )
|
436
|
+
else
|
437
|
+
instance.update_attribute( name, value )
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
result = nil
|
442
|
+
|
443
|
+
expect {
|
444
|
+
result = RSpecModelManualDateTest.manually_dated_update_in(
|
445
|
+
@context,
|
446
|
+
attributes: { 'data' => Hoodoo::UUID.generate() }
|
447
|
+
)
|
448
|
+
}.to change( RSpecModelManualDateTest, :count ).by( 1 )
|
449
|
+
|
450
|
+
expect( result ).to_not be_nil
|
451
|
+
expect( result.errors ).to be_empty
|
452
|
+
expect( result.persisted? ).to eq( true )
|
453
|
+
end
|
454
|
+
|
455
|
+
it 'gives up after two deadlocks' do
|
456
|
+
allow_any_instance_of( RSpecModelManualDateTest ).to receive( :update_column ) do | instance, name, value |
|
457
|
+
raise ::ActiveRecord::StatementInvalid.new( 'MOCK DEADLOCK EXCEPTION' )
|
458
|
+
end
|
459
|
+
|
460
|
+
expect_correct_rollback do
|
461
|
+
expect {
|
462
|
+
result = RSpecModelManualDateTest.manually_dated_update_in(
|
463
|
+
@context,
|
464
|
+
attributes: { 'data' => Hoodoo::UUID.generate() }
|
465
|
+
)
|
466
|
+
}.to raise_exception( ::ActiveRecord::StatementInvalid )
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
context '#manually_dated_destruction_in' do
|
473
|
+
before :each do
|
474
|
+
@data = Hoodoo::UUID.generate()
|
475
|
+
@record = RSpecModelManualDateTest.new( {
|
476
|
+
:data => @data,
|
477
|
+
:created_at => @now,
|
478
|
+
:updated_at => @now
|
479
|
+
} )
|
480
|
+
|
481
|
+
@record.save!
|
482
|
+
sleep( ( 0.1 ** Hoodoo::ActiveRecord::ManuallyDated::SECONDS_DECIMAL_PLACES ) * 2 )
|
483
|
+
|
484
|
+
@old_updated_at = @record.updated_at
|
485
|
+
|
486
|
+
@context.request.instance_variable_set( '@ident', @record.uuid )
|
487
|
+
end
|
488
|
+
|
489
|
+
# Call only for 'successful' delete cases. Pass the result of a call to
|
490
|
+
# #manually_dated_update_in. Expects:
|
491
|
+
#
|
492
|
+
# * One fewer 'current' items
|
493
|
+
# * One more 'historic' item
|
494
|
+
# * One unscoped thing can now be found by @record's UUID
|
495
|
+
# * It should have the correct bounding dates and data.
|
496
|
+
#
|
497
|
+
# Remember that the very top of this file seeds in 2 contemporary and
|
498
|
+
# 4 historical entries, so counts are relative to that baseline.
|
499
|
+
#
|
500
|
+
def run_expectations( result )
|
501
|
+
expect( RSpecModelManualDateTest.manually_dated_contemporary.count ).to eq( 2 )
|
502
|
+
expect( RSpecModelManualDateTest.manually_dated_historic.count ).to eq( 5 )
|
503
|
+
expect( RSpecModelManualDateTest.manually_dated_contemporary.where( :uuid => @record.uuid ).count ).to eq( 0 )
|
504
|
+
expect( RSpecModelManualDateTest.manually_dated_historic.where( :uuid => @record.uuid ).count ).to eq( 1 )
|
505
|
+
|
506
|
+
# Current record is now at 'no'/nil time - i.e. actually Time.now. The
|
507
|
+
# time frozen into "@now" at the top of this file is by this point the
|
508
|
+
# historic time of the old record.
|
509
|
+
|
510
|
+
historic = RSpecModelManualDateTest.manually_dated_at( @now ).find_by_uuid( @record.uuid )
|
511
|
+
|
512
|
+
expect( historic.data ).to eq( @data )
|
513
|
+
|
514
|
+
expect( historic.effective_end ).to_not eq( @eot )
|
515
|
+
expect( historic.updated_at ).to eq( @old_updated_at )
|
516
|
+
end
|
517
|
+
|
518
|
+
it 'via context alone' do
|
519
|
+
result = RSpecModelManualDateTest.manually_dated_destruction_in(
|
520
|
+
@context
|
521
|
+
)
|
522
|
+
|
523
|
+
run_expectations( result )
|
524
|
+
end
|
525
|
+
|
526
|
+
# Generate a random => invalid UUID in the request data to prove that
|
527
|
+
# the valid one given in the input parameter is used as an override.
|
528
|
+
#
|
529
|
+
it 'specifying "ident"' do
|
530
|
+
@context.request.instance_variable_set( '@ident', Hoodoo::UUID.generate() )
|
531
|
+
|
532
|
+
result = RSpecModelManualDateTest.manually_dated_destruction_in(
|
533
|
+
@context,
|
534
|
+
ident: @record.uuid
|
535
|
+
)
|
536
|
+
|
537
|
+
run_expectations( result )
|
538
|
+
end
|
539
|
+
|
540
|
+
it 'uses a given scope' do
|
541
|
+
|
542
|
+
# We expect the custom scope to be customised to find an
|
543
|
+
# contemporary dated record for locking, if it's being used
|
544
|
+
# OK. This is fragile; depends heavily on implementation.
|
545
|
+
|
546
|
+
custom_scope = RSpecModelManualDateTest.where( :data => [ 'one', 'two', 'three' ] + [ @data ] )
|
547
|
+
expect( custom_scope ).to receive( :manually_dated_contemporary ).and_call_original
|
548
|
+
|
549
|
+
result = RSpecModelManualDateTest.manually_dated_destruction_in(
|
550
|
+
@context,
|
551
|
+
scope: custom_scope
|
552
|
+
)
|
553
|
+
|
554
|
+
run_expectations( result )
|
555
|
+
end
|
556
|
+
|
557
|
+
context 'handles not-found' do
|
558
|
+
it 'because of a bad identifier' do
|
559
|
+
result = RSpecModelManualDateTest.manually_dated_destruction_in(
|
560
|
+
@context,
|
561
|
+
ident: Hoodoo::UUID.generate() # Random => invalid
|
562
|
+
)
|
563
|
+
|
564
|
+
expect( result ).to be_nil
|
565
|
+
end
|
566
|
+
|
567
|
+
it 'because of a scope' do
|
568
|
+
result = RSpecModelManualDateTest.manually_dated_destruction_in(
|
569
|
+
@context,
|
570
|
+
scope: RSpecModelManualDateTest.where( :data => [ Hoodoo::UUID.generate() ] ) # Random => invalid
|
571
|
+
)
|
572
|
+
|
573
|
+
expect( result ).to be_nil
|
574
|
+
end
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
context 'update then delete' do
|
579
|
+
it 'works' do
|
580
|
+
record = RSpecModelManualDateTest.new( {
|
581
|
+
:data => Hoodoo::UUID.generate(),
|
582
|
+
:created_at => @now,
|
583
|
+
:updated_at => @now
|
584
|
+
} )
|
585
|
+
|
586
|
+
record.save!
|
587
|
+
|
588
|
+
3.times do
|
589
|
+
result = RSpecModelManualDateTest.manually_dated_update_in(
|
590
|
+
@context,
|
591
|
+
ident: record.uuid,
|
592
|
+
attributes: { 'data' => Hoodoo::UUID.generate() }
|
593
|
+
)
|
594
|
+
|
595
|
+
expect( result ).to_not be_nil
|
596
|
+
expect( result.persisted? ).to eq( true )
|
597
|
+
end
|
598
|
+
|
599
|
+
result = RSpecModelManualDateTest.manually_dated_destruction_in(
|
600
|
+
@context,
|
601
|
+
ident: record.uuid
|
602
|
+
)
|
603
|
+
|
604
|
+
expect( result ).to_not be_nil
|
605
|
+
|
606
|
+
# We start with one current record, but it gets updated three times,
|
607
|
+
# creating three history entries. Then it gets deleted, creating
|
608
|
+
# another history entry and leaving no current ones.
|
609
|
+
#
|
610
|
+
# Remember that the very top of this file seeds in 2 contemporary and
|
611
|
+
# 4 historical entries, so counts are relative to that baseline.
|
612
|
+
|
613
|
+
expect( RSpecModelManualDateTest.manually_dated_contemporary.count ).to eq( 2 )
|
614
|
+
expect( RSpecModelManualDateTest.manually_dated_historic.count ).to eq( 8 )
|
615
|
+
expect( RSpecModelManualDateTest.manually_dated_contemporary.where( :uuid => record.uuid ).count ).to eq( 0 )
|
616
|
+
expect( RSpecModelManualDateTest.manually_dated_historic.where( :uuid => record.uuid ).count ).to eq( 4 )
|
617
|
+
end
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
context 'concurrent' do
|
622
|
+
before :all do
|
623
|
+
DatabaseCleaner.strategy = :truncation
|
624
|
+
end
|
625
|
+
|
626
|
+
after :all do
|
627
|
+
DatabaseCleaner.strategy = DATABASE_CLEANER_STRATEGY # spec_helper.rb
|
628
|
+
end
|
629
|
+
|
630
|
+
before :each do
|
631
|
+
@uuids = []
|
632
|
+
@results = {}
|
633
|
+
@mutex = Mutex.new
|
634
|
+
|
635
|
+
# From a pool of UUIDs, create a bunch of records.
|
636
|
+
|
637
|
+
50.times { @uuids << Hoodoo::UUID.generate }
|
638
|
+
@uuids.uniq! # Just in case I should've entered the lottery this week
|
639
|
+
|
640
|
+
@uuids.each do | uuid |
|
641
|
+
RSpecModelManualDateTest.new( {
|
642
|
+
:uuid => uuid,
|
643
|
+
:data => '0'
|
644
|
+
} ).save!
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
def add_result( uuid, result )
|
649
|
+
@mutex.synchronize do
|
650
|
+
@results[ uuid ] ||= []
|
651
|
+
@results[ uuid ] << result
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
# - Start threads that update the records with values, one update per thread
|
656
|
+
# - Check that the combined unscoped set has all integers and nothing more
|
657
|
+
# - Check there is only one contemporary entry per UUID and num-ints-minus-one
|
658
|
+
# history entries
|
659
|
+
#
|
660
|
+
it 'updates work' do
|
661
|
+
values = ( '1'..'9' ).to_a
|
662
|
+
threads = []
|
663
|
+
|
664
|
+
@uuids.each do | uuid |
|
665
|
+
values.each do | value |
|
666
|
+
threads << Thread.new do
|
667
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
668
|
+
sleep 0.001 # Force Thread scheduler to run
|
669
|
+
|
670
|
+
result = RSpecModelManualDateTest.manually_dated_update_in(
|
671
|
+
@context,
|
672
|
+
ident: uuid,
|
673
|
+
attributes: { 'data' => value }
|
674
|
+
)
|
675
|
+
|
676
|
+
add_result( uuid, result )
|
677
|
+
end
|
678
|
+
end
|
679
|
+
end
|
680
|
+
end
|
681
|
+
|
682
|
+
threads.each { | thread | thread.join() }
|
683
|
+
|
684
|
+
@uuids.each do | uuid |
|
685
|
+
contemporary = RSpecModelManualDateTest.manually_dated_contemporary.where( :uuid => uuid ).to_a
|
686
|
+
historic = RSpecModelManualDateTest.manually_dated_historic.where( :uuid => uuid ).to_a
|
687
|
+
|
688
|
+
expect( contemporary.count ).to eq( 1 )
|
689
|
+
expect( historic.count ).to eq( values.count )
|
690
|
+
|
691
|
+
# Across all records, the starting data value of "0" plus any
|
692
|
+
# new items in "values" should be present exactly once.
|
693
|
+
|
694
|
+
combined = ( contemporary + historic ).map( & :data )
|
695
|
+
expect( combined ).to match_array( [ '0' ] + values )
|
696
|
+
|
697
|
+
# All results should be persisted model instances which, once
|
698
|
+
# reloaded, only have one contemporary entry and the rest historic.
|
699
|
+
# This double-checks the database query tests a few lines above.
|
700
|
+
|
701
|
+
results = @results[ uuid ]
|
702
|
+
|
703
|
+
results.each do | result |
|
704
|
+
expect( result ).to be_a( RSpecModelManualDateTest )
|
705
|
+
expect( result.errors ).to be_empty
|
706
|
+
expect( result.persisted? ).to eq( true )
|
707
|
+
|
708
|
+
result.reload
|
709
|
+
end
|
710
|
+
|
711
|
+
dates = results.map( & :effective_end )
|
712
|
+
eots = dates.select() { | date | date == @eot }
|
713
|
+
others = dates.reject() { | date | date == @eot }
|
714
|
+
|
715
|
+
# "- 1" because the results Hash only contains results of the
|
716
|
+
# *updates* we did, not the original starting record.
|
717
|
+
|
718
|
+
expect( eots.count ).to eq( 1 )
|
719
|
+
expect( others.count ).to eq( values.count - 1 )
|
720
|
+
end
|
721
|
+
end
|
722
|
+
|
723
|
+
# - Start several threads that delete the same records
|
724
|
+
# - Push results into the results hash
|
725
|
+
# - Check that only one thread succeeded, rest of them got 'nil' (not found),
|
726
|
+
# only one history entry per UUID, no contemporary entry per UUID.
|
727
|
+
#
|
728
|
+
it 'deletions work' do
|
729
|
+
threads = []
|
730
|
+
attempts = 10
|
731
|
+
|
732
|
+
@uuids.each do | uuid |
|
733
|
+
attempts.times do
|
734
|
+
threads << Thread.new do
|
735
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
736
|
+
result = RSpecModelManualDateTest.manually_dated_destruction_in(
|
737
|
+
@context,
|
738
|
+
ident: uuid
|
739
|
+
)
|
740
|
+
|
741
|
+
add_result( uuid, result )
|
742
|
+
end
|
743
|
+
end
|
744
|
+
end
|
745
|
+
end
|
746
|
+
|
747
|
+
threads.each { | thread | thread.join() }
|
748
|
+
|
749
|
+
@uuids.each do | uuid |
|
750
|
+
contemporary = RSpecModelManualDateTest.manually_dated_contemporary.where( :uuid => uuid ).to_a
|
751
|
+
historic = RSpecModelManualDateTest.manually_dated_historic.where( :uuid => uuid ).to_a
|
752
|
+
|
753
|
+
expect( contemporary.count ).to eq( 0 )
|
754
|
+
expect( historic.count ).to eq( 1 )
|
755
|
+
|
756
|
+
# We expect all results to contain one success and lots of failures.
|
757
|
+
|
758
|
+
results = @results[ uuid ]
|
759
|
+
failures = results.select() { | result | result.nil? }
|
760
|
+
successes = results.reject() { | result | result.nil? }
|
761
|
+
|
762
|
+
expect( failures.count ).to eq( attempts - 1 )
|
763
|
+
expect( successes.count ).to eq( 1 )
|
764
|
+
|
765
|
+
success = successes.first
|
766
|
+
success.reload
|
767
|
+
|
768
|
+
expect( success ).to be_a( RSpecModelManualDateTest )
|
769
|
+
expect( success.errors ).to be_empty
|
770
|
+
expect( success.persisted? ).to eq( true )
|
771
|
+
expect( success.effective_end ).to_not eq( @eot )
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
end
|
776
|
+
end
|