One last touch of describe_environment: explain missing checksum
[dbsrgits/DBIx-Class-Historic.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';
843b427a 55use List::Util 'max';
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_marker_len = max map { length $_ } ( '$INC[99]', keys %{ $seen_known_markers || {} } );
55c6fb91 361
362my $discl = <<'EOD';
363
3cd1b172 364List of loadable modules within both the core and *OPTIONAL* dependency chains present on this system
55c6fb91 365Note that *MANY* of these modules will *NEVER* be loaded during normal operation of DBIx::Class
3cd1b172 366(modules sourced from ./lib and ./t with versions identical to their parent namespace were omitted for brevity)
55c6fb91 367EOD
cebc0cc8 368
3cd1b172 369# pre-assemble everything and print it in one shot
370# makes it less likely for parallel test execution to insert bogus lines
371my $final_out = "\n$discl\n";
cebc0cc8 372
373
3cd1b172 374if ($seen_known_markers) {
375
376 $final_out .= join "\n", 'Sourcing markers:', (map
377 {
378 sprintf "%*s: %s",
379 $max_marker_len => $_->{marker},
380 ($_->{config_key} ? "\$Config{$_->{config_key}}" : "$_->{relpath}/*" )
381 }
382 sort
55c6fb91 383 {
3cd1b172 384 !!$b->{config_key} cmp !!$a->{config_key}
385 or
386 ( $a->{marker}||'') cmp ($b->{marker}||'')
cebc0cc8 387 }
3cd1b172 388 @{$known_libpaths}{keys %$seen_known_markers}
389 ), '', '';
cebc0cc8 390
cebc0cc8 391}
392
3cd1b172 393$final_out .= "=============================\n";
cebc0cc8 394
3cd1b172 395$final_out .= join "\n", (map
396 { sprintf (
843b427a 397 "%*s %*s %*s%s",
3cd1b172 398 $max_marker_len => $interesting_modules->{$_}{source_marker} || '',
399 $max_ver_len => ( defined $interesting_modules->{$_}{version}
400 ? $interesting_modules->{$_}{version}
401 : ''
402 ),
843b427a 403 -76 => $_,
3cd1b172 404 ($interesting_modules->{$_}{full_path}
843b427a 405 ? " [ MD5: @{[ get_md5( $interesting_modules->{$_}{full_path} ) ]} ]"
406 : "not -f \$INC{'@{[ module_notional_filename($_) ]}'}"
3cd1b172 407 ),
408 ) }
409 sort { lc($a) cmp lc($b) } keys %$interesting_modules
410), '';
411
412$final_out .= "=============================\n$discl\n\n";
4bfba4ee 413
3cd1b172 414diag $final_out;
4bfba4ee 415
55c6fb91 416exit 0;
4bfba4ee 417
4bfba4ee 418
4bfba4ee 419
3cd1b172 420sub say_err { print STDERR @_, "\n\n" };
cebc0cc8 421
55c6fb91 422# do !!!NOT!!! use Module::Runtime's require_module - it breaks CORE::require
423sub try_module_require {
424 # trap deprecation warnings and whatnot
425 local $SIG{__WARN__} = sub {};
426 local $@;
427 eval "require $_[0]";
428}
cebc0cc8 429
55c6fb91 430sub full_path {
431 return '' unless ( defined $_[0] and -e $_[0] );
432
3cd1b172 433 # File::Spec's rel2abs does not resolve symlinks
434 # we *need* to look at the filesystem to be sure
435 my $fn = abs_path($_[0]);
55c6fb91 436
437 if ( $^O eq 'MSWin32' and $fn ) {
438
439 # sometimes we can get a short/longname mix, normalize everything to longnames
440 $fn = Win32::GetLongPathName($fn);
441
442 # Fixup (native) slashes in Config not matching (unixy) slashes in INC
443 $fn =~ s|\\|/|g;
444 }
cebc0cc8 445
55c6fb91 446 $fn;
cebc0cc8 447}
448
55c6fb91 449sub shorten_fn {
450 my $fn = shift;
451
452 my $l = matching_known_lib( $fn )
453 or return $fn;
454
455 if ($l->{relpath}) {
3cd1b172 456 $fn =~ s!\Q$l->{full_path}!$l->{relpath}!;
55c6fb91 457 }
458 elsif ($l->{config_key}) {
3cd1b172 459 $fn =~ s!\Q$l->{full_path}!<<$l->{marker}>>!
460 and
461 $seen_known_markers->{$l->{marker}} = 1;
55c6fb91 462 }
463
464 $fn;
465}
466
467sub matching_known_lib {
468 my $fn = full_path( $_[0] )
469 or return '';
470
471 for my $l (
472 sort { length( $b->{full_path} ) <=> length( $a->{full_path} ) }
473 values %$known_libpaths
474 ) {
3cd1b172 475 # run through the matcher twice - first always append a /
476 # then try without
477 # important to avoid false positives
478 for my $suff ( '/', '' ) {
479 return { %$l } if 0 == index( $fn, "$l->{full_path}$suff" );
480 }
55c6fb91 481 }
482}
483
3cd1b172 484sub module_found_at_inc_index {
485 my ($mod, $dirs) = @_;
486
487 my $fn = module_notional_filename($mod);
488
489 for my $i ( 0 .. $#$dirs ) {
490 # searching from here on out won't mean anything
491 return undef if length ref $dirs->[$i];
492
493 if (
494 -d $dirs->[$i]
495 and
496 -f "$dirs->[$i]/$fn"
497 and
498 -r "$dirs->[$i]/$fn"
499 ) {
500 return $i;
501 }
502 }
503
504 return undef;
505}
506
507sub purge_identically_versioned_submodules_with_markers {
508 my $markers = shift;
509
510 return unless @$markers;
511
512 for my $mod ( sort { length($b) <=> length($a) } keys %$interesting_modules ) {
513
514 next unless defined $interesting_modules->{$mod}{version};
515
516 my $marker = $interesting_modules->{$mod}{source_marker}
517 or next;
518
519 next unless grep { $marker eq $_ } @$markers;
520
521 my $parent = $mod;
522
523 while ( $parent =~ s/ :: (?: . (?! :: ) )+ $ //x ) {
524 $interesting_modules->{$parent}
525 and
526 ($interesting_modules->{$parent}{version}||'') eq $interesting_modules->{$mod}{version}
527 and
528 ($interesting_modules->{$parent}{source_marker}||'') eq $interesting_modules->{$mod}{source_marker}
529 and
530 delete $interesting_modules->{$mod}
531 and
532 last
533 }
534 }
535}
536
537sub module_notional_filename {
538 (my $fn = $_[0] . '.pm') =~ s|::|/|g;
539 $fn;
540}
541
55c6fb91 542sub get_md5 {
543 # we already checked for -r/-f, just bail if can't open
544 open my $fh, '<:raw', $_[0] or return '';
55c6fb91 545 Digest::MD5->new->addfile($fh)->hexdigest;
546}