File Coverage

lib/Test/Pod/Links.pm
Criterion Covered Total %
statement 137 137 100.0
branch 55 56 98.2
condition 14 15 93.3
subroutine 16 16 100.0
pod 3 3 100.0
total 225 227 99.1


line stmt bran cond sub pod time code
1             package Test::Pod::Links;
2              
3 12     12   802833 use 5.006;
  12         142  
4 12     12   70 use strict;
  12         25  
  12         299  
5 12     12   62 use warnings;
  12         34  
  12         553  
6              
7             our $VERSION = '0.003';
8              
9 12     12   86 use Carp ();
  12         23  
  12         310  
10 12     12   8209 use HTTP::Tiny 0.014 ();
  12         582886  
  12         390  
11 12     12   7097 use Pod::Simple::Search ();
  12         71582  
  12         297  
12 12     12   4746 use Pod::Simple::SimpleTree ();
  12         344981  
  12         299  
13 12     12   98 use Scalar::Util ();
  12         23  
  12         198  
14 12     12   66 use Test::Builder ();
  12         30  
  12         160  
15 12     12   6028 use Test::XTFiles ();
  12         213003  
  12         13792  
16              
17             my $TEST = Test::Builder->new();
18              
19             # - Do not use subtests because subtests cannot be tested with
20             # Test::Builder:Tester.
21             # - Do not use a plan because a method that sets a plan cannot be tested
22             # with Test::Builder:Tester.
23             # - Do not call done_testing in a method that should be tested by
24             # Test::Builder::Tester because TBT cannot test them.
25              
26             sub all_pod_files_ok {
27 6     6 1 31 my $self = shift;
28              
29 6         86 my @files = Test::XTFiles->new->all_files();
30 6 100       44266 if ( !@files ) {
31 1         18 $TEST->skip_all("No files found\n");
32 1         11 return 1;
33             }
34              
35 5         82 my @pod_files = grep { Pod::Simple::Search->new->contains_pod($_) } @files;
  6         191  
36 5 100       662 if ( !@pod_files ) {
37 2         12 $TEST->skip_all("No files with Pod found\n");
38 2         24 return 1;
39             }
40              
41 3         8 my $rc = 1;
42 3         11 for my $file (@pod_files) {
43 4 100       35 if ( !$self->pod_file_ok($file) ) {
44 1         9 $rc = 0;
45             }
46             }
47              
48 3         29 $TEST->done_testing;
49              
50 3 100       23 return 1 if $rc;
51 1         8 return;
52             }
53              
54             sub new {
55 37     37 1 51891 my $class = shift;
56              
57 37 100       258 Carp::croak 'Odd number of arguments' if @_ % 2;
58 36         100 my %args = @_;
59              
60 36         91 my $self = bless {}, $class;
61              
62             #
63 36         300 $self->{_cache} = {};
64              
65             #
66 36   66     246 $self->_ua( $args{ua} || HTTP::Tiny->new );
67              
68             #
69 35         61 my @ignores;
70 35 100       98 if ( exists $args{ignore} ) {
71 7         16 my $ignore = $args{ignore};
72 7 100       33 if ( ref $ignore eq ref [] ) {
73 2         4 @ignores = @{$ignore};
  2         6  
74             }
75             else {
76 5         13 @ignores = $ignore;
77             }
78             }
79              
80             #
81 35         59 my @ignores_match;
82 35 100       106 if ( exists $args{ignore_match} ) {
83 8         17 my $ignore_match = $args{ignore_match};
84 8 100       26 if ( ref $ignore_match eq ref [] ) {
85 4         6 @ignores_match = @{$ignore_match};
  4         9  
86             }
87             else {
88 4         10 @ignores_match = $ignore_match;
89             }
90             }
91              
92             ## no critic (RegularExpressions::RequireDotMatchAnything)
93             ## no critic (RegularExpressions::RequireExtendedFormatting)
94             ## no critic (RegularExpressions::RequireLineBoundaryMatching)
95 35         105 my $ignore_regex = join q{|}, @ignores_match, map { qr{^\Q$_\E$} } @ignores;
  8         104  
96 35 100       368 $self->_ignore_regex( $ignore_regex ne q{} ? qr{$ignore_regex} : undef );
97             ## use critic
98              
99             KEY:
100 35         118 for my $key ( keys %args ) {
101 29 100       70 next KEY if $key eq 'ignore';
102 22 100       52 next KEY if $key eq 'ignore_match';
103 14 100       37 next KEY if $key eq 'ua';
104              
105 1         118 Carp::croak "new() knows nothing about argument '$key'";
106             }
107              
108 34         151 return $self;
109             }
110              
111             sub pod_file_ok {
112 16     16 1 24281 my ( $self, $file ) = @_;
113              
114 16 100 100     406 Carp::croak 'usage: pod_file_ok(FILE)' if @_ != 2 || !defined $file;
115              
116 13         30 my $parse_msg = "Parse Pod ($file)";
117              
118 13 100       253 if ( !-f $file ) {
119 1         6 $TEST->ok( 0, $parse_msg );
120 1         1059 $TEST->diag("\n");
121 1         238 $TEST->diag("File $file does not exist or is not a file");
122 1         230 return;
123             }
124              
125 12         112 my $pod = Pod::Simple::SimpleTree->new->parse_file($file);
126              
127 12 100       25416 if ( $pod->any_errata_seen ) {
128              
129             # Pod contains errors
130 1         11 $TEST->ok( 0, $parse_msg );
131 1         1006 return;
132             }
133              
134 11         94 $TEST->ok( 1, $parse_msg );
135              
136             my @links =
137 32 50       189 grep { defined && m{ ^ http(?:s)? :// }xsmi }
138 32         46 map { ${ $_->{to} }[2] }
  32         59  
139 11         2859 grep { $_->{type} eq 'url' } $self->_extract_links_from_pod( $pod->root );
  39         77  
140              
141 11         31 my $ignore_regex = $self->_ignore_regex;
142 11 100       24 if ( defined $ignore_regex ) {
143 3         5 @links = grep { $_ !~ $ignore_regex } @links;
  15         63  
144             }
145              
146 11         19 my $rc = 1;
147 11         23 my $ua = $self->_ua;
148 11         17 my %url_checked_in_this_file;
149              
150             LINK:
151 11         21 for my $link (@links) {
152 26 100       534 next LINK if exists $url_checked_in_this_file{$link};
153 24         43 $url_checked_in_this_file{$link} = 1;
154              
155 24 100       57 if ( !exists $self->{_cache}->{$link} ) {
156 21         59 $self->{_cache}->{$link} = $ua->head($link);
157             }
158 24         225 my $res = $self->{_cache}->{$link};
159              
160 24         105 $TEST->ok( $res->{success}, "$link ($file)" );
161              
162 24 100       7170 if ( !$res->{success} ) {
163 2         5 $rc = 0;
164 2         7 $TEST->diag("\n");
165 2         470 $TEST->diag( $res->{reason} );
166 2         542 $TEST->diag("\n");
167             }
168             }
169              
170 11 100       138 return 1 if $rc;
171 2         30 return;
172             }
173              
174             sub _extract_links_from_pod {
175 142     142   9653 my ( $self, $node_ref ) = @_;
176              
177 142 100 100     808 Carp::croak 'usage: _extract_links_from_pod([ elementname, \%attributes, ...subnodes... ])' if @_ != 2 || ref $node_ref ne ref [] || scalar @{$node_ref} < 2;
  139   100     392  
178              
179 138         188 my @links;
180 138         172 my ( $elem_name, $attr_ref, @subnodes ) = @{$node_ref};
  138         281  
181              
182 138 100       249 if ( $elem_name eq 'L' ) {
183 48         71 push @links, $attr_ref;
184             }
185              
186             SUBNODE:
187 138         197 for my $subnode (@subnodes) {
188 285 100       581 next SUBNODE if ref $subnode ne ref [];
189              
190 124         220 push @links, $self->_extract_links_from_pod($subnode);
191             }
192              
193 138         297 return @links;
194             }
195              
196             sub _ignore_regex {
197 88     88   3878 my $self = shift;
198              
199 88 100       228 if (@_) {
200 37         60 my $ignore_regex = shift;
201 37         84 $self->{_ignore_regex} = $ignore_regex;
202             }
203              
204 88         480 return $self->{_ignore_regex};
205             }
206              
207             sub _ua {
208 53     53   4329 my $self = shift;
209              
210 53 100       139 if (@_) {
211 40         64 my $ua = shift;
212 40 100 100     841 Carp::croak q{ua must have method 'head'} if !Scalar::Util::blessed($ua) || !$ua->can('head');
213 37         106 $self->{_ua} = $ua;
214             }
215              
216 50         111 return $self->{_ua};
217             }
218              
219             1;
220              
221             __END__