File Coverage

blib/lib/Test/Out.pm
Criterion Covered Total %
statement 106 120 88.3
branch 11 22 50.0
condition 12 33 36.3
subroutine 18 24 75.0
pod 10 13 76.9
total 157 212 74.0


line stmt bran cond sub pod time code
1             ## $Id:$ ##
2              
3             package Test::Out;
4              
5 5     5   28092 use strict;
  5         10  
  5         170  
6 5     5   25 use warnings;
  5         8  
  5         123  
7              
8 5     5   24 use Carp;
  5         16  
  5         432  
9 5     5   5269 use Carp::Assert;
  5         6729  
  5         31  
10 5     5   8120 use File::Temp;
  5         179897  
  5         414  
11 5     5   46 use File::Basename;
  5         11  
  5         435  
12 5     5   4714 use Test::Builder;
  5         60744  
  5         996  
13              
14             my $tstobj = Test::Builder->new;
15              
16             our $VERSION = '0.21';
17              
18             =head1 NAME
19              
20             Test::Out - Test output from FILEHANDLE
21              
22             =head1 SYNOPSIS
23              
24             use Test::Out;
25             my $out = Test::Out->new( output => *STDOUT, tests => 4 );
26              
27             # Or ...
28              
29             my $out = Test::Out->new(tests => 4);
30             $out->redirect( output => *STDOUT );
31              
32             ## This will go to a place that your harness can see
33             $out->diag("Testing is* functions");
34              
35             ## But this will not be displayed but captured for test methods
36             $some->method_that_prints("This is a test\n");
37             $out->is_output("This is a test\n", "test 1");
38             $out->isnt_output("Han shot first", "test 2");
39              
40             $out->diag("Testing regex functions");
41             CORE::print "A random number: @{[int rand 100]}\n";
42             $out->like_output(qr/random number: \d+/, "test 3");
43             $out->unlike_output(qr/i like pickles$/, "test 4");
44              
45             $out->restore;
46              
47             ## This will be printed to STDOUT
48             print "Done.\n";
49              
50             =head1 DESCRIPTION
51              
52             Test out is another Test::Builder application that implements a few of the well known test facilities
53             except the result of output to an IO::Handle is used. This could be used to capture output being
54             printed to a file, but it's ideal for output being sent to STDOUT or STDERR.
55              
56             See the SYNOPSIS for an example use.
57              
58             =begin RCS
59              
60             $Id$
61              
62             =end RCS
63              
64             =head1 AUTHOR
65              
66             Lane Davis
67              
68             =head1 FUNCTIONS
69              
70             =head2 METHODS
71              
72             =over
73              
74             =item B<$out-Enew(%options)>
75              
76             The F package constructor has several arguments, some required some optional
77              
78             =over
79              
80             =item B
81              
82             The following options must be present in the hash passed to the constructor:
83              
84             =over
85              
86             =item B
87              
88             =over
89              
90             =item tests =E $Tests
91              
92             The number of tests are simply passed to Test::Builder
93              
94             =back
95              
96             =back
97              
98             =item B
99              
100             =over
101              
102             =item B
103              
104             Actually the C key is required, but you have the option of passing the key into the constructor or to the B
105             method. This is useful if you have several segments of tests wrapped with B and B.
106              
107             =over
108              
109             =item output =E *FH
110              
111             The output argument is required and may contain either a FILEHANDLE typeglob, or
112              
113             =item output =E \*FHREF
114              
115             The C key may also point to a typeglob reference
116              
117             =back
118              
119             =back
120              
121             =back
122              
123             =cut
124              
125             sub new {
126 4     4 1 516670 my $proto = shift;
127 4   33     40 my $class = ref $proto || $proto;
128 4         8 my ($object);
129              
130             ##
131             ## Ensure key/value pairs are passed
132 4         29 assert(!(@_ & 1));
133              
134 4         31 $object = { @_ };
135 4 50       20 croak("Missing planned tests key: 'tests'") unless exists $object->{tests};
136              
137             ##
138             ## Remove it from the object so it can't be reused
139 4         38 $tstobj->plan(tests => delete $object->{tests});
140              
141 4         2591 my $self = bless($object, $class);
142 4 50       67 $self->redirect(%$object) if exists $object->{output};
143              
144 4         15 return $self;
145             }
146              
147             =item B<$out-Eredirect>
148              
149             =item B<$out-Eredirect(output =E *FH)>
150              
151             =item B<$out-Eredirect(output =E \*FH)>
152              
153             This method will be automatically invoked by the constructor if the output key is passed to new.
154              
155             =cut
156              
157             sub redirect {
158 4     4 1 9 my $self = shift;
159 4         13 my $obj = { @_ };
160              
161 4         10 eval {
162 5     5   43 no strict 'refs';
  5         9  
  5         5959  
163 4 50 33     24 die unless exists $obj->{output} && *{ $obj->{output} }{IO}->isa("IO::Handle");
  4         135  
164             };
165 4 50       16 croak("Not a valid filehandle or output key missing") if $@;
166              
167 4         17 $self->_init_redirect($obj);
168              
169 4         286 ( my $basename = basename $0 ) =~ s/\.[^.]*$//;
170              
171 4 50       99 $self->{_temp_file} = File::Temp->new(
172             DIR => "/tmp",
173             TEMPLATE => join(".", "", $basename, 'X' x 8),
174             SUFFIX => '.' . int($$ ^ time & ( 2 ** 16 )),
175             UNLINK => 0,
176             ) or croak("File::Temp: $!");
177              
178             ##
179             ## Save the prior autoflush value and turn it on
180 4         3871 $self->{_autoflush} = $self->{_io_handle}->autoflush(1);
181              
182             ##
183             ## dup(2) the IO handle to a saved descriptor then redirect it to the temp file
184 4         333 open($self->{_saved_handle}, "+>&" . fileno($self->{_io_handle}));
185 4         125 open($self->{_io_handle}, "+>&" . fileno($self->{_temp_file}));
186              
187             ##
188             ## This is where diag() messages will go. Note that I haven't implemented any
189             ## TODO handling, but I put the filehandle here in case it's added later.
190 4         29 $tstobj->failure_output($self->{_saved_handle});
191 4         105 $tstobj->todo_output($self->{_saved_handle});
192 4         60 return $self;
193             }
194              
195             sub _init_redirect {
196 4     4   8 my $self = shift;
197 4         9 my $obj = shift;
198              
199             ##
200             ## Internalize the output descriptor and ensure integrity of calling sequence
201 4         14 $self->{_io_handle} = delete $obj->{output};
202 4         11 delete $self->{_saved_handle};
203 4         7 delete $self->{_autoflush};
204 4         17 $self->_cleanup_temp(1);
205 4         11 $self->{_read_offset} = $self->{_buffer_size} = 0;
206 4         7 return $self;
207             }
208              
209             sub restore {
210 4     4 0 3414 my $self = shift;
211              
212             ##
213             ## Ensure proper calling sequence
214 4 50 33     44 croak("Output is not being redirected")
215             unless ( exists $self->{_io_handle} and exists $self->{_saved_handle} );
216              
217             ##
218             ## Restore the saved descriptor and restore the autoflush flag
219 4         104 open($self->{_io_handle}, ">&" . fileno($self->{_saved_handle}));
220 4         23 $self->{_io_handle}->autoflush($self->{_autoflush});
221              
222             ##
223             ## Grab any data remaining in the buffer and clean up
224 4         160 my $data = $self->drain_out;
225 4         65 close($self->{_temp_file});
226 4         20 $self->_cleanup_temp(1);
227 4         73 delete $self->{_saved_handle};
228 4         13 delete $self->{_io_handle};
229 4         11 return $data;
230             }
231              
232             sub DESTROY {
233 0     0   0 my $self = shift;
234 0 0 0     0 $self->restore if exists $self->{_io_handle} and exists $self->{_saved_handle};
235 0         0 return $self;
236             }
237              
238             sub drain_out {
239 8     8 0 15 my $self = shift;
240 8         18 my $fh = $self->{_temp_file};
241 8         32 local ($/);
242              
243             ##
244             ## This does something similar to fstat(2)
245 8         112 my $cursize = ( stat $fh )[ 7 ];
246 8 100       48 if ($cursize > $self->{_buffer_size}) {
247 4         11 $self->{_read_offset} = $self->{_buffer_size};
248 4         9 $self->{_buffer_size} = $cursize
249             }
250              
251             ##
252             ## Seek to the end of the last read offset (SEEK_SET)
253 8         56 seek $fh, $self->{_read_offset}, 0;
254              
255             ##
256             ## Read it all into one scalar
257 8         239 my $curout = join '', <$fh>;
258              
259             ##
260             ## Reposition to the EOF (SEEK_END)
261 8         52 seek $fh, 0, 2;
262              
263 8         28 return $curout;
264             }
265              
266             sub _cleanup_temp {
267 8     8   16 my $self = shift;
268 8         13 my $unlink = shift;
269 8 100       30 return unless exists $self->{_temp_file};
270 4         22 my $file = $self->{_temp_file}->filename;
271 4         34 close $self->{_temp_file};
272 4         26 delete $self->{_temp_file};
273 4 50 33     800 unlink $file if $unlink && -f $file;
274 4         13 return 1;
275             }
276              
277             =pod
278              
279             =back
280              
281             =head2 TESTS
282              
283             =over
284              
285             =item B<$out-Eis_output(EXPR, NAME)>
286              
287             =item B<$out-Ewas_output(EXPR, NAME)>
288              
289             Tests the last output buffer against EXPR. If there isn't a perfect string comparison the test fails. Pay particular
290             attention to possible newlines in the last output. If you're unsure either paste the contents of $\ to your comparitor
291             or use the C method.
292              
293             The was_output method is an alias for is_output.
294              
295             =item B<$out-Eisnt_output(EXPR, NAME)>
296              
297             =item B<$out-Ewasnt_output(EXPR, NAME)>
298              
299             This is the inverse of is_output, the negation of the comparing EXPR to the last printed output is performed.
300              
301             The wasnt_output method is an alias for isnt_output.
302              
303             =item B<$out-Elike_output(qr/STRING/, NAME)>
304              
305             This performs a test with the last output and a compiled regular expression as its first argument.
306              
307             =item B<$out-Eunlike_output(qr/STRING/, NAME)>
308              
309             This performs a negated test with the last output and a compiled regular expression as its first argument.
310              
311             =item B<$out-Ecmp_output(OP, EXPR, NAME)>
312              
313             This performs a comparison allowing you to pass your own perl binary operator as the first arugment (e.g., "==", "eq", etc).
314              
315             =cut
316              
317             sub is_output {
318 1     1 1 69 my $self = shift;
319 1   50     5 my $comp = shift || return $self->do_fail(q{$obj->is_output("string")});
320 1   50     4 my $name = shift || '';
321 1         4 my $last = $self->drain_out;
322 1         6 return $tstobj->is_eq($comp, $last, $name);
323             }
324              
325             sub was_output {
326 0     0 1 0 goto &is_output;
327             }
328              
329             sub isnt_output {
330 1     1 1 79 my $self = shift;
331 1   50     5 my $comp = shift || return $self->do_fail(q{$obj->isnt_output("string")});
332 1   50     3 my $name = shift || '';
333 1         5 my $last = $self->drain_out;
334 1         6 return $tstobj->isnt_eq($comp, $last, $name);
335             }
336              
337             sub wasnt_output {
338 0     0 1 0 goto &isnt_output;
339             }
340              
341             sub like_output {
342 1     1 1 90 my $self = shift;
343 1   50     6 my $regex = shift || return $self->do_fail(q{$obj->like_output(qr/regex/)});
344 1   50     5 my $name = shift || '';
345 1         5 my $last = $self->drain_out;
346 1         9 return $tstobj->like($last, $regex, $name);
347             }
348              
349             sub unlike_output {
350 1     1 1 83 my $self = shift;
351 1   50     4 my $regex = shift || return $self->do_fail(q{$obj->unlike_output(qr/regex/)});
352 1   50     4 my $name = shift || '';
353 1         3 my $last = $self->drain_out;
354 1         6 return $tstobj->unlike($last, $regex, $name);
355             }
356              
357             sub cmp_output {
358 0     0 1   my $self = shift;
359 0 0         @_ > 1 or return $self->do_fail(q{$obj->cmp_ok(...)});
360 0           my ($type, $that, $name) = @_;
361 0   0       $name ||= '';
362 0           my $last = $self->drain_out;
363 0           return $tstobj->cmp_ok($last, $type, $that, $name);
364             }
365              
366             sub do_fail {
367 0     0 0   return $tstobj->ok(0, @_);
368             }
369              
370             =item B<$out-Ediag(@messages)>
371              
372             Prints a diagnostic message
373              
374             =cut
375              
376             sub diag {
377 0     0 1   my $self = shift;
378 0           return $tstobj->diag(@_);
379             }
380              
381             1;
382              
383             __END__