Massive cleanup of transaction handlers
[dbsrgits/DBIx-Class.git] / t / storage / txn.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 txn_do() @_ aliasing support
67 {
68   my $res = 'original';
69   $schema->storage->txn_do (sub { $_[0] = 'changed' }, $res);
70   is ($res, 'changed', "Arguments properly aliased for txn_do");
71 }
72
73 # Test nested successful txn_do()
74 {
75   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
76
77   my $nested_code = sub {
78     my ($schema, $artist, $code) = @_;
79
80     my @titles1 = map {'nested txn_do test CD ' . $_} (1..5);
81     my @titles2 = map {'nested txn_do test CD ' . $_} (6..10);
82
83     $schema->txn_do($code, $artist, @titles1);
84     $schema->txn_do($code, $artist, @titles2);
85   };
86
87   my $artist = $schema->resultset('Artist')->find(2);
88   my $count_before = $artist->cds->count;
89
90   lives_ok (sub {
91     $schema->txn_do($nested_code, $schema, $artist, $code);
92   }, 'nested txn_do succeeded');
93
94   is($artist->cds({
95     title => 'nested txn_do test CD '.$_,
96   })->first->year, 2006, qq{nested txn_do CD$_ year ok}) for (1..10);
97   is($artist->cds->count, $count_before+10, 'nested txn_do added all CDs');
98
99   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
100 }
101
102 # test nested txn_begin on fresh connection
103 {
104   my $schema = DBICTest->init_schema(sqlite_use_file => 1, no_deploy => 1);
105   $schema->storage->ensure_connected;
106
107   is ($schema->storage->transaction_depth, 0, 'Start outside txn');
108
109   my @pids;
110   for my $action (
111     sub {
112       my $s = shift;
113       die "$$ starts in txn!" if $s->storage->transaction_depth != 0;
114       $s->txn_do ( sub {
115         die "$$ not in txn!" if $s->storage->transaction_depth == 0;
116         $s->storage->dbh->do('SELECT 1') } 
117       );
118       die "$$ did not finish txn!" if $s->storage->transaction_depth != 0;
119     },
120     sub {
121       $_[0]->txn_begin;
122       $_[0]->storage->dbh->do('SELECT 1');
123       $_[0]->txn_commit
124     },
125     sub {
126       my $guard = $_[0]->txn_scope_guard;
127       $_[0]->storage->dbh->do('SELECT 1');
128       $guard->commit
129     },
130   ) {
131     push @pids, fork();
132     die "Unable to fork: $!\n"
133       if ! defined $pids[-1];
134
135     if ($pids[-1]) {
136       next;
137     }
138
139     $action->($schema);
140     exit 0;
141   }
142
143   is ($schema->storage->transaction_depth, 0, 'Parent still outside txn');
144
145   for my $pid (@pids) {
146     waitpid ($pid, 0);
147     ok (! $?, "Child $pid exit ok");
148   }
149 }
150
151 # Test txn_do/scope_guard with forking: outer txn_do
152 {
153   my $schema = DBICTest->init_schema( sqlite_use_file => 1 );
154
155   for my $pass (1..2) {
156
157     # do something trying to destabilize the depth count
158     for (1..2) {
159       eval {
160         my $guard = $schema->txn_scope_guard;
161         $schema->txn_do( sub { die } );
162       };
163       $schema->txn_do( sub {
164         ok ($schema->storage->_dbh->do ('SELECT 1'), "Query after exceptions ok ($_)");
165       });
166     }
167
168     for my $pid ( $schema->txn_do ( sub { _forking_action ($schema) } ) ) {
169       waitpid ($pid, 0);
170       ok (! $?, "Child $pid exit ok (pass $pass)");
171       isa_ok ($schema->resultset ('Artist')->find ({ name => "forking action $pid" }), 'DBIx::Class::Row');
172     }
173   }
174 }
175
176 # same test with outer guard
177 {
178   my $schema = DBICTest->init_schema( sqlite_use_file => 1 );
179
180   for my $pass (1..2) {
181
182     # do something trying to destabilize the depth count
183     for (1..2) {
184       eval {
185         my $guard = $schema->txn_scope_guard;
186         $schema->txn_do( sub { die } );
187       };
188       $schema->txn_do( sub {
189         ok ($schema->storage->_dbh->do ('SELECT 1'), "Query after exceptions ok ($_)");
190       });
191     }
192
193     my @pids;
194     my $guard = $schema->txn_scope_guard;
195     _forking_action ($schema);
196     $guard->commit;
197
198     for my $pid (@pids) {
199       waitpid ($pid, 0);
200       ok (! $?, "Child $pid exit ok (pass $pass)");
201       isa_ok ($schema->resultset ('Artist')->find ({ name => "forking action $pid" }), 'DBIx::Class::Row');
202     }
203   }
204 }
205
206 sub _forking_action {
207   my $schema = shift;
208
209   my @pids;
210   while (@pids < 5) {
211
212     push @pids, fork();
213     die "Unable to fork: $!\n"
214       if ! defined $pids[-1];
215
216     if ($pids[-1]) {
217       next;
218     }
219
220     if (@pids % 2) {
221       $schema->txn_do (sub {
222         my $depth = $schema->storage->transaction_depth;
223         die "$$(txn_do)unexpected txn depth $depth!" if $depth != 1;
224         $schema->resultset ('Artist')->create ({ name => "forking action $$"});
225       });
226     }
227     else {
228       my $guard = $schema->txn_scope_guard;
229       my $depth = $schema->storage->transaction_depth;
230       die "$$(scope_guard) unexpected txn depth $depth!" if $depth != 1;
231       $schema->resultset ('Artist')->create ({ name => "forking action $$"});
232       $guard->commit;
233     }
234
235     exit 0;
236   }
237
238   return @pids;
239 }
240
241 my $fail_code = sub {
242   my ($artist) = @_;
243   $artist->create_related('cds', {
244     title => 'this should not exist',
245     year => 2005,
246   });
247   die "the sky is falling";
248 };
249
250 # Test failed txn_do()
251 {
252
253   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
254
255   my $artist = $schema->resultset('Artist')->find(3);
256
257   throws_ok (sub {
258     $schema->txn_do($fail_code, $artist);
259   }, qr/the sky is falling/, 'failed txn_do threw an exception');
260
261   my $cd = $artist->cds({
262     title => 'this should not exist',
263     year => 2005,
264   })->first;
265   ok(!defined($cd), q{failed txn_do didn't change the cds table});
266
267   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
268 }
269
270 # do the same transaction again
271 {
272   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
273
274   my $artist = $schema->resultset('Artist')->find(3);
275
276   throws_ok (sub {
277     $schema->txn_do($fail_code, $artist);
278   }, qr/the sky is falling/, 'failed txn_do threw an exception');
279
280   my $cd = $artist->cds({
281     title => 'this should not exist',
282     year => 2005,
283   })->first;
284   ok(!defined($cd), q{failed txn_do didn't change the cds table});
285
286   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
287 }
288
289 # Test failed txn_do() with failed rollback
290 {
291   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
292
293   my $artist = $schema->resultset('Artist')->find(3);
294
295   # Force txn_rollback() to throw an exception
296   no warnings 'redefine';
297   no strict 'refs';
298
299   # die in rollback
300   local *{"DBIx::Class::Storage::DBI::SQLite::txn_rollback"} = sub{
301     my $storage = shift;
302     die 'FAILED';
303   };
304
305   throws_ok (
306     sub {
307       $schema->txn_do($fail_code, $artist);
308     },
309     qr/the sky is falling.+Rollback failed/s,
310     'txn_rollback threw a rollback exception (and included the original exception'
311   );
312
313   my $cd = $artist->cds({
314     title => 'this should not exist',
315     year => 2005,
316   })->first;
317   isa_ok($cd, 'DBICTest::CD', q{failed txn_do with a failed txn_rollback }.
318          q{changed the cds table});
319   $cd->delete; # Rollback failed
320   $cd = $artist->cds({
321     title => 'this should not exist',
322     year => 2005,
323   })->first;
324   ok(!defined($cd), q{deleted the failed txn's cd});
325   $schema->storage->_dbh->rollback;
326
327 }
328
329 # reset schema object (the txn_rollback meddling screws it up)
330 undef $schema;
331
332 # Test nested failed txn_do()
333 {
334   my $schema = DBICTest->init_schema();
335
336   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
337
338   my $nested_fail_code = sub {
339     my ($schema, $artist, $code1, $code2) = @_;
340
341     my @titles = map {'nested txn_do test CD ' . $_} (1..5);
342
343     $schema->txn_do($code1, $artist, @titles); # successful txn
344     $schema->txn_do($code2, $artist);          # failed txn
345   };
346
347   my $artist = $schema->resultset('Artist')->find(3);
348
349   throws_ok ( sub {
350     $schema->txn_do($nested_fail_code, $schema, $artist, $code, $fail_code);
351   }, qr/the sky is falling/, 'nested failed txn_do threw exception');
352
353   ok(!defined($artist->cds({
354     title => 'nested txn_do test CD '.$_,
355     year => 2006,
356   })->first), qq{failed txn_do didn't add first txn's cd $_}) for (1..5);
357   my $cd = $artist->cds({
358     title => 'this should not exist',
359     year => 2005,
360   })->first;
361   ok(!defined($cd), q{failed txn_do didn't add failed txn's cd});
362 }
363
364 # Grab a new schema to test txn before connect
365 {
366   my $schema = DBICTest->init_schema(no_deploy => 1);
367   lives_ok (sub {
368     $schema->txn_begin();
369     $schema->txn_begin();
370   }, 'Pre-connection nested transactions.');
371
372   # although not connected DBI would still warn about rolling back at disconnect
373   $schema->txn_rollback;
374   $schema->txn_rollback;
375 }
376
377 # Test txn_scope_guard
378 {
379   my $schema = DBICTest->init_schema();
380
381   is($schema->storage->transaction_depth, 0, "Correct transaction depth");
382   my $artist_rs = $schema->resultset('Artist');
383
384   my $fn = __FILE__;
385   throws_ok {
386    my $guard = $schema->txn_scope_guard;
387
388     $artist_rs->create({
389       name => 'Death Cab for Cutie',
390       made_up_column => 1,
391     });
392
393    $guard->commit;
394   } qr/No such column made_up_column .*? at .*?$fn line \d+/s, "Error propogated okay";
395
396   ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
397
398   my $inner_exception = '';  # set in inner() below
399   throws_ok (sub {
400     outer($schema, 1);
401   }, qr/$inner_exception/, "Nested exceptions propogated");
402
403   ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
404
405   lives_ok (sub {
406
407     # this weird assignment is to stop perl <= 5.8.9 leaking $schema on nested sub{}s
408     my $s = $schema;
409
410     warnings_exist ( sub {
411       # The 0 arg says don't die, just let the scope guard go out of scope
412       # forcing a txn_rollback to happen
413       outer($s, 0);
414     }, qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./, 'Out of scope warning detected');
415
416     ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
417
418   }, 'rollback successful withot exception');
419
420   sub outer {
421     my ($schema, $fatal) = @_;
422
423     my $guard = $schema->txn_scope_guard;
424     $schema->resultset('Artist')->create({
425       name => 'Death Cab for Cutie',
426     });
427     inner($schema, $fatal);
428   }
429
430   sub inner {
431     my ($schema, $fatal) = @_;
432
433     my $inner_guard = $schema->txn_scope_guard;
434     is($schema->storage->transaction_depth, 2, "Correct transaction depth");
435
436     my $artist = $schema->resultset('Artist')->find({ name => 'Death Cab for Cutie' });
437
438     eval {
439       $artist->cds->create({
440         title => 'Plans',
441         year => 2005,
442         $fatal ? ( foo => 'bar' ) : ()
443       });
444     };
445     if ($@) {
446       # Record what got thrown so we can test it propgates out properly.
447       $inner_exception = $@;
448       die $@;
449     }
450
451     # inner guard should commit without consequences
452     $inner_guard->commit;
453   }
454 }
455
456 # make sure the guard does not eat exceptions
457 {
458   my $schema = DBICTest->init_schema;
459
460   no strict 'refs';
461   no warnings 'redefine';
462   local *{DBIx::Class::Storage::DBI::txn_rollback} = sub { die 'die die my darling' };
463
464   throws_ok (sub {
465     my $guard = $schema->txn_scope_guard;
466     $schema->resultset ('Artist')->create ({ name => 'bohhoo'});
467
468     # this should freak out the guard rollback
469     # but it won't work because DBD::SQLite is buggy
470     # instead just install a toxic rollback above
471     #$schema->storage->_dbh( $schema->storage->_dbh->clone );
472
473     die 'Deliberate exception';
474   }, qr/Deliberate exception.+Rollback failed/s);
475 }
476
477 # make sure it warns *big* on failed rollbacks
478 {
479   my $schema = DBICTest->init_schema();
480
481   no strict 'refs';
482   no warnings 'redefine';
483   local *{DBIx::Class::Storage::DBI::txn_rollback} = sub { die 'die die my darling' };
484
485 #The warn from within a DESTROY callback freaks out Test::Warn, do it old-school
486 =begin
487   warnings_exist (
488     sub {
489       my $guard = $schema->txn_scope_guard;
490       $schema->resultset ('Artist')->create ({ name => 'bohhoo'});
491
492       # this should freak out the guard rollback
493       # but it won't work because DBD::SQLite is buggy
494       # instead just install a toxic rollback above
495       #$schema->storage->_dbh( $schema->storage->_dbh->clone );
496     },
497     [
498       qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./,
499       qr/\*+ ROLLBACK FAILED\!\!\! \*+/,
500     ],
501     'proper warnings generated on out-of-scope+rollback failure'
502   );
503 =cut
504
505 # delete this once the above works properly (same test)
506   my @want = (
507     qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./,
508     qr/\*+ ROLLBACK FAILED\!\!\! \*+/,
509   );
510
511   my @w;
512   local $SIG{__WARN__} = sub {
513     if (grep {$_[0] =~ $_} (@want)) {
514       push @w, $_[0];
515     }
516     else {
517       warn $_[0];
518     }
519   };
520   {
521       my $guard = $schema->txn_scope_guard;
522       $schema->resultset ('Artist')->create ({ name => 'bohhoo'});
523   }
524
525   is (@w, 2, 'Both expected warnings found');
526 }
527
528 # make sure AutoCommit => 0 on external handles behaves correctly with scope_guard
529 warnings_are {
530   my $factory = DBICTest->init_schema (AutoCommit => 0);
531   cmp_ok ($factory->resultset('CD')->count, '>', 0, 'Something to delete');
532   my $dbh = $factory->storage->dbh;
533
534   ok (!$dbh->{AutoCommit}, 'AutoCommit is off on $dbh');
535   my $schema = DBICTest::Schema->connect (sub { $dbh });
536
537   lives_ok ( sub {
538     my $guard = $schema->txn_scope_guard;
539     $schema->resultset('CD')->delete;
540     $guard->commit;
541   }, 'No attempt to start a transaction with scope guard');
542
543   is ($schema->resultset('CD')->count, 0, 'Deletion successful in txn');
544
545   # this will commit the implicitly started txn
546   $dbh->commit;
547
548 } [], 'No warnings on AutoCommit => 0 with txn_guard';
549
550 # make sure AutoCommit => 0 on external handles behaves correctly with txn_do
551 warnings_are {
552   my $factory = DBICTest->init_schema (AutoCommit => 0);
553   cmp_ok ($factory->resultset('CD')->count, '>', 0, 'Something to delete');
554   my $dbh = $factory->storage->dbh;
555
556   ok (!$dbh->{AutoCommit}, 'AutoCommit is off on $dbh');
557   my $schema = DBICTest::Schema->connect (sub { $dbh });
558
559
560   lives_ok ( sub {
561     $schema->txn_do (sub { $schema->resultset ('CD')->delete });
562   }, 'No attempt to start a atransaction with txn_do');
563
564   is ($schema->resultset('CD')->count, 0, 'Deletion successful');
565
566   # this will commit the implicitly started txn
567   $dbh->commit;
568
569 } [], 'No warnings on AutoCommit => 0 with txn_do';
570
571 done_testing;