File Coverage

blib/lib/Test/Snapshots.pm
Criterion Covered Total %
statement 104 122 85.2
branch 24 38 63.1
condition 2 2 100.0
subroutine 16 22 72.7
pod 9 10 90.0
total 155 194 79.9


line stmt bran cond sub pod time code
1             package Test::Snapshots;
2 6     6   164151 use strict;
  6         16  
  6         224  
3 6     6   32 use warnings;
  6         10  
  6         156  
4              
5 6     6   151 use 5.008005;
  6         22  
  6         486  
6              
7             our $VERSION = '0.02';
8              
9             =head1 NAME
10              
11             Test::Snapshots - for testing stand alone scripts and executables
12              
13             =head1 SYNOPSIS
14              
15             use Test::More;
16             use Test::Snapshots;
17              
18             test_all_snapshots('eg');
19              
20             Will go over all the .pl files in the eg/ directory, run them using
21             with the content of the SCRIPT.out and SCRIPT.err files
22              
23              
24             Optional configurations before calling test_all_snapshots:
25              
26             Test::Snapshots::debug(1);
27              
28             Get some extra diag messages
29              
30             Test::Snapshots::combine(1);
31              
32             Combines the stdout and stderr and compares them to the SCRIPT.out file
33              
34              
35             Test::Snapshots::set_glob('*.t');
36              
37             Change the way we locate the scripts to be executed.
38              
39              
40             Test::Snapshots::set_accessories_dir('path/to/dir');
41              
42             Change the place where TS looks for .out files.
43              
44             =head1 WARNING
45              
46             This is alpha software. The API will most certainly change as
47             the requirements get clearer.
48              
49             =head1 Examples
50              
51             Many of the unit test of this module are actually simple use cases
52             with the files to be tested located in the eg/ subdirectory of the
53             distribution. Check them out.
54              
55             =head1 TODO
56              
57             =over 4
58              
59             =item *
60              
61             Add more test this module. Especially, we don't yet have failing tests.
62              
63             =item *
64              
65             Change the API to look more OO. Probably sg. like:
66              
67             Test::Snapshots->set_glob()
68             ->combine()
69             ->set_accessories_dir()
70             ->set_directories('eg')
71             ->test_all_snapshots();
72              
73             =item *
74              
75             Allow subclassing or extending the module in some other way.
76              
77             =item *
78              
79             Deal with command line arguments. (.argv ?)
80              
81             =item *
82              
83             Deal with single file asseccories: A single file that holds the contents of
84             the .in , .our, .err etc... file in sections.
85              
86             E.g. the PHP core testing has .phpt files with sections:
87              
88             --TEST--
89             Name of the test
90             --FILE--
91             The code that needs to be saved in a file and executed
92             --EXPECT--
93             The expected output
94              
95             Test::Snapshots should be able to support that with the code
96             to be executed inside as in the case of php or being outside
97             as when testing executables.
98              
99             =item *
100              
101             Allow to pass several directories to traverse
102              
103             =item *
104              
105             Allow multiple runs in the same test script. (This will probably
106             mean the test counting needs to be done separately or we will have
107             to use the new "add plan" feature of Test::More.
108              
109             =item *
110              
111             Allow definiton of expected exit code for each file in some
112             centralized form maybe similar to the way skip is defined.
113              
114             =item *
115              
116             Do we need a TODO test capability here?
117              
118             =item *
119              
120             Use L ?
121              
122             =back
123              
124             =head1 DESCRIPTION
125              
126             Test::Snapshots was created especially to be able to test a
127             large number of command line oriented executables. It does not
128             matter if the executable is something compiled from C, a Perl,
129             Python or PHP script.
130              
131             Test::Snapshot can be seen as a very simple replacement of L.
132             It will go over the designated direcory and run every execute like this:
133              
134             executable arguments < input_file > output_file 2> error_file
135            
136             It will then check if the output_file is the same as the exepcted output file
137             and if the error_file is the sameas the expected error file.
138              
139             If an input file is not supplied then the < input_file part will be
140             omitted.
141              
142             The input file, the list of arguments and the expected output and
143             error files all have the same name as the executable. So if you have
144             an executable called C then you'd create the following
145             files:
146              
147             fabricate.exe.in
148             fabricate.exe.argv
149            
150             fabricate.exe.out
151             fabricate.exe.err
152             fabricate.exe.exit
153            
154             If .in is omitted we assume there is no input
155              
156             If .argv is omitted then no arguments are provided
157              
158             If .err or .out is omitted then it is assumed to be empty.
159              
160             If .exit is omitted then it is expected that the exit code will be
161             equal to the default exit code which is 0.
162              
163             =head2 Multiple test cases
164              
165             Sometime a single executable file should have multiple test cases. That is
166             we might want to provide different .in and .argv files and expect different
167             .out/.err/.exit values.
168              
169             In order to allow such mode the files need to have a number in their name.
170             So if you are testing I the files need to be
171              
172             xyz.01.in
173             xyz.01.out
174            
175             xyz.02.in
176             xyz.02.out
177             xyz.02.err
178              
179             The expected number of test is the number of different numbers so if you have
180             two files xyz.01.in and xyz.27.err then Test::Snapshots will run two test. One
181             of them has no input and some expected error while the other has only input
182             and not expected output or error.
183              
184             =head2 Timeout
185              
186             In order to avoid stuck test cases (e.g. waiting on STDIN)
187             by default every test case can run up to 10 secs.
188              
189             =head1 METHODS
190              
191             =cut
192              
193 6     6   31 use Carp ();
  6         10  
  6         149  
194 6     6   8770 use File::Temp qw(tempdir);
  6         183261  
  6         523  
195 6     6   6252 use Text::Diff qw(diff);
  6         65222  
  6         484  
196 6     6   7731 use File::Find::Rule;
  6         56882  
  6         81  
197 6     6   451 use List::Util qw(sum);
  6         15  
  6         836  
198              
199 6     6   35 use base 'Test::Builder::Module';
  6         12  
  6         906  
200 6     6   122 use base 'Exporter';
  6         14  
  6         8753  
201              
202             our @EXPORT = qw(test_all_snapshots);
203              
204             my $debug;
205             my $combine;
206             my $glob = '*.pl';
207             my $command = $^X;
208             my $skip = {};
209             my $accessories_dir;
210             my $default_expected_exit = 0;
211             my $multiple;
212             my $timeout = 10;
213              
214             =head2 timeout
215              
216             Set timeout for the executions so if one of them gets stuck
217             (e.g. waiting on STDIN) the whole test suit won't suffer.
218              
219             Default 10 secs.
220              
221             =head2 combine
222              
223             Set to 1 if you'd like to combine the STDOUT and STDERR and compare the
224             combined output to the .out file.
225              
226             Default is 0 meaning they will be captured separatelly and compared
227             separatelly to the .out and .err files.
228              
229             =cut
230              
231             sub combine {
232 1     1 1 9 $combine = shift;
233             }
234              
235             =head2 set_glob
236              
237             Set what glob to use to fine the files to be executed. Currently it
238             defaults to '*.pl' but maybe it should have no default forcing the user
239             to set one.
240              
241             =cut
242              
243             sub set_glob {
244 0     0 1 0 $glob = shift;
245             }
246              
247             =head2 skip
248              
249             Pass to it a hash ref of path => 'explanation' pairs
250             for all the files that need to be skipped.
251              
252             skip({
253             path => 'good reason',
254             path2 => 'some excuse',
255             });
256              
257             =cut
258              
259             sub skip {
260 0     0 1 0 $skip = shift;
261             }
262              
263             =head2 set_accessories_dir
264              
265             We are calling the .out, .err etc files accessories.
266            
267             In some cases you don't want them to be next to the script that
268             are being tested. In such cases you can use the above function
269             to tell Test::Snapshots where those files can be found.
270              
271             =cut
272              
273             sub set_accessories_dir {
274 1     1 1 7 $accessories_dir = shift;
275             }
276              
277              
278             sub multiple {
279 1     1 0 7 $multiple = shift;
280             }
281              
282             =head2 command
283              
284             By default Test::Snapshots will assume the files to be tested
285             are stand alone executables or that at least they know where their
286             interpreter is. So they will be executed directly.
287              
288             In most of the cases you will want to run them with some
289             specific command. e.g. You might want to make sure they run with the
290             same perl interpreter as your test script runs. In that case call the following:
291              
292             command($^X)
293              
294             In other cases the files need to be executed with some other tool, eg.
295             the perl 6 or python interpreter which is in the path:
296              
297             command("perl6");
298              
299             or
300              
301             command("python");
302              
303             =cut
304              
305             sub command {
306 0     0 1 0 $command = shift;
307             }
308              
309             =head2 default_expected_exit_code
310              
311             The exepceted exit code can be defined on a perl case basis
312             in the .exit file. If the .exit file does not exist
313             then there is a default expected exit code. Which is 0 by default.
314              
315             Use this method to chane the default.
316              
317             =cut
318              
319             sub default_expected_exit_code {
320 0     0 1 0 $default_expected_exit = shift;
321             }
322              
323             =head2 debug
324              
325             You can turn on the debug flag by calling debug(1).
326             If it is set Test::Snippets will call diag() with all kinds of
327             information during the test execution.
328              
329             =cut
330              
331             sub debug {
332 0     0 1 0 $debug = shift;
333             }
334              
335             =head2 test_all_snapshots
336              
337             This is the call that actually goes out, locates all the
338             files to be tested, sets the C and executes all the test.
339             Currently one should give a directory as a paramter to it but
340             I plan to move that parameter to a separate method and to allow
341             the setting of multiple directories.
342              
343             =cut
344              
345             sub test_all_snapshots {
346 4     4 1 24 my ($dir) = @_;
347              
348 4 50       17 Carp::croak("Need to supply directory name") if not defined $dir;
349            
350 4         171 my @files = sort File::Find::Rule->file()->name($glob)->in($dir);
351 4         4733 my $prefix_length = length $dir;
352            
353             # go over all the files and count the different .in, .out, .err, .exit files
354 4         9 my %tests;
355 4 100       16 if ($multiple) {
356 1         3 foreach my $file (@files) {
357 1         2 my %seen;
358 6         16 my @extras = grep { !$seen{$_}++ }
  6         18  
359 1         118 map {$_ =~ /\.(\d+)\.(out|err|in|exit)$/; $1}
  6         17  
360             glob "$file.*";
361 1         7 $tests{$file} = \@extras;
362             }
363             }
364              
365 4         43 my $T = Test::Builder->new;
366            
367 4 100       53 my $cnt = $combine ? 1 : 2;
368 4         9 $cnt++; # for exit codes
369 4         30 my $test_count = @files * $cnt;
370              
371             #use Data::Dumper;
372             #$T->diag(Dumper \@files);
373             #$T->diag(Dumper \%tests);
374 4 100       19 if ($multiple) {
375             #$T->diag(sum (map { scalar @{ $tests{$_} } } @files));
376 1         2 $test_count = $cnt * sum (map { scalar @{ $tests{$_} } } @files);
  1         2  
  1         14  
377             }
378 4         23 $T->plan(tests => $test_count );
379              
380 4         1258 foreach my $file (@files) {
381 7 50       227 if ($skip->{$file}) {
382 0 0       0 my $count = $cnt * ($multiple ? scalar(@{ $tests{$file} }) : 1);
  0         0  
383 0         0 $T->skip($skip->{$file}) for 1..$count;
384 0         0 next;
385             }
386 7 100       23 if ($multiple) {
387 1         2 foreach my $case (@{ $tests{$file} }) {
  1         3  
388 2         16 test_single_file($file, $prefix_length, ".$case");
389             }
390             } else {
391 6         25 test_single_file($file, $prefix_length,);
392             }
393             }
394             }
395              
396             =head2 test_single_file
397              
398             Testing a single file. It gets the path to the file to be tested.
399             The length of the prefix and optionally a case which is the 01, 02 etc.
400             name of the test case for the multple-test-cases.
401              
402             Currently this is considered an internal method.
403              
404             =cut
405              
406              
407             sub test_single_file {
408 8     8 1 61 my ($file, $prefix_length, $case) = @_;
409 8   100     50 $case ||= '';
410              
411 8         90 my $tempdir = tempdir( CLEANUP => 1 );
412 8         5965 my $T = Test::Builder->new;
413              
414 8 100       88 my $accessories_path = $accessories_dir ? $accessories_dir . substr($file, $prefix_length) : $file;
415             #$T->diag($accessories_path);
416 8         27 my $in_file = "$accessories_path$case.in";
417              
418 8         18 my %std;
419 8         29 $std{out} = "$tempdir/out";
420 8         22 $std{err} = "$tempdir/err";
421              
422 8         26 my $cmd = "$command $file";
423 8 100       22 if ($combine) {
424 2         7 $cmd .= " >$std{out} 2>&1";
425             } else {
426 6         202 $cmd .= " >$std{out} 2>$std{err}";
427             }
428 8 50       154 if (-e $in_file) {
429 8         45 $cmd .= " < $in_file";
430             }
431 8 50       27 if ($debug) {
432 0         0 $T->diag($cmd);
433             }
434              
435 8 100       32 my @stds = $combine ? qw(out) : qw(err out);
436              
437              
438 8         13 my $exit;
439             #$T->diag($file);
440 8     0   233 $SIG{ALRM} = sub { die "TIMEOUT\n" };
  0         0  
441 8         96 alarm($timeout);
442             eval {
443 8         170516 system $cmd;
444 8         504 $exit = $?;
445 8         310 1;
446              
447 8 50       17 } or do {
448 0         0 alarm(0);
449 0 0       0 if ($@ eq "TIMEOUT\n") {
450 0         0 $T->ok(0, "Timeout. No result") for 1..@stds+1;
451 0         0 return;
452             } else {
453 0         0 die $@; # unknown exception
454             }
455             };
456 8         92 alarm(0);
457             #$T->diag("Exit '$exit'");
458              
459              
460 8         555 foreach my $ext (@stds) {
461 14         6215 my $expected = "$accessories_path$case.$ext";
462 14 50       599 if (-e $expected) {
463 14         582 my $diff = diff($expected, "$std{$ext}");
464 14 50       12127 $T->ok(!$diff, "$ext of $file") or $T->diag($diff);
465             } else {
466 0         0 my $data = _slurp($std{$ext});
467 0 0       0 $T->ok($data eq '', "$ext of $file")
468             or $T->diag("Expected nothing.\nReceived\n\n$data");
469             }
470             }
471             # exit code
472             {
473 8         11371 my $expected_exit = $default_expected_exit;
  8         26  
474 8         28 my $expected_file = "$accessories_path$case.exit";
475 8 100       163 if (-e $expected_file) {
476 3         34 $expected_exit = _slurp($expected_file);
477 3         11 chomp $expected_exit;
478             }
479 8         82 $T->is_eq($exit >> 8, $expected_exit, "Exit code of $file");
480             }
481              
482 8         5024 return;
483             }
484              
485              
486             # a private slurp method.
487             sub _slurp {
488 3     3   9 my $file = shift;
489 3 50       295 open my $fh, '<', $file or die $!;
490 3         22 local $/ = undef;
491 3         270 return <$fh>;
492             }
493              
494             =head1 See Also
495              
496             L, L and L.
497              
498             L, L, L,
499              
500             =head1 COPYRIGHT
501              
502             Copyright 2009 Gabor Szabo gabor@szabgab.com http://szabgab.com/
503              
504             =head1 LICENSE
505              
506             This program is free software; you can redistribute it and/or
507             modify it under the same terms as Perl 5 itself.
508              
509             =head1 DISCLAIMER OF WARRANTY
510              
511             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
512             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
513             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
514             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
515             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
516             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
517             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
518             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
519             NECESSARY SERVICING, REPAIR, OR CORRECTION.
520              
521             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
522             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
523             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
524             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
525             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
526             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
527             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
528             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
529             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
530             SUCH DAMAGES.
531              
532              
533             =cut
534              
535              
536             1;