The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

#!/usr/bin/env perl -w
use strict;
use Test::More tests => 94;
#use Test::More 'no_plan';
my $CLASS;
BEGIN {
$CLASS = 'DBIx::Connector';
use_ok $CLASS or die;
}
ok my $conn = $CLASS->new( 'dbi:ExampleP:dummy', '', '' ),
'Get a connection';
my $module = Test::MockModule->new($CLASS);
# Test with no existing dbh.
$module->mock( _connect => sub {
pass '_connect should be called';
$module->original('_connect')->(@_);
});
ok my $dbh = $conn->dbh, 'Fetch the database handle';
ok $dbh->{AutoCommit}, 'We should not be in a txn';
ok !$conn->in_txn, 'in_txn() should know that, too';
ok !$conn->{_in_run}, '_in_run should be false';
# Set up a DBI mocker.
my $dbi_mock = Test::MockModule->new(ref $dbh, no_auto => 1);
my $ping = 0;
$dbi_mock->mock( ping => sub { ++$ping } );
is $conn->{_dbh}, $dbh, 'The dbh should be stored';
is $ping, 0, 'No pings yet';
ok $conn->connected, 'We should be connected';
is $ping, 1, 'Ping should have been called';
ok $conn->txn(sub {
is $ping, 1, 'Ping should not have been called before the txn';
ok !shift->{AutoCommit}, 'Inside, we should be in a transaction';
ok $conn->in_txn, 'We should be in a txn';
ok $conn->{_in_run}, '_in_run should be true';
is $conn->dbh, $dbh, 'Should get same dbh from dbh()';
is $ping, 1, 'ping should not have been called again';
}), 'Do something with no existing handle';
$module->unmock( '_connect');
ok !$conn->{_in_run}, '_in_run should be false again';
ok $dbh->{AutoCommit}, 'Transaction should be committed';
ok !$conn->in_txn, 'in_txn() should know it';
# Test with instantiated dbh.
is $conn->{_dbh}, $dbh, 'The dbh should be stored';
ok $conn->connected, 'We should be connected';
ok $conn->txn(sub {
my $dbha = shift;
is $dbha, $dbh, 'The handle should have been passed';
is $_, $dbh, 'It should also be in $_';
is $_, $dbh, 'Should have dbh in $_';
$ping = 0;
is $conn->dbh, $dbh, 'Should get same dbh from dbh()';
$ping = 1;
ok !$dbha->{AutoCommit}, 'We should be in a transaction';
ok $conn->in_txn, 'in_txn() should know about it';
}), 'Do something with stored handle';
ok $dbh->{AutoCommit}, 'New transaction should be committed';
ok !$conn->in_txn, 'in_txn() should know it, too';
# Test the return value.
ok my $foo = $conn->txn(sub {
return (2, 3, 5);
}), 'Do in scalar context';
is $foo, 5, 'The return value should be the last value';
ok $foo = $conn->txn(sub {
return wantarray ? (2, 3, 5) : 'scalar';
}), 'Do in scalar context';
is $foo, 'scalar', 'Callback should know when its context is scalar';
ok my @foo = $conn->txn(sub {
return (2, 3, 5);
}), 'Do in array context';
is_deeply \@foo, [2, 3, 5], 'The return value should be the list';
ok @foo = $conn->txn(sub {
return wantarray ? (2, 3, 5) : 'scalar';
}), 'Do in scalar context';
is_deeply \@foo, [2, 3, 5], 'Callback should know when its context is list';
# Test an exception.
eval { $conn->txn(sub { die 'WTF?' }) };
ok $@, 'We should have died';
ok $dbh->{AutoCommit}, 'New transaction should rolled back';
ok !$conn->in_txn, 'in_txn() should know that';
# Make sure nested calls work.
$conn->txn(sub {
my $dbh = shift;
ok !$dbh->{AutoCommit}, 'We should be in a txn';
ok $conn->in_txn, 'in_txn() should know about it';
local $dbh->{Active} = 0;
$conn->txn(sub {
isnt shift, $dbh, 'Nested txn should not get inactive dbh';
ok !$dbh->{AutoCommit}, 'Nested txn should be in the txn';
ok $conn->in_txn, 'in_txn() should know it';
});
});
# Make sure that it does nothing transactional if we've started the
# transaction.
$dbh = $conn->dbh;
my $driver = $conn->driver;
$driver->begin_work($dbh);
ok !$dbh->{AutoCommit}, 'Transaction should be started';
ok $conn->in_txn, 'in_txn() should know it';
$conn->txn(sub {
my $dbha = shift;
is $dbha, $dbh, 'We should have the same database handle';
is $_, $dbh, 'It should also be in $_';
$ping = 0;
is $conn->dbh, $dbh, 'Should get same dbh from dbh()';
$ping = 1;
ok !$dbha->{AutoCommit}, 'Transaction should still be going';
ok $conn->in_txn, 'in_txn() should know it';
});
ok !$dbh->{AutoCommit}, 'Transaction should stil be live after txn';
ok $conn->in_txn, 'in_txn() should know it';
$driver->rollback($dbh);
# Make sure nested calls when ping returns false.
$conn->txn(sub {
my $dbh = shift;
ok !$dbh->{AutoCommit}, 'We should be in a txn';
ok $conn->in_txn, 'in_txn() should know that, too';
$dbi_mock->mock( ping => 0 );
$conn->txn(sub {
is shift, $dbh, 'Nested txn should get same dbh, even though inactive';
ok !$dbh->{AutoCommit}, 'Nested txn should be in the txn';
ok $conn->in_txn, 'in_txn() should know that, too';
});
});
# Test mode.
$conn->txn(sub {
is $conn->mode, 'no_ping', 'Default mode should be no_ping';
});
$conn->txn(ping => sub {
is $conn->mode, 'ping', 'Mode should be "ping" inside ping txn'
});
is $conn->mode, 'no_ping', 'Back outside, should be "no_ping" again';
$conn->txn(fixup => sub {
is $conn->mode, 'fixup', 'Mode should be "fixup" inside fixup txn'
});
is $conn->mode, 'no_ping', 'Back outside, should be "no_ping" again';
ok $conn->mode('ping'), 'Se mode to "ping"';
$conn->txn(sub {
is $conn->mode, 'ping', 'Mode should implicitly be "ping"'
});
ok $conn->mode('fixup'), 'Se mode to "fixup"';
$conn->txn(sub {
is $conn->mode, 'fixup', 'Mode should implicitly be "fixup"'
});
NOEXIT: {
no warnings;
my $dr_mock = Test::MockModule->new(ref $driver, no_auto => 1);
$dr_mock->mock(begin_work => sub { shift });
my $keyword;
$dr_mock->mock(commit => sub {
pass "Commit should be called when returning via $keyword"
});
# Make sure we don't exit the app via `next` or `last`.
for my $mode (qw(ping no_ping fixup)) {
$conn->mode($mode);
$keyword = 'next';
ok !$conn->txn(sub { next }), "Return via $keyword should fail";
$keyword = 'last';
ok !$conn->txn(sub { last }), "Return via $keyword should fail";
}
}
# Have the rollback die.
$dbi_mock->mock(begin_work => undef );
$dbi_mock->mock(rollback => sub { die 'Rollback WTF' });
eval { $conn->txn(sub {
die 'Transaction WTF';
}) };
ok my $err = $@, 'We should have died';
isa_ok $err, 'DBIx::Connector::TxnRollbackError', 'The exception';
like $err, qr/Transaction aborted: Transaction WTF/, 'Should have the transaction error';
like $err, qr/Transaction rollback failed: Rollback WTF/, 'Should have the rollback error';
like $err->rollback_error, qr/Rollback WTF/, 'Should have rollback error';
like $err->error, qr/Transaction WTF/, 'Should have transaction error';
# Try a nested transaction.
eval { $conn->txn(sub {
local $_->{AutoCommit} = 0;
$conn->txn(sub { die 'Nested WTF' });
}) };
ok $err = $@, 'We should have died again';
isa_ok $err, 'DBIx::Connector::TxnRollbackError', 'The exception';
like $err->rollback_error, qr/Rollback WTF/, 'Should have rollback error';
like $err->error, qr/Nested WTF/, 'Should have nested transaction error';
ok !ref $err->error, 'The nested error should not be an object';