Make test suite pass under DBICTEST_SQLITE_USE_FILE=1
[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 my $fail_code = sub {
103   my ($artist) = @_;
104   $artist->create_related('cds', {
105     title => 'this should not exist',
106     year => 2005,
107   });
108   die "the sky is falling";
109 };
110
111 # Test failed txn_do()
112 {
113
114   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
115
116   my $artist = $schema->resultset('Artist')->find(3);
117
118   throws_ok (sub {
119     $schema->txn_do($fail_code, $artist);
120   }, qr/the sky is falling/, 'failed txn_do threw an exception');
121
122   my $cd = $artist->cds({
123     title => 'this should not exist',
124     year => 2005,
125   })->first;
126   ok(!defined($cd), q{failed txn_do didn't change the cds table});
127
128   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
129 }
130
131 # do the same transaction again
132 {
133   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
134
135   my $artist = $schema->resultset('Artist')->find(3);
136
137   throws_ok (sub {
138     $schema->txn_do($fail_code, $artist);
139   }, qr/the sky is falling/, 'failed txn_do threw an exception');
140
141   my $cd = $artist->cds({
142     title => 'this should not exist',
143     year => 2005,
144   })->first;
145   ok(!defined($cd), q{failed txn_do didn't change the cds table});
146
147   is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
148 }
149
150 # Test failed txn_do() with failed rollback
151 {
152   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
153
154   my $artist = $schema->resultset('Artist')->find(3);
155
156   # Force txn_rollback() to throw an exception
157   no warnings 'redefine';
158   no strict 'refs';
159
160   # die in rollback
161   local *{"DBIx::Class::Storage::DBI::SQLite::txn_rollback"} = sub{
162     my $storage = shift;
163     die 'FAILED';
164   };
165
166   throws_ok (
167     sub {
168       $schema->txn_do($fail_code, $artist);
169     },
170     qr/the sky is falling.+Rollback failed/s,
171     'txn_rollback threw a rollback exception (and included the original exception'
172   );
173
174   my $cd = $artist->cds({
175     title => 'this should not exist',
176     year => 2005,
177   })->first;
178   isa_ok($cd, 'DBICTest::CD', q{failed txn_do with a failed txn_rollback }.
179          q{changed the cds table});
180   $cd->delete; # Rollback failed
181   $cd = $artist->cds({
182     title => 'this should not exist',
183     year => 2005,
184   })->first;
185   ok(!defined($cd), q{deleted the failed txn's cd});
186   $schema->storage->_dbh->rollback;
187
188 }
189
190 # reset schema object (the txn_rollback meddling screws it up)
191 undef $schema;
192
193 # Test nested failed txn_do()
194 {
195   my $schema = DBICTest->init_schema();
196
197   is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
198
199   my $nested_fail_code = sub {
200     my ($schema, $artist, $code1, $code2) = @_;
201
202     my @titles = map {'nested txn_do test CD ' . $_} (1..5);
203
204     $schema->txn_do($code1, $artist, @titles); # successful txn
205     $schema->txn_do($code2, $artist);          # failed txn
206   };
207
208   my $artist = $schema->resultset('Artist')->find(3);
209
210   throws_ok ( sub {
211     $schema->txn_do($nested_fail_code, $schema, $artist, $code, $fail_code);
212   }, qr/the sky is falling/, 'nested failed txn_do threw exception');
213
214   ok(!defined($artist->cds({
215     title => 'nested txn_do test CD '.$_,
216     year => 2006,
217   })->first), qq{failed txn_do didn't add first txn's cd $_}) for (1..5);
218   my $cd = $artist->cds({
219     title => 'this should not exist',
220     year => 2005,
221   })->first;
222   ok(!defined($cd), q{failed txn_do didn't add failed txn's cd});
223 }
224
225 # Grab a new schema to test txn before connect
226 {
227   my $schema = DBICTest->init_schema(no_deploy => 1);
228   lives_ok (sub {
229     $schema->txn_begin();
230     $schema->txn_begin();
231   }, 'Pre-connection nested transactions.');
232
233   # although not connected DBI would still warn about rolling back at disconnect
234   $schema->txn_rollback;
235   $schema->txn_rollback;
236 }
237
238 # Test txn_scope_guard
239 {
240   my $schema = DBICTest->init_schema();
241
242   is($schema->storage->transaction_depth, 0, "Correct transaction depth");
243   my $artist_rs = $schema->resultset('Artist');
244
245   my $fn = __FILE__;
246   throws_ok {
247    my $guard = $schema->txn_scope_guard;
248
249     $artist_rs->create({
250       name => 'Death Cab for Cutie',
251       made_up_column => 1,
252     });
253
254    $guard->commit;
255   } qr/No such column made_up_column .*? at .*?$fn line \d+/s, "Error propogated okay";
256
257   ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
258
259   my $inner_exception = '';  # set in inner() below
260   throws_ok (sub {
261     outer($schema, 1);
262   }, qr/$inner_exception/, "Nested exceptions propogated");
263
264   ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
265
266   lives_ok (sub {
267
268     # this weird assignment is to stop perl <= 5.8.9 leaking $schema on nested sub{}s
269     my $s = $schema;
270
271     warnings_exist ( sub {
272       # The 0 arg says don't die, just let the scope guard go out of scope
273       # forcing a txn_rollback to happen
274       outer($s, 0);
275     }, qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./, 'Out of scope warning detected');
276
277     ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
278
279   }, 'rollback successful withot exception');
280
281   sub outer {
282     my ($schema, $fatal) = @_;
283
284     my $guard = $schema->txn_scope_guard;
285     $schema->resultset('Artist')->create({
286       name => 'Death Cab for Cutie',
287     });
288     inner($schema, $fatal);
289   }
290
291   sub inner {
292     my ($schema, $fatal) = @_;
293
294     my $inner_guard = $schema->txn_scope_guard;
295     is($schema->storage->transaction_depth, 2, "Correct transaction depth");
296
297     my $artist = $schema->resultset('Artist')->find({ name => 'Death Cab for Cutie' });
298
299     eval {
300       $artist->cds->create({
301         title => 'Plans',
302         year => 2005,
303         $fatal ? ( foo => 'bar' ) : ()
304       });
305     };
306     if ($@) {
307       # Record what got thrown so we can test it propgates out properly.
308       $inner_exception = $@;
309       die $@;
310     }
311
312     # inner guard should commit without consequences
313     $inner_guard->commit;
314   }
315 }
316
317 # make sure the guard does not eat exceptions
318 {
319   my $schema = DBICTest->init_schema();
320   throws_ok (sub {
321     my $guard = $schema->txn_scope_guard;
322     $schema->resultset ('Artist')->create ({ name => 'bohhoo'});
323
324     $schema->storage->disconnect;  # this should freak out the guard rollback
325
326     die 'Deliberate exception';
327   }, qr/Deliberate exception.+Rollback failed/s);
328 }
329
330 # make sure it warns *big* on failed rollbacks
331 {
332   my $schema = DBICTest->init_schema();
333
334   # something is really confusing Test::Warn here, no time to debug
335 =begin
336   warnings_exist (
337     sub {
338       my $guard = $schema->txn_scope_guard;
339       $schema->resultset ('Artist')->create ({ name => 'bohhoo'});
340
341       $schema->storage->disconnect;  # this should freak out the guard rollback
342     },
343     [
344       qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./,
345       qr/\*+ ROLLBACK FAILED\!\!\! \*+/,
346     ],
347     'proper warnings generated on out-of-scope+rollback failure'
348   );
349 =cut
350
351   my @want = (
352     qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./,
353     qr/\*+ ROLLBACK FAILED\!\!\! \*+/,
354   );
355
356   my @w;
357   local $SIG{__WARN__} = sub {
358     if (grep {$_[0] =~ $_} (@want)) {
359       push @w, $_[0];
360     }
361     else {
362       warn $_[0];
363     }
364   };
365   {
366       my $guard = $schema->txn_scope_guard;
367       $schema->resultset ('Artist')->create ({ name => 'bohhoo'});
368
369       $schema->storage->disconnect;  # this should freak out the guard rollback
370   }
371
372   is (@w, 2, 'Both expected warnings found');
373 }
374
375 # make sure AutoCommit => 0 on external handles behaves correctly with scope_guard
376 warnings_are {
377   my $factory = DBICTest->init_schema (AutoCommit => 0);
378   cmp_ok ($factory->resultset('CD')->count, '>', 0, 'Something to delete');
379   my $dbh = $factory->storage->dbh;
380
381   ok (!$dbh->{AutoCommit}, 'AutoCommit is off on $dbh');
382   my $schema = DBICTest::Schema->connect (sub { $dbh });
383
384   lives_ok ( sub {
385     my $guard = $schema->txn_scope_guard;
386     $schema->resultset('CD')->delete;
387     $guard->commit;
388   }, 'No attempt to start a transaction with scope guard');
389
390   is ($schema->resultset('CD')->count, 0, 'Deletion successful in txn');
391
392   # this will commit the implicitly started txn
393   $dbh->commit;
394
395 } [], 'No warnings on AutoCommit => 0 with txn_guard';
396
397 # make sure AutoCommit => 0 on external handles behaves correctly with txn_do
398 warnings_are {
399   my $factory = DBICTest->init_schema (AutoCommit => 0);
400   cmp_ok ($factory->resultset('CD')->count, '>', 0, 'Something to delete');
401   my $dbh = $factory->storage->dbh;
402
403   ok (!$dbh->{AutoCommit}, 'AutoCommit is off on $dbh');
404   my $schema = DBICTest::Schema->connect (sub { $dbh });
405
406
407   lives_ok ( sub {
408     $schema->txn_do (sub { $schema->resultset ('CD')->delete });
409   }, 'No attempt to start a atransaction with txn_do');
410
411   is ($schema->resultset('CD')->count, 0, 'Deletion successful');
412
413   # this will commit the implicitly started txn
414   $dbh->commit;
415
416 } [], 'No warnings on AutoCommit => 0 with txn_do';
417
418 done_testing;