File Coverage

blib/lib/Test/Copyright.pm
Criterion Covered Total %
statement 210 235 89.3
branch 45 72 62.5
condition 7 15 46.6
subroutine 32 32 100.0
pod 1 1 100.0
total 295 355 83.1


line stmt bran cond sub pod time code
1             package Test::Copyright;
2              
3 2     2   50446 use warnings;
  2         5  
  2         64  
4 2     2   10 use strict;
  2         4  
  2         63  
5 2     2   10 use Carp;
  2         9  
  2         196  
6 2     2   53 use 5.008;
  2         6  
  2         181  
7              
8 2     2   11 use Test::Builder;
  2         2  
  2         49  
9 2     2   72 use Test::More;
  2         3  
  2         11  
10 2     2   8578 use CPAN::Meta;
  2         1332671  
  2         67  
11 2     2   1959 use Software::LicenseUtils;
  2         1467077  
  2         217  
12 2     2   3290 use Readonly;
  2         11493  
  2         326  
13 2     2   3948 use Perl6::Slurp;
  2         6442  
  2         18  
14 2     2   3191 use UNIVERSAL::require;
  2         6313  
  2         30  
15 2     2   3769 use Lingua::EN::NameParse;
  2         298095  
  2         127  
16 2     2   2026 use Email::Address;
  2         72996  
  2         34  
17 2     2   202 use File::Spec;
  2         4  
  2         23  
18              
19             our $VERSION = '0.0_1';
20              
21             # Module implementation here
22              
23             my $nameparse = Lingua::EN::NameParse->new;
24              
25             Readonly my $DEFAULT => '';
26             Readonly my @META_FILES => ('META.yml','META.json');
27             Readonly my @LICENSE_FILES => ('LICENSE','COPYING','README');
28             Readonly my $DUMMY_COPYRIGHT => 'XYZ';
29             Readonly my %LICENSE_SPECIALS => (
30             perl => [
31             # This string is generated by Module::Starter::PBP by default.
32             'This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See perlartistic.',
33             ],
34             );
35              
36             # This line draws inspiration from licensecheck.
37             # (C) 2007-2008, Adam D. Barratt
38             Readonly my $COPYRIGHT_REGEX =>
39             qr{
40             \A # Beginning of line
41             \#? # Can be commented out
42             \s* # Arbitrary amount of space
43             (?:
44             [Cc]opyright # The full word
45             |[Cc]opr\. # Legally-valid abbreviation
46             |\x{00a9} # Unicode character COPYRIGHT SIGN
47             |\xc2\xa9 # Unicode copyright sign encoded in iso8859
48             |\([Cc]\) # Legally-null representation of sign
49             |Copyright\s+\([Cc]\) # Generated by Module::Starter::PBP
50             )
51             \:? # Optional colon
52             \s+ # Space
53             (?:(\d{4})-)? # Optional initial year
54             (\d{4}) # Actual year
55             \,?\s+ # Comma and space
56             ([^\n\r]+) # Copyright holder
57             \z
58             }xms;
59              
60             # This list was copied from Test::Pod.
61             # Copyright 2006-2010, Andy Lester. Some Rights Reserved.
62             Readonly my %IGNORE_DIRS => (
63             '.bzr' => 'Bazaar',
64             '.git' => 'Git',
65             '.hg' => 'Mercurial',
66             '.pc' => 'quilt',
67             '.svn' => 'Subversion',
68             CVS => 'CVS',
69             RCS => 'RCS',
70             SCCS => 'SCCS',
71             _darcs => 'darcs',
72             _sgbak => 'Vault/Fortress',
73             );
74              
75             my $Test = Test::Builder->new;
76             my %copyright_data = ();
77              
78             sub import {
79 2     2   25 my $self = shift;
80 2         5 my $caller = caller;
81              
82 2         6 for my $func ( qw( copyright_ok) ) {
83 2     2   724 no strict 'refs'; ## no critic
  2         3  
  2         5904  
84 2         9 *{$caller."::".$func} = \&$func;
  2         19  
85             }
86              
87 2         16 $Test->exported_to($caller);
88 2         25 $Test->plan(@_);
89             }
90              
91             sub copyright_ok {
92 1     1 1 15 my $meta = _cpan_meta_ok();
93 1 50       379 if ($meta) {
94 1         12 my @classes = Software::LicenseUtils->guess_license_from_meta($meta);
95 1         39 $Test->ok(length @classes > 0, "more than zero licenses");
96 1         460 my @licenses = _software_licenses_ok(@classes);
97 1         6 $Test->ok(length @licenses > 0, "more than zero recognised licenses");
98 1         457 my $license_file_contents = _license_file_ok(@licenses);
99 1         3 my $copyright_details = undef;
100 1 50       4 if ($license_file_contents) {
101 1         5 $copyright_details = _parse_copyright($license_file_contents);
102 1         6 foreach my $file (_find_files_to_check()) {
103 1         6 _check_file_for_copyright($file, $copyright_details);
104             }
105             }
106             else {
107 0         0 fail('Parse copyright details');
108             }
109             }
110             else {
111 0         0 $Test->skip('No CPAN::Meta object', 3);
112             }
113              
114 1         139 return;
115             }
116              
117             sub _software_licenses_ok {
118 1     1   4 my @classes = @_;
119 1         2 my $all_valid = 1;
120 1         3 my @licenses;
121 1         1 foreach my $class (@classes) {
122 1 50       4 if (defined $class) {
123 1 50       17 if ($class->require) {
124 1         71 my $license = $class->new({holder=>$DUMMY_COPYRIGHT});
125 1 50 33     51 if ($license and $license->isa($class)) {
126 1         4 push @licenses, $license;
127             }
128             else {
129 0         0 $all_valid = 0;
130             }
131             }
132             else {
133 0         0 $all_valid = 0;
134             }
135             }
136             else {
137 0         0 $all_valid = 0;
138             }
139             }
140 1         160 $Test->ok($all_valid, 'Found a good license object');
141 1         819 return @licenses;
142             }
143              
144             sub _cpan_meta_ok {
145 1     1   12 foreach my $file (@META_FILES) {
146 1 50       21 if (-r $file) {
147 1         230 my $meta = CPAN::Meta->load_file($file);
148 1 50       26851 return if not isa_ok($meta, 'CPAN::Meta', 'found CPAN::Meta file');
149 1         978 return slurp $file;
150             }
151             }
152 0         0 $Test->ok(0, 'found CPAN::Meta file');
153 0         0 return;
154             }
155              
156             sub _license_file_ok {
157 1     1   4 my @licenses = @_;
158 1         2 my $found_file = undef;
159 1         2 my $file_name = undef;
160 1         8 foreach my $file (@LICENSE_FILES) {
161 3 100       61 if (-r $file) {
162 1         33 $found_file = slurp $file;
163 1         177 $file_name = $file;
164 1         3 last;
165             }
166             }
167 1         7 $Test->ok($found_file, "found license file: $file_name");
168 1 50       399 if ($found_file) {
169 1         4 foreach my $license (@licenses) {
170 1         5 $found_file = _verify_license($found_file, $license, $file_name);
171             }
172             }
173 1         5 return $found_file;
174             }
175              
176             sub _verify_license {
177 1     1   1 my $file_contents = shift;
178 1         2 my $license = shift;
179 1         2 my $file_name = shift;
180 1         14 my $holder = $license->holder;
181 1         12 my $year = $license->year;
182 1         287 my $meta = $license->meta_name;
183 1         9 my $test_name = "Found license $meta in file $file_name";
184 1         4 my $dummy_copyright = "This software is copyright (c) $year by $holder.\n";
185 1         12 my $full_text = _purge_dummy($license->fulltext, $dummy_copyright);
186 1         8 my $notice = _purge_dummy($license->notice, $dummy_copyright);
187 1         7 my $remainder = _remove_license($file_contents, $full_text);
188 1         4 my @specials = @{$LICENSE_SPECIALS{$meta}};
  1         28  
189 1 50       48 if ($remainder) {
    50          
    50          
190 0         0 $file_contents = $remainder;
191 0         0 pass($test_name);
192             }
193 1         3 elsif ($remainder = _remove_license($file_contents, $notice)) {
194 0         0 $file_contents = $remainder;
195 0         0 pass($test_name);
196             }
197             elsif (grep {$remainder = _remove_license($file_contents, $_)} @specials) {
198 1         2 $file_contents = $remainder;
199 1         8 pass($test_name);
200             }
201             else {
202 0         0 fail($test_name);
203             }
204 1         634 return $file_contents;
205             }
206              
207             sub _purge_dummy {
208 2     2   37678 my $text = shift;
209 2         3 my $dummy_copyright = shift;
210 2 50       92 croak "Cannot find dummy copyright: ".substr($text, 0, 100)
211             if $dummy_copyright ne substr($text, 0, length $dummy_copyright);
212 2         40 return substr($text, 1+length $dummy_copyright);
213             }
214              
215             sub _remove_license {
216 3     3   7 my $file_contents = shift;
217 3         6 my $license_text = shift;
218 3         485 $license_text
219             =~ s{
220             ([\\\!\"\$\%\^\&\*\(\)\-\_\=\+\{\[\]\}\#\~\;\-\'\@\,\<\.\>\/\?])
221             }{\\$1}xmsg;
222 3         1471 $license_text
223             =~ s{
224             (\s+)
225             }{\\s+}xmsg;
226 3         9 my $remainder = undef;
227 3 100       2390 if ($file_contents =~ m{\A(.*)$license_text(.*)\z}xms) {
228 1         10 $remainder = "$1$2";
229             }
230 3         23 return $remainder;
231             }
232              
233             sub _parse_copyright {
234 1     1   2 my $license_file_contents = shift;
235 1         50 my @lines = split /\n/, $license_file_contents;
236 1         6 my $copyright = undef;
237 1         2 foreach my $line (@lines) {
238 128 100       193 if (my $detail = _parse_copyright_line($line)) {
239             # diag "(C) $detail->{initial_year}-$detail->{final_year}, $detail->{holder}";
240 3         11 $copyright = _push_copyright($copyright, $DEFAULT, $detail)
241             # TODO pick details for individual files
242             }
243             }
244 1         4 ok(exists $copyright->{$DEFAULT}, "Found default copyright details");
245 1         748 _verify_copyright_final_year($copyright);
246 1         1382 return $copyright;
247             }
248              
249             sub _push_copyright {
250 3     3   8 my $copyright = shift;
251 3         26 my $file = shift;
252 3         17 my $detail = shift;
253 3         11 my $holder = delete $detail->{holder};
254 3 100       10 if (not defined $copyright) {
255 1         3 $copyright = {};
256             }
257 3 100       11 if (exists $copyright->{$file}) {
258 2         6 $copyright->{$file}->{$holder} = $detail;
259             }
260             else {
261 1         5 $copyright->{$file} = {$holder=>$detail};
262             }
263 3         9 return $copyright;
264             }
265              
266             sub _parse_copyright_line {
267 678     678   852 my $line = shift;
268 678         661 my $details = undef;
269 678 100       1633 if ($line =~ $COPYRIGHT_REGEX) {
270 8         117 $details = {};
271 8         42 $details->{final_year} = $2;
272 8   66     50 $details->{initial_year} = $1 || $details->{final_year};
273 8         47 $nameparse->parse($3);
274 8         183914 my %properties = $nameparse->properties;
275 8         115 $details->{holder} = $nameparse->case_all;
276 8 100       2188 if ($properties{non_matching}
277             =~ m{\<($Email::Address::addr_spec)\>}xms) {
278 2         15 $details->{holder} .= " <$1>";
279             }
280             }
281 678         5690 return $details;
282             }
283              
284             sub _check_file_for_copyright {
285 1     1   2 my $file = shift;
286 1         2 my $copyright = shift;
287 1         7 my $file_contents = slurp $file;
288 1         409 my @lines = split /\n/, $file_contents;
289 1         18 my $file_has_copyright = 0;
290 1         2 my $all_copyright_known = 1;
291 1         3 foreach my $line (@lines) {
292 550 100       887 if (my $detail = _parse_copyright_line($line)) {
293 5   33     76 $all_copyright_known
294             &&= _check_copyright_details($file, $detail, $copyright);
295 5         23 $file_has_copyright = 1;
296             }
297             }
298 1         11 ok($file_has_copyright, "File $file has copyright statement");
299 1         808 ok($all_copyright_known, "Copyright for $file is described centrally");
300 1         860 return;
301             }
302              
303             sub _check_copyright_details {
304 5     5   10 my $file = shift;
305 5         9 my $detail = shift;
306 5         8 my $copyright = shift;
307 5         13 my $holder = $detail->{holder};
308 5 50       48 if (not exists $copyright->{$DEFAULT}->{$holder}) {
309 0         0 diag "Unlisted copyright holder: $holder [$file]";
310 0         0 return 0;
311             }
312 5         50 my $years = $copyright->{$DEFAULT}->{$holder};
313 5 50       48 if ($detail->{initial_year} < $years->{initial_year}) {
314 0         0 diag "Year mismatch: ($detail->{initial_year}, $holder) [$file]";
315 0         0 return 0;
316             }
317 5 50       24 if ($detail->{final_year} > $years->{final_year}) {
318 0         0 diag "Year mismatch: ($detail->{final_year}, $holder) [$file]";
319 0         0 return 0;
320             }
321 5         23 return 1;
322             }
323              
324             sub _verify_copyright_final_year {
325 1     1   3 my $copyright = shift;
326 1         2 my $year = undef;
327 1         3 foreach my $author (keys %{$copyright->{$DEFAULT}}) {
  1         4  
328 3         17 my $test = $copyright->{$DEFAULT}->{$author}->{final_year};
329 3 100 100     35 if (not defined $year or $test > $year) {
330 2         5 $year = $test;
331             }
332             }
333 1         72 my @localtime = localtime();
334 1         8 is($year, 1900+$localtime[5], 'final copyright year is uptodate');
335             }
336              
337             # This function is copied from Test::Pod.
338             sub _find_files_to_check {
339 1 50   1   7 my @queue = @_ ? @_ : _starting_points();
340 1         3 my @pod = ();
341              
342 1         4 while ( @queue ) {
343 5         12 my $file = shift @queue;
344 5 100       75 if ( -d $file ) {
345 4         10 local *DH;
346 4 50       107 opendir DH, $file or next;
347 4         116 my @newfiles = readdir DH;
348 4         60 closedir DH;
349              
350 4         67 @newfiles = File::Spec->no_upwards( @newfiles );
351 4         50 @newfiles = grep { not exists $IGNORE_DIRS{ $_ } } @newfiles;
  4         44  
352              
353 4         34 foreach my $newfile (@newfiles) {
354 4         59 my $filename = File::Spec->catfile( $file, $newfile );
355 4 100       82 if ( -f $filename ) {
356 1         6 push @queue, $filename;
357             }
358             else {
359 3         37 push @queue, File::Spec->catdir( $file, $newfile );
360             }
361             }
362             }
363 5 100       85 if ( -f $file ) {
364 1 50       5 push @pod, $file if _is_perl( $file );
365             }
366             } # while
367 1         4 return @pod;
368             }
369              
370             sub _starting_points {
371 1 50   1   34 return 'blib' if -e 'blib';
372 0         0 return 'lib';
373             }
374              
375             sub _is_perl {
376 1     1   2 my $file = shift;
377              
378 1 50       10 return 1 if $file =~ /\.PL$/;
379 1 50       14 return 1 if $file =~ /\.p(?:l|m|od)$/;
380 0 0         return 1 if $file =~ /\.t$/;
381              
382 0 0         open my $fh, '<', $file or return;
383 0           my $first = <$fh>;
384 0           close $fh;
385              
386 0 0 0       return 1 if defined $first && ($first =~ /(?:^#!.*perl)|--\*-Perl-\*--/);
387              
388 0           return;
389             }
390              
391              
392             1; # Magic true value required at end of module
393             __END__