Move expensive test to xt/, add malloc-canary preventing false-negatives
[dbsrgits/DBIx-Class.git] / t / lib / DBICTest / Util.pm
CommitLineData
65d35121 1package DBICTest::Util;
2
3use warnings;
4use strict;
5
c0329273 6use ANFANG;
7
08a8d8f1 8use DBICTest::RunMode;
9
10use constant {
11
12 DEBUG_TEST_CONCURRENCY_LOCKS => (
13 ( ($ENV{DBICTEST_DEBUG_CONCURRENCY_LOCKS}||'') =~ /^(\d+)$/ )[0]
14 ||
15 0
16 ),
17
18 # During 5.13 dev cycle HELEMs started to leak on copy
19 # add an escape for these perls ON SMOKERS - a user/CI will still get death
20 # constname a homage to http://theoatmeal.com/comics/working_home
21 PEEPEENESS => (
22 DBICTest::RunMode->is_smoker
23 and
24 ! DBICTest::RunMode->is_ci
25 and
26 ( "$]" >= 5.013005 and "$]" <= 5.013006)
27 ),
28};
69016f65 29
8d6b1478 30use Config;
bbf6a9a5 31use Carp qw(cluck confess croak);
439a7283 32use Fcntl qw( :DEFAULT :flock );
e48635f7 33use Scalar::Util qw( blessed refaddr openhandle );
439a7283 34use DBIx::Class::_Util qw( scope_guard parent_dir mkdir_p );
65d35121 35
36use base 'Exporter';
69016f65 37our @EXPORT_OK = qw(
38 dbg stacktrace
e48635f7 39 local_umask slurp_bytes tmpdir find_co_root rm_rf
08a8d8f1 40 visit_namespaces PEEPEENESS
69016f65 41 check_customcond_args
630e2ea8 42 await_flock DEBUG_TEST_CONCURRENCY_LOCKS
69016f65 43);
44
45if (DEBUG_TEST_CONCURRENCY_LOCKS) {
46 require DBI;
47 my $oc = DBI->can('connect');
48 no warnings 'redefine';
49 *DBI::connect = sub {
50 DBICTest::Util::dbg("Connecting to $_[1]");
51 goto $oc;
52 }
53}
54
55sub dbg ($) {
56 require Time::HiRes;
57 printf STDERR "\n%.06f %5s %-78s %s\n",
58 scalar Time::HiRes::time(),
59 $$,
60 $_[0],
61 $0,
62 ;
63}
8d6b1478 64
630e2ea8 65# File locking is hard. Really hard. By far the best lock implementation
66# I've seen is part of the guts of File::Temp. However it is sadly not
67# reusable. Since I am not aware of folks doing NFS parallel testing,
68# nor are we known to work on VMS, I am just going to punt this and
69# use the portable-ish flock() provided by perl itself. If this does
70# not work for you - patches more than welcome.
71#
72# This figure esentially means "how long can a single test hold a
73# resource before everyone else gives up waiting and aborts" or
74# in other words "how long does the longest test-group legitimally run?"
75my $lock_timeout_minutes = 15; # yes, that's long, I know
76my $wait_step_seconds = 0.25;
77
78sub await_flock ($$) {
79 my ($fh, $locktype) = @_;
80
81 my ($res, $tries);
82 while(
83 ! ( $res = flock( $fh, $locktype | LOCK_NB ) )
84 and
85 ++$tries <= $lock_timeout_minutes * 60 / $wait_step_seconds
86 ) {
87 select( undef, undef, undef, $wait_step_seconds );
88
89 # "say something" every 10 cycles to work around RT#108390
90 # jesus christ our tooling is such a crock of shit :(
91 print "#\n" if not $tries % 10;
92 }
93
94 return $res;
95}
96
bbf6a9a5 97
98sub local_umask ($) {
8d6b1478 99 return unless defined $Config{d_umask};
100
bbf6a9a5 101 croak 'Calling local_umask() in void context makes no sense'
8d6b1478 102 if ! defined wantarray;
103
bbf6a9a5 104 my $old_umask = umask($_[0]);
e48635f7 105 croak "Setting umask failed: $!" unless defined $old_umask;
8d6b1478 106
bbf6a9a5 107 scope_guard(sub {
108 local ($@, $!, $?);
109
110 eval {
111 defined(umask $old_umask) or die "nope";
112 1;
113 } or cluck (
114 "Unable to reset old umask '$old_umask': " . ($! || 'Unknown error')
115 );
116 });
8d6b1478 117}
118
e3be2b6f 119# Try to determine the root of a checkout/untar if possible
120# OR throws an exception
121my $co_root;
122sub find_co_root () {
123
124 $co_root ||= do {
125
126 my @mod_parts = split /::/, (__PACKAGE__ . '.pm');
127 my $inc_key = join ('/', @mod_parts); # %INC stores paths with / regardless of OS
128
129 # a bit convoluted, but what we do here essentially is:
130 # - get the file name of this particular module
131 # - do 'cd ..' as many times as necessary to get to t/lib/../..
132
133 my $root = $INC{$inc_key}
134 or croak "\$INC{'$inc_key'} seems to be missing, this can't happen...";
135
136 $root = parent_dir $root
137 for 1 .. @mod_parts + 2;
138
139 # do the check twice so that the exception is more informative in the
140 # very unlikely case of realpath returning garbage
141 # (Paththools are in really bad shape - handholding all the way down)
142 for my $call_realpath (0,1) {
143
144 require Cwd and $root = ( Cwd::realpath($root) . '/' )
145 if $call_realpath;
146
147 croak "Unable to find root of DBIC checkout/untar: '${root}Makefile.PL' does not exist"
148 unless -f "${root}Makefile.PL";
149 }
150
439a7283 151 # at this point we are pretty sure this is the right thing - detaint
152 ($root =~ /(.+)/)[0];
e3be2b6f 153 }
154}
155
439a7283 156my $tempdir;
157sub tmpdir () {
158 $tempdir ||= do {
159
160 require File::Spec;
161 my $dir = File::Spec->tmpdir;
162 $dir .= '/' unless $dir =~ / [\/\\] $ /x;
163
164 # the above works but not always, test it to bits
165 my $reason_dir_unusable;
166
167 # PathTools has a bug where on MSWin32 it will often return / as a tmpdir.
168 # This is *really* stupid and the result of having our lockfiles all over
169 # the place is also rather obnoxious. So we use our own heuristics instead
170 # https://rt.cpan.org/Ticket/Display.html?id=76663
171 my @parts = File::Spec->splitdir($dir);
172
173 # deal with how 'C:\\\\\\\\\\\\\\' decomposes
174 pop @parts while @parts and ! length $parts[-1];
175
176 if (
177 @parts < 2
178 or
179 ( @parts == 2 and $parts[1] =~ /^ [\/\\] $/x )
180 ) {
181 $reason_dir_unusable =
182 'File::Spec->tmpdir returned a root directory instead of a designated '
183 . 'tempdir (possibly https://rt.cpan.org/Ticket/Display.html?id=76663)';
184 }
185 else {
186 # make sure we can actually create and sysopen a file in this dir
187
188 my $fn = $dir . "_dbictest_writability_test_$$";
189
190 my $u = local_umask(0); # match the umask we use in DBICTest(::Schema)
191 my $g = scope_guard { unlink $fn };
192
193 eval {
194
195 if (-e $fn) {
196 unlink $fn or die "Unable to unlink pre-existing $fn: $!\n";
197 }
198
199 sysopen (my $tmpfh, $fn, O_RDWR|O_CREAT) or die "Opening $fn failed: $!\n";
200
201 print $tmpfh 'deadbeef' x 1024 or die "Writing to $fn failed: $!\n";
202
203 close $tmpfh or die "Closing $fn failed: $!\n";
204
205 1;
206 }
207 or
208 do {
209 chomp( my $err = $@ );
210
211 my @x_tests = map
212 { (defined $_) ? ( $_ ? 1 : 0 ) : 'U' }
213 map
214 { (-e, -d, -f, -r, -w, -x, -o)}
215 ($dir, $fn)
216 ;
217
218 $reason_dir_unusable = sprintf <<"EOE", $fn, $err, scalar $>, scalar $), umask(), (stat($dir))[4,5,2], @x_tests;
219File::Spec->tmpdir returned a directory which appears to be non-writeable:
220
221Error encountered while testing '%s': %s
222Process EUID/EGID: %s / %s
223Effective umask: %o
224TmpDir UID/GID: %s / %s
225TmpDir StatMode: %o
226TmpDir X-tests: -e:%s -d:%s -f:%s -r:%s -w:%s -x:%s -o:%s
227TmpFile X-tests: -e:%s -d:%s -f:%s -r:%s -w:%s -x:%s -o:%s
228EOE
229 };
230 }
231
232 if ($reason_dir_unusable) {
233 # Replace with our local project tmpdir. This will make multiple tests
234 # from different runs conflict with each other, but is much better than
235 # polluting the root dir with random crap or failing outright
236 my $local_dir = find_co_root . 't/var/';
237
238 mkdir_p $local_dir;
239
240 warn "\n\nUsing '$local_dir' as test scratch-dir instead of '$dir': $reason_dir_unusable\n\n";
241 $dir = $local_dir;
242 }
243
244 $dir;
245 };
246}
247
e3be2b6f 248
e48635f7 249sub slurp_bytes ($) {
250 croak "Expecting a file name, not a filehandle" if openhandle $_[0];
251 croak "'$_[0]' is not a readable filename" unless -f $_[0] && -r $_[0];
252 open my $fh, '<:raw', $_[0] or croak "Unable to open '$_[0]': $!";
253 local $/ unless wantarray;
254 <$fh>;
255}
256
257
258sub rm_rf ($) {
259 croak "No valid argument supplied to rm_rf()" unless length "$_[0]";
260
261 return unless -e $_[0];
262
263### I do not trust myself - check for subsuming ( the right way )
264### Avoid things like https://rt.cpan.org/Ticket/Display.html?id=111637
265 require Cwd;
266
267 my ($target, $tmp, $co_tmp) = map {
268
269 my $abs_fn = Cwd::abs_path("$_");
270
271 if ( $^O eq 'MSWin32' and length $abs_fn ) {
272
273 # sometimes we can get a short/longname mix, normalize everything to longnames
274 $abs_fn = Win32::GetLongPathName($abs_fn);
275
276 # Fixup for unixy (as opposed to native) slashes
277 $abs_fn =~ s|\\|/|g;
278 }
279
280 $abs_fn =~ s| (?<! / ) $ |/|x
281 if -d $abs_fn;
282
283 ( $abs_fn =~ /(.+)/s )[0]
284
285 } ( $_[0], tmpdir, find_co_root . 't/var' );
286
287 croak(
288 "Path supplied to rm_rf() '$target' is neither within the local nor the "
289 . "global scratch dirs ( '$co_tmp' and '$tmp' ): REFUSING TO `rm -rf` "
290 . 'at random'
291 ) unless (
292 ( index($target, $co_tmp) == 0 and $target ne $co_tmp )
293 or
294 ( index($target, $tmp) == 0 and $target ne $tmp )
295 );
296###
297
298 require File::Path;
299
300 # do not ask for a recent version, use 1.x API calls
301 File::Path::rmtree([ $target ]);
302}
303
304
24fbd7fb 305# This is an absolutely horrible thing to do on an end-user system
306# DO NOT use it indiscriminately - ideally under nothing short of ->is_smoker
307# Not added to EXPORT_OK on purpose
308sub can_alloc_MB ($) {
309 my $arg = shift;
310 $arg = 'UNDEF' if not defined $arg;
311
312 croak "Expecting a positive integer, got '$arg'"
313 if $arg !~ /^[1-9][0-9]*$/;
314
315 my ($perl) = $^X =~ /(.+)/;
316 local $ENV{PATH};
317 local $ENV{PERL5LIB} = join ($Config{path_sep}, @INC);
318
319 local ( $!, $^E, $?, $@ );
320
321 system( $perl, qw( -Mt::lib::ANFANG -e ), <<'EOS', $arg );
322$0 = 'malloc_canary';
323my $tail_character_of_reified_megastring = substr( ( join '', map chr, 0..255 ) x (4 * 1024 * $ARGV[0]), -1 );
324EOS
325
326 !!( $? == 0 )
327}
328
65d35121 329sub stacktrace {
330 my $frame = shift;
331 $frame++;
332 my (@stack, @frame);
333
821edc09 334 while (@frame = CORE::caller($frame++)) {
65d35121 335 push @stack, [@frame[3,1,2]];
336 }
337
338 return undef unless @stack;
339
340 $stack[0][0] = '';
341 return join "\tinvoked as ", map { sprintf ("%s at %s line %d\n", @$_ ) } @stack;
342}
343
a3a17a15 344sub check_customcond_args ($) {
345 my $args = shift;
346
347 confess "Expecting a hashref"
348 unless ref $args eq 'HASH';
349
a446d7f8 350 for (qw(rel_name foreign_relname self_alias foreign_alias)) {
a3a17a15 351 confess "Custom condition argument '$_' must be a plain string"
352 if length ref $args->{$_} or ! length $args->{$_};
353 }
354
a446d7f8 355 confess "Current and legacy rel_name arguments do not match"
356 if $args->{rel_name} ne $args->{foreign_relname};
357
a3a17a15 358 confess "Custom condition argument 'self_resultsource' must be a rsrc instance"
359 unless defined blessed $args->{self_resultsource} and $args->{self_resultsource}->isa('DBIx::Class::ResultSource');
360
361 confess "Passed resultsource has no record of the supplied rel_name - likely wrong \$rsrc"
a446d7f8 362 unless ref $args->{self_resultsource}->relationship_info($args->{rel_name});
363
e884e5d9 364 my $struct_cnt = 0;
1adbd3fc 365
98def3ef 366 if (defined $args->{self_result_object} or defined $args->{self_rowobj} ) {
e884e5d9 367 $struct_cnt++;
98def3ef 368 for (qw(self_result_object self_rowobj)) {
a446d7f8 369 confess "Custom condition argument '$_' must be a result instance"
370 unless defined blessed $args->{$_} and $args->{$_}->isa('DBIx::Class::Row');
371 }
a3a17a15 372
98def3ef 373 confess "Current and legacy self_result_object arguments do not match"
374 if refaddr($args->{self_result_object}) != refaddr($args->{self_rowobj});
a3a17a15 375 }
376
e884e5d9 377 if (defined $args->{foreign_values}) {
378 $struct_cnt++;
1adbd3fc 379
e884e5d9 380 confess "Custom condition argument 'foreign_values' must be a hash reference"
381 unless ref $args->{foreign_values} eq 'HASH';
1adbd3fc 382 }
383
e884e5d9 384 confess "Data structures supplied on both ends of a relationship"
385 if $struct_cnt == 2;
1adbd3fc 386
a3a17a15 387 $args;
388}
389
c9abd679 390sub visit_namespaces {
391 my $args = { (ref $_[0]) ? %{$_[0]} : @_ };
392
393 my $visited_count = 1;
394
395 # A package and a namespace are subtly different things
396 $args->{package} ||= 'main';
397 $args->{package} = 'main' if $args->{package} =~ /^ :: (?: main )? $/x;
398 $args->{package} =~ s/^:://;
399
400 if ( $args->{action}->($args->{package}) ) {
401 my $ns =
402 ( ($args->{package} eq 'main') ? '' : $args->{package} )
403 .
404 '::'
405 ;
406
407 $visited_count += visit_namespaces( %$args, package => $_ ) for
408 grep
409 # this happens sometimes on %:: traversal
410 { $_ ne '::main' }
411 map
412 { $_ =~ /^(.+?)::$/ ? "$ns$1" : () }
413 do { no strict 'refs'; keys %$ns }
414 ;
415 }
416
417 return $visited_count;
418}
419
65d35121 4201;