Merge 'trunk' into 'replication_dedux'
[dbsrgits/DBIx-Class.git] / t / 81transactions.t
1 use strict;
2 use warnings;  
3
4 use Test::More;
5 use Test::Exception;
6 use lib qw(t/lib);
7 use DBICTest;
8
9 my $schema = DBICTest->init_schema();
10
11 plan tests => 67;
12
13 my $code = sub {
14   my ($artist, @cd_titles) = @_;
15   
16   $artist->create_related('cds', {
17     title => $_,
18     year => 2006,
19   }) foreach (@cd_titles);
20   
21   return $artist->cds->all;
22 };
23
24 # Test checking of parameters
25 {
26   eval {
27     (ref $schema)->txn_do(sub{});
28   };
29   like($@, qr/storage/, "can't call txn_do without storage");
30   eval {
31     $schema->txn_do('');
32   };
33   like($@, qr/must be a CODE reference/, '$coderef parameter check ok');
34 }
35
36 # Test successful txn_do() - scalar context
37 {
38   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
39
40   my @titles = map {'txn_do test CD ' . $_} (1..5);
41   my $artist = $schema->resultset('Artist')->find(1);
42   my $count_before = $artist->cds->count;
43   my $count_after = $schema->txn_do($code, $artist, @titles);
44   is($count_after, $count_before+5, 'successful txn added 5 cds');
45   is($artist->cds({
46     title => "txn_do test CD $_",
47   })->first->year, 2006, "new CD $_ year correct") for (1..5);
48
49   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
50 }
51
52 # Test successful txn_do() - list context
53 {
54   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
55
56   my @titles = map {'txn_do test CD ' . $_} (6..10);
57   my $artist = $schema->resultset('Artist')->find(1);
58   my $count_before = $artist->cds->count;
59   my @cds = $schema->txn_do($code, $artist, @titles);
60   is(scalar @cds, $count_before+5, 'added 5 CDs and returned in list context');
61   is($artist->cds({
62     title => "txn_do test CD $_",
63   })->first->year, 2006, "new CD $_ year correct") for (6..10);
64
65   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
66 }
67
68 # Test nested successful txn_do()
69 {
70   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
71
72   my $nested_code = sub {
73     my ($schema, $artist, $code) = @_;
74
75     my @titles1 = map {'nested txn_do test CD ' . $_} (1..5);
76     my @titles2 = map {'nested txn_do test CD ' . $_} (6..10);
77
78     $schema->txn_do($code, $artist, @titles1);
79     $schema->txn_do($code, $artist, @titles2);
80   };
81
82   my $artist = $schema->resultset('Artist')->find(2);
83   my $count_before = $artist->cds->count;
84
85   eval {
86     $schema->txn_do($nested_code, $schema, $artist, $code);
87   };
88
89   my $error = $@;
90
91   ok(!$error, 'nested txn_do succeeded');
92   is($artist->cds({
93     title => 'nested txn_do test CD '.$_,
94   })->first->year, 2006, qq{nested txn_do CD$_ year ok}) for (1..10);
95   is($artist->cds->count, $count_before+10, 'nested txn_do added all CDs');
96
97   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
98 }
99
100 my $fail_code = sub {
101   my ($artist) = @_;
102   $artist->create_related('cds', {
103     title => 'this should not exist',
104     year => 2005,
105   });
106   die "the sky is falling";
107 };
108
109 # Test failed txn_do()
110 {
111
112   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
113
114   my $artist = $schema->resultset('Artist')->find(3);
115
116   eval {
117     $schema->txn_do($fail_code, $artist);
118   };
119
120   my $error = $@;
121
122   like($error, qr/the sky is falling/, 'failed txn_do threw an exception');
123   my $cd = $artist->cds({
124     title => 'this should not exist',
125     year => 2005,
126   })->first;
127   ok(!defined($cd), q{failed txn_do didn't change the cds table});
128
129   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
130 }
131
132 # do the same transaction again
133 {
134   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
135
136   my $artist = $schema->resultset('Artist')->find(3);
137
138   eval {
139     $schema->txn_do($fail_code, $artist);
140   };
141
142   my $error = $@;
143
144   like($error, qr/the sky is falling/, 'failed txn_do threw an exception');
145   my $cd = $artist->cds({
146     title => 'this should not exist',
147     year => 2005,
148   })->first;
149   ok(!defined($cd), q{failed txn_do didn't change the cds table});
150
151   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
152 }
153
154 # Test failed txn_do() with failed rollback
155 {
156   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
157
158   my $artist = $schema->resultset('Artist')->find(3);
159
160   # Force txn_rollback() to throw an exception
161   no warnings 'redefine';
162   no strict 'refs';
163
164   # die in rollback, but maintain sanity for further tests ...
165   local *{"DBIx::Class::Storage::DBI::SQLite::txn_rollback"} = sub{
166     my $storage = shift;
167     $storage->{transaction_depth}--;
168     die 'FAILED';
169   };
170
171   eval {
172     $schema->txn_do($fail_code, $artist);
173   };
174
175   my $error = $@;
176
177   like($error, qr/Rollback failed/, 'failed txn_do with a failed '.
178        'txn_rollback threw a rollback exception');
179   like($error, qr/the sky is falling/, 'failed txn_do with a failed '.
180        'txn_rollback included the original exception');
181
182   my $cd = $artist->cds({
183     title => 'this should not exist',
184     year => 2005,
185   })->first;
186   isa_ok($cd, 'DBICTest::CD', q{failed txn_do with a failed txn_rollback }.
187          q{changed the cds table});
188   $cd->delete; # Rollback failed
189   $cd = $artist->cds({
190     title => 'this should not exist',
191     year => 2005,
192   })->first;
193   ok(!defined($cd), q{deleted the failed txn's cd});
194   $schema->storage->_dbh->rollback;
195 }
196
197 # Test nested failed txn_do()
198 {
199   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
200
201   my $nested_fail_code = sub {
202     my ($schema, $artist, $code1, $code2) = @_;
203
204     my @titles = map {'nested txn_do test CD ' . $_} (1..5);
205
206     $schema->txn_do($code1, $artist, @titles); # successful txn
207     $schema->txn_do($code2, $artist);          # failed txn
208   };
209
210   my $artist = $schema->resultset('Artist')->find(3);
211
212   eval {
213     $schema->txn_do($nested_fail_code, $schema, $artist, $code, $fail_code);
214   };
215
216   my $error = $@;
217
218   like($error, qr/the sky is falling/, 'nested failed txn_do threw exception');
219   ok(!defined($artist->cds({
220     title => 'nested txn_do test CD '.$_,
221     year => 2006,
222   })->first), qq{failed txn_do didn't add first txn's cd $_}) for (1..5);
223   my $cd = $artist->cds({
224     title => 'this should not exist',
225     year => 2005,
226   })->first;
227   ok(!defined($cd), q{failed txn_do didn't add failed txn's cd});
228 }
229
230 # Grab a new schema to test txn before connect
231 {
232     my $schema2 = DBICTest->init_schema(no_deploy => 1);
233     eval {
234         $schema2->txn_begin();
235         $schema2->txn_begin();
236     };
237     my $err = $@;
238     ok(($err eq ''), 'Pre-connection nested transactions.');
239 }
240
241 # Test txn_rollback with nested
242 {
243   local $TODO = "Work out how this should work";
244   my $local_schema = DBICTest->init_schema();
245
246   my $artist_rs = $local_schema->resultset('Artist');
247   throws_ok {
248    
249     $local_schema->txn_begin;
250     $artist_rs->create({ name => 'Test artist rollback 1'});
251     $local_schema->txn_begin;
252     is($local_schema->storage->transaction_depth, 2, "Correct transaction depth");
253     $artist_rs->create({ name => 'Test artist rollback 2'});
254     $local_schema->txn_rollback;
255   } qr/Not sure what this should be.... something tho/, "Rolled back okay";
256   is($local_schema->storage->transaction_depth, 0, "Correct transaction depth");
257
258   ok(!$artist_rs->find({ name => 'Test artist rollback 1'}), "Test Artist not created")
259     || $artist_rs->find({ name => 'Test artist rollback 1'})->delete;
260 }
261
262 # Test txn_scope_guard
263 {
264   local $TODO = "Work out how this should work";
265   my $schema = DBICTest->init_schema();
266
267   is($schema->storage->transaction_depth, 0, "Correct transaction depth");
268   my $artist_rs = $schema->resultset('Artist');
269   throws_ok {
270    my $guard = $schema->txn_scope_guard;
271
272
273     $artist_rs->create({
274       name => 'Death Cab for Cutie',
275       made_up_column => 1,
276     });
277     
278    $guard->commit;
279   } qr/No such column made_up_column.*?line 16/, "Error propogated okay";
280
281   ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
282
283   my $inner_exception;
284   eval {
285     outer($schema, 1);
286   };
287   is($@, $inner_exception, "Nested exceptions propogated");
288
289   ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
290
291
292   eval {
293     # The 0 arg says done die, just let the scope guard go out of scope 
294     # forcing a txn_rollback to happen
295     outer($schema, 0);
296   };
297   is($@, "Not sure what we want here, but something", "Rollback okay");
298
299   ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
300
301   sub outer {
302     my ($schema) = @_;
303    
304     my $guard = $schema->txn_scope_guard;
305     $schema->resultset('Artist')->create({
306       name => 'Death Cab for Cutie',
307     });
308     inner(@_);
309     $guard->commit;
310   }
311
312   sub inner {
313     my ($schema, $fatal) = @_;
314     my $guard = $schema->txn_scope_guard;
315
316     my $artist = $artist_rs->find({ name => 'Death Cab for Cutie' });
317
318     is($schema->storage->transaction_depth, 2, "Correct transaction depth");
319     undef $@;
320     eval {
321       $artist->cds->create({ 
322         title => 'Plans',
323         year => 2005, 
324         $fatal ? ( foo => 'bar' ) : ()
325       });
326     };
327     if ($@) {
328       # Record what got thrown so we can test it propgates out properly.
329       $inner_exception = $@;
330       die $@;
331     }
332
333     # See what happens if we dont $guard->commit;
334   }
335 }