1 # Helper functions to test the podlators distribution.
3 # This module is an internal implementation detail of the podlators test
4 # suite. It provides some supporting functions to make it easier to write
7 # Copyright 2015, 2016 Russ Allbery <rra@cpan.org>
9 # This program is free software; you may redistribute it and/or modify it
10 # under the same terms as Perl itself.
12 package Test::Podlators;
18 use Encode qw(decode encode);
23 # For Perl 5.006 compatibility.
24 ## no critic (ClassHierarchies::ProhibitExplicitISA)
26 # Declare variables that should be set in BEGIN for robustness.
27 our (@EXPORT_OK, @ISA, $VERSION);
29 # Set $VERSION and everything export-related in a BEGIN block for robustness
30 # against circular module loading (not that we load any modules, but
31 # consistency is good).
36 read_snippet read_test_data slurp test_snippet test_snippet_with_io
40 # The file handle used to capture STDERR while we mess with file descriptors.
43 # The file name used to capture standard error output.
46 # Internal function to clean up the standard error output file. Leave the
47 # temporary directory in place, since otherwise we race with other test
48 # scripts trying to create the temporary directory when running tests in
51 if ($SAVED_STDERR && -f $SAVED_STDERR) {
52 unlink($SAVED_STDERR);
57 # Remove saved standard error on exit, even if we have an abnormal exit.
62 # Internal function to redirect stderr to a file. Stores the name in
65 my $tmpdir = File::Spec->catdir('t', 'tmp');
67 mkdir($tmpdir, 0777) or BAIL_OUT("cannot create $tmpdir: $!");
69 my $path = File::Spec->catfile($tmpdir, "out$$.err");
71 ## no critic(InputOutput::RequireBriefOpen)
72 open($OLD_STDERR, '>&', STDERR) or BAIL_OUT("cannot dup STDERR: $!");
73 open(STDERR, '>', $path) or BAIL_OUT("cannot redirect STDERR: $!");
76 $SAVED_STDERR = $path;
80 # Internal function to restore stderr.
82 # Returns: The contents of the stderr file.
84 return if !$SAVED_STDERR;
85 close(STDERR) or BAIL_OUT("cannot close STDERR: $!");
86 open(STDERR, '>&', $OLD_STDERR) or BAIL_OUT("cannot dup STDERR: $!");
87 close($OLD_STDERR) or BAIL_OUT("cannot close redirected STDERR: $!");
88 my $stderr = slurp($SAVED_STDERR);
93 # Read one test snippet from the provided relative file name and return it.
94 # For the format, see t/data/snippets/README.
96 # $path - Relative path to read test data from
98 # Returns: Reference to hash of test data with the following keys:
99 # name - Name of the test for status reporting
100 # options - Hash of options
101 # input - The input block of the test data
102 # output - The output block of the test data
103 # errors - Expected errors
104 # exception - Text of exception (with file and line stripped)
107 $path = File::Spec->catfile('t', 'data', 'snippets', $path);
110 # Read the sections and store them in the %data hash.
111 my ($line, $section);
112 open(my $fh, '<', $path) or BAIL_OUT("cannot open $path: $!");
113 while (defined($line = <$fh>)) {
114 $line = decode('UTF-8', $line);
115 if ($line =~ m{ \A \s* \[ (\S+) \] \s* \z }xms) {
118 $data{$section} ||= q{};
119 $data{$section} .= $line;
122 close($fh) or BAIL_OUT("cannot close $path: $!");
124 # Strip trailing blank lines from all sections.
125 for my $section (keys %data) {
126 $data{$section} =~ s{ \n\s+ \z }{\n}xms;
129 # Clean up the name section by removing newlines and extra space.
131 $data{name} =~ s{ \A \s+ }{}xms;
132 $data{name} =~ s{ \s+ \z }{}xms;
133 $data{name} =~ s{ \s+ }{ }xmsg;
136 # Turn the options section into a hash.
137 if ($data{options}) {
138 my @lines = split(m{ \n }xms, $data{options});
139 delete $data{options};
140 for my $optline (@lines) {
141 next if $optline !~ m{ \S }xms;
142 my ($option, $value) = split(q{ }, $optline, 2);
143 if (defined($value)) {
148 $data{options}{$option} = $value;
152 # Return the results.
156 # Read one set of test data from the provided file handle and return it.
157 # There are several different possible formats, which are specified by the
160 # The data read from the file handle will be ignored until a line consisting
161 # solely of "###" is found. Then, two or more blocks separated by "###" are
162 # read, ending with another line of "###". There will always be at least an
163 # input and an output block, and may be more blocks based on the format
166 # $fh - File handle to read the data from
167 # $format_ref - Reference to a hash of options describing the data
168 # errors - Set to true to read expected errors after the output section
169 # options - Set to true to read a hash of options as the first data block
171 # Returns: Reference to hash of test data with the following keys:
172 # input - The input block of the test data
173 # output - The output block of the test data
174 # errors - Expected errors if errors was set in $format_ref
175 # options - Hash of options if options was set in $format_ref
176 # or returns undef if no more test data is found.
178 my ($fh, $format_ref) = @_;
182 # Find the first block of test data.
184 while (defined($line = <$fh>)) {
185 last if $line eq "###\n";
187 if (!defined($line)) {
191 # If the format contains the options key, read the options into a hash.
192 if ($format_ref->{options}) {
193 while (defined($line = <$fh>)) {
194 last if $line eq "###\n";
195 my ($option, $value) = split(q{ }, $line, 2);
196 if (defined($value)) {
201 $data{options}{$option} = $value;
205 # Read the input and output sections.
206 my @sections = qw(input output);
207 if ($format_ref->{errors}) {
208 push(@sections, 'errors');
210 for my $key (@sections) {
212 while (defined($line = <$fh>)) {
213 last if $line eq "###\n";
214 $data{$key} .= $line;
220 # Slurp output data back from a file handle. It would be nice to use
221 # Perl6::Slurp, but this is a core module, so we have to implement our own
222 # wheels. BAIL_OUT is called on any failure to read the file.
224 # $file - File to read
225 # $strip - If set to "man", strip out the Pod::Man header
227 # Returns: Contents of the file, possibly stripped
229 my ($file, $strip) = @_;
230 open(my $fh, '<', $file) or BAIL_OUT("cannot open $file: $!");
232 # If told to strip the man header, do so.
233 if (defined($strip) && $strip eq 'man') {
234 while (defined(my $line = <$fh>)) {
235 last if $line eq ".nh\n";
239 # Read the rest of the file and return it.
240 my $data = do { local $/ = undef; <$fh> };
241 close($fh) or BAIL_OUT("cannot read from $file: $!");
245 # Test a formatter on a particular POD snippet. This does all the work of
246 # loading the snippet, creating the formatter, running it, and checking the
247 # results, and reports those results with Test::More.
249 # $class - Class name of the formatter, as a string
250 # $snippet - Path to the snippet file defining the test
252 my ($class, $snippet) = @_;
253 my $data_ref = read_snippet($snippet);
255 # Create the formatter object.
256 my $parser = $class->new(%{ $data_ref->{options} }, name => 'TEST');
257 isa_ok($parser, $class, 'Parser object');
259 # Save stderr to a temporary file and then run the parser, storing the
260 # output into a Perl variable.
261 my $errors = _stderr_save();
263 $parser->output_string(\$got);
264 eval { $parser->parse_string_document($data_ref->{input}) };
266 my $stderr = _stderr_restore();
268 # If we were testing Pod::Man, strip off everything prior to .nh from the
269 # output so that we aren't testing the generated header.
270 if ($class eq 'Pod::Man') {
271 $got =~ s{ \A .* \n [.]nh \n }{}xms;
274 # Check the output, errors, and any exception.
275 is($got, $data_ref->{output}, "$data_ref->{name}: output");
276 if ($data_ref->{errors}) {
277 is($stderr, $data_ref->{errors}, "$data_ref->{name}: errors");
279 if ($data_ref->{exception} || $exception) {
281 $exception =~ s{ [ ] at [ ] .* }{\n}xms;
283 is($exception, $data_ref->{exception}, "$data_ref->{name}: exception");
288 # Test a formatter with I/O streams on a particular POD snippet. This does
289 # all the work of loading the snippet, creating the formatter, running it, and
290 # checking the results, and reports those results with Test::More. It's
291 # similar to test_snippet, but uses input and output temporary files instead
292 # to test encoding layers and also checks the Pod::Man accent output.
294 # $class - Class name of the formatter, as a string
295 # $snippet - Path to the snippet file defining the test
296 # $options_ref - Hash of options with the following keys:
297 # perlio_utf8 - Set to 1 to set a PerlIO UTF-8 encoding on the output file
298 sub test_snippet_with_io {
299 my ($class, $snippet, $options_ref) = @_;
300 my $data_ref = read_snippet($snippet);
302 # Create the formatter object.
303 my $parser = $class->new(%{ $data_ref->{options} }, name => 'TEST');
304 isa_ok($parser, $class, 'Parser object');
306 # Write the input POD to a temporary file prefaced by the encoding
308 my $tmpdir = File::Spec->catdir('t', 'tmp');
310 mkdir($tmpdir, 0777) or BAIL_OUT("cannot create $tmpdir: $!");
312 my $input_file = File::Spec->catfile('t', 'tmp', "tmp$$.pod");
313 open(my $input, '>', $input_file)
314 or BAIL_OUT("cannot create $input_file: $!");
315 print {$input} encode('UTF-8', $data_ref->{input})
316 or BAIL_OUT("cannot write to $input_file: $!");
317 close($input) or BAIL_OUT("cannot flush output to $input_file: $!");
319 # Create an output file and parse from the input file to the output file.
320 my $output_file = File::Spec->catfile('t', 'tmp', "out$$.tmp");
321 open(my $output, '>', $output_file)
322 or BAIL_OUT("cannot create $output_file: $!");
323 if ($options_ref->{perlio_utf8}) {
324 ## no critic (BuiltinFunctions::ProhibitStringyEval)
325 ## no critic (ValuesAndExpressions::RequireInterpolationOfMetachars)
326 eval 'binmode($output, ":encoding(utf-8)")';
330 # Parse the input file into the output file.
331 $parser->parse_from_file($input_file, $output);
332 close($output) or BAIL_OUT("cannot flush output to $output_file: $!");
334 # Read back in the results, checking to ensure that we didn't output the
335 # accent definitions if we wrote UTF-8 output.
336 open(my $results, '<', $output_file)
337 or BAIL_OUT("cannot open $output_file: $!");
338 my ($line, $saw_accents);
339 while (defined($line = <$results>)) {
340 $line = decode('UTF-8', $line);
341 if ($line =~ m{ Accent [ ] mark [ ] definitions }xms) {
344 last if $line =~ m{ \A [.]nh }xms;
346 my $saw = do { local $/ = undef; <$results> };
347 $saw = decode('UTF-8', $saw);
348 close($results) or BAIL_OUT("cannot close output file: $!");
351 unlink($input_file, $output_file);
353 # Check the accent definitions and the output.
354 my $perlio = $options_ref->{perlio_utf8} ? ' (PerlIO)' : q{};
357 $data_ref->{options}{utf8} ? undef : 1,
358 "$data_ref->{name}: accent definitions$perlio"
360 is($saw, $data_ref->{output}, "$data_ref->{name}: output$perlio");
368 Allbery podlators PerlIO UTF-8 formatter FH whitespace
372 Test::Podlators - Helper functions for podlators tests
376 use Test::Podlators qw(read_test_data);
378 # Read the next block of test data, including options.
379 my $data = read_test_data(\*DATA, { options => 1 });
383 This module collects various utility functions that are useful for writing
384 test cases for the podlators distribution. It is not intended to be, and
385 probably isn't, useful outside of the test suite for that module.
389 None of these functions are imported by default. The ones used by a script
390 should be explicitly imported.
394 =item read_snippet(PATH[, OPTIONS])
396 Read one test snippet from the provided relative file name and return it. The
397 path should be relative to F<t/data/snippets>. For the format, see
398 F<t/data/snippets/README>.
400 OPTIONS, if present, is a hash that currently supports only one key: C<utf8>,
401 to set a PerlIO input encoding layer of UTF-8 when reading the snippet.
403 The result will be a hash with the following keys:
409 The name of the test, for reporting purposes.
413 A hash of any options to values, if any options were specified.
417 Input POD to try formatting.
425 Expected errors from the POD formatter.
429 An expected exception from the POD formatter, with the file and line
430 information stripped from the end of the exception.
434 =item read_test_data(FH, FORMAT)
436 Reads a block of test data from FH, looking for test information according to
437 the description provided in FORMAT. All data prior to the first line
438 consisting of only C<###> will be ignored. Then, the test data must consist
439 of two or more blocks separated by C<###> and ending in a final C<###> line.
441 FORMAT is optional, in which case the block of test data should be just input
442 text and output text. If provided, it should be a reference to a hash with
443 one or more of the following keys:
449 If set, the first block of data in the test description is a set of options in
450 the form of a key, whitespace, and a value, one per line. The value may be
451 missing, in which case the value associated with the key is the empty string.
455 The return value is a hash with at least some of the following keys:
461 The input data for the test. This is always present.
465 If C<options> is set in the FORMAT argument, this is the hash of keys and
466 values in the options section of the test data.
470 The output data for the test. This is always present.
474 =item slurp(FILE[, STRIP])
476 Read the contents of FILE and return it as a string. If STRIP is set to
477 C<man>, strip off any Pod::Man header from the file before returning it.
479 =item test_snippet(CLASS, SNIPPET)
481 Test a formatter on a particular POD snippet. This does all the work of
482 loading the snippet, creating the formatter by instantiating CLASS, running
483 it, and checking the results. Results are reported with Test::More.
485 =item test_snippet_with_io(CLASS, SNIPPET[, OPTIONS])
487 The same as test_snippet(), except, rather than parsing the input into a
488 string buffer, this function uses real, temporary input and output files.
489 This can be used to test I/O layer handling and proper encoding.
491 OPTIONS, if present, is a reference to a hash of options. Currently, only
492 one key is supported: C<perlio>, which, if set to true, will set a PerlIO
493 UTF-8 encoding layer on the output file before writing to it.
499 Russ Allbery <rra@cpan.org>
501 =head1 COPYRIGHT AND LICENSE
503 Copyright 2015 Russ Allbery <rra@cpan.org>
505 This program is free software; you may redistribute it and/or modify it
506 under the same terms as Perl itself.