xref: /openbsd-src/gnu/usr.bin/perl/ext/Pod-Html/t/lib/Testing.pm (revision fc405d53b73a2d73393cb97f684863d17b583e38)
1package Testing;
2use 5.10.0;
3use warnings;
4use Exporter 'import';
5our $VERSION = 1.33; # Let's keep this same as lib/Pod/Html.pm
6$VERSION = eval $VERSION;
7our @EXPORT_OK = qw(
8    setup_testing_dir
9    xconvert
10    record_state_of_cache
11);
12use Cwd;
13use Pod::Html;
14use Config;
15use File::Basename;
16use File::Copy;
17use File::Path ( qw| make_path | );
18use File::Spec::Functions ':ALL';
19use File::Temp ( qw| tempdir | );
20use Data::Dumper;$Data::Dumper::Sortkeys=1;
21use Pod::Html::Util qw(
22    unixify
23);
24
25*ok = \&Test::More::ok;
26*is = \&Test::More::is;
27
28our @no_arg_switches = ( qw|
29    flush recurse norecurse
30    quiet noquiet verbose noverbose
31    index noindex backlink nobacklink
32    header noheader poderrors nopoderrors
33| );
34
35=head1 NAME
36
37Testing - Helper functions for testing Pod-Html
38
39=head1 SYNOPSIS
40
41    use Testing qw( setup_testing_dir xconvert );
42
43    my $tdir = setup_testing_dir( {
44        debug       => $debug,
45    } );
46
47    $args = {
48        podstub => "htmldir1",
49        description => "test --htmldir and --htmlroot 1a",
50        expect => $expect_raw,
51        p2h => {
52            podpath => File::Spec::Unix->catdir($relcwd, 't') . ":" .
53                       File::Spec::Unix->catdir($relcwd, 'corpus/test.lib'),
54            podroot => catpath($v, '/', ''),
55            htmldir => 't',
56            quiet   => 1,
57        },
58        debug => $debug,
59    };
60    xconvert($args);
61
62=head1 DESCRIPTION
63
64This module exports, upon request only, 2 subroutines which are used in most
65of the files in the core distribution test suite for Pod-HTML
66(F<ext/Pod-Html/t/*.t>).  In the future we may add additional subroutines,
67particularly to better diagnose problems with Pod-Html.
68
69=head2 Pod-Html's Testing Structure
70
71As of version 1.26 of this module (early 2021), the testing structure consists
72of 16 F<.pod> files and 18 F<.t> files located in two subdirectories,
73F<corpus/> and F<t/>.  Let's analyze these by directory.
74
75=head3 Files in F<corpus/>
76
77There are currently 2 F<.pod> files in F<corpus/> both of which are old
78versions of F<pod/*.pod> files selected to give some complexity to the test
79suite.  Since we don't actually attempt to make HTML out of their POD, we
80don't need to discuss them further.
81
82=head3 Files in F<t/>
83
84There are currently 14 F<.pod> files and 18 F<.t> files in F<t/>.  Both of
85these numbers may change in the future.
86
87Currently there are 2 F<t/.t> files (F<t/anchorify.t> and F<t/eol.t>) which
88exercise certain functionality of F<Pod::Html> but which do not require
89F<t/*.pod> files as data input.  These files do not make use of the
90subroutines exported by this module.  We may add more test files like this in
91the future to ensure high test coverage, but don't need to discuss them
92further here.
93
94The remaining 16 F<t/*.t> test programs make use of the testing subroutines
95exported by this module.  Most, but not all, of these test programs make use
96of the F<t/*.pod> files.  Each such test program makes use of only 1
97F<t/*.pod> file at a time, though there are several cases where several,
98similarly named, test programs make use of the same F<t/*.pod> file for data
99input.  For example,
100
101    t/crossref.t
102    t/crossref2.t
103    t/crossref3.t
104
105all make use of
106
107    t/crossref.pod
108
109Each F<t/*.pod> file consists solely of simple documentation in POD format.
110
111=head3 High-level description of programs which use F<.pod> files as input
112
113Each of the F<t/*.t> programs which makes use of a given F<t/*.pod> file
114slurps the text of a single such F<t/*.pod> file into memory.  The test
115program holds text in a C<DATA> handle which serves as a B<template> for the
116HTML expected to be generated by running the F<t/*.pod> file through
117C<Pod::Html::pod2html()>.  The HTML output by C<Pod::Html::pod2html()> can
118vary greatly, particularly with respect to links, depending on the arguments
119passed to that function.  The HTML output will also be affected by the
120underlying operating system, I<e.g.,> with respect to path separators.  Hence,
121we cannot hard-code the expected HTML output into the C<DATA> template or any
122place else.  We have to allow C<Pod::Html::pod2html()> to massage the template
123data to get an "expected output" against which we match the "actual output"
124which come from running C<Pod::Html::pod2html()> over the text originally
125slurped into memory from the F<t/*.pod> file.
126
127Granted, there is a certain amount of circularity in this testing regimen.  On
128a given operating system, with a given F<t/*.pod> file as raw input, a given
129POD parser invoked within C<Pod::Html::pod2html()> and a given set of
130arguments passed to C<pod2html()>, there can and should be only one possible
131HTML string generated as output.  What we currently have in a given test
132program's C<DATA> handle is merely that HTML string retrofitted with certain
133template elements as needed to make the "got" and the "expected" identical.
134We're not testing whether we're generating "good" HTML.  We're simply testing
135that we get consistent results out of C<pod2html()> year after year.
136
137=head3 How a test program works step-by-step
138
139Here we continue to focus on those test programs which make use of the testing
140functions exported by F<Testing> and which take a F<t/*.pod> file as input.
141
142We assume that we begin our tests from the top level of the Perl 5 core distribution and are using F<t/harness>.  Hence, to run the test files we say:
143
144    cd t; ./perl harness ../ext/Pod-Html/t/*.t; cd -
145
146The program then slurps contents of the C<DATA> handle into memory.
147
148The program then calls C<setup_testing_dir()> from this module to create a
149temporary directory and populate it as needed.  C<setup_testing_dir()> returns
150the absolute path to that directory, but at the point of return you are
151actually located two levels beneath the temporary directory in a directory whose relative path is
152F<ext/Pod-Html/>.  (This is equivalent to being in F<toplevel/ext/Pod-Html/>
153for tests in versions of Pod-Html distributed with earlier versions of
154F<perl>.)
155
156Note that this means that at the end of the program you will have to
157switch back to your starting directory so that the tempdir can automatically
158be cleaned up.  We automate this via an C<END> block.
159
160You then prepare arguments for our principal testing function, C<xconvert()>
161(which supersedes the former C<convert_n_test()>.  These arguments take the
162form of a single hash reference.  One customary but optional element in that
163hashref, C<p2h>, is itself a hashref of key-value pairs corresponding to
164switches passed to the F<pod2html> command-line utility or to
165C<Pod::Html::pod2html()>.  The other elements in the hashref passed to
166C<xconvert()> include the stub of the basename of the F<t/*.pod> file being
167used, the text of that file (which we've already slurped into memory), the
168test description, and whether we want extra debugging output or not.  The
169program then adds a key-value pair to indicate whether we're running via core
170distribution test harness or not.
171
172The hashref is then passed to C<xconvert()> which internally generates an
173expected HTML output string by massaging the text read in from the C<DATA>
174handle.   C<xconvert()> reads in the relevant F<t/*.pod> file and passes it to
175C<Pod::Html::pod2html()>, which parses the POD and generates the actual HTML
176output.  If "got" matches "expected", a PASS is recorded for this instance of
177C<xconvert()>.
178
179As the example of F<t/htmldir1.t> illustrates:
180
181=over 4
182
183=item *
184
185The user can define a variety of arguments to be passed through to C<Pod::Html::pod2html()>.
186
187    my ($v, $d) = splitpath(cwd(), 1);
188    my @dirs = splitdir($d);
189    shift @dirs if $dirs[0] eq '';
190    my $relcwd = join '/', @dirs;
191
192    $args = {
193        ...
194        p2h => {
195            podpath => File::Spec::Unix->catdir($relcwd, 't') . ":" .
196                       File::Spec::Unix->catdir($relcwd, 'corpus/test.lib'),
197            podroot => catpath($v, '/', ''),
198            htmldir => 't',
199            quiet   => 1,
200        },
201        ...
202    };
203
204=item *
205
206The user can try out a variety of different arguments in the C<p2h> element
207and end up with the same HTML output as predicted by the C<DATA> template by
208calling C<xconvert()> more than once per file.
209
210    $args = {
211        podstub => "htmldir1",
212        description => "test --htmldir and --htmlroot 1a",
213        expect => $expect_raw,
214        p2h => {
215            podpath => File::Spec::Unix->catdir($relcwd, 't') . ":" .
216                       File::Spec::Unix->catdir($relcwd, 'corpus/test.lib'),
217            podroot => catpath($v, '/', ''),
218            htmldir => 't',
219            quiet   => 1,
220        },
221    };
222    xconvert($args);
223
224    $args = {
225        podstub => "htmldir1",
226        description => "test --htmldir and --htmlroot 1b",
227        expect => $expect_raw,
228        p2h => {
229            podpath     => $relcwd,
230            podroot     => catpath($v, '/', ''),
231            htmldir     => catdir($relcwd, 't'),
232            htmlroot    => '/',
233            quiet       => 1,
234        },
235    };
236    xconvert($args);
237
238Note that in the two "runs" above, the values for C<podstub> are the
239same, but the arguments to C<p2h> differ; we've distinguished the two runs
240by different values for C<description>.
241
242=back
243
244Note that all runs within an individual F<t/*.t> program share the same
245temporary directory.  Since C<Pod::Html::pod2html()> typically caches its
246understanding of where F<.pod> files are located, there is a possibility that
247the contents of the cache may affect the generated HTML output in an adverse
248way.  This possibility will be addressed in an upcoming version of this
249program.
250
251When all runs have been completed (as noted above), the C<END> block brings us
252back to the directory we started from to permit the temporary directory and
253its contents to be cleanly deleted.
254
255=head1 SUBROUTINES
256
257=head2 C<setup_testing_dir()>
258
259=over 4
260
261=item * Purpose
262
263Create and populate a temporary directory to hold all activity for a single F<t/*.t> program.
264
265=item * Arguments
266
267    $tdir = setup_testing_dir( {
268        startdir    => $startdir,
269        debug       => $debug,
270    } );
271
272Single hash reference with two possible elements.
273
274=over 4
275
276=item * C<debug>
277
278A Boolean which you will typically set at the start of your program.  A
279Perl-true value prints out your location and creates a temporary directory
280which is B<not> cleaned up at the program's completion, thereby permitting you
281to examine the intermediate files created by the program.
282
283=back
284
285=item * Return Value
286
287String holding the absolute path of the temporary directory.
288
289=item * Comments
290
291The function C<chdir>s internally and leaves you in a directory called
292F<ext/Pod-Html> beneath the temporary directory found in the return value.
293
294The function is somewhat equivalent to testing helper function
295C<make_test_dir> in F<t/pod2html-lib.pl> in versions of Pod-Html shipped with
296versions of F<perl> up through 5.32.
297
298=back
299
300=cut
301
302sub setup_testing_dir {
303    my $args = shift;
304    my $cwd = cwd();
305    my $toptempdir = $args->{debug} ? tempdir() : tempdir( CLEANUP => 1 );
306    if ($args->{debug}) {
307        print STDERR "toptempdir: $toptempdir\n";
308    }
309    chdir $toptempdir or die "Unable to change to $toptempdir: $!";
310
311    my $ephdir = catdir($toptempdir, 'ext', 'Pod-Html');
312    my ($fromdir, $targetdir, $pod_glob, @testfiles);
313
314    # Copy ext/Pod-Html/t/*.pod files into position under tempdir
315    $fromdir = catdir($cwd, 't');
316    # Per Craig Berry: Avoid hard-coded '/' to keep VMS happy
317    $pod_glob = catfile($fromdir, '*.pod');
318    @testfiles = glob($pod_glob);
319
320    $targetdir = catdir($ephdir, 't');
321    make_path($targetdir) or die("Cannot mkdir $targetdir for testing: $!");
322    for my $f (@testfiles) {
323        copy $f => $targetdir or die "Unable to copy: $!";
324    }
325
326    # Copy ext/Pod-Html/corpus/*.pod files into position under tempdir
327    $fromdir = catdir($cwd, 'corpus');
328    # Per Craig Berry: Avoid hard-coded '/' to keep VMS happy
329    $pod_glob = catfile($fromdir, '*.pod');
330    @testfiles = glob($pod_glob);
331
332    $targetdir = catdir($ephdir, 'corpus', 'test.lib');
333    make_path($targetdir) or die "Could not make $targetdir for testing: $!";
334
335    my %copying = ();
336    for my $g (@testfiles) {
337        my $basename = basename($g);
338        my ($stub) = $basename =~ m{^(.*)\.pod};
339        $stub =~ s{^perl(.*)}{$1};
340        $copying{$stub} = {
341            source => $g,
342            target => catfile($targetdir, "${stub}.pod")
343        };
344    }
345
346    for my $k (keys %copying) {
347        copy $copying{$k}{source} => $copying{$k}{target}
348            or die "Unable to copy: $!";
349    }
350
351    # Move into tempdir/ext/Pod-Html
352    chdir $ephdir or die "Unable to change to $ephdir: $!";
353    return $toptempdir;
354}
355
356=head2 C<xconvert()>
357
358=over 4
359
360=item * Purpose
361
362Compare whether the HTML generated by C<Pod::Html::pod2html()>'s parsing of a
363F<.pod> file matches the expectation generated by parsing the C<DATA> block
364within the test file.
365
366=item * Arguments
367
368Single hash reference.
369
370    $args = {
371        podstub => "htmldir5",
372        description => "test --htmldir and --htmlroot 5",
373        expect => $expect_raw,
374        p2h => {
375            podpath     => 't:corpus/test.lib',
376            podroot     => $cwd,
377            htmldir     => $cwd,
378            htmlroot    => '/',
379            quiet       => 1,
380        },
381        debug => $debug,
382    };
383    $args->{core} = 1 if $ENV{PERL_CORE};
384
385Elements are as follows:
386
387=over 4
388
389=item * C<podstub>
390
391String holding the stub (or stem) of the F<.pod> file being used as input.
392The stub is the basename of the file less the file extension or suffix.
393(Equivalent to the first argument passed to the former C<convert_and_test>
394test helper routine.)  Required.
395
396=item * C<description>
397
398String holding the description (or name or label) in typical TAP syntax.
399(Equivalent to the second argument passed to the former C<convert_and_test>
400helper routine.)  Required.
401
402=item * C<expect>
403
404String holding the "raw" expectations read in from the C<DATA> handle.  Each
405run of C<xconvert()> within a given test file should have the same value for
406this key.  Required.
407
408=item * C<p2h>
409
410Hash reference holding arguments passed to C<Pod::Html::pod2html()> (though
411without the leading double hyphens (C<-->).  See documentation for
412F<Pod::Html>.  Optional, but mostly necessary.  In particular, if a F<.pod>
413file contains any C<LE<lt>E<gt>> tags, a C<podpath> element almost always
414needs to be supplied with a colon-delimited list of directories from which to
415begin a search for files containing POD.
416
417=item * C<debug>
418
419Boolean, generally set once at the program's top.  When Perl-true, displays
420extra debugging output, including turning on C<Pod::Html::pod2html()>'s
421C<verbose> option.  Optional.
422
423=item * C<core>
424
425Boolean. This should be set to a Perl-true value when the file is to be run
426from the test harness rather than from the top-level of the repository.
427
428=back
429
430=item * Return Value
431
432Not explicitly defined, but should return a Perl-true value upon completion.
433
434=item * Comment
435
436This function essentially asks, "Are we getting the same HTML output the last time we tinkered with the code in this distribution?"  Hence, it is dependent on the particular parsing and HTML composition functionality found within C<Pod::Html::pod2html()>, which is a somewhat customized subclass of F<Pod::Simple::XHTML>.  If, in the future, we offer functionality based on other parsing classes, then the C<DATA> sections of the F<t/*.t> files will have to be revised and perhaps the guts of C<xconvert()> as well.
437
438This function is roughly equivalent to test helper function C<convert_n_test()> in earlier
439versions of Pod-Html.
440
441=back
442
443=cut
444
445sub xconvert {
446    my $args = shift;
447    for my $k ('podstub', 'description', 'expect') {
448        die("convert_n_test() must have $k element")
449            unless length($args->{$k});
450    }
451    my $podstub = $args->{podstub};
452    my $description = $args->{description};
453    my $debug = $args->{debug} // 0;
454    $args->{expect_fail} //= 0;
455    if (defined $args->{p2h}) {
456        die "Value for 'p2h' must be hashref"
457            unless ref($args->{p2h}) eq 'HASH'; # TEST ME
458    }
459    my $cwd = unixify( Cwd::cwd() );
460    my ($vol, $dir) = splitpath($cwd, 1);
461    my @dirs = splitdir($dir);
462    shift @dirs if $dirs[0] eq '';
463    my $relcwd = join '/', @dirs;
464
465    my $new_dir  = catdir $dir, "t";
466    my $infile   = catpath $vol, $new_dir, "$podstub.pod";
467    my $outfile  = catpath $vol, $new_dir, "$podstub.html";
468
469    my $args_table = _prepare_argstable( {
470        infile      => $infile,
471        outfile     => $outfile,
472        cwd         => $cwd,
473        p2h         => $args->{p2h},
474    } );
475    my @args_list = _prepare_argslist($args_table);
476    Pod::Html::pod2html( @args_list );
477
478    $cwd =~ s|\/$||;
479
480    my $expect = _set_expected_html($args->{expect}, $relcwd, $cwd);
481    my $result = _get_html($outfile);
482
483    _process_diff( {
484        expect      => $expect,
485        result      => $result,
486        description => $description,
487        podstub     => $podstub,
488        outfile     => $outfile,
489        debug       => $debug,
490        expect_fail => $args->{expect_fail},
491    } );
492
493    # pod2html creates these
494    unless ($debug) {
495        1 while unlink $outfile;
496        1 while unlink "pod2htmd.tmp";
497    }
498}
499
500sub _prepare_argstable {
501    my $args = shift;
502    my %args_table = (
503        infile      =>    $args->{infile},
504        outfile     =>    $args->{outfile},
505    );
506    my %no_arg_switches = map { $_ => 1 } @no_arg_switches;
507    if (defined $args->{p2h}) {
508        for my $sw (keys %{$args->{p2h}}) {
509            if ($no_arg_switches{$sw}) {
510                $args_table{$sw} = undef;
511            }
512            else {
513                $args_table{$sw} = $args->{p2h}->{$sw};
514            }
515        }
516    }
517    return \%args_table;
518}
519
520sub _prepare_argslist {
521    my $args_table = shift;
522    my @args_list = ();
523    for my $k (keys %{$args_table}) {
524        if (defined $args_table->{$k}) {
525            push @args_list, "--" . $k . "=" . $args_table->{$k};
526        }
527        else {
528            push @args_list, "--" . $k;
529        }
530    }
531    return @args_list;
532}
533
534sub _set_expected_html {
535    my ($expect, $relcwd, $cwd) = @_;
536    $expect =~ s/\[PERLADMIN\]/$Config::Config{perladmin}/;
537    $expect =~ s/\[RELCURRENTWORKINGDIRECTORY\]/$relcwd/g;
538    $expect =~ s/\[ABSCURRENTWORKINGDIRECTORY\]/$cwd/g;
539    if (ord("A") == 193) { # EBCDIC.
540        $expect =~ s/item_mat_3c_21_3e/item_mat_4c_5a_6e/;
541    }
542    $expect =~ s/\n\n(some html)/$1/m;
543    $expect =~ s{(TESTING FOR AND BEGIN</h1>)\n\n}{$1}m;
544    return $expect;
545}
546
547sub _get_html {
548    my $outfile = shift;
549    local $/;
550
551    open my $in, '<', $outfile or die "cannot open $outfile: $!";
552    my $result = <$in>;
553    close $in;
554    return $result;
555}
556
557sub _process_diff {
558    my $args = shift;
559    die("process_diff() takes hash ref") unless ref($args) eq 'HASH';
560    my %keys_needed = map { $_ => 1 } (qw| expect result description podstub outfile |);
561    my %keys_seen   = map { $_ => 1 } ( keys %{$args} );
562    my @keys_missing = ();
563    for my $kn (keys %keys_needed) {
564        push @keys_missing, $kn unless exists $keys_seen{$kn};
565    }
566    die("process_diff() arguments missing: @keys_missing") if @keys_missing;
567
568    my $diff = '/bin/diff';
569    -x $diff or $diff = '/usr/bin/diff';
570    -x $diff or $diff = undef;
571    my $diffopt = $diff ? $^O =~ m/(linux|darwin)/ ? '-u' : '-c'
572                        : '';
573    $diff = 'fc/n' if $^O =~ /^MSWin/;
574    $diff = 'differences' if $^O eq 'VMS';
575    if ($diff) {
576        my $outcome = $args->{expect} eq $args->{result};
577        if ($outcome) {
578            ok($outcome, $args->{description});
579        }
580        else {
581            if ($args->{expect_fail}) {
582                ok(! $outcome, $args->{description});
583            }
584            else {
585                ok($outcome, $args->{description}) or do {
586                    my $expectfile = $args->{podstub} . "_expected.tmp";
587                    open my $tmpfile, ">", $expectfile or die $!;
588                    print $tmpfile $args->{expect}, "\n";
589                    close $tmpfile;
590                    open my $diff_fh, "-|", "$diff $diffopt $expectfile $args->{outfile}"
591                        or die("problem diffing: $!");
592                    print STDERR "# $_" while <$diff_fh>;
593                    close $diff_fh;
594                    unlink $expectfile unless $args->{debug};
595                };
596            }
597        }
598    }
599    else {
600        # This is fairly evil, but lets us get detailed failure modes
601        # anywhere that we've failed to identify a diff program.
602        is($args->{expect}, $args->{result}, $args->{description});
603    }
604    return 1;
605}
606
607=head2 C<record_state_of_cache()>
608
609=over 4
610
611=item * Purpose
612
613During debugging, enable developer to examine the state of the Pod-Html cache
614after each call to C<xconvert()>.
615
616=item * Arguments
617
618Single hash reference.
619
620    record_state_of_cache( {
621        outdir => "$ENV{P5P_DIR}/pod-html",
622        stub => $args->{podstub},
623        run => 1,
624    } );
625
626Hash reference has the following key-value pairs:
627
628=over 4
629
630=item * C<outdir>
631
632Any directory of your system to which you want a sorted copy of the cache to
633be printed.
634
635=item * C<stub>
636
637The same value you passed in C<$args> to C<xconvert()>.
638
639=item * C<run>
640
641Integer which you set manually to distinguish among multiple runs of this
642function within the same test file (presumably corresponding to multiple
643invocations of C<xconvert()>).
644
645=back
646
647=item * Return Value
648
649Implicitly returns Perl-true value.
650
651=item * Comment
652
653Function will print out location of cache files and other information.
654
655=back
656
657=cut
658
659sub record_state_of_cache {
660    my $args = shift;
661    die("record_state_of_cache() takes hash reference")
662        unless ref($args) eq 'HASH';
663    for my $k ( qw| outdir stub run | ) {
664        die("Argument to record_state_of_cache() lacks defined $k element")
665            unless defined $args->{$k};
666    }
667    my $cwd = cwd();
668    my $cache = catfile($cwd, 'pod2htmd.tmp');
669    die("Could not locate file $cache") unless -f $cache;
670    die("Could not locate directory $args->{outdir}") unless -d $args->{outdir};
671    die("'run' element takes integer") unless $args->{run} =~ m/^\d+$/;
672
673    my @cachelines = ();
674    open my $in, '<', $cache or die "Unable to open $cache for reading";
675    while (my $l = <$in>) {
676        chomp $l;
677        push @cachelines, $l;
678    }
679    close $in  or die "Unable to close $cache after reading";
680
681    my $outfile = catfile($args->{outdir}, "$args->{run}.cache.$args->{stub}.$$.txt");
682    die("$outfile already exists; did you remember to increment the 'run' argument?")
683        if -f $outfile;
684    open my $out, '>', $outfile or die "Unable to open $outfile for writing";
685    print $out "$_\n" for (sort @cachelines);
686    close $out or die "Unable to close after writing";
687    print STDERR "XXX: cache (sorted): $outfile\n";
688}
689
690=head1 AUTHORS
691
692The testing code reworked into its present form has many authors and dates
693back to the dawn of Perl 5, perhaps beyond.  The documentation was written by
694James E Keenan in March 2021.
695
696=cut
697
6981;
699