a13c651f383a2cae282af9b01fb8036cacee237c
[dbsrgits/DBIx-Class.git] / t / 81transactions.t
1 use strict;
2 use warnings;
3
4 use Test::More;
5 use Test::Warn;
6 use Test::Exception;
7 use lib qw(t/lib);
8 use DBICTest;
9
10 my $schema = DBICTest->init_schema();
11
12 my $code = sub {
13   my ($artist, @cd_titles) = @_;
14
15   $artist->create_related('cds', {
16     title => $_,
17     year => 2006,
18   }) foreach (@cd_titles);
19
20   return $artist->cds->all;
21 };
22
23 # Test checking of parameters
24 {
25   throws_ok (sub {
26     (ref $schema)->txn_do(sub{});
27   }, qr/storage/, "can't call txn_do without storage");
28
29   throws_ok ( sub {
30     $schema->txn_do('');
31   }, qr/must be a CODE reference/, '$coderef parameter check ok');
32 }
33
34 # Test successful txn_do() - scalar context
35 {
36   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
37
38   my @titles = map {'txn_do test CD ' . $_} (1..5);
39   my $artist = $schema->resultset('Artist')->find(1);
40   my $count_before = $artist->cds->count;
41   my $count_after = $schema->txn_do($code, $artist, @titles);
42   is($count_after, $count_before+5, 'successful txn added 5 cds');
43   is($artist->cds({
44     title => "txn_do test CD $_",
45   })->first->year, 2006, "new CD $_ year correct") for (1..5);
46
47   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
48 }
49
50 # Test successful txn_do() - list context
51 {
52   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
53
54   my @titles = map {'txn_do test CD ' . $_} (6..10);
55   my $artist = $schema->resultset('Artist')->find(1);
56   my $count_before = $artist->cds->count;
57   my @cds = $schema->txn_do($code, $artist, @titles);
58   is(scalar @cds, $count_before+5, 'added 5 CDs and returned in list context');
59   is($artist->cds({
60     title => "txn_do test CD $_",
61   })->first->year, 2006, "new CD $_ year correct") for (6..10);
62
63   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
64 }
65
66 # Test nested successful txn_do()
67 {
68   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
69
70   my $nested_code = sub {
71     my ($schema, $artist, $code) = @_;
72
73     my @titles1 = map {'nested txn_do test CD ' . $_} (1..5);
74     my @titles2 = map {'nested txn_do test CD ' . $_} (6..10);
75
76     $schema->txn_do($code, $artist, @titles1);
77     $schema->txn_do($code, $artist, @titles2);
78   };
79
80   my $artist = $schema->resultset('Artist')->find(2);
81   my $count_before = $artist->cds->count;
82
83   lives_ok (sub {
84     $schema->txn_do($nested_code, $schema, $artist, $code);
85   }, 'nested txn_do succeeded');
86
87   is($artist->cds({
88     title => 'nested txn_do test CD '.$_,
89   })->first->year, 2006, qq{nested txn_do CD$_ year ok}) for (1..10);
90   is($artist->cds->count, $count_before+10, 'nested txn_do added all CDs');
91
92   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
93 }
94
95 my $fail_code = sub {
96   my ($artist) = @_;
97   $artist->create_related('cds', {
98     title => 'this should not exist',
99     year => 2005,
100   });
101   die "the sky is falling";
102 };
103
104 # Test failed txn_do()
105 {
106
107   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
108
109   my $artist = $schema->resultset('Artist')->find(3);
110
111   throws_ok (sub {
112     $schema->txn_do($fail_code, $artist);
113   }, qr/the sky is falling/, 'failed txn_do threw an exception');
114
115   my $cd = $artist->cds({
116     title => 'this should not exist',
117     year => 2005,
118   })->first;
119   ok(!defined($cd), q{failed txn_do didn't change the cds table});
120
121   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
122 }
123
124 # do the same transaction again
125 {
126   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
127
128   my $artist = $schema->resultset('Artist')->find(3);
129
130   throws_ok (sub {
131     $schema->txn_do($fail_code, $artist);
132   }, qr/the sky is falling/, 'failed txn_do threw an exception');
133
134   my $cd = $artist->cds({
135     title => 'this should not exist',
136     year => 2005,
137   })->first;
138   ok(!defined($cd), q{failed txn_do didn't change the cds table});
139
140   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
141 }
142
143 # Test failed txn_do() with failed rollback
144 {
145   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
146
147   my $artist = $schema->resultset('Artist')->find(3);
148
149   # Force txn_rollback() to throw an exception
150   no warnings 'redefine';
151   no strict 'refs';
152
153   # die in rollback
154   local *{"DBIx::Class::Storage::DBI::SQLite::txn_rollback"} = sub{
155     my $storage = shift;
156     die 'FAILED';
157   };
158
159   throws_ok (
160     sub {
161       $schema->txn_do($fail_code, $artist);
162     },
163     qr/the sky is falling.+Rollback failed/s,
164     'txn_rollback threw a rollback exception (and included the original exception'
165   );
166
167   my $cd = $artist->cds({
168     title => 'this should not exist',
169     year => 2005,
170   })->first;
171   isa_ok($cd, 'DBICTest::CD', q{failed txn_do with a failed txn_rollback }.
172          q{changed the cds table});
173   $cd->delete; # Rollback failed
174   $cd = $artist->cds({
175     title => 'this should not exist',
176     year => 2005,
177   })->first;
178   ok(!defined($cd), q{deleted the failed txn's cd});
179   $schema->storage->_dbh->rollback;
180 }
181
182 # reset schema object (the txn_rollback meddling screws it up)
183 $schema = DBICTest->init_schema();
184
185 # Test nested failed txn_do()
186 {
187   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
188
189   my $nested_fail_code = sub {
190     my ($schema, $artist, $code1, $code2) = @_;
191
192     my @titles = map {'nested txn_do test CD ' . $_} (1..5);
193
194     $schema->txn_do($code1, $artist, @titles); # successful txn
195     $schema->txn_do($code2, $artist);          # failed txn
196   };
197
198   my $artist = $schema->resultset('Artist')->find(3);
199
200   throws_ok ( sub {
201     $schema->txn_do($nested_fail_code, $schema, $artist, $code, $fail_code);
202   }, qr/the sky is falling/, 'nested failed txn_do threw exception');
203
204   ok(!defined($artist->cds({
205     title => 'nested txn_do test CD '.$_,
206     year => 2006,
207   })->first), qq{failed txn_do didn't add first txn's cd $_}) for (1..5);
208   my $cd = $artist->cds({
209     title => 'this should not exist',
210     year => 2005,
211   })->first;
212   ok(!defined($cd), q{failed txn_do didn't add failed txn's cd});
213 }
214
215 # Grab a new schema to test txn before connect
216 {
217     my $schema2 = DBICTest->init_schema(no_deploy => 1);
218     lives_ok (sub {
219         $schema2->txn_begin();
220         $schema2->txn_begin();
221     }, 'Pre-connection nested transactions.');
222
223     # although not connected DBI would still warn about rolling back at disconnect
224     $schema2->txn_rollback;
225     $schema2->txn_rollback;
226     $schema2->storage->disconnect;
227 }
228 $schema->storage->disconnect;
229
230 # Test txn_scope_guard
231 {
232   my $schema = DBICTest->init_schema();
233
234   is($schema->storage->transaction_depth, 0, "Correct transaction depth");
235   my $artist_rs = $schema->resultset('Artist');
236   throws_ok {
237    my $guard = $schema->txn_scope_guard;
238
239
240     $artist_rs->create({
241       name => 'Death Cab for Cutie',
242       made_up_column => 1,
243     });
244
245    $guard->commit;
246   } qr/No such column made_up_column .*? at .*?81transactions.t line \d+/s, "Error propogated okay";
247
248   ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
249
250   my $inner_exception = '';  # set in inner() below
251   throws_ok (sub {
252     outer($schema, 1);
253   }, qr/$inner_exception/, "Nested exceptions propogated");
254
255   ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
256
257   lives_ok (sub {
258     warnings_exist ( sub {
259       # The 0 arg says don't die, just let the scope guard go out of scope
260       # forcing a txn_rollback to happen
261       outer($schema, 0);
262     }, qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./, 'Out of scope warning detected');
263     ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
264   }, 'rollback successful withot exception');
265
266   sub outer {
267     my ($schema) = @_;
268
269     my $guard = $schema->txn_scope_guard;
270     $schema->resultset('Artist')->create({
271       name => 'Death Cab for Cutie',
272     });
273     inner(@_);
274   }
275
276   sub inner {
277     my ($schema, $fatal) = @_;
278
279     my $inner_guard = $schema->txn_scope_guard;
280     is($schema->storage->transaction_depth, 2, "Correct transaction depth");
281
282     my $artist = $artist_rs->find({ name => 'Death Cab for Cutie' });
283
284     eval {
285       $artist->cds->create({
286         title => 'Plans',
287         year => 2005,
288         $fatal ? ( foo => 'bar' ) : ()
289       });
290     };
291     if ($@) {
292       # Record what got thrown so we can test it propgates out properly.
293       $inner_exception = $@;
294       die $@;
295     }
296
297     # inner guard should commit without consequences
298     $inner_guard->commit;
299   }
300 }
301
302 # make sure the guard does not eat exceptions
303 {
304   my $schema = DBICTest->init_schema();
305   throws_ok (sub {
306     my $guard = $schema->txn_scope_guard;
307     $schema->resultset ('Artist')->create ({ name => 'bohhoo'});
308
309     $schema->storage->disconnect;  # this should freak out the guard rollback
310
311     die 'Deliberate exception';
312   }, qr/Deliberate exception.+Rollback failed/s);
313 }
314
315 # make sure it warns *big* on failed rollbacks
316 {
317   my $schema = DBICTest->init_schema();
318
319   # something is really confusing Test::Warn here, no time to debug
320 =begin
321   warnings_exist (
322     sub {
323       my $guard = $schema->txn_scope_guard;
324       $schema->resultset ('Artist')->create ({ name => 'bohhoo'});
325
326       $schema->storage->disconnect;  # this should freak out the guard rollback
327     },
328     [
329       qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./,
330       qr/\*+ ROLLBACK FAILED\!\!\! \*+/,
331     ],
332     'proper warnings generated on out-of-scope+rollback failure'
333   );
334 =cut
335
336   my @want = (
337     qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./,
338     qr/\*+ ROLLBACK FAILED\!\!\! \*+/,
339   );
340
341   my @w;
342   local $SIG{__WARN__} = sub {
343     if (grep {$_[0] =~ $_} (@want)) {
344       push @w, $_[0];
345     }
346     else {
347       warn $_[0];
348     }
349   };
350   {
351       my $guard = $schema->txn_scope_guard;
352       $schema->resultset ('Artist')->create ({ name => 'bohhoo'});
353
354       $schema->storage->disconnect;  # this should freak out the guard rollback
355   }
356
357   is (@w, 2, 'Both expected warnings found');
358 }
359
360 # make sure AutoCommit => 0 on external handles behaves correctly with scope_guard
361 {
362   my $factory = DBICTest->init_schema (AutoCommit => 0);
363   cmp_ok ($factory->resultset('CD')->count, '>', 0, 'Something to delete');
364   my $dbh = $factory->storage->dbh;
365
366   ok (!$dbh->{AutoCommit}, 'AutoCommit is off on $dbh');
367   my $schema = DBICTest::Schema->connect (sub { $dbh });
368
369
370   lives_ok ( sub {
371     my $guard = $schema->txn_scope_guard;
372     $schema->resultset('CD')->delete;
373     $guard->commit;
374   }, 'No attempt to start a transaction with scope guard');
375
376   is ($schema->resultset('CD')->count, 0, 'Deletion successful');
377 }
378
379 # make sure AutoCommit => 0 on external handles behaves correctly with txn_do
380 {
381   my $factory = DBICTest->init_schema (AutoCommit => 0);
382   cmp_ok ($factory->resultset('CD')->count, '>', 0, 'Something to delete');
383   my $dbh = $factory->storage->dbh;
384
385   ok (!$dbh->{AutoCommit}, 'AutoCommit is off on $dbh');
386   my $schema = DBICTest::Schema->connect (sub { $dbh });
387
388
389   lives_ok ( sub {
390     $schema->txn_do (sub { $schema->resultset ('CD')->delete });
391   }, 'No attempt to start a atransaction with txn_do');
392
393   is ($schema->resultset('CD')->count, 0, 'Deletion successful');
394 }
395
396 done_testing;