X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=t%2Fstorage%2Ftxn.t;h=4c6d50558006ad9a7476f2e06efbb502a4ccfa1a;hb=b74b15b066a19f07b575883abd397ea4d3b045db;hp=89dddc57c02405248d2a2150d5568ddce1700077;hpb=7d216b101775c472b012b257d848f50d78b193c8;p=dbsrgits%2FDBIx-Class-Historic.git diff --git a/t/storage/txn.t b/t/storage/txn.t index 89dddc5..4c6d505 100644 --- a/t/storage/txn.t +++ b/t/storage/txn.t @@ -7,8 +7,6 @@ use Test::Exception; use lib qw(t/lib); use DBICTest; -my $schema = DBICTest->init_schema(); - my $code = sub { my ($artist, @cd_titles) = @_; @@ -22,49 +20,49 @@ my $code = sub { # Test checking of parameters { + my $schema = DBICTest->init_schema; + throws_ok (sub { (ref $schema)->txn_do(sub{}); }, qr/storage/, "can't call txn_do without storage"); - throws_ok ( sub { + throws_ok { $schema->txn_do(''); - }, qr/must be a CODE reference/, '$coderef parameter check ok'); + } qr/\Qrun() requires a coderef to execute as its first argument/, + '$coderef parameter check ok'; } -# Test successful txn_do() - scalar context -{ - is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0'); +# Test successful txn_do() - scalar/list context +for my $want (0,1) { + my $schema = DBICTest->init_schema; + + is( $schema->storage->transaction_depth, 0, 'txn depth starts at 0'); my @titles = map {'txn_do test CD ' . $_} (1..5); my $artist = $schema->resultset('Artist')->find(1); my $count_before = $artist->cds->count; - my $count_after = $schema->txn_do($code, $artist, @titles); - is($count_after, $count_before+5, 'successful txn added 5 cds'); - is($artist->cds({ - title => "txn_do test CD $_", - })->first->year, 2006, "new CD $_ year correct") for (1..5); - - is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset'); -} -# Test successful txn_do() - list context -{ - is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0'); + my @res; + if ($want) { + @res = $schema->txn_do($code, $artist, @titles); + is(scalar @res, $count_before+5, 'successful txn added 5 cds'); + } + else { + $res[0] = $schema->txn_do($code, $artist, @titles); + is($res[0], $count_before+5, 'successful txn added 5 cds'); + } - my @titles = map {'txn_do test CD ' . $_} (6..10); - my $artist = $schema->resultset('Artist')->find(1); - my $count_before = $artist->cds->count; - my @cds = $schema->txn_do($code, $artist, @titles); - is(scalar @cds, $count_before+5, 'added 5 CDs and returned in list context'); is($artist->cds({ title => "txn_do test CD $_", - })->first->year, 2006, "new CD $_ year correct") for (6..10); + })->first->year, 2006, "new CD $_ year correct") for (1..5); - is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset'); + is( $schema->storage->transaction_depth, 0, 'txn depth has been reset'); } # Test txn_do() @_ aliasing support { + my $schema = DBICTest->init_schema; + my $res = 'original'; $schema->storage->txn_do (sub { $_[0] = 'changed' }, $res); is ($res, 'changed', "Arguments properly aliased for txn_do"); @@ -72,7 +70,9 @@ my $code = sub { # Test nested successful txn_do() { - is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0'); + my $schema = DBICTest->init_schema; + + is( $schema->storage->transaction_depth, 0, 'txn depth starts at 0'); my $nested_code = sub { my ($schema, $artist, $code) = @_; @@ -96,7 +96,7 @@ my $code = sub { })->first->year, 2006, qq{nested txn_do CD$_ year ok}) for (1..10); is($artist->cds->count, $count_before+10, 'nested txn_do added all CDs'); - is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset'); + is( $schema->storage->transaction_depth, 0, 'txn depth has been reset'); } # test nested txn_begin on fresh connection @@ -113,7 +113,7 @@ my $code = sub { die "$$ starts in txn!" if $s->storage->transaction_depth != 0; $s->txn_do ( sub { die "$$ not in txn!" if $s->storage->transaction_depth == 0; - $s->storage->dbh->do('SELECT 1') } + $s->storage->dbh->do('SELECT 1') } ); die "$$ did not finish txn!" if $s->storage->transaction_depth != 0; }, @@ -128,11 +128,12 @@ my $code = sub { $guard->commit }, ) { - push @pids, fork(); + my $pid = fork(); die "Unable to fork: $!\n" - if ! defined $pids[-1]; + if ! defined $pid; - if ($pids[-1]) { + if ($pid) { + push @pids, $pid; next; } @@ -160,16 +161,13 @@ my $code = sub { my $guard = $schema->txn_scope_guard; $schema->txn_do( sub { die } ); }; + is( $schema->storage->transaction_depth, 0, 'Transaction successfully aborted' ); $schema->txn_do( sub { ok ($schema->storage->_dbh->do ('SELECT 1'), "Query after exceptions ok ($_)"); }); } - for my $pid ( $schema->txn_do ( sub { _forking_action ($schema) } ) ) { - waitpid ($pid, 0); - ok (! $?, "Child $pid exit ok (pass $pass)"); - isa_ok ($schema->resultset ('Artist')->find ({ name => "forking action $pid" }), 'DBIx::Class::Row'); - } + $schema->txn_do ( sub { _test_forking_action ($schema, $pass) } ); } } @@ -185,39 +183,38 @@ my $code = sub { my $guard = $schema->txn_scope_guard; $schema->txn_do( sub { die } ); }; + is( $schema->storage->transaction_depth, 0, 'Transaction successfully aborted' ); $schema->txn_do( sub { ok ($schema->storage->_dbh->do ('SELECT 1'), "Query after exceptions ok ($_)"); }); } - my @pids; my $guard = $schema->txn_scope_guard; - _forking_action ($schema); + my @pids = _test_forking_action ($schema, $pass); $guard->commit; - - for my $pid (@pids) { - waitpid ($pid, 0); - ok (! $?, "Child $pid exit ok (pass $pass)"); - isa_ok ($schema->resultset ('Artist')->find ({ name => "forking action $pid" }), 'DBIx::Class::Row'); - } } } -sub _forking_action { - my $schema = shift; +sub _test_forking_action { + my ($schema, $pass) = @_; my @pids; - while (@pids < 5) { - push @pids, fork(); + SKIP: for my $count (1 .. 5) { + + skip 'Weird DBI General Protection Faults, skip forking tests (RT#63104)', 5 + if $^O eq 'MSWin32'; + + my $pid = fork(); die "Unable to fork: $!\n" - if ! defined $pids[-1]; + if ! defined $pid; - if ($pids[-1]) { + if ($pid) { + push @pids, $pid; next; } - if (@pids % 2) { + if ($count % 2) { $schema->txn_do (sub { my $depth = $schema->storage->transaction_depth; die "$$(txn_do)unexpected txn depth $depth!" if $depth != 1; @@ -235,7 +232,17 @@ sub _forking_action { exit 0; } - return @pids; + for my $pid (@pids) { + waitpid ($pid, 0); + ok (! $?, "Child $pid exit ok (pass $pass)"); + } + + # it is important to reap all children before checking the final db-state + # otherwise a deadlock may occur between the transactions running in the + # children and the query of the parent + for my $pid (@pids) { + isa_ok ($schema->resultset ('Artist')->find ({ name => "forking action $pid" }), 'DBIx::Class::Row'); + } } my $fail_code = sub { @@ -247,93 +254,75 @@ my $fail_code = sub { die "the sky is falling"; }; -# Test failed txn_do() { + my $schema = DBICTest->init_schema; - is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0'); - - my $artist = $schema->resultset('Artist')->find(3); - - throws_ok (sub { - $schema->txn_do($fail_code, $artist); - }, qr/the sky is falling/, 'failed txn_do threw an exception'); - - my $cd = $artist->cds({ - title => 'this should not exist', - year => 2005, - })->first; - ok(!defined($cd), q{failed txn_do didn't change the cds table}); - - is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset'); -} - -# do the same transaction again -{ - is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0'); - - my $artist = $schema->resultset('Artist')->find(3); - - throws_ok (sub { - $schema->txn_do($fail_code, $artist); - }, qr/the sky is falling/, 'failed txn_do threw an exception'); - - my $cd = $artist->cds({ - title => 'this should not exist', - year => 2005, - })->first; - ok(!defined($cd), q{failed txn_do didn't change the cds table}); + # Test failed txn_do() + for my $pass (1,2) { - is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset'); -} + is( $schema->storage->transaction_depth, 0, "txn depth starts at 0 (pass $pass)"); -# Test failed txn_do() with failed rollback -{ - is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0'); + my $artist = $schema->resultset('Artist')->find(3); - my $artist = $schema->resultset('Artist')->find(3); + throws_ok (sub { + $schema->txn_do($fail_code, $artist); + }, qr/the sky is falling/, "failed txn_do threw an exception (pass $pass)"); - # Force txn_rollback() to throw an exception - no warnings 'redefine'; - no strict 'refs'; + my $cd = $artist->cds({ + title => 'this should not exist', + year => 2005, + })->first; + ok(!defined($cd), qq{failed txn_do didn't change the cds table (pass $pass)}); - # die in rollback - local *{"DBIx::Class::Storage::DBI::SQLite::txn_rollback"} = sub{ - my $storage = shift; - die 'FAILED'; - }; + is( $schema->storage->transaction_depth, 0, "txn depth has been reset (pass $pass)"); + } - throws_ok ( - sub { - $schema->txn_do($fail_code, $artist); - }, - qr/the sky is falling.+Rollback failed/s, - 'txn_rollback threw a rollback exception (and included the original exception' - ); - - my $cd = $artist->cds({ - title => 'this should not exist', - year => 2005, - })->first; - isa_ok($cd, 'DBICTest::CD', q{failed txn_do with a failed txn_rollback }. - q{changed the cds table}); - $cd->delete; # Rollback failed - $cd = $artist->cds({ - title => 'this should not exist', - year => 2005, - })->first; - ok(!defined($cd), q{deleted the failed txn's cd}); - $schema->storage->_dbh->rollback; + # Test failed txn_do() with failed rollback + { + is( $schema->storage->transaction_depth, 0, 'txn depth starts at 0'); + + my $artist = $schema->resultset('Artist')->find(3); + + # Force txn_rollback() to throw an exception + no warnings qw/once redefine/; + + # this should logically work just fine - but it does not, + # only direct override of the existing method dtrt + #local *DBIx::Class::Storage::DBI::SQLite::txn_rollback = sub { die 'FAILED' }; + + local *DBIx::Class::Storage::DBI::txn_rollback = sub { die 'FAILED' }; + Class::C3->reinitialize() if DBIx::Class::_ENV_::OLD_MRO; + + throws_ok ( + sub { + $schema->txn_do($fail_code, $artist); + }, + qr/the sky is falling.+Rollback failed/s, + 'txn_rollback threw a rollback exception (and included the original exception' + ); + + my $cd = $artist->cds({ + title => 'this should not exist', + year => 2005, + })->first; + isa_ok($cd, 'DBICTest::CD', q{failed txn_do with a failed txn_rollback }. + q{changed the cds table}); + $cd->delete; # Rollback failed + $cd = $artist->cds({ + title => 'this should not exist', + year => 2005, + })->first; + ok(!defined($cd), q{deleted the failed txn's cd}); + $schema->storage->_dbh->rollback; + } } -# reset schema object (the txn_rollback meddling screws it up) -undef $schema; - # Test nested failed txn_do() { my $schema = DBICTest->init_schema(); - is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0'); + is( $schema->storage->transaction_depth, 0, 'txn depth starts at 0'); my $nested_fail_code = sub { my ($schema, $artist, $code1, $code2) = @_; @@ -361,7 +350,9 @@ undef $schema; ok(!defined($cd), q{failed txn_do didn't add failed txn's cd}); } + # Grab a new schema to test txn before connect +# also test nested txn exception { my $schema = DBICTest->init_schema(no_deploy => 1); lives_ok (sub { @@ -369,167 +360,15 @@ undef $schema; $schema->txn_begin(); }, 'Pre-connection nested transactions.'); - # although not connected DBI would still warn about rolling back at disconnect - $schema->txn_rollback; - $schema->txn_rollback; -} - -# Test txn_scope_guard -{ - my $schema = DBICTest->init_schema(); - - is($schema->storage->transaction_depth, 0, "Correct transaction depth"); - my $artist_rs = $schema->resultset('Artist'); - - my $fn = __FILE__; - throws_ok { - my $guard = $schema->txn_scope_guard; - - $artist_rs->create({ - name => 'Death Cab for Cutie', - made_up_column => 1, - }); - - $guard->commit; - } qr/No such column made_up_column .*? at .*?$fn line \d+/s, "Error propogated okay"; - - ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created"); - - my $inner_exception = ''; # set in inner() below - throws_ok (sub { - outer($schema, 1); - }, qr/$inner_exception/, "Nested exceptions propogated"); - - ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created"); - - lives_ok (sub { - - # this weird assignment is to stop perl <= 5.8.9 leaking $schema on nested sub{}s - my $s = $schema; - - warnings_exist ( sub { - # The 0 arg says don't die, just let the scope guard go out of scope - # forcing a txn_rollback to happen - outer($s, 0); - }, qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./, 'Out of scope warning detected'); - - ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created"); - - }, 'rollback successful withot exception'); - - sub outer { - my ($schema, $fatal) = @_; - - my $guard = $schema->txn_scope_guard; - $schema->resultset('Artist')->create({ - name => 'Death Cab for Cutie', - }); - inner($schema, $fatal); - } - - sub inner { - my ($schema, $fatal) = @_; - - my $inner_guard = $schema->txn_scope_guard; - is($schema->storage->transaction_depth, 2, "Correct transaction depth"); - - my $artist = $schema->resultset('Artist')->find({ name => 'Death Cab for Cutie' }); - - eval { - $artist->cds->create({ - title => 'Plans', - year => 2005, - $fatal ? ( foo => 'bar' ) : () - }); - }; - if ($@) { - # Record what got thrown so we can test it propgates out properly. - $inner_exception = $@; - die $@; - } - - # inner guard should commit without consequences - $inner_guard->commit; - } -} - -# make sure the guard does not eat exceptions -{ - my $schema = DBICTest->init_schema; - - no strict 'refs'; - no warnings 'redefine'; - local *{DBIx::Class::Storage::DBI::txn_rollback} = sub { die 'die die my darling' }; - - throws_ok (sub { - my $guard = $schema->txn_scope_guard; - $schema->resultset ('Artist')->create ({ name => 'bohhoo'}); - - # this should freak out the guard rollback - # but it won't work because DBD::SQLite is buggy - # instead just install a toxic rollback above - #$schema->storage->_dbh( $schema->storage->_dbh->clone ); - - die 'Deliberate exception'; - }, qr/Deliberate exception.+Rollback failed/s); -} - -# make sure it warns *big* on failed rollbacks -{ - my $schema = DBICTest->init_schema(); - - no strict 'refs'; - no warnings 'redefine'; - local *{DBIx::Class::Storage::DBI::txn_rollback} = sub { die 'die die my darling' }; - -#The warn from within a DESTROY callback freaks out Test::Warn, do it old-school -=begin - warnings_exist ( - sub { - my $guard = $schema->txn_scope_guard; - $schema->resultset ('Artist')->create ({ name => 'bohhoo'}); - - # this should freak out the guard rollback - # but it won't work because DBD::SQLite is buggy - # instead just install a toxic rollback above - #$schema->storage->_dbh( $schema->storage->_dbh->clone ); - }, - [ - qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./, - qr/\*+ ROLLBACK FAILED\!\!\! \*+/, - ], - 'proper warnings generated on out-of-scope+rollback failure' - ); -=cut - -# delete this once the above works properly (same test) - my @want = ( - qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./, - qr/\*+ ROLLBACK FAILED\!\!\! \*+/, - ); - - my @w; - local $SIG{__WARN__} = sub { - if (grep {$_[0] =~ $_} (@want)) { - push @w, $_[0]; - } - else { - warn $_[0]; - } - }; - { - my $guard = $schema->txn_scope_guard; - $schema->resultset ('Artist')->create ({ name => 'bohhoo'}); - } - - is (@w, 2, 'Both expected warnings found'); + throws_ok( sub { $schema->txn_rollback }, 'DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION', 'got proper nested rollback exception' ); } # make sure AutoCommit => 0 on external handles behaves correctly with scope_guard warnings_are { - my $factory = DBICTest->init_schema (AutoCommit => 0); + my $factory = DBICTest->init_schema; cmp_ok ($factory->resultset('CD')->count, '>', 0, 'Something to delete'); my $dbh = $factory->storage->dbh; + $dbh->{AutoCommit} = 0; ok (!$dbh->{AutoCommit}, 'AutoCommit is off on $dbh'); my $schema = DBICTest::Schema->connect (sub { $dbh }); @@ -549,14 +388,14 @@ warnings_are { # make sure AutoCommit => 0 on external handles behaves correctly with txn_do warnings_are { - my $factory = DBICTest->init_schema (AutoCommit => 0); + my $factory = DBICTest->init_schema; cmp_ok ($factory->resultset('CD')->count, '>', 0, 'Something to delete'); my $dbh = $factory->storage->dbh; + $dbh->{AutoCommit} = 0; ok (!$dbh->{AutoCommit}, 'AutoCommit is off on $dbh'); my $schema = DBICTest::Schema->connect (sub { $dbh }); - lives_ok ( sub { $schema->txn_do (sub { $schema->resultset ('CD')->delete }); }, 'No attempt to start a atransaction with txn_do'); @@ -568,4 +407,41 @@ warnings_are { } [], 'No warnings on AutoCommit => 0 with txn_do'; + +# make sure we are not fucking up the stacktrace on broken overloads +{ + package DBICTest::BrokenOverload; + + use overload '""' => sub { $_[0] }; +} + +{ + my @w; + local $SIG{__WARN__} = sub { + $_[0] =~ /\QExternal exception class DBICTest::BrokenOverload implements partial (broken) overloading preventing its instances from being used in simple (\E\$x eq \$y\Q) comparisons/ + ? push @w, @_ + : warn @_ + }; + + my $s = DBICTest->init_schema(no_deploy => 1); + $s->stacktrace(0); + my $g = $s->storage->txn_scope_guard; + my $broken_exception = bless {}, 'DBICTest::BrokenOverload'; + + # FIXME - investigate what confuses the regex engine below + + # do not reformat - line-num part of the test + my $ln = __LINE__ + 6; + throws_ok { + $s->txn_do( sub { + $s->txn_do( sub { + $s->storage->_dbh->disconnect; + die $broken_exception + }); + }) + } qr/\QTransaction aborted: $broken_exception. Rollback failed: lost connection to storage at @{[__FILE__]} line $ln\E\n/; # FIXME wtf - ...\E$/m doesn't work here + + is @w, 1, 'One matching warning only'; +} + done_testing;