File Coverage

blib/lib/Test/FileReferenced.pm
Criterion Covered Total %
statement 142 142 100.0
branch 50 50 100.0
condition 3 3 100.0
subroutine 22 22 100.0
pod 4 4 100.0
total 221 221 100.0


line stmt bran cond sub pod time code
1             # Copyright 2010, Bartłomiej Syguła (natanael@natanael.krakow.pl)
2             #
3             # This is free software. It is licensed, and can be distributed under the same terms as Perl itself.
4             #
5             # For more, see my website: http://natanael.krakow.pl/
6              
7             package Test::FileReferenced;
8              
9 11     11   263075 use 5.005003;
  11         42  
  11         474  
10 11     11   65 use strict;
  11         30  
  11         402  
11 11     11   61 use base qw( Exporter );
  11         27  
  11         1517  
12              
13             our $VERSION = '0.02';
14             our @EXPORT = qw(
15             is_referenced_ok
16             is_referenced_in_file
17             );
18             our @EXPORT_OK = qw(
19             set_serializer
20             at_exit
21             );
22              
23 11     11   65 use Carp qw( croak );
  11         23  
  11         878  
24 11     11   64 use Cwd qw( cwd );
  11         28  
  11         645  
25 11     11   9435 use English qw( -no_match_vars );
  11         85145  
  11         74  
26 11     11   59671 use FindBin qw( $Bin );
  11         8059  
  11         1203  
27 11     11   73 use File::Basename;
  11         21  
  11         1419  
28 11     11   60 use File::Spec;
  11         19  
  11         1388  
29 11     11   123 use Test::More;
  11         23  
  11         130  
30 11     11   41235 use YAML::Any qw( LoadFile DumpFile );
  11         28169  
  11         98  
31              
32             =encoding UTF-8
33              
34             =head1 NAME
35              
36             Test::FileReferenced - Test against reference data stored in file(s).
37              
38             =head1 SYNOPSIS
39              
40             use Test::FileReferenced;
41            
42             # Optional:
43             Test::FileReferenced::set_serializer('mydump', \&My::Dumper::Load, \&My::Dumper::Dump);
44            
45             is_referenced_ok( complex_data_structure(), "complex data structure" );
46            
47             is_referenced_in_file ( data_structure(), "data structure", "data_structure" );
48            
49             # Optional:
50             Test::FileReferenced::at_exit();
51              
52             =head1 DESCRIPTION
53              
54             Test::FileReferenced helps testing routines returning complex data structures.
55             This is achieved by serializing test's output (using YAML::Any),
56             and allowing the Developer to compare it with reference data.
57              
58             In case there are differences between reference and actual result,
59             comparison can be made using traditional UNIX diff-like (diff, vimdiff, gvimdiff, kdiff) utilities.
60              
61             In such case, Test::FileReferenced - after the test completes - will ask the Developer to run diff on result and reference data.
62             If all differences ware intended, Developer may just replace reference data with actual test results.
63              
64             =cut
65              
66             my $serializer_ext = 'yaml';
67             my $serializer_load = \&LoadFile;
68             my $serializer_dump = \&DumpFile;
69              
70             my $default_reference_filename;
71             my $default_results_filename;
72              
73             # Data storeage:
74             my $reference = undef; # Becomes {} once initialized.
75             my $output = {};
76              
77             # Flags:
78             my $exited_cleanly = 0;
79             my $failure_count = 0;
80              
81             END {
82 7     7   7259 at_exit();
83             }
84              
85             =head1 SUBROUTINES
86              
87             =over
88              
89             =item is_referenced_ok ( $data, $name, $comparator )
90              
91             Compare C<$data> with reference stored under key C<$name> in default reference file.
92              
93             If C<$comparator> is a CODE reference, it is used to compare results. If this parameter is not given, Test::More::is_deeply is used.
94              
95             Returns:
96              
97             Value returned by comparison routine. By default (when is_deeply is used)
98             it will be C<1> if the test passed, and C<0> if it failed.
99              
100             =cut
101              
102             sub is_referenced_ok { # {{{
103 15     15 1 1889 my ( $tested_data, $test_name, $comparator ) = @_;
104              
105 15         98 _init_if_you_need();
106              
107 15         143 _load_reference_if_you_need();
108              
109             # Test name is mandatory, since without it, it's hard to reliably
110             # identify reference output in the reference file.
111 14 100       55 if (not $test_name) {
112 1         33 croak("Test name missing, but it is mandatory!");
113             }
114              
115             # Check if the test name is unique.
116 13 100       75 if ($output->{$test_name}) {
117 1         37 croak("Test name: '$test_name' is not unique.");
118             }
119              
120 12 100       42 if (not $comparator) {
121 8         20 $comparator = \&is_deeply;
122             }
123              
124 12         38 $output->{$test_name} = $tested_data;
125              
126             # Check if We have a reference data for given test.
127 12 100       47 if (not exists $reference->{$test_name}) {
128 2         13 diag("No reference for test '$test_name' found. Test will fail.");
129              
130             # Fixme: provide some more detailed information, what happened,
131             # and how to react. But, display it only on first occurance.
132              
133 2         14 $failure_count++;
134              
135 2         9 return fail($test_name);
136             }
137              
138 10         21 my $status;
139 10 100       56 if (not $status = $comparator->($tested_data, $reference->{$test_name}, $test_name)) {
140 2         13 $failure_count++;
141             }
142              
143 10         8713 return $status;
144             } # }}}
145              
146             =item is_referenced_in_file ( $data, $file_basename, $name, $comparator )
147              
148             Compare C<$data> with reference stored in custom file: F<$file_basename.yaml> (assuming the serializer is YAML::Any).
149              
150             If C<$comparator> is a CODE reference, it is used to compare results. If this parameter is not given, Test::More::is_deeply is used.
151              
152             Both C<$name> and C<$comparator> are optional parameters.
153              
154             Returns:
155              
156             Value returned by comparison routine. By default (when is_deeply is used)
157             it will be C<1> if the test passed, and C<0> if it failed.
158              
159             =cut
160              
161             sub is_referenced_in_file { # {{{
162 4     4 1 474 my ( $tested_data, $reference_filename, $test_name, $comparator ) = @_;
163              
164 4         20 _init_if_you_need();
165              
166 4 100       17 if (not $comparator) {
167 3         8 $comparator = \&is_deeply;
168             }
169              
170             # Construct path to reference file.
171 4         8 my ($reference_path, $output_path );
172 4 100       57 if (File::Spec->file_name_is_absolute($reference_filename)) {
173 1         5 $reference_path = $reference_filename . q{.} . $serializer_ext;
174 1         3 $output_path = $reference_filename . q{-result.} . $serializer_ext;
175             }
176             else {
177 3         42 $reference_path = File::Spec->catfile($Bin, $reference_filename . q{.} . $serializer_ext);
178 3         30 $output_path = File::Spec->catfile($Bin, $reference_filename . q{-result.} . $serializer_ext);
179             }
180              
181             # Load reference data.
182 4         44 my $reference_data = _Load($reference_path);
183              
184 4         10 my $status;
185 4 100       29 if (not $status = $comparator->($tested_data, $reference_data, $test_name)) {
186 1         7 _Dump($output_path, $tested_data);
187              
188             # Test failed, display prompt....
189 1         4 _display_failure_prompt($output_path, $reference_path);
190             }
191             else {
192             # If there are output files from previous run - clear them up.
193 3 100       22156 if (-e $output_path) {
194 2         259 unlink $output_path;
195             }
196             }
197              
198 4         762 return $status;
199             } # }}}
200              
201             =item set_serializer ( $extension, $load_coderef, $dump_coderef )
202              
203             Changes default serializing functions to ones provided by the Developer. C<$extension> must also be provided, so Test::FileReferenced can
204             automatically create the default reference file, if needed.
205              
206             You do not need to use this function, if You are happy with YAML::Any usage.
207              
208             Returns: undef
209              
210             =cut
211              
212             sub set_serializer { # {{{
213 6     6 1 4496 ( $serializer_ext, $serializer_load, $serializer_dump ) = @_;
214            
215             # Validate what was given to us.
216              
217 6 100       19 if (not $serializer_ext) {
218 1         23 croak "Missing file extension!";
219             }
220              
221 5 100       13 if (not $serializer_load) {
222 1         11 croak "Missing de-serializer!";
223             }
224 4 100       15 if (ref $serializer_load ne 'CODE') {
225 1         10 croak "De-serializer not a CODE-ref!";
226             }
227            
228 3 100       8 if (not $serializer_dump) {
229 1         10 croak "Missing serializer!";
230             }
231 2 100       6 if (ref $serializer_dump ne 'CODE') {
232 1         11 croak "Serializer not a CODE-ref!";
233             }
234              
235 1         3 return;
236             } # }}}
237              
238             =item at_exit ()
239              
240             If there ware failed tests, C will dump results from the test in temporary file, and then prompt to inspect changes.
241              
242             If there ware no failures, C will check, if results file (from any previous run) exists, and if so - remove it.
243             Nothing will be printed in this case.
244              
245             Normally this function does not need to be run explicitly, as Test::FileReferenced will run it from it's C sections.
246              
247             Returns: undef
248              
249             =cut
250              
251             sub at_exit { # {{{
252 16 100   16 1 201 if ($exited_cleanly) {
253 5         108 return;
254             }
255              
256             # Ware there any failures?
257 11 100       51 if ($failure_count > 0) {
258 3         23 _Dump($default_results_filename, $output);
259            
260 3         15 _display_failure_prompt($default_results_filename, $default_reference_filename);
261             }
262             else {
263 8         37 _clean_up();
264             }
265              
266 11         36 $exited_cleanly = 1;
267              
268 11         70 return;
269             } # }}}
270              
271             # Strictly internal routines.
272             #
273             # (please DO NOT use, for Your own comfort and safety)
274              
275             sub _init_if_you_need { # {{{
276             # Do We need to initialize anything?
277 19 100   19   82 if ($default_results_filename) {
278             # No, thank You.
279 9         17 return;
280             }
281              
282             # Prepare basename for the default files:
283 10         948 my $basename = basename($PROGRAM_NAME, q{.t});
284              
285 10         234 $default_reference_filename = File::Spec->catfile($Bin, $basename . q{.} . $serializer_ext);
286 10         106 $default_results_filename = File::Spec->catfile($Bin, $basename . q{-result.} . $serializer_ext);
287            
288             # warn "Ref: ". $default_reference_filename;
289             # warn "Res: ". $default_results_filename;
290              
291 10         36 return;
292             } # }}}
293              
294             sub _clean_up { # {{{
295 8 100 100 8   423 if ($default_results_filename and -f $default_results_filename) {
296 1         113 unlink $default_results_filename;
297             }
298              
299 8         22 return;
300             } # }}}
301              
302             =back
303              
304             =head1 REFERENCE FILES
305              
306             Reference files are data dumps using - by default - YAML::Any.
307              
308             =over
309              
310             =item Default reference file
311              
312             Default reference file contains data for all C calls in the test.
313             Each test case has it's own key in the file. For the following example test:
314              
315             is_referenced_ok(\%ENV, 'env');
316             is_referenced_ok(\@INC, 'inc');
317              
318             ...we have the following reference file:
319              
320             ---
321             env:
322             LANG: pl_PL
323             LANGUAGE: pl_PL
324             LC_ALL: pl_PL.UTF-8
325             inc:
326             /usr/lib/perl5/site_perl
327             /usr/lib/perl5/vendor_perl/5.10.1
328             /usr/lib/perl5/vendor_perl
329             /usr/lib/perl5/5.10.1
330              
331             Name for the reference file is based on the tests's filename, with I<.t> replaced with extension native to the used dumper.
332             Example: if default serializer (YAML::Any) is used, F will use F.
333              
334             =cut
335              
336             sub _load_reference_if_you_need { # {{{
337 15 100   15   61 if ($reference) {
338             # Reference already loaded or initialized.
339 6         10 return $reference;
340             }
341              
342             # Is there a reference file?
343 9 100       320 if (not -f $default_reference_filename) {
344             # Nope. Warn the User, but don't make a tragedy of it.
345 1         4 diag("No reference file found. All calls to is_referenced_ok WILL fail.");
346              
347 1         17 return $reference = {};
348             }
349              
350 8         34 return $reference = _Load($default_reference_filename);
351             } # }}}
352              
353             =item Custom reference files
354              
355             Custom reference files are used by C function. Each file contains reference data
356             for single test case. For the following example test:
357              
358             is_referenced_in_file(\%ENV, 'env', 'environment');
359              
360             ...we have the following reference file, named F:
361              
362             ---
363             LANG: pl_PL
364             LANGUAGE: pl_PL
365             LC_ALL: pl_PL.UTF-8
366              
367             =back
368              
369             =head1 TEST FAILURES
370              
371             If there are differences between referenced, and actual data, at the end of the test prompt will be printed, similar to:
372              
373             Resulting and reference files differ. To see differences run one of:
374             diff foo-results.yaml foo.yaml
375             gvimdiff foo-results.yaml foo.yaml
376            
377             If the differences ware intended, reference data can be updated by running:
378             mv foo-results.yaml foo.yaml
379              
380             If there is no F yet (first test run, for example) then the message will be similar to:
381            
382             No reference file found. It'a a good idea to create one from scratch manually.
383             To inspect current results run:
384             cat foo-results.yaml
385              
386             If You trust Your test output, You can use it to initialize deference file, by running:
387             mv foo-results.yaml foo.yaml
388              
389             In this case, the first time is_referenced_ok is used, it will dump the following diagnostic message:
390              
391             No reference file found. All calls to is_referenced_ok WILL fail.
392              
393             This is to ensure, that the User get's the idea, that something is not OK,
394             even if - for some reason - the END block does not run.
395              
396             =cut
397              
398             sub _display_failure_prompt { # {{{
399 4     4   11 my ( $results_filename, $reference_filename ) = @_;
400              
401 4 100       23 if ($ENV{'FILE_REFERENCED_NO_PROMPT'}) {
402 1         3 return;
403             }
404              
405             # Try to make the paths a bit more Humar-readable.
406 3         16675 my $cwd = cwd();
407              
408 3         1253 $results_filename = File::Spec->abs2rel($results_filename, $cwd);
409 3         271 $reference_filename = File::Spec->abs2rel($reference_filename, $cwd);
410              
411             # We basically have two use cases:
412             # 1) reference exist, but there are changes.
413             # 2) reference does not exist at all
414 3 100       135 if (-f $reference_filename) {
415             # First major use case: reference exist, but there are changes.
416              
417 2         73 my @shell_path = File::Spec->path();
418              
419 2         54 diag("Resulting and reference files differ. To see differences run one of:");
420 2         74 diag(sprintf(q{%10s %s %s}, q{diff}, $results_filename, $reference_filename));
421 2         46 foreach my $diff_command (qw( vimdiff gvimdiff kdiff )) {
422 6         30 foreach my $path (@shell_path) {
423 6 100       319 if (-x File::Spec->catfile($path, $diff_command)) {
424 2         32 diag(sprintf(q{%10s %s %s}, $diff_command, $results_filename, $reference_filename));
425 2         32 last;
426             }
427             }
428             }
429 2         18 diag("\n");
430 2         20 diag("If the differences ware intended, reference data can be updated by running:");
431 2         50 diag(sprintf(q{%10s %s %s}, q{mv}, $results_filename, $reference_filename));
432             }
433             else {
434             # Second major use case: reference does not exist at all.
435 1         32 diag("No reference file found. It'a a good idea to create one from scratch manually.");
436 1         24 diag("To inspect current results run:");
437 1         28 diag(sprintf(q{%10s %s}, "cat", $results_filename));
438 1         21 diag("\n");
439 1         18 diag("If You trust Your test output, You can use it to initialize deference file, by running:");
440 1         16 diag(sprintf(q{%10s %s %s}, q{mv}, $results_filename, $reference_filename));
441             }
442              
443 3         65 return;
444             } # }}}
445              
446             # ToDo: describe custom serializer/deserializer usage.
447              
448             sub _Load { # {{{
449 12     12   50 my ( $path ) = @_;
450              
451 12         50 my $data = eval {
452 12         169 return $serializer_load->($path);
453             };
454 12 100       215297 if ($EVAL_ERROR) {
455 1         15 croak("De-serializer error!\nUnable to load from:\n\t" . $path . "\nEval error:\n" . $EVAL_ERROR );
456             }
457              
458 11         49 return $data;
459             } # }}}
460              
461             sub _Dump { # {{{
462 5     5   88 my ( $path, $data ) = @_;
463              
464 5         13 eval {
465 5         28 $serializer_dump->($path, $data);
466             };
467 5 100       70363 if ($EVAL_ERROR) {
468 1         16 croak("Serializer error!\nUnable to dump to:\n\t" . $path . "\nEval error:\n" . $EVAL_ERROR );
469             }
470              
471 4         69 return $data;
472             } # }}}
473              
474             =head1 CUSTOM COMPARISON ROUTINES
475              
476             For the moment, it's an undocumented, experimental feature. Use at Your own risk.
477              
478             =head1 TDD
479              
480             Test-driven development is possible with Test::FileReferenced. One of the ways, is to follow the following steps:
481              
482             =over
483              
484             =item Initialize reference files
485              
486             To initialize the reference file(s), run a script similar to the example bellow:
487              
488             #!/usr/bin/perl -w
489             use strict;
490             use Test::More tests=>3;
491             use Test::FileReferenced;
492              
493             is_referenced_ok(undef, "First test");
494             is_referenced_ok(undef, "Second test");
495              
496             is_referenced_in_file(undef, "foo", "Second test");
497              
498             This will allow You to create an empty default reference file for the test, and one ('foo.yaml') custom reference file.
499              
500             =item Fill reference files
501              
502             At this point, test should pass cleanly. Our goal is to write the data structures, that We expect to have, into reference files created above.
503              
504             After doing this, test will no longer pass.
505              
506             =item Generate test data
507              
508             At this point, test fails because test script provides incorrect data: undef's have to be replaced with actual data - probably generated by calls to tested subroutines.
509              
510             =item Implement tested code
511              
512             At this point, test still fails. Tested subroutines have to be properly implemented. Once this is done, test should pass, and the process is completed.
513              
514             =back
515              
516             =cut
517              
518             1;
519              
520             __END__