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