Further enhance 00describe_environment.t:
[dbsrgits/DBIx-Class.git] / t / 00describe_environment.t
CommitLineData
cebc0cc8 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
3cd1b172 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;
cebc0cc8 10BEGIN {
3cd1b172 11 @initial_INC = @INC;
12}
13
14BEGIN {
15 unshift @INC, 't/lib';
16
cebc0cc8 17 if ($] < 5.010) {
18
19 # Pre-5.10 perls pollute %INC on unsuccesfull module
20 # require, making it appear as if the module is already
21 # loaded on subsequent require()s
22 # Can't seem to find the exact RT/perldelta entry
23 #
24 # The reason we can't just use a sane, clean loader, is because
25 # if a Module require()s another module the %INC will still
26 # get filled with crap and we are back to square one. A global
27 # fix is really the only way for this test, as we try to load
28 # each available module separately, and have no control (nor
29 # knowledge) over their common dependencies.
30 #
31 # we want to do this here, in the very beginning, before even
32 # warnings/strict are loaded
33
cebc0cc8 34 require DBICTest::Util::OverrideRequire;
35
36 DBICTest::Util::OverrideRequire::override_global_require( sub {
37 my $res = eval { $_[0]->() };
38 if ($@ ne '') {
39 delete $INC{$_[1]};
40 die $@;
41 }
42 return $res;
43 } );
44 }
45}
46
cebc0cc8 47use strict;
48use warnings;
49
50use Test::More 'no_plan';
51use Config;
52use File::Find 'find';
3cd1b172 53use Digest::MD5 ();
54use Cwd 'abs_path';
4bfba4ee 55use List::Util qw(max min);
cebc0cc8 56use ExtUtils::MakeMaker;
57use DBICTest::Util 'visit_namespaces';
58
59# load these two to pull in the t/lib armada
60use DBICTest;
61use DBICTest::Schema;
62
55c6fb91 63my $known_libpaths = {
64 SA => {
65 config_key => 'sitearch',
66 },
67 SL => {
68 config_key => 'sitelib',
69 },
70 VA => {
71 config_key => 'vendorarch',
72 },
73 VL => {
74 config_key => 'vendorlib',
75 },
76 PA => {
77 config_key => 'archlib',
78 },
79 PL => {
80 config_key => 'privlib',
81 },
82 INC => {
83 relpath => './inc',
84 },
85 LIB => {
86 relpath => './lib',
87 },
3cd1b172 88 T => {
89 relpath => './t',
90 },
55c6fb91 91 HOME => {
92 relpath => '~',
93 full_path => full_path (
94 eval { require File::HomeDir and File::HomeDir->my_home }
95 ||
96 $ENV{HOME}
97 ||
98 glob('~')
99 ),
100 },
cebc0cc8 101};
102
55c6fb91 103for my $k (keys %$known_libpaths) {
104 my $v = $known_libpaths->{$k};
4bfba4ee 105
55c6fb91 106 # never use home as a found-in-dir marker - it is too broad
107 # HOME is only used by the shortener
3cd1b172 108 $v->{marker} = $k unless $k eq 'HOME';
4bfba4ee 109
55c6fb91 110 unless ( $v->{full_path} ) {
111 if ( $v->{relpath} ) {
112 $v->{full_path} = full_path( $v->{relpath} );
113 }
114 elsif ( $Config{ $v->{config_key} || '' } ) {
115 $v->{full_path} = full_path (
116 $Config{"$v->{config_key}exp"} || $Config{$v->{config_key}}
117 );
118 }
119 }
cebc0cc8 120
55c6fb91 121 delete $known_libpaths->{$k} unless $v->{full_path} and -d $v->{full_path};
cebc0cc8 122}
3cd1b172 123my $seen_known_markers;
cebc0cc8 124
55c6fb91 125# first run through lib/ and *try* to load anything we can find
cebc0cc8 126# within our own project
127find({
128 wanted => sub {
129 -f $_ or return;
130
131 # can't just `require $fn`, as we need %INC to be
132 # populated properly
133 my ($mod) = $_ =~ /^ lib [\/\\] (.+) \.pm $/x
134 or return;
135
55c6fb91 136 try_module_require(join ('::', File::Spec->splitdir($mod)) )
cebc0cc8 137 },
138 no_chdir => 1,
139}, 'lib' );
140
55c6fb91 141
142
cebc0cc8 143# now run through OptDeps and attempt loading everything else
144#
145# some things needs to be sorted before other things
146# positive - load first
147# negative - load last
148my $load_weights = {
149 # Make sure oracle is tried last - some clients (e.g. 10.2) have symbol
150 # clashes with libssl, and will segfault everything coming after them
151 "DBD::Oracle" => -999,
152};
3cd1b172 153
154my @known_modules = sort
cebc0cc8 155 { ($load_weights->{$b}||0) <=> ($load_weights->{$a}||0) }
156 keys %{
157 DBIx::Class::Optional::Dependencies->req_list_for([
250d9e55 158 grep
159 # some DBDs are notoriously problematic to load
160 # hence only show stuff based on test_rdbms which will
161 # take into account necessary ENVs
55c6fb91 162 { $_ !~ /^ (?: rdbms | dist )_ /x }
250d9e55 163 keys %{DBIx::Class::Optional::Dependencies->req_group_list}
cebc0cc8 164 ])
165 }
166;
167
3cd1b172 168try_module_require($_) for @known_modules;
cebc0cc8 169
55c6fb91 170my $has_versionpm = eval { require version };
3cd1b172 171
172
173# At this point we've loaded everything we ever could, but some modules
174# (understandably) crapped out. For an even more thorough report, note
175# everthing present in @INC we excplicitly know about (via OptDeps)
176# *even though* it didn't load
177my $known_failed_loads;
178
179for my $mod (@known_modules) {
180 my $inc_key = module_notional_filename($mod);
181 next if defined $INC{$inc_key};
182
183 if (defined( my $idx = module_found_at_inc_index( $mod, \@INC ) ) ) {
184 $known_failed_loads->{$mod} = full_path( "$INC[$idx]/$inc_key" );
185 }
186
187}
188
189my $perl = 'perl';
190
191# This is a cool idea, but the line is too long even with shortening :(
192#
193#for my $i ( 1 .. $Config{config_argc} ) {
194# my $conf_arg = $Config{"config_arg$i"};
195# $conf_arg =~ s!
196# \= (.+)
197# !
198# '=' . shorten_fn(full_path($1) )
199# !ex;
200#
201# $perl .= " $conf_arg";
202#}
203
204my $interesting_modules = {
205 # pseudo module
206 $perl => {
207 version => $],
208 full_path => $^X,
209 }
55c6fb91 210};
3cd1b172 211
212
213# drill through the *ENTIRE* symtable and build a map of intereseting modules
cebc0cc8 214visit_namespaces( action => sub {
4bfba4ee 215 no strict 'refs';
cebc0cc8 216 my $pkg = shift;
217
218 # keep going, but nothing to see here
219 return 1 if $pkg eq 'main';
220
4bfba4ee 221 # private - not interested, including no further descent
cebc0cc8 222 return 0 if $pkg =~ / (?: ^ | :: ) _ /x;
223
3cd1b172 224 my $inc_key = module_notional_filename($pkg);
cebc0cc8 225
3cd1b172 226 my $full_path = (
227 $INC{$inc_key}
4bfba4ee 228 and
3cd1b172 229 -f $INC{$inc_key}
4bfba4ee 230 and
3cd1b172 231 -r $INC{$inc_key}
55c6fb91 232 and
3cd1b172 233 full_path($INC{$inc_key})
234 );
cebc0cc8 235
3cd1b172 236 # handle versions first (not interested in synthetic classes)
cebc0cc8 237 if (
3cd1b172 238 defined ${"${pkg}::VERSION"}
cebc0cc8 239 and
3cd1b172 240 ${"${pkg}::VERSION"} !~ /\Qset by base.pm/
cebc0cc8 241 ) {
cebc0cc8 242
3cd1b172 243 # make sure a version can be extracted, be noisy when it doesn't work
244 # do this even if we are throwing away the result below in lieu of EUMM
245 my $mod_ver = eval { $pkg->VERSION };
246
247 if (my $err = $@) {
248 $err =~ s/^/ /mg;
249 say_err (
250 "Calling `$pkg->VERSION` resulted in an exception, which should never "
251 . "happen - please file a bug with the distribution containing $pkg. "
252 . "Complete exception text below:\n\n$err"
253 );
254 }
255 elsif( ! defined $mod_ver or ! length $mod_ver ) {
256 my $ret = defined $mod_ver
257 ? "the empty string ''"
258 : "'undef'"
259 ;
260
261 say_err (
262 "Calling `$pkg->VERSION` returned $ret, even though \$${pkg}::VERSION "
263 . "is defined, which should never happen - please file a bug with the "
264 . "distribution containing $pkg."
265 );
266
267 undef $mod_ver;
268 }
269
270 if (
271 $full_path
272 and
273 defined ( my $eumm_ver = eval { MM->parse_version( $full_path ) } )
274 ) {
275
276 # can only run the check reliably if v.pm is there
277 if (
278 $has_versionpm
279 and
280 defined $mod_ver
281 and
282 $eumm_ver ne $mod_ver
283 and
284 (
285 ( eval { version->parse( do { (my $v = $eumm_ver) =~ s/_//g; $v } ) } || 0 )
286 !=
287 ( eval { version->parse( do { (my $v = $mod_ver) =~ s/_//g; $v } ) } || 0 )
288 )
289 ) {
290 say_err (
291 "Mismatch of versions '$mod_ver' and '$eumm_ver', obtained respectively "
292 . "via `$pkg->VERSION` and parsing the version out of @{[ shorten_fn( $full_path ) ]} "
293 . "with ExtUtils::MakeMaker\@@{[ ExtUtils::MakeMaker->VERSION ]}. "
294 . "This should never happen - please check whether this is still present "
295 . "in the latest version, and then file a bug with the distribution "
296 . "containing $pkg."
297 );
298 }
299
300 $interesting_modules->{$pkg}{version} = $eumm_ver;
301 }
302 elsif( defined $mod_ver ) {
303
304 $interesting_modules->{$pkg}{version} = $mod_ver;
305 }
cebc0cc8 306 }
3cd1b172 307 elsif ( $full_path = $known_failed_loads->{$pkg} ) {
308 $interesting_modules->{$pkg}{version} = '!! LOAD FAILED !!';
55c6fb91 309 }
310
3cd1b172 311 if ($full_path) {
312 my $marker;
313 if (my $m = ( matching_known_lib( $full_path ) || {} )->{marker} ) {
314 $marker = $m;
315 }
316 elsif (defined ( my $idx = module_found_at_inc_index($pkg, \@initial_INC) ) ) {
317 $marker = sprintf '$INC[%d]', $idx;
318 }
55c6fb91 319
3cd1b172 320 # we are only interested if there was a declared version already above
321 # OR if the module came from somewhere other than T or LIB
322 if (
323 $marker
324 and
325 (
326 $interesting_modules->{$pkg}
327 or
328 $marker !~ /^ (?: T | LIB ) $/x
329 )
330 ) {
331 $interesting_modules->{$pkg}{source_marker} = $marker;
332 $seen_known_markers->{$marker} = 1
333 if $known_libpaths->{$marker};
55c6fb91 334 }
3cd1b172 335
336 # at this point only fill in the path (md5 calc) IFF it is interesting
337 # in any respect
338 $interesting_modules->{$pkg}{full_path} = $full_path
339 if $interesting_modules->{$pkg};
cebc0cc8 340 }
341
342 1;
343});
344
3cd1b172 345# compress identical versions sourced from ./lib and ./t as close to the root
346# of a namespace as we can
347purge_identically_versioned_submodules_with_markers([qw( LIB T )]);
cebc0cc8 348
3cd1b172 349ok 1, (scalar keys %$interesting_modules) . " distinctly versioned modules found";
cebc0cc8 350
351# do not announce anything under ci - we are watching for STDERR silence
3cd1b172 352exit 0 if DBICTest::RunMode->is_ci;
cebc0cc8 353
cebc0cc8 354
55c6fb91 355# diag the result out
356my $max_ver_len = max map
357 { length "$_" }
3cd1b172 358 ( 'xxx.yyyzzz_bbb', map { $_->{version} || '' } values %$interesting_modules )
55c6fb91 359;
3cd1b172 360my $max_mod_len = max map { length $_ } keys %$interesting_modules;
361my $max_marker_len = max map { length $_ } ( '$INC[99]', keys %{ $seen_known_markers || {} } );
55c6fb91 362
363my $discl = <<'EOD';
364
3cd1b172 365List of loadable modules within both the core and *OPTIONAL* dependency chains present on this system
55c6fb91 366Note that *MANY* of these modules will *NEVER* be loaded during normal operation of DBIx::Class
3cd1b172 367(modules sourced from ./lib and ./t with versions identical to their parent namespace were omitted for brevity)
55c6fb91 368EOD
cebc0cc8 369
3cd1b172 370# pre-assemble everything and print it in one shot
371# makes it less likely for parallel test execution to insert bogus lines
372my $final_out = "\n$discl\n";
cebc0cc8 373
374
3cd1b172 375if ($seen_known_markers) {
376
377 $final_out .= join "\n", 'Sourcing markers:', (map
378 {
379 sprintf "%*s: %s",
380 $max_marker_len => $_->{marker},
381 ($_->{config_key} ? "\$Config{$_->{config_key}}" : "$_->{relpath}/*" )
382 }
383 sort
55c6fb91 384 {
3cd1b172 385 !!$b->{config_key} cmp !!$a->{config_key}
386 or
387 ( $a->{marker}||'') cmp ($b->{marker}||'')
cebc0cc8 388 }
3cd1b172 389 @{$known_libpaths}{keys %$seen_known_markers}
390 ), '', '';
cebc0cc8 391
cebc0cc8 392}
393
3cd1b172 394$final_out .= "=============================\n";
cebc0cc8 395
3cd1b172 396$final_out .= join "\n", (map
397 { sprintf (
398 "%*s %*s %s%s",
399 $max_marker_len => $interesting_modules->{$_}{source_marker} || '',
400 $max_ver_len => ( defined $interesting_modules->{$_}{version}
401 ? $interesting_modules->{$_}{version}
402 : ''
403 ),
404 $_,
405 ($interesting_modules->{$_}{full_path}
406 ? ' ' x (80 - min( 78, length($_) )) . "[ MD5: @{[ get_md5( $interesting_modules->{$_}{full_path} ) ]} ]"
407 : ''
408 ),
409 ) }
410 sort { lc($a) cmp lc($b) } keys %$interesting_modules
411), '';
412
413$final_out .= "=============================\n$discl\n\n";
4bfba4ee 414
3cd1b172 415diag $final_out;
4bfba4ee 416
55c6fb91 417exit 0;
4bfba4ee 418
4bfba4ee 419
4bfba4ee 420
3cd1b172 421sub say_err { print STDERR @_, "\n\n" };
cebc0cc8 422
55c6fb91 423# do !!!NOT!!! use Module::Runtime's require_module - it breaks CORE::require
424sub try_module_require {
425 # trap deprecation warnings and whatnot
426 local $SIG{__WARN__} = sub {};
427 local $@;
428 eval "require $_[0]";
429}
cebc0cc8 430
55c6fb91 431sub full_path {
432 return '' unless ( defined $_[0] and -e $_[0] );
433
3cd1b172 434 # File::Spec's rel2abs does not resolve symlinks
435 # we *need* to look at the filesystem to be sure
436 my $fn = abs_path($_[0]);
55c6fb91 437
438 if ( $^O eq 'MSWin32' and $fn ) {
439
440 # sometimes we can get a short/longname mix, normalize everything to longnames
441 $fn = Win32::GetLongPathName($fn);
442
443 # Fixup (native) slashes in Config not matching (unixy) slashes in INC
444 $fn =~ s|\\|/|g;
445 }
cebc0cc8 446
55c6fb91 447 $fn;
cebc0cc8 448}
449
55c6fb91 450sub shorten_fn {
451 my $fn = shift;
452
453 my $l = matching_known_lib( $fn )
454 or return $fn;
455
456 if ($l->{relpath}) {
3cd1b172 457 $fn =~ s!\Q$l->{full_path}!$l->{relpath}!;
55c6fb91 458 }
459 elsif ($l->{config_key}) {
3cd1b172 460 $fn =~ s!\Q$l->{full_path}!<<$l->{marker}>>!
461 and
462 $seen_known_markers->{$l->{marker}} = 1;
55c6fb91 463 }
464
465 $fn;
466}
467
468sub matching_known_lib {
469 my $fn = full_path( $_[0] )
470 or return '';
471
472 for my $l (
473 sort { length( $b->{full_path} ) <=> length( $a->{full_path} ) }
474 values %$known_libpaths
475 ) {
3cd1b172 476 # run through the matcher twice - first always append a /
477 # then try without
478 # important to avoid false positives
479 for my $suff ( '/', '' ) {
480 return { %$l } if 0 == index( $fn, "$l->{full_path}$suff" );
481 }
55c6fb91 482 }
483}
484
3cd1b172 485sub module_found_at_inc_index {
486 my ($mod, $dirs) = @_;
487
488 my $fn = module_notional_filename($mod);
489
490 for my $i ( 0 .. $#$dirs ) {
491 # searching from here on out won't mean anything
492 return undef if length ref $dirs->[$i];
493
494 if (
495 -d $dirs->[$i]
496 and
497 -f "$dirs->[$i]/$fn"
498 and
499 -r "$dirs->[$i]/$fn"
500 ) {
501 return $i;
502 }
503 }
504
505 return undef;
506}
507
508sub purge_identically_versioned_submodules_with_markers {
509 my $markers = shift;
510
511 return unless @$markers;
512
513 for my $mod ( sort { length($b) <=> length($a) } keys %$interesting_modules ) {
514
515 next unless defined $interesting_modules->{$mod}{version};
516
517 my $marker = $interesting_modules->{$mod}{source_marker}
518 or next;
519
520 next unless grep { $marker eq $_ } @$markers;
521
522 my $parent = $mod;
523
524 while ( $parent =~ s/ :: (?: . (?! :: ) )+ $ //x ) {
525 $interesting_modules->{$parent}
526 and
527 ($interesting_modules->{$parent}{version}||'') eq $interesting_modules->{$mod}{version}
528 and
529 ($interesting_modules->{$parent}{source_marker}||'') eq $interesting_modules->{$mod}{source_marker}
530 and
531 delete $interesting_modules->{$mod}
532 and
533 last
534 }
535 }
536}
537
538sub module_notional_filename {
539 (my $fn = $_[0] . '.pm') =~ s|::|/|g;
540 $fn;
541}
542
55c6fb91 543sub get_md5 {
544 # we already checked for -r/-f, just bail if can't open
545 open my $fh, '<:raw', $_[0] or return '';
55c6fb91 546 Digest::MD5->new->addfile($fh)->hexdigest;
547}