Ensure describe_environment does not break its output in half
[dbsrgits/DBIx-Class.git] / t / 00describe_environment.t
CommitLineData
0a70f4be 1###
2### This version is rather 5.8-centric, because DBIC itself is 5.8
3### It certainly can be rewritten to degrade well on 5.6
4###
5
645e7af4 6# Very important to grab the snapshot early, as we will be reporting
7# the INC indices from the POV of whoever ran the script, *NOT* from
8# the POV of the internals
9my @initial_INC;
10BEGIN {
11 @initial_INC = @INC;
12}
0a70f4be 13
14BEGIN {
645e7af4 15 local @INC = ( 't/lib', @INC );
16
17
18 if ( "$]" < 5.010) {
0a70f4be 19
20 # Pre-5.10 perls pollute %INC on unsuccesfull module
21 # require, making it appear as if the module is already
22 # loaded on subsequent require()s
23 # Can't seem to find the exact RT/perldelta entry
24 #
25 # The reason we can't just use a sane, clean loader, is because
26 # if a Module require()s another module the %INC will still
27 # get filled with crap and we are back to square one. A global
28 # fix is really the only way for this test, as we try to load
29 # each available module separately, and have no control (nor
30 # knowledge) over their common dependencies.
31 #
32 # we want to do this here, in the very beginning, before even
33 # warnings/strict are loaded
34
0a70f4be 35 require DBICTest::Util::OverrideRequire;
36
37 DBICTest::Util::OverrideRequire::override_global_require( sub {
38 my $res = eval { $_[0]->() };
39 if ($@ ne '') {
40 delete $INC{$_[1]};
41 die $@;
42 }
43 return $res;
44 } );
645e7af4 45
0a70f4be 46 }
0a70f4be 47
645e7af4 48 require DBICTest::RunMode;
49 require DBICTest::Util;
50}
0a70f4be 51
52use strict;
53use warnings;
54
55use Test::More 'no_plan';
56use Config;
57use File::Find 'find';
645e7af4 58use Digest::MD5 ();
59use Cwd 'abs_path';
60use File::Spec;
61use List::Util 'max';
0a70f4be 62use ExtUtils::MakeMaker;
b602aabd 63
645e7af4 64use DBIx::Class::Optional::Dependencies;
0a70f4be 65
645e7af4 66my $known_paths = {
67 SA => {
68 config_key => 'sitearch',
69 },
70 SL => {
71 config_key => 'sitelib',
72 },
73 SS => {
74 config_key => 'sitelib_stem',
75 match_order => 1,
76 },
77 SP => {
78 config_key => 'siteprefix',
79 match_order => 2,
80 },
81 VA => {
82 config_key => 'vendorarch',
83 },
84 VL => {
85 config_key => 'vendorlib',
86 },
87 VS => {
88 config_key => 'vendorlib_stem',
89 match_order => 3,
90 },
91 VP => {
92 config_key => 'vendorprefix',
93 match_order => 4,
94 },
95 PA => {
96 config_key => 'archlib',
97 },
98 PL => {
99 config_key => 'privlib',
100 },
101 PP => {
102 config_key => 'prefix',
103 match_order => 5,
104 },
105 BLA => {
106 rel_path => './blib/arch',
107 skip_unversioned_modules => 1,
108 },
109 BLL => {
110 rel_path => './blib/lib',
111 skip_unversioned_modules => 1,
112 },
113 INC => {
114 rel_path => './inc',
115 },
116 LIB => {
117 rel_path => './lib',
118 skip_unversioned_modules => 1,
119 },
120 T => {
121 rel_path => './t',
122 skip_unversioned_modules => 1,
123 },
124 XT => {
125 rel_path => './xt',
126 skip_unversioned_modules => 1,
127 },
128 CWD => {
129 rel_path => '.',
130 },
131 HOME => {
132 rel_path => '~',
133 abs_unix_path => abs_unix_path (
134 eval { require File::HomeDir and File::HomeDir->my_home }
135 ||
136 $ENV{USERPROFILE}
137 ||
138 $ENV{HOME}
139 ||
140 glob('~')
141 ),
142 },
0a70f4be 143};
144
645e7af4 145for my $k (keys %$known_paths) {
146 my $v = $known_paths->{$k};
b602aabd 147
645e7af4 148 # never use home as a found-in-dir marker - it is too broad
149 # HOME is only used by the shortener
150 $v->{marker} = $k unless $k eq 'HOME';
b602aabd 151
645e7af4 152 unless ( $v->{abs_unix_path} ) {
153 if ( $v->{rel_path} ) {
154 $v->{abs_unix_path} = abs_unix_path( $v->{rel_path} );
155 }
156 elsif ( $Config{ $v->{config_key} || '' } ) {
157 $v->{abs_unix_path} = abs_unix_path (
158 $Config{"$v->{config_key}exp"} || $Config{$v->{config_key}}
159 );
160 }
161 }
0a70f4be 162
645e7af4 163 delete $known_paths->{$k} unless $v->{abs_unix_path} and -d $v->{abs_unix_path};
0a70f4be 164}
645e7af4 165my $seen_markers = {};
0a70f4be 166
645e7af4 167# first run through lib/ and *try* to load anything we can find
0a70f4be 168# within our own project
169find({
170 wanted => sub {
171 -f $_ or return;
172
645e7af4 173 $_ =~ m|lib/DBIx/Class/_TempExtlib| and return;
174
0a70f4be 175 # can't just `require $fn`, as we need %INC to be
176 # populated properly
177 my ($mod) = $_ =~ /^ lib [\/\\] (.+) \.pm $/x
178 or return;
179
645e7af4 180 try_module_require(join ('::', File::Spec->splitdir($mod)) )
0a70f4be 181 },
182 no_chdir => 1,
183}, 'lib' );
184
645e7af4 185
186
0a70f4be 187# now run through OptDeps and attempt loading everything else
188#
189# some things needs to be sorted before other things
190# positive - load first
191# negative - load last
192my $load_weights = {
193 # Make sure oracle is tried last - some clients (e.g. 10.2) have symbol
194 # clashes with libssl, and will segfault everything coming after them
195 "DBD::Oracle" => -999,
196};
197
645e7af4 198my @known_modules = sort
199 { ($load_weights->{$b}||0) <=> ($load_weights->{$a}||0) }
200 qw( Data::Dumper ),
0a70f4be 201 map
202 { $_ => 1 }
203 map
645e7af4 204 { keys %{ DBIx::Class::Optional::Dependencies->req_list_for($_) } }
0a70f4be 205 grep
645e7af4 206 # some DBDs are notoriously problematic to load
207 # hence only show stuff based on test_rdbms which will
208 # take into account necessary ENVs
209 { $_ !~ /^ (?: rdbms | dist )_ /x }
0a70f4be 210 keys %{DBIx::Class::Optional::Dependencies->req_group_list}
0a70f4be 211;
212
645e7af4 213try_module_require($_) for @known_modules;
214
0a70f4be 215my $has_versionpm = eval { require version };
216
645e7af4 217
218# At this point we've loaded everything we ever could, but some modules
219# (understandably) crapped out. For an even more thorough report, note
220# everthing present in @INC we excplicitly know about (via OptDeps)
221# *even though* it didn't load
222my $known_failed_loads;
223
224for my $mod (@known_modules) {
225 my $inc_key = module_notional_filename($mod);
226 next if defined $INC{$inc_key};
227
228 if (defined( my $idx = module_found_at_inc_index( $mod, \@INC ) ) ) {
229 $known_failed_loads->{$mod} = abs_unix_path( "$INC[$idx]/$inc_key" );
230 }
231
232}
233
234my $perl = 'perl';
235
236# This is a cool idea, but the line is too long even with shortening :(
237#
238#for my $i ( 1 .. $Config{config_argc} ) {
239# my $conf_arg = $Config{"config_arg$i"};
240# $conf_arg =~ s!
241# \= (.+)
242# !
243# '=' . shorten_fn($1)
244# !ex;
245#
246# $perl .= " $conf_arg";
247#}
248
249my $interesting_modules = {
250 # pseudo module
251 $perl => {
252 version => $],
253 abs_unix_path => abs_unix_path($^X),
254 }
255};
256
257
258# drill through the *ENTIRE* symtable and build a map of interesting modules
259DBICTest::Util::visit_namespaces( action => sub {
b602aabd 260 no strict 'refs';
0a70f4be 261 my $pkg = shift;
262
263 # keep going, but nothing to see here
264 return 1 if $pkg eq 'main';
265
b602aabd 266 # private - not interested, including no further descent
0a70f4be 267 return 0 if $pkg =~ / (?: ^ | :: ) _ /x;
268
645e7af4 269 my $inc_key = module_notional_filename($pkg);
0a70f4be 270
645e7af4 271 my $abs_unix_path = (
272 $INC{$inc_key}
b602aabd 273 and
645e7af4 274 -f $INC{$inc_key}
b602aabd 275 and
645e7af4 276 -r $INC{$inc_key}
b602aabd 277 and
645e7af4 278 abs_unix_path($INC{$inc_key})
279 );
0a70f4be 280
645e7af4 281 # handle versions first (not interested in synthetic classes)
0a70f4be 282 if (
645e7af4 283 defined ${"${pkg}::VERSION"}
0a70f4be 284 and
645e7af4 285 ${"${pkg}::VERSION"} !~ /\Qset by base.pm/
0a70f4be 286 ) {
0a70f4be 287
645e7af4 288 # make sure a version can be extracted, be noisy when it doesn't work
289 # do this even if we are throwing away the result below in lieu of EUMM
290 my $mod_ver = eval { $pkg->VERSION };
291
292 if (my $err = $@) {
293 $err =~ s/^/ /mg;
294 say_err (
295 "Calling `$pkg->VERSION` resulted in an exception, which should never "
296 . "happen - please file a bug with the distribution containing $pkg. "
297 . "Complete exception text below:\n\n$err"
298 );
299 }
300 elsif( ! defined $mod_ver or ! length $mod_ver ) {
301 my $ret = defined $mod_ver
302 ? "the empty string ''"
303 : "'undef'"
304 ;
305
306 say_err (
307 "Calling `$pkg->VERSION` returned $ret, even though \$${pkg}::VERSION "
308 . "is defined, which should never happen - please file a bug with the "
309 . "distribution containing $pkg."
310 );
311
312 undef $mod_ver;
313 }
314
315 if (
316 $abs_unix_path
317 and
318 defined ( my $eumm_ver = eval { MM->parse_version( $abs_unix_path ) } )
319 ) {
320
321 # can only run the check reliably if v.pm is there
322 if (
323 $has_versionpm
324 and
325 defined $mod_ver
326 and
327 $eumm_ver ne $mod_ver
328 and
329 (
330 ( eval { version->parse( do { (my $v = $eumm_ver) =~ s/_//g; $v } ) } || 0 )
331 !=
332 ( eval { version->parse( do { (my $v = $mod_ver) =~ s/_//g; $v } ) } || 0 )
333 )
334 ) {
335 say_err (
336 "Mismatch of versions '$mod_ver' and '$eumm_ver', obtained respectively "
337 . "via `$pkg->VERSION` and parsing the version out of @{[ shorten_fn( $abs_unix_path ) ]} "
338 . "with ExtUtils::MakeMaker\@@{[ ExtUtils::MakeMaker->VERSION ]}. "
339 . "This should never happen - please check whether this is still present "
340 . "in the latest version, and then file a bug with the distribution "
341 . "containing $pkg."
342 );
343 }
344
345 $interesting_modules->{$pkg}{version} = $eumm_ver;
346 }
347 elsif( defined $mod_ver ) {
348
349 $interesting_modules->{$pkg}{version} = $mod_ver;
350 }
0a70f4be 351 }
645e7af4 352 elsif ( $known_failed_loads->{$pkg} ) {
353 $abs_unix_path = $known_failed_loads->{$pkg};
354 $interesting_modules->{$pkg}{version} = '!! LOAD FAIL !!';
0a70f4be 355 }
356
645e7af4 357 if ($abs_unix_path) {
358 my ($marker, $initial_inc_idx);
0a70f4be 359
645e7af4 360 my $current_inc_idx = module_found_at_inc_index($pkg, \@INC);
361 my $p = subpath_of_known_path( $abs_unix_path );
0a70f4be 362
645e7af4 363 if (
364 defined $current_inc_idx
0a70f4be 365 and
645e7af4 366 $p->{marker}
0a70f4be 367 and
645e7af4 368 abs_unix_path($INC[$current_inc_idx]) eq $p->{abs_unix_path}
369 ) {
370 $marker = $p->{marker};
371 }
372 elsif (defined ( $initial_inc_idx = module_found_at_inc_index($pkg, \@initial_INC) ) ) {
373 $marker = "\$INC[$initial_inc_idx]";
0a70f4be 374 }
0a70f4be 375
645e7af4 376 # we are only interested if there was a declared version already above
377 # OR if the module came from somewhere other than skip_unversioned_modules
378 if (
379 $marker
380 and
381 (
382 $interesting_modules->{$pkg}
383 or
384 !$p->{skip_unversioned_modules}
385 )
386 ) {
387 $interesting_modules->{$pkg}{source_marker} = $marker;
388 $seen_markers->{$marker} = 1;
389 }
0a70f4be 390
645e7af4 391 # at this point only fill in the path (md5 calc) IFF it is interesting
392 # in any respect
393 $interesting_modules->{$pkg}{abs_unix_path} = $abs_unix_path
394 if $interesting_modules->{$pkg};
395 }
0a70f4be 396
645e7af4 397 1;
398});
0a70f4be 399
645e7af4 400# compress identical versions sourced from ./blib, ./lib, ./t and ./xt
401# as close to the root of a namespace as we can
402purge_identically_versioned_submodules_with_markers([ map {
403 ( $_->{skip_unversioned_modules} && $_->{marker} ) || ()
404} values %$known_paths ]);
0a70f4be 405
645e7af4 406ok 1, (scalar keys %$interesting_modules) . " distinctly versioned modules found";
0a70f4be 407
645e7af4 408# do not announce anything under ci - we are watching for STDERR silence
409exit 0 if DBICTest::RunMode->is_ci;
0a70f4be 410
0a70f4be 411
412# diag the result out
b602aabd 413my $max_ver_len = max map
645e7af4 414 { length "$_" }
415 ( 'xxx.yyyzzz_bbb', map { $_->{version} || '' } values %$interesting_modules )
b602aabd 416;
645e7af4 417my $max_marker_len = max map { length $_ } ( '$INC[999]', keys %$seen_markers );
0a70f4be 418
b602aabd 419my $discl = <<'EOD';
420
645e7af4 421List of loadable modules within both the core and *OPTIONAL* dependency chains
422present on this system (modules sourced from ./blib, ./lib, ./t, and ./xt
423with versions identical to their parent namespace were omitted for brevity)
424
425 *** Note that *MANY* of these modules will *NEVER* be loaded ***
426 *** during normal operation of DBIx::Class ***
b602aabd 427EOD
428
645e7af4 429# pre-assemble everything and print it in one shot
430# makes it less likely for parallel test execution to insert bogus lines
431my $final_out = "\n$discl\n";
b602aabd 432
645e7af4 433$final_out .= "\@INC at startup (does not reflect manipulation at runtime):\n";
b602aabd 434
645e7af4 435my $in_inc_skip;
436for (0.. $#initial_INC) {
b602aabd 437
645e7af4 438 my $shortname = shorten_fn( $initial_INC[$_] );
0a70f4be 439
645e7af4 440 # when *to* print a line of INC
441 if (
442 ! $ENV{AUTOMATED_TESTING}
443 or
444 @initial_INC < 11
445 or
446 $seen_markers->{"\$INC[$_]"}
447 or
448 ! -e $shortname
449 or
450 ! File::Spec->file_name_is_absolute($shortname)
451 ) {
452 $in_inc_skip = 0;
453 $final_out .= sprintf ( "% 3s: %s\n",
454 $_,
455 $shortname
456 );
457 }
458 elsif(! $in_inc_skip++) {
459 $final_out .= " ...\n";
460 }
461}
462
463$final_out .= "\n";
464
465if (my @seen_known_paths = grep { $known_paths->{$_} } keys %$seen_markers) {
466
467 $final_out .= join "\n", 'Sourcing markers:', (map
468 {
469 sprintf "%*s: %s",
470 $max_marker_len => $_->{marker},
471 ($_->{config_key} ? "\$Config{$_->{config_key}}" : "$_->{rel_path}/" )
472 }
473 sort
474 {
475 !!$b->{config_key} cmp !!$a->{config_key}
476 or
477 ( $a->{marker}||'') cmp ($b->{marker}||'')
478 }
479 @{$known_paths}{@seen_known_paths}
480 ), '', '';
0a70f4be 481
645e7af4 482}
483
484$final_out .= "=============================\n";
485
486$final_out .= join "\n", (map
487 { sprintf (
488 "%*s %*s %*s%s",
489 $max_marker_len => $interesting_modules->{$_}{source_marker} || '',
490 $max_ver_len => ( defined $interesting_modules->{$_}{version}
491 ? $interesting_modules->{$_}{version}
0a70f4be 492 : ''
493 ),
645e7af4 494 -78 => $_,
495 ($interesting_modules->{$_}{abs_unix_path}
496 ? " [ MD5: @{[ get_md5( $interesting_modules->{$_}{abs_unix_path} ) ]} ]"
497 : "! -f \$INC{'@{[ module_notional_filename($_) ]}'}"
498 ),
499 ) }
500 sort { lc($a) cmp lc($b) } keys %$interesting_modules
501), '';
502
503$final_out .= "=============================\n$discl\n\n";
504
505diag $final_out;
506
a2da3dc6 507# *very* large printouts may not finish flushing before the test exits
508# injecting a <testname> ... ok in the middle of the diag
509# http://www.cpantesters.org/cpan/report/fbdac74c-35ca-11e6-ab41-c893a58a4b8c
510select( undef, undef, undef, 0.2 );
511
645e7af4 512exit 0;
0a70f4be 513
645e7af4 514
515
516sub say_err { print STDERR "\n", @_, "\n\n" };
517
518# do !!!NOT!!! use Module::Runtime's require_module - it breaks CORE::require
519sub try_module_require {
520 # trap deprecation warnings and whatnot
521 local $SIG{__WARN__} = sub {};
522 local $@;
523 eval "require $_[0]";
0a70f4be 524}
525
645e7af4 526sub abs_unix_path {
527 return '' unless (
528 defined $_[0]
529 and
530 ( -e $_[0] or File::Spec->file_name_is_absolute($_[0]) )
531 );
532
533 # File::Spec's rel2abs does not resolve symlinks
534 # we *need* to look at the filesystem to be sure
535 my $abs_fn = abs_path($_[0]);
536
537 if ( $^O eq 'MSWin32' and $abs_fn ) {
538
539 # sometimes we can get a short/longname mix, normalize everything to longnames
540 $abs_fn = Win32::GetLongPathName($abs_fn);
541
542 # Fixup (native) slashes in Config not matching (unixy) slashes in INC
543 $abs_fn =~ s|\\|/|g;
544 }
545
546 $abs_fn;
547}
548
549sub shorten_fn {
550 my $fn = shift;
551
552 my $abs_fn = abs_unix_path($fn);
553
554 if (my $p = subpath_of_known_path( $fn ) ) {
555 $abs_fn =~ s| (?<! / ) $|/|x
556 if -d $abs_fn;
557
558 if ($p->{rel_path}) {
559 $abs_fn =~ s!\Q$p->{abs_unix_path}!$p->{rel_path}!
560 and return $abs_fn;
561 }
562 elsif ($p->{config_key}) {
563 $abs_fn =~ s!\Q$p->{abs_unix_path}!<<$p->{marker}>>!
564 and
565 $seen_markers->{$p->{marker}} = 1
566 and
567 return $abs_fn;
568 }
569 }
570
571 # we got so far - not a known path
572 # return the unixified version it if was absolute, leave as-is otherwise
573 my $rv = ( $abs_fn and File::Spec->file_name_is_absolute( $fn ) )
574 ? $abs_fn
575 : $fn
576 ;
577
578 $rv = "( ! -e ) $rv" unless -e $rv;
579
580 return $rv;
581}
582
583sub subpath_of_known_path {
584 my $abs_fn = abs_unix_path( $_[0] )
585 or return '';
586
587 for my $p (
588 sort {
589 length( $b->{abs_unix_path} ) <=> length( $a->{abs_unix_path} )
590 or
591 ( $a->{match_order} || 0 ) <=> ( $b->{match_order} || 0 )
592 }
593 values %$known_paths
594 ) {
595 # run through the matcher twice - first always append a /
596 # then try without
597 # important to avoid false positives
598 for my $suff ( '/', '' ) {
599 return { %$p } if 0 == index( $abs_fn, "$p->{abs_unix_path}$suff" );
600 }
601 }
602}
603
604sub module_found_at_inc_index {
605 my ($mod, $inc_dirs) = @_;
606
607 return undef unless @$inc_dirs;
608
609 my $fn = module_notional_filename($mod);
610
611 # trust INC if it specifies an existing path
612 if( -f ( my $existing_path = abs_unix_path( $INC{$fn} ) ) ) {
613 for my $i ( 0 .. $#$inc_dirs ) {
614
615 # searching from here on out won't mean anything
616 # FIXME - there is actually a way to interrogate this safely, but
617 # that's a fight for another day
618 return undef if length ref $inc_dirs->[$i];
619
620 return $i
621 if 0 == index( $existing_path, abs_unix_path( $inc_dirs->[$i] ) . '/' );
622 }
623 }
624
625 for my $i ( 0 .. $#$inc_dirs ) {
626
627 if (
628 -d $inc_dirs->[$i]
629 and
630 -f "$inc_dirs->[$i]/$fn"
631 and
632 -r "$inc_dirs->[$i]/$fn"
633 ) {
634 return $i;
635 }
636 }
637
638 return undef;
639}
640
641sub purge_identically_versioned_submodules_with_markers {
642 my $markers = shift;
643
644 return unless @$markers;
645
646 for my $mod ( sort { length($b) <=> length($a) } keys %$interesting_modules ) {
647
648 next unless defined $interesting_modules->{$mod}{version};
649
650 my $marker = $interesting_modules->{$mod}{source_marker}
651 or next;
652
653 next unless grep { $marker eq $_ } @$markers;
654
655 my $parent = $mod;
656
657 while ( $parent =~ s/ :: (?: . (?! :: ) )+ $ //x ) {
658 $interesting_modules->{$parent}
659 and
660 ($interesting_modules->{$parent}{version}||'') eq $interesting_modules->{$mod}{version}
661 and
662 ($interesting_modules->{$parent}{source_marker}||'') eq $interesting_modules->{$mod}{source_marker}
663 and
664 delete $interesting_modules->{$mod}
665 and
666 last
667 }
668 }
669}
670
671sub module_notional_filename {
672 (my $fn = $_[0] . '.pm') =~ s|::|/|g;
673 $fn;
674}
675
676sub get_md5 {
677 # we already checked for -r/-f, just bail if can't open
678 open my $fh, '<:raw', $_[0] or return '';
679 Digest::MD5->new->addfile($fh)->hexdigest;
680}