X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=t%2Flib%2FDBICTest%2FBaseSchema.pm;h=aaaf955cafa15695d0e6cd75a5dfac19be6e0aad;hb=4ee643f5a0b025634b15077d4d82028a6ee8e21b;hp=c68d7fded0694f63c499bcc6fe110c0c46148686;hpb=961d79dbe25ef8a92e867fcc84914b4bf568c11d;p=dbsrgits%2FDBIx-Class.git diff --git a/t/lib/DBICTest/BaseSchema.pm b/t/lib/DBICTest/BaseSchema.pm index c68d7fd..aaaf955 100644 --- a/t/lib/DBICTest/BaseSchema.pm +++ b/t/lib/DBICTest/BaseSchema.pm @@ -6,11 +6,104 @@ use warnings; use base qw(DBICTest::Base DBIx::Class::Schema); use Fcntl qw(:DEFAULT :seek :flock); -use Time::HiRes 'sleep'; +use IO::Handle (); +use DBIx::Class::_Util 'scope_guard'; use DBICTest::Util::LeakTracer qw(populate_weakregistry assert_empty_weakregistry); -use DBICTest::Util 'local_umask'; +use DBICTest::Util qw( local_umask tmpdir await_flock dbg DEBUG_TEST_CONCURRENCY_LOCKS ); use namespace::clean; +if( $ENV{DBICTEST_ASSERT_NO_SPURIOUS_EXCEPTION_ACTION} ) { + my $ea = __PACKAGE__->exception_action( sub { + + # Can not rely on $^S here at all - the exception_action + # itself is always called in an eval so that the goto-guard + # can work (see 7cb35852) + + my ( $fr_num, $disarmed, $throw_exception_fr_num, $eval_fr_num ); + while( ! $disarmed and my @fr = caller(++$fr_num) ) { + + $throw_exception_fr_num ||= ( + $fr[3] =~ /^DBIx::Class::(?:ResultSource|Schema|Storage|Exception)::throw(?:_exception)?$/ + and + # there may be evals in the throwers themselves - skip those + ( $eval_fr_num ) = ( undef ) + and + $fr_num + ); + + # now that the above stops un-setting us, we can find the first + # ineresting eval + $eval_fr_num ||= ( + $fr[3] eq '(eval)' + and + $fr_num + ); + + $disarmed = !! ( + $fr[1] =~ / \A (?: \. [\/\\] )? x?t [\/\\] .+ \.t \z /x + and + ( + $fr[3] =~ /\A (?: + Test::Exception::throws_ok + | + Test::Exception::dies_ok + | + Try::Tiny::try + | + \Q(eval)\E + ) \z /x + or + ( + $fr[3] eq 'Test::Exception::lives_ok' + and + ( $::TODO or Test::Builder->new->in_todo ) + ) + ) + ); + } + + Test::Builder->new->ok(0, join "\n", + 'Unexpected &exception_action invocation', + '', + ' You almost certainly used eval/try instead of dbic_internal_try()', + " Adjust *one* of the eval-ish constructs in the callstack starting" . DBICTest::Util::stacktrace($throw_exception_fr_num||()) + ) if ( + ! $disarmed + and + ( + $eval_fr_num + or + ! $throw_exception_fr_num + ) + ); + + DBIx::Class::Exception->throw( $_[0] ); + }); + + my $interesting_ns_rx = qr/^ (?: main$ | DBIx::Class:: | DBICTest:: ) /x; + + # hard-set $SIG{__DIE__} to the class-wide exception_action + # with a little escape preceeding it + $SIG{__DIE__} = sub { + + # without this there would be false positives everywhere :( + die @_ if ( + # blindly rethrow if nobody is waiting for us + ( defined $^S and ! $^S ) + or + (caller(0))[0] !~ $interesting_ns_rx + or + ( + caller(0) eq 'main' + and + ( (caller(1))[0] || '' ) !~ $interesting_ns_rx + ) + ); + + &$ea; + }; +} + sub capture_executed_sql_bind { my ($self, $cref) = @_; @@ -23,10 +116,20 @@ sub capture_executed_sql_bind { local *DBIx::Class::Storage::DBI::_format_for_trace = sub { $_[1] }; Class::C3->reinitialize if DBIx::Class::_ENV_::OLD_MRO; + # can not use local() due to an unknown number of storages + # (think replicated) + my $orig_states = { map + { $_ => $self->storage->$_ } + qw(debugcb debugobj debug) + }; + + my $sg = scope_guard { + $self->storage->$_ ( $orig_states->{$_} ) for keys %$orig_states; + }; - local $self->storage->{debugcb}; - local $self->storage->{debugobj} = my $tracer_obj = DBICTest::SQLTracerObj->new; - local $self->storage->{debug} = 1; + $self->storage->debugcb(undef); + $self->storage->debugobj( my $tracer_obj = DBICTest::SQLTracerObj->new ); + $self->storage->debug(1); local $Test::Builder::Level = $Test::Builder::Level + 2; $cref->(); @@ -108,7 +211,8 @@ our $locker; END { # we need the $locker to be referenced here for delayed destruction if ($locker->{lock_name} and ($ENV{DBICTEST_LOCK_HOLDER}||0) == $$) { - #warn "$$ $0 $locker->{type} LOCK RELEASED"; + DEBUG_TEST_CONCURRENCY_LOCKS + and dbg "$locker->{type} LOCK RELEASED (END): $locker->{lock_name}"; } } @@ -139,13 +243,6 @@ sub connection { # an envvar, we can not detect when a user invokes prove -jN. Hence # perform the locking at all times, it shouldn't hurt. # the lock fh *should* inherit across forks/subprocesses - # - # File locking is hard. Really hard. By far the best lock implementation - # I've seen is part of the guts of File::Temp. However it is sadly not - # reusable. Since I am not aware of folks doing NFS parallel testing, - # nor are we known to work on VMS, I am just going to punt this and - # use the portable-ish flock() provided by perl itself. If this does - # not work for you - patches more than welcome. if ( ! $DBICTest::global_exclusive_lock and @@ -153,31 +250,37 @@ sub connection { and ref($_[0]) ne 'CODE' and - ($_[0]||'') !~ /^ (?i:dbi) \: SQLite \: (?: dbname\= )? (?: \:memory\: | t [\/\\] var [\/\\] DBIxClass\-) /x + ($_[0]||'') !~ /^ (?i:dbi) \: SQLite (?: \: | \W ) .*? (?: dbname\= )? (?: \:memory\: | t [\/\\] var [\/\\] DBIxClass\-) /x ) { - my $locktype = do { + my $locktype; + + { # guard against infinite recursion local $ENV{DBICTEST_LOCK_HOLDER} = -1; - # we need to connect a forced fresh clone so that we do not upset any state + # we need to work with a forced fresh clone so that we do not upset any state # of the main $schema (some tests examine it quite closely) local $SIG{__WARN__} = sub {}; + local $SIG{__DIE__} if $SIG{__DIE__}; local $@; - my $storage = eval { - my $st = ref($self)->connect(@{$self->storage->connect_info})->storage; - $st->ensure_connected; # do connect here, to catch a possible throw - $st; + + # this will either give us an undef $locktype or will determine things + # properly with a default ( possibly connecting in the process ) + eval { + my $cur_storage = $self->storage; + + $cur_storage = $cur_storage->master + if $cur_storage->isa('DBIx::Class::Storage::DBI::Replicated'); + + my $s = ref($self)->connect(@{$cur_storage->connect_info})->storage; + + $locktype = $s->sqlt_type || 'generic'; + + # in case sqlt_type did connect, doesn't matter if it fails or something + $s->disconnect; }; - $storage - ? do { - my $t = $storage->sqlt_type || 'generic'; - eval { $storage->disconnect }; - $t; - } - : undef - ; - }; + } # Never hold more than one lock. This solves the "lock in order" issues # unrelated tests may have @@ -186,18 +289,27 @@ sub connection { # this will release whatever lock we may currently be holding # which is fine since the type does not match as checked above + DEBUG_TEST_CONCURRENCY_LOCKS + and $locker + and dbg "$locker->{type} LOCK RELEASED (UNDEF): $locker->{lock_name}"; + undef $locker; - my $lockpath = DBICTest::RunMode->tmpdir->file("_dbictest_$locktype.lock"); + my $lockpath = tmpdir . "_dbictest_$locktype.lock"; + + DEBUG_TEST_CONCURRENCY_LOCKS + and dbg "Waiting for $locktype LOCK: $lockpath..."; - #warn "$$ $0 $locktype GRABBING LOCK"; my $lock_fh; { my $u = local_umask(0); # so that the file opens as 666, and any user can lock sysopen ($lock_fh, $lockpath, O_RDWR|O_CREAT) or die "Unable to open $lockpath: $!"; } - flock ($lock_fh, LOCK_EX) or die "Unable to lock $lockpath: $!"; - #warn "$$ $0 $locktype LOCK GRABBED"; + + await_flock ($lock_fh, LOCK_EX) or die "Unable to lock $lockpath: $!"; + + DEBUG_TEST_CONCURRENCY_LOCKS + and dbg "Got $locktype LOCK: $lockpath"; # see if anyone was holding a lock before us, and wait up to 5 seconds for them to terminate # if we do not do this we may end up trampling over some long-running END or somesuch @@ -208,12 +320,17 @@ sub connection { and ($old_pid) = $old_pid =~ /^(\d+)$/ ) { + DEBUG_TEST_CONCURRENCY_LOCKS + and dbg "Post-grab WAIT for $old_pid START: $lockpath"; + for (1..50) { kill (0, $old_pid) or last; - sleep 0.1; + select( undef, undef, undef, 0.1 ); } + + DEBUG_TEST_CONCURRENCY_LOCKS + and dbg "Post-grab WAIT for $old_pid FINISHED: $lockpath"; } - #warn "$$ $0 $locktype POST GRAB WAIT"; truncate $lock_fh, 0; seek ($lock_fh, 0, SEEK_SET) or die "seek failed $!"; @@ -258,7 +375,6 @@ sub clone { END { # Make sure we run after any cleanup in other END blocks - require B; push @{ B::end_av()->object_2svref }, sub { assert_empty_weakregistry($weak_registry, 'quiet'); };