File Coverage

blib/lib/Test/HTML/Form.pm
Criterion Covered Total %
statement 204 221 92.3
branch 50 80 62.5
condition 13 28 46.4
subroutine 33 33 100.0
pod 16 16 100.0
total 316 378 83.6


line stmt bran cond sub pod time code
1             package Test::HTML::Form;
2 2     2   36353 use strict;
  2         4  
  2         68  
3 2     2   11 use warnings;
  2         4  
  2         62  
4 2     2   19 no warnings 'redefine';
  2         5  
  2         105  
5              
6             =head1 NAME
7              
8             Test::HTML::Form - HTML Testing and Value Extracting
9              
10             =head1 VERSION
11              
12             0.05
13              
14             =head1 SYNOPSIS
15              
16             use Test::HTML::Form;
17              
18             my $filename = 't/form_with_errors.html';
19              
20             my $response = $ua->request($request)
21              
22             # test functions
23              
24             title_matches($filename,'Foo Bar','title matches');
25              
26             no_title($filename,'test site','no english title');
27              
28             tag_matches($response,
29             'p',
30             { class => 'formError',
31             _content => 'There is an error in this form.' },
32             'main error message appears as expected' );
33              
34             no_tag($filename,
35             'p',
36             { class => 'formError',
37             _content => 'Error' },
38             'no unexpected errors' );
39              
40              
41             text_matches($filename,'koncerty','found text : koncerty'); # check text found in file
42              
43             no_text($filename,'Concert','no text matching : Concert'); # check text found in file
44              
45             image_matches($filename,'/images/error.gif','matching image found image in HTML');
46              
47             link_matches($filename,'/post/foo.html','Found link in HTML');
48              
49             script_matches($response, qr/function someWidget/, 'found widget in JS');
50              
51             form_field_value_matches($response,'category_id', 12345678, undef, 'category_id matches');
52              
53             form_select_field_matches($filename,{ field_name => $field_name, selected => $field_value, form_name => $form_name}, $description);
54              
55             form_checkbox_field_matches($response,{ field_name => $field_name, selected => $field_value, form_name => $form_name}, $description);
56              
57             # Data extraction functions
58              
59             my $form_values = Test::HTML::Form->get_form_values({filename => $filename, form_name => 'form1'});
60              
61             my $posting_id = Test::HTML::Form->extract_text({filename => 'publish.html', pattern => 'Reference :\s(\d+)'});
62              
63             =head1 DESCRIPTION
64              
65             Test HTML pages and forms, and extract values.
66              
67             Developed for and released with permission of Slando (http://www.slando.com)
68              
69             All test functions will take either a filename or an HTTP::Response compatible object (i.e. any object with a content method)
70              
71             =cut
72              
73 2     2   1416 use Data::Dumper;
  2         7383  
  2         129  
74 2     2   2490 use HTML::TreeBuilder;
  2         89771  
  2         28  
75              
76 2     2   95 use base qw( Exporter Test::Builder::Module);
  2         5  
  2         4589  
77             our @EXPORT = qw(
78             link_matches no_link
79             image_matches no_image
80             tag_matches no_tag
81             text_matches no_text
82             script_matches
83             title_matches no_title
84             form_field_value_matches
85             form_select_field_matches
86             form_checkbox_field_matches
87             );
88              
89              
90             $Data::Dumper::Maxdepth = 2;
91             my $Test = Test::Builder->new;
92             my $CLASS = __PACKAGE__;
93             my %parsed_files = ();
94             my %parsed_file_forms = ();
95              
96             our $VERSION = 0.05;
97              
98             =head1 FUNCTIONS
99              
100             =head2 image_matches
101              
102             Test that some HTML contains an img tag with a src attribute matching the link provided.
103              
104             image_matches($filename,$image_source,'matching image found image in HTML');
105              
106             Passes when at least one instance found, fails if no matches found.
107              
108             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
109              
110             =cut
111              
112             sub image_matches {
113 1     1 1 8 my ($filename,$link,$name) = (@_);
114 1         2 local $Test::Builder::Level = 2;
115 1         7 return tag_matches($filename,'img',{ src => $link },$name);
116             };
117              
118              
119             =head2 no_image
120              
121             Test that some HTML doesn't contain any img tag with a src attribute matching the link provided.
122              
123             no_image($response,$image_source,'no matching image found in HTML');
124              
125             Passes when no matches found, fails if any matches found.
126              
127             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
128              
129             =cut
130              
131             sub no_image {
132 1     1 1 9 my ($filename,$link,$name) = (@_);
133 1         2 local $Test::Builder::Level = 2;
134 1         6 return no_tag($filename,'img',{ src => $link },$name);
135             };
136              
137              
138             =head2 link_matches
139              
140             Test that some HTML contains a href tag with a src attribute matching the link provided.
141              
142             link_matches($response,$link_destination,'Found link in HTML');
143              
144             Passes when at least one instance found, fails if no matches found.
145              
146             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
147              
148             =cut
149              
150             sub link_matches {
151 2     2 1 12 my ($filename,$link,$name) = (@_);
152 2         5 local $Test::Builder::Level = 2;
153 2         10 return tag_matches($filename,['a','link'],{ href => $link },$name);
154             };
155              
156             =head2 no_link
157              
158             Test that some HTML does not contain a href tag with a src attribute matching the link provided.
159              
160             link_matches($filename,$link_destination,'Link not in HTML');
161              
162             Passes when if no matches found, fails when at least one instance found.
163              
164             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
165              
166             =cut
167              
168             sub no_link {
169 1     1 1 8 my ($filename,$link,$name) = (@_);
170 1         2 local $Test::Builder::Level = 2;
171 1         5 return no_tag($filename,'a',{ href => $link },$name);
172             };
173              
174             =head2 title_matches
175              
176             Test that some HTML contains a title tag with content matching the pattern/string provided.
177              
178             title_matches($filename,'Foo bar home page','title matches');
179              
180             Passes when at least one instance found, fails if no matches found.
181              
182             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
183              
184             =cut
185              
186             sub title_matches {
187 1     1 1 13 my ($filename,$title,$name) = @_;
188 1         3 local $Test::Builder::Level = 2;
189 1         7 return tag_matches($filename,"title", { _content => $title } ,$name);
190             };
191              
192             =head2 no_title
193              
194             Test that some HTML does not contain a title tag with content matching the pattern/string provided.
195              
196             no_title($filename,'Foo bar home page','title matches');
197              
198             Passes if no matches found, fails when at least one instance found.
199              
200             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
201              
202             =cut
203              
204             sub no_title {
205 1     1 1 10 my ($filename,$title,$name) = (@_);
206 1         2 local $Test::Builder::Level = 2;
207 1     1   9 return no_tag($filename,'title', sub { shift->as_trimmed_text =~ m/$title/ },$name);
  1         69  
208             }
209              
210              
211             =head2 tag_matches
212              
213             Test that some HTML contains a tag with content or attributes matching the pattern/string provided.
214              
215             tag_matches($filename,'a',{ href => $link },$name); # check matching tag found in file
216              
217             Passes when at least one instance found, fails if no matches found.
218              
219             Takes a list of arguments
220              
221             =over 4
222              
223             =item filename/response - string of path/name of file, or an HTTP::Response object
224              
225             =item tag type(s) - string or arrarref of strings naming which tag(s) to match
226              
227             =item attributes - hashref of attributes and strings or quoted-regexps to match
228              
229             =item comment - an optional test comment/name
230              
231             =back
232              
233             =cut
234              
235             sub tag_matches {
236 6     6 1 30 my ($filename,$tag,$attr_ref,$name) = @_;
237 6         9 my $count = 0;
238              
239 6 100       17 if (ref $tag ) {
240 3         6 foreach my $this_tag (@$tag) {
241 6         13 $count += _tag_count($filename, $this_tag, $attr_ref);
242             }
243             } else {
244 3         11 $count = _tag_count($filename, $tag, $attr_ref);
245             }
246              
247 6         36 my $tb = $CLASS->builder;
248 6         66 my $ok = $tb->ok( $count, $name);
249 6 50       1666 unless ($ok) {
250 0 0       0 my $tagname = ( ref $tag ) ? join (' or ', @$tag) : $tag ;
251 0         0 $tb->diag("Expected at least one tag of type '$tagname' in file $filename matching condition, but got 0\n");
252             }
253 6         17 return $ok;
254             }
255              
256              
257              
258             =head2 no_tag
259              
260             Test that some HTML does not contain a tag with content or attributes matching the pattern/string provided.
261              
262             no_tag($filename,'a',{ href => $link },$name); # check matching tag NOT found in file
263              
264             Passes if no matches found, fails when at least one instance found.
265              
266             Takes a list of arguments filename/response, hashref of attributes and strings or quoted-regexps to match, and optional test comment/name
267              
268             =cut
269              
270             sub no_tag {
271 4     4 1 15 my ($filename,$tag,$attr_ref,$name) = @_;
272 4         10 my $count = _tag_count($filename, $tag, $attr_ref);
273 4         27 my $tb = $CLASS->builder;
274 4         39 my $ok = $tb->is_eq( $count, 0, $name);
275 4 50       1633 unless ($ok) {
276 0         0 $tb->diag("Expected no tags of type $tag matching criteria in file $filename, but got $count\n");
277             }
278 4         11 return $ok;
279             };
280              
281             =head2 text_matches
282              
283             Test that some HTML contains some content matching the pattern/string provided.
284              
285             text_matches($filename,$text,$name); # check text found in file
286              
287             Passes when at least one instance found, fails if no matches found.
288              
289             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
290              
291             =cut
292              
293             sub text_matches {
294 1     1 1 7 my ($filename,$text,$name) = @_;
295 1         7 my $count = _count_text({filename => $filename, text => $text });
296 1         7 my $tb = $CLASS->builder;
297 1         49 my $ok = $tb->ok( $count, $name);
298 1 50       295 unless ($ok) {
299 0         0 $tb->diag("Expected HTML to contain at least one instance of text '$text' in file $filename but not found\n");
300             }
301 1         11 return $ok;
302             };
303              
304             =head2 no_text
305              
306             Test that some HTML does not contain some content matching the pattern/string provided.
307              
308             no_text($filename,$text,$name); # check text NOT found in file
309              
310             Passes if no matches found, fails when at least one instance found.
311              
312             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
313              
314             =cut
315              
316             sub no_text {
317 1     1 1 5 my ($filename,$text,$name) = @_;
318 1         6 my $count = _count_text({filename => $filename, text => $text });
319 1         13 my $tb = $CLASS->builder;
320 1         20 my $ok = $tb->is_eq( $count, 0 , $name);
321 1 50       526 unless ($ok) {
322 0         0 $tb->diag("Expected HTML to not contain text '$text' in file $filename but $count instances found\n");
323             }
324 1         4 return $ok;
325             };
326              
327              
328             =head2 script_matches
329              
330             Test that HTML script element contains text matcging that provided.
331              
332             script_matches($response, qr/function someWidget/, 'found widget in JS');
333              
334             Passes when at least one instance found, fails if no matches found.
335              
336             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
337              
338             =cut
339              
340             sub script_matches {
341 1     1 1 10 my ($filename,$text_to_match,$name) = @_;
342 1         2 my $pattern;
343 1 50       6 if (ref($text_to_match) eq 'Regexp') {
344 1         3 $pattern = $text_to_match;
345             }
346 1         5 my $tree = _get_tree($filename);
347              
348             my @parse_args = sub {
349 1     1   102 my $elem = shift;
350 1 50       5 return 0 unless (ref $elem eq 'HTML::Element' );
351 1         2 my $ok = 0;
352 1         5 (my $text = $elem->as_HTML) =~ s/<(.|\n)*?>//g;
353 1 50       237 if ($pattern) {
354 1         5 my $ok = $text =~ m/$pattern/;
355 1   33     7 return $ok || $text =~ m/$pattern/;
356             } else {
357 0         0 $text eq $text_to_match;
358             }
359 1         8 };
360              
361 1         6 my $count = $tree->look_down( _tag => 'script', @parse_args );
362              
363 1         11 my $tb = $CLASS->builder;
364 1         10 my $ok = $tb->ok( $count, $name);
365 1 50       265 unless ($ok) {
366 0         0 $tb->diag("Expected script tag in file $filename matching $text_to_match, but got 0\n");
367             }
368 1         7 return $ok;
369             };
370              
371              
372              
373             =head2 form_field_value_matches
374              
375             Test that the HTML contains a form element with the value matching that provided.
376              
377             form_field_value_matches($filename,$field_name, $field_value, $form_name, $description);
378              
379             form_field_value_matches($filename,$field_name, qr/some pattern/, undef, 'test for foo in bar form field');
380              
381             Takes a list of arguments : filename/response, string or quoted-regexp to match, optional form_name, and optional test comment/name
382              
383             Field value argument can be a string (for exact matches) or a quoted regexp (for pattern matches)
384              
385             Use form_select_field_matches for select elements.
386              
387             Use form_checkbox_field_matches for checkbox elements
388              
389             =cut
390              
391             sub form_field_value_matches {
392 2     2 1 13 my ($filename,$field_name, $field_value, $form_name, $description) = @_;
393 2         13 my $form_fields = __PACKAGE__->get_form_values({ filename => $filename, form_name => $form_name });
394 2         10 my $tb = $CLASS->builder;
395              
396 2         16 my $elems = $form_fields->{$field_name};
397              
398 2         3 my $ok = 0;
399 2         5 foreach my $elem (@$elems) {
400 2         5 my $matches = _compare($elem,$field_value);
401 2 50       6 if ($matches) {
402 2         7 $ok = $tb->ok( $matches , $description);
403 2         503 last;
404             }
405             }
406              
407 2 50       7 unless ($ok) {
408 0         0 $tb->ok( 0 , $description);
409 0         0 $tb->diag("Expected form to contain field '$field_name' and have value of '$field_value' but not found in file $filename\n");
410             }
411 2         5 return $ok;
412             };
413              
414             =head2 form_select_field_matches
415              
416             Test that the HTML contains a form element with the value matching that provided.
417              
418             form_select_field_matches($filename,{ field_name => $field_name, selected => $field_value, form_name => $form_name}, $description);
419              
420             Takes a mixed list/ hashref of arguments :
421              
422             =over 4
423              
424             =item filename/response,
425              
426             =item hashref of search attributes, keys are : field_name, selected, form_name (optional)
427              
428             =item optional test comment/name
429              
430             =back
431              
432             Selected field value can be string or quoted regexp
433              
434             =cut
435              
436             sub form_select_field_matches {
437 1     1 1 10 my ($filename, $field_value_args, $description) = @_;
438 1         5 my $form_fields = __PACKAGE__->get_form_values({ filename => $filename, form_name => $field_value_args->{form_name} });
439 1         6 my $tb = $CLASS->builder;
440              
441 1         7 my $field_value = $field_value_args->{selected};
442 1         3 my $field_name = $field_value_args->{field_name};
443              
444 1         3 my $select_elem = $form_fields->{$field_name}[0];
445 1 50       7 unless (UNIVERSAL::can($select_elem,'descendants')) {
446 0         0 die "$select_elem (",$select_elem->tag,") is not a select html element for field : $field_name - did you mean to call form_checkbox_field_matches ?";
447             }
448 1         2 my $selected_option;
449 1         4 foreach my $option ( $select_elem->descendants ) {
450 10 50 33     566 next unless (ref($option) && ( lc($option->tag) eq 'option') );
451 10 100       86 if ( _compare($option, $field_value) ) {
452 1         3 $selected_option = $option;
453 1         3 last;
454             }
455             }
456              
457 1   50     11 my $ok = $tb->ok( $selected_option && scalar grep (m/selected/i && $selected_option->attr($_), $selected_option->all_external_attr_names), $description);
458 1 50       339 unless ($ok) {
459 0         0 $tb->diag("Expected form to contain field '$field_name' and have option with value of '$field_value' selected but not found in file $filename \n");
460             }
461 1         4 return $ok;
462             }
463              
464             =head2 form_checkbox_field_matches
465              
466             Test that the HTML contains a form element with the value matching that provided.
467              
468             form_checkbox_field_matches($filename,{ field_name => $field_name, selected => $field_value, form_name => $form_name}, $description);
469              
470             Takes a mixed list/ hashref of arguments :
471              
472             =over 4
473              
474             =item filename/response,
475              
476             =item hashref of search attributes, keys are : field_name, selected, form_name (optional)
477              
478             =item optional test comment/name
479              
480             =back
481              
482             Selected field value can be string or quoted regexp
483              
484             =cut
485              
486             sub form_checkbox_field_matches {
487 1     1 1 11 my ($filename, $field_value_args, $description) = @_;
488 1         6 my $form_fields = __PACKAGE__->get_form_values({ filename => $filename, form_name => $field_value_args->{form_name} });
489 1         6 my $tb = $CLASS->builder;
490              
491 1         8 my $field_value = $field_value_args->{selected};
492 1         3 my $field_name = $field_value_args->{field_name};
493 1         1 my $selected_box;
494 1   50     4 my $checkbox_elems = $form_fields->{$field_name} || [];
495              
496 1         4 foreach my $checkbox ( @$checkbox_elems ) {
497 1 50       2 if ( _compare($checkbox, $field_value) ) {
498 1         2 $selected_box = $checkbox;
499 1         3 last;
500             }
501             }
502              
503 1   50     9 my $ok = $tb->ok( $selected_box && scalar grep (m/checked/i && $selected_box->attr($_), $selected_box->all_attr_names), $description);
504 1 50       280 unless ($ok) {
505 0         0 $tb->diag("Expected form to contain field '$field_name' and have option with value of '$field_value' selected but not found in file $filename\n");
506             }
507 1         4 return $ok;
508             }
509              
510             =head2 get_form_values
511              
512             Extract form fields and their values from HTML content
513              
514             my $form_values = Test::HTML::Form->get_form_values({filename => $filename, form_name => 'form1'});
515              
516             Takes a hashref of arguments : filename (name of file or an HTTP::Response object, required), form_name (optional).
517              
518             Returns a hashref of form fields, with name as key, and arrayref of XML elements for that field.
519              
520             =cut
521              
522             sub get_form_values {
523 5     5 1 571 my $class = shift;
524 5         8 my $args = shift;
525 2     2   15 no warnings 'uninitialized';
  2         2  
  2         2249  
526 5         8 my $form_name = $args->{form_name};
527 5         13 my $internal_form_name = $form_name . ' form';
528 5 100       21 if ($parsed_file_forms{$args->{filename}}{$internal_form_name}) {
529 4         14 return $parsed_file_forms{$args->{filename}}{$internal_form_name};
530             } else {
531 1         4 my $tree = _get_tree($args->{filename});
532 1         6 my $form_fields = { };
533             my ($form) = $tree->look_down('_tag', 'form',
534             sub {
535 1     1   196 my $form = shift;
536 1 50       5 if ($form_name) {
537 0 0       0 return 1 if $form->attr('name') eq $form_name;
538             } else {
539 1         4 return 1;
540             }
541             }
542 1         9 );
543 1 50       603 if (ref $form) {
544 1         7 my @form_nodes = $form->descendants();
545 1         1407 foreach my $node (@form_nodes) {
546 63 50       489 next unless (ref($node));
547 63 100       135 if (lc($node->tag) =~ /^(input|select|textarea|button)$/i) {
548 11 100       100 if (lc $node->attr('type') =~ /(radio|checkbox)/) {
549 3         37 push (@{$form_fields->{$node->attr('name')}},$node);
  3         12  
550             } else {
551 8         96 $form_fields->{$node->attr('name')} = [ $node ];
552             }
553             }
554             }
555             }
556 1         16 $parsed_file_forms{$args->{filename}}{$internal_form_name} = $form_fields;
557              
558 1         3 return $form_fields;
559             }
560             }
561              
562             =head2 extract_text
563              
564             my $posting_id = Test::HTML::Form->extract_text({filename => 'publish.html', pattern => 'Reference :\s(\d+)'});
565              
566             =cut
567              
568             sub extract_text {
569 1     1 1 9 my $class = shift;
570 1         2 my $args = shift;
571 1         3 my $tree = _get_tree($args->{filename});
572 1         2 my $pattern = $args->{pattern};
573             my ($node) = $tree->look_down( sub {
574 76     76   5846 my $thisnode = shift;
575 76         192 $thisnode->normalize_content;
576 76 100       1425 return 1 if ($thisnode->as_trimmed_text =~ m/$pattern/i);
577 1         8 });
578 1         46 my ($match) = ($node->as_trimmed_text =~ m/$pattern/i);
579              
580 1         767 return $match;
581             }
582              
583              
584              
585             #
586             ##########################################
587             # Private / Internal methods and functions
588              
589             sub _compare {
590 13     13   23 my ($elem, $field_value) = @_;
591 13 50 33     69 unless ($elem && (ref$elem eq 'HTML::Element') ) {
592 0         0 warn "_compare passed $elem and value $field_value, $elem should be HTML::Element but is : ", ref $elem, "\n";
593 0         0 return 0 ;
594             }
595              
596 13 50       27 my $have_regexp = ( ref($field_value) eq 'Regexp' ) ? 1 : 0;
597 13         33 my $value = $elem->attr('value') ;
598 13 100       129 unless (defined $value) {
599 11         26 $value = $elem->as_trimmed_text;
600             }
601 13         354 my $ok;
602 13 50       22 if ($have_regexp) {
603 0 0 0     0 $ok = ( $elem && $value =~ m/$field_value/ ) ? 1 : 0 ;
604             } else {
605 13 100 66     58 $ok = ( $elem && $value eq $field_value ) ? 1 : 0 ;
606             }
607 13         35 return $ok
608             }
609              
610             sub _tag_count {
611 13     13   22 my ($filename, $tag, $attr_ref) = @_;
612 13         28 my $tree = _get_tree($filename);
613 13         19 my @parse_args = ();
614 13 100       34 if ( ref $attr_ref eq 'HASH' ) {
615 12         14 my $pattern;
616 12 100       40 if (ref($attr_ref->{_content}) eq 'Regexp') {
617 3         5 $pattern = $attr_ref->{_content};
618 3         8 delete $attr_ref->{_content};
619             }
620              
621 12         36 @parse_args = %$attr_ref ;
622 12 100       27 if ($pattern) {
623             push( @parse_args, sub {
624 2 50   2   276 return 0 unless (ref $_[0] eq 'HTML::Element' );
625 2         32 return $_[0]->as_trimmed_text =~ m/$pattern/;
626 3         17 } );
627             }
628             } else {
629 1         2 @parse_args = $attr_ref ;
630             }
631 13         76 my $count = $tree->look_down( _tag => $tag, @parse_args );
632              
633 13   100     5750 return $count || 0;
634             }
635              
636              
637             sub _count_text {
638 2     2   4 my $args = shift;
639 2         6 my $tree = _get_tree($args->{filename});
640 2         5 my $text = $args->{text};
641             my $count = $tree->look_down( sub {
642 77     77   7132 my $node = shift;
643 77         184 $node->normalize_content;
644 77 100       3857 return 1 if ($node->as_trimmed_text =~ m/$text/);
645 2         23 });
646 2   100     812 return $count || 0;
647             }
648              
649             sub _get_tree {
650 18     18   22 my $filename = shift;
651 18 100       53 unless ($parsed_files{$filename}) {
652 1         10 my $tree = HTML::TreeBuilder->new;
653 1         317 $tree->store_comments(1);
654 1 50 33     15 if (ref $filename && $filename->can('content')) {
655 0         0 $tree->parse_content($filename->decoded_content);
656             } else {
657 1 50       33 die "can't find file $filename" unless (-f $filename);
658 1         11 $tree->parse_file($filename);
659             }
660 1         23169 $parsed_files{$filename} = $tree;
661             }
662 18         42 return $parsed_files{$filename};
663             }
664              
665              
666             =head1 SEE ALSO
667              
668             =over 4
669              
670             =item Test::HTML::Content
671              
672             =item HTML::TreeBuilder
673              
674             =item Test::HTTP::Response
675              
676             =back
677              
678             =head1 AUTHOR
679              
680             Aaron Trevena
681              
682             =head1 BUGS
683              
684             Please report any bugs or feature requests to http://rt.cpan.org
685              
686             =head1 COPYRIGHT & LICENSE
687              
688             Copyright 2008 Slando.
689             Copyright 2009 Aaron Trevena.
690              
691             This library is free software; you can redistribute it and/or modify
692             it under the same terms as Perl itself, either Perl version 5.8.8 or,
693             at your option, any later version of Perl 5 you may have available.
694              
695             =cut
696              
697              
698             1;