File Coverage

lib/Test/Pod/Links.pm
Criterion Covered Total %
statement 171 171 100.0
branch 77 78 98.7
condition 14 15 93.3
subroutine 19 19 100.0
pod 3 3 100.0
total 284 286 99.3


line stmt bran cond sub pod time code
1             package Test::Pod::Links;
2              
3 8     8   524898 use 5.006;
  8         76  
4 8     8   32 use strict;
  8         12  
  8         162  
5 8     8   34 use warnings;
  8         9  
  8         303  
6              
7             our $VERSION = '0.002';
8              
9 8     8   37 use Carp ();
  8         13  
  8         122  
10 8     8   44 use File::Find ();
  8         11  
  8         160  
11 8     8   4339 use HTTP::Tiny 0.014 ();
  8         283625  
  8         219  
12 8     8   4109 use Pod::Simple::Search ();
  8         38201  
  8         155  
13 8     8   2422 use Pod::Simple::SimpleTree ();
  8         172520  
  8         175  
14 8     8   48 use Scalar::Util ();
  8         14  
  8         93  
15 8     8   32 use Test::Builder ();
  8         16  
  8         9831  
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 16     16 1 82 my $self = shift;
28              
29 16 100       54 my @args = scalar @_ ? @_ : $self->_default_dirs();
30 16 100       33 if ( !@args ) {
31 1         4 $TEST->skip_all("No files found\n");
32 1         23 return 1;
33             }
34              
35 15         29 my @files;
36             ARG:
37 15         42 for my $arg (@args) {
38 18 100       252 if ( !-e $arg ) {
39 1         9 $TEST->carp("File '$arg' does not exist");
40 1         7 next ARG;
41             }
42              
43 17 100       167 if ( -l $arg ) {
44 1         9 $TEST->carp("Ignoring symlink '$arg'");
45 1         7 next ARG;
46             }
47              
48 16 100       154 if ( -f $arg ) {
49 3         9 push @files, $arg;
50 3         7 next ARG;
51             }
52              
53 13 100       118 if ( !-d $arg ) {
54 1         8 $TEST->carp("File '$arg' is not a file nor a directory. Ignoring it.");
55 1         6 next ARG;
56             }
57              
58             File::Find::find(
59             {
60             no_chdir => 1,
61             preprocess => sub {
62 19     19   55 my @sorted = sort grep { !-l "$File::Find::dir/$_" } @_;
  57         688  
63 19         579 return @sorted;
64             },
65             wanted => sub {
66 29 100   29   1168 return if !-f $File::Find::name;
67 10         122 push @files, $File::Find::name;
68             },
69             },
70 12         1315 $arg,
71             );
72             }
73              
74 15 100       32 if ( !@files ) {
75 7         37 $TEST->skip_all("No files found in (@args)\n");
76 7         87 return 1;
77             }
78              
79 8         14 my @pod_files = grep { Pod::Simple::Search->new->contains_pod($_) } @files;
  13         660  
80 8 100       819 if ( !@pod_files ) {
81 1         18 $TEST->skip_all("No files with Pod found in (@args)\n");
82 1         9 return 1;
83             }
84              
85 7         17 my $rc = 1;
86 7         29 for my $file (@pod_files) {
87 12 100       67 if ( !$self->pod_file_ok($file) ) {
88 1         18 $rc = 0;
89             }
90             }
91              
92 7         52 $TEST->done_testing;
93              
94 7 100       107 return 1 if $rc;
95 1         18 return;
96             }
97              
98             sub new {
99 53     53 1 111778 my $class = shift;
100              
101 53 100       268 Carp::croak 'Odd number of arguments' if @_ % 2;
102 52         123 my %args = @_;
103              
104 52         126 my $self = bless {}, $class;
105              
106             #
107 52         329 $self->{_cache} = {};
108              
109             #
110 52   66     456 $self->_ua( $args{ua} || HTTP::Tiny->new );
111              
112             #
113 51         66 my @ignores;
114 51 100       96 if ( exists $args{ignore} ) {
115 7         12 my $ignore = $args{ignore};
116 7 100       19 if ( ref $ignore eq ref [] ) {
117 2         3 @ignores = @{$ignore};
  2         4  
118             }
119             else {
120 5         10 @ignores = $ignore;
121             }
122             }
123              
124             #
125 51         65 my @ignores_match;
126 51 100       108 if ( exists $args{ignore_match} ) {
127 8         12 my $ignore_match = $args{ignore_match};
128 8 100       21 if ( ref $ignore_match eq ref [] ) {
129 4         5 @ignores_match = @{$ignore_match};
  4         7  
130             }
131             else {
132 4         7 @ignores_match = $ignore_match;
133             }
134             }
135              
136             ## no critic (RegularExpressions::RequireDotMatchAnything)
137             ## no critic (RegularExpressions::RequireExtendedFormatting)
138             ## no critic (RegularExpressions::RequireLineBoundaryMatching)
139 51         130 my $ignore_regex = join q{|}, @ignores_match, map { qr{^\Q$_\E$} } @ignores;
  8         75  
140 51 100       355 $self->_ignore_regex( $ignore_regex ne q{} ? qr{$ignore_regex} : undef );
141             ## use critic
142              
143             KEY:
144 51         152 for my $key ( keys %args ) {
145 29 100       48 next KEY if $key eq 'ignore';
146 22 100       54 next KEY if $key eq 'ignore_match';
147 14 100       28 next KEY if $key eq 'ua';
148              
149 1         90 Carp::croak "new() knows nothing about argument '$key'";
150             }
151              
152 50         164 return $self;
153             }
154              
155             sub pod_file_ok {
156 16     16 1 18835 my ( $self, $file ) = @_;
157              
158 16 100 100     328 Carp::croak 'usage: pod_file_ok(FILE)' if @_ != 2 || !defined $file;
159              
160 13         23 my $parse_msg = "Parse Pod ($file)";
161              
162 13 100       207 if ( !-f $file ) {
163 1         5 $TEST->ok( 0, $parse_msg );
164 1         829 $TEST->diag("\n");
165 1         188 $TEST->diag("File $file does not exist or is not a file");
166 1         183 return;
167             }
168              
169 12         93 my $pod = Pod::Simple::SimpleTree->new->parse_file($file);
170              
171 12 100       20700 if ( $pod->any_errata_seen ) {
172              
173             # Pod contains errors
174 1         9 $TEST->ok( 0, $parse_msg );
175 1         902 return;
176             }
177              
178 11         73 $TEST->ok( 1, $parse_msg );
179              
180             my @links =
181 32 50       140 grep { defined && m{ ^ http(?:s)? :// }xsmi }
182 32         37 map { ${ $_->{to} }[2] }
  32         58  
183 11         2315 grep { $_->{type} eq 'url' } $self->_extract_links_from_pod( $pod->root );
  39         82  
184              
185 11         26 my $ignore_regex = $self->_ignore_regex;
186 11 100       21 if ( defined $ignore_regex ) {
187 3         4 @links = grep { $_ !~ $ignore_regex } @links;
  15         48  
188             }
189              
190 11         14 my $rc = 1;
191 11         19 my $ua = $self->_ua;
192 11         15 my %url_checked_in_this_file;
193              
194             LINK:
195 11         17 for my $link (@links) {
196 26 100       425 next LINK if exists $url_checked_in_this_file{$link};
197 24         45 $url_checked_in_this_file{$link} = 1;
198              
199 24 100       38 if ( !exists $self->{_cache}->{$link} ) {
200 21         53 $self->{_cache}->{$link} = $ua->head($link);
201             }
202 24         192 my $res = $self->{_cache}->{$link};
203              
204 24         82 $TEST->ok( $res->{success}, "$link ($file)" );
205              
206 24 100       5845 if ( !$res->{success} ) {
207 2         3 $rc = 0;
208 2         8 $TEST->diag("\n");
209 2         377 $TEST->diag( $res->{reason} );
210 2         370 $TEST->diag("\n");
211             }
212             }
213              
214 11 100       110 return 1 if $rc;
215 2         41 return;
216             }
217              
218             sub _default_dirs {
219 14     14   3989 my ($self) = @_;
220              
221 14         19 my @dirs;
222 14 100       277 if ( -d 'blib' ) {
    100          
223 3         13 push @dirs, 'blib';
224             }
225             elsif ( -d 'lib' ) {
226 6         46 push @dirs, 'lib';
227             }
228              
229 14 100       150 if ( -d 'bin' ) {
230 4         12 push @dirs, 'bin';
231             }
232              
233 14 100       120 if ( -d 'script' ) {
234 2         18 push @dirs, 'script';
235             }
236              
237 14         54 my @sorted = sort @dirs;
238 14         46 return @sorted;
239             }
240              
241             sub _extract_links_from_pod {
242 142     142   7221 my ( $self, $node_ref ) = @_;
243              
244 142 100 100     651 Carp::croak 'usage: _extract_links_from_pod([ elementname, \%attributes, ...subnodes... ])' if @_ != 2 || ref $node_ref ne ref [] || scalar @{$node_ref} < 2;
  139   100     330  
245              
246 138         151 my @links;
247 138         130 my ( $elem_name, $attr_ref, @subnodes ) = @{$node_ref};
  138         246  
248              
249 138 100       186 if ( $elem_name eq 'L' ) {
250 48         52 push @links, $attr_ref;
251             }
252              
253             SUBNODE:
254 138         170 for my $subnode (@subnodes) {
255 285 100       495 next SUBNODE if ref $subnode ne ref [];
256              
257 124         190 push @links, $self->_extract_links_from_pod($subnode);
258             }
259              
260 138         223 return @links;
261             }
262              
263             sub _ignore_regex {
264 104     104   3011 my $self = shift;
265              
266 104 100       181 if (@_) {
267 53         73 my $ignore_regex = shift;
268 53         115 $self->{_ignore_regex} = $ignore_regex;
269             }
270              
271 104         395 return $self->{_ignore_regex};
272             }
273              
274             sub _ua {
275 69     69   4760 my $self = shift;
276              
277 69 100       139 if (@_) {
278 56         71 my $ua = shift;
279 56 100 100     815 Carp::croak q{ua must have method 'head'} if !Scalar::Util::blessed($ua) || !$ua->can('head');
280 53         160 $self->{_ua} = $ua;
281             }
282              
283 66         117 return $self->{_ua};
284             }
285              
286             1;
287              
288             __END__