File Coverage

blib/lib/App/newver.pm
Criterion Covered Total %
statement 29 139 20.8
branch 0 48 0.0
condition 0 15 0.0
subroutine 10 23 43.4
pod 2 5 40.0
total 41 230 17.8


line stmt bran cond sub pod time code
1             package App::newver;
2 1     1   382115 use 5.016;
  1         4  
3 1     1   7 use strict;
  1         3  
  1         39  
4 1     1   6 use warnings;
  1         2  
  1         89  
5             our $VERSION = '0.02';
6              
7 1     1   40 use File::Basename;
  1         4  
  1         139  
8 1     1   8 use File::Spec;
  1         2  
  1         62  
9 1     1   920 use Getopt::Long qw(GetOptionsFromArray);
  1         16970  
  1         6  
10 1     1   239 use JSON::PP;
  1         3  
  1         96  
11              
12 1     1   870 use Parallel::ForkManager;
  1         81038  
  1         58  
13              
14 1     1   681 use App::newver::INI qw(read_ini);
  1         4  
  1         91  
15 1     1   502 use App::newver::Scanner qw(scan_version);
  1         3  
  1         1428  
16              
17             my $PRGNAM = 'newver';
18             my $PRGVER = $VERSION;
19              
20             my $USAGE = <<"HERE";
21             Usage:
22             newver [options] file [program] ...
23              
24             Options:
25             -j|--json Output report in JSON
26             -s|--serial Scan pages serially instead of in parallel
27             -V|--verbose Enable verbose output
28             -h|--help Print usage and exit
29             -v|--version Print version and exit
30             HERE
31              
32             sub _scan_for_version {
33              
34 0     0     my ($file, $match) = @_;
35              
36 0 0         $match =~ s/\@VERSION\@/$App::newver::Scanner::MAYBE_VERSION_RX/g
37             or die "'Match' missing '\@VERSION\@'\n";
38 0           $match = qr/$match/;
39              
40 0 0         open my $fh, '<', $file or die "Failed to open $file for reading: $!\n";
41 0           while (my $l = readline $fh) {
42 0           chomp $l;
43 0 0         $l =~ $match or next;
44 0           return $+{ Version };
45             }
46              
47 0           return undef;
48              
49             }
50              
51             sub _read_config {
52              
53 0     0     my ($self) = @_;
54              
55 0           my $ini = read_ini($self->{ ScanFile });
56 0           for my $program (keys %$ini) {
57 0           my $version;
58 0 0         if (defined $ini->{ $program }{ Version }) {
    0          
59 0           $version = $ini->{ $program }{ Version };
60             } elsif (defined $ini->{ $program }{ VersionScan }) {
61 0           my ($f, $m) = $ini->{ $program }{ VersionScan } =~ /^\s*(.+?)\s*--\s*(.+?)\s*$/;
62 0           my $inidir = (fileparse($self->{ ScanFile }))[1];
63 0 0         if (!File::Spec->file_name_is_absolute($f)) {
64 0           $f = File::Spec->catfile($inidir, $f);
65             }
66 0   0       $version = _scan_for_version($f, $m)
67             // die "Found no version matching /$m/ in $f\n";
68             } else {
69 0           die "[$program] missing required field 'Version' or 'VersionScan'\n";
70             }
71             my $page = $ini->{ $program }{ Page }
72 0   0       // die "[$program] missing required field 'Page'\n";
73             my $match = $ini->{ $program }{ Match }
74 0   0       // die "[$program] missing required field 'Match'\n";
75 0           my $return = $ini->{ $program }{ ReturnURL };
76 0           push @{ $self->{ Programs } }, {
  0            
77             Program => $program,
78             Version => $version,
79             Page => $page,
80             Match => $match,
81             ReturnURL => $return,
82             };
83             }
84              
85             }
86              
87             sub init {
88              
89 0     0 1   my ($class, @argv) = @_;
90              
91 0           my $self = {
92             ScanFile => undef,
93             ToDos => undef,
94             Programs => [],
95             JSON => 0,
96             Parallel => 1,
97             Verbose => 0,
98             };
99              
100 0           Getopt::Long::config("no_ignore_case");
101             GetOptionsFromArray(\@argv,
102             'j|json' => \$self->{ JSON },
103 0     0     's|serial' => sub { $self->{ Parallel } = 0 },
104             'V|verbose' => \$self->{ Verbose },
105 0     0     'h|help' => sub { print $USAGE; exit 0 },
  0            
106 0     0     'v|version' => sub { say $PRGVER; exit 0 },
  0            
107 0 0         ) or die $USAGE;
108              
109 0           $self->{ ScanFile } = shift @argv;
110 0 0         if (not defined $self->{ ScanFile }) {
111 0           die $USAGE;
112             }
113              
114 0 0         if (@argv) {
115 0           %{ $self->{ ToDos } } = map { $_ => 1 } @argv;
  0            
  0            
116             }
117              
118 0           bless $self, $class;
119              
120 0           $self->_read_config;
121 0 0         if (!@{ $self->{ Programs } }) {
  0            
122 0           die "$self->{ ScanFile } does not contain any programs\n";
123             }
124              
125 0           return $self;
126              
127             }
128              
129             sub log_msg {
130              
131 0     0 0   my ($self, @msg) = @_;
132              
133 0 0         if ($self->{ Verbose }) {
134 0           say STDERR @msg;
135             }
136              
137             }
138              
139             sub _print_scans_text {
140              
141 0     0     my (@scans) = @_;
142              
143 0           @scans = sort { $a->{ program } cmp $b->{ program } } @scans;
  0            
144              
145 0           for my $s (@scans) {
146 0           print <<"HERE";
147             $s->{ program }
148             Current: $s->{ current }
149             New: $s->{ version }
150             URL: $s->{ url }
151             HERE
152             }
153              
154             }
155              
156             sub _print_scans_json {
157              
158 0     0     my (@scans) = @_;
159              
160 0           my $marshaler = {};
161 0           for my $s (@scans) {
162             # Force all values into strings
163 0           for my $k (keys %$s) {
164 0           $marshaler->{ $s->{ program } }{ $k } = "$s->{ $k }";
165             }
166             }
167              
168 0           my $json = JSON::PP->new->canonical->pretty;
169              
170 0           print $json->encode($marshaler);
171              
172             }
173              
174             sub run_parallel {
175              
176 0     0 0   my ($self) = @_;
177              
178 0           my @scanned;
179              
180 0           my $pm = Parallel::ForkManager->new(10);
181             $pm->run_on_finish(sub {
182 0     0     my ($code, $job) = @_[1, 5];
183 0 0 0       if ($code == 0 and defined $job) {
184 0           push @scanned, $job;
185             }
186 0           });
187              
188             SCAN:
189 0           for my $j (@{ $self->{ Programs } }) {
  0            
190 0 0         my $pid = $pm->start and next SCAN;
191 0 0 0       if (defined $self->{ ToDos } and !$self->{ ToDos }{ $j->{ Program } }) {
192 0           $pm->finish(0, undef);
193             }
194             my $scan = scan_version(
195             program => $j->{ Program },
196             version => $j->{ Version },
197             match => $j->{ Match },
198             page => $j->{ Page },
199 0           );
200 0           $self->log_msg("Scanned $j->{ Page }");
201 0 0         if (defined $scan) {
202 0           $scan->{ current } = $j->{ Version };
203 0 0         if (defined $j->{ ReturnURL }) {
204 0           $scan->{ url } = $j->{ ReturnURL } =~ s/\@VERSION\@/$scan->{ version }/gr;
205             }
206             }
207 0           $pm->finish(0, $scan);
208             }
209              
210 0           $pm->wait_all_children;
211              
212 0 0         if (!@scanned) {
213 0           $self->log_msg("No new versions found");
214             }
215              
216 0 0         if ($self->{ JSON }) {
217 0           _print_scans_json(@scanned);
218             } else {
219 0           _print_scans_text(@scanned);
220             }
221              
222             }
223              
224             sub run_serial {
225              
226 0     0 0   my ($self) = @_;
227              
228 0           my @scanned;
229 0           for my $j (@{ $self->{ Programs } }) {
  0            
230 0 0 0       if (defined $self->{ ToDos } and !$self->{ ToDos }{ $j->{ Program } }) {
231 0           continue;
232             }
233             my $scan = scan_version(
234             program => $j->{ Program },
235             version => $j->{ Version },
236             match => $j->{ Match },
237             page => $j->{ Page },
238 0           );
239 0           $self->log_msg("Scanned $j->{ Page }");
240 0 0         if (defined $scan) {
241 0           $scan->{ current } = $j->{ Version };
242 0 0         if (defined $j->{ ReturnURL }) {
243 0           $scan->{ url } = $j->{ ReturnURL } =~ s/\@VERSION\@/$scan->{ version }/gr;
244             }
245 0           push @scanned, $scan;
246             }
247             }
248              
249 0 0         if (!@scanned) {
250 0           $self->log_msg("No new versions found");
251             }
252              
253 0 0         if ($self->{ JSON }) {
254 0           _print_scans_json(@scanned);
255             } else {
256 0           _print_scans_text(@scanned);
257             }
258              
259             }
260              
261              
262             sub run {
263              
264 0     0 1   my ($self) = @_;
265              
266 0 0         if ($self->{ Parallel }) {
267 0           $self->run_parallel;
268             } else {
269 0           $self->run_serial;
270             }
271              
272             }
273              
274             1;
275              
276             =head1 NAME
277              
278             App::newver - Scan upstream for new software versions
279              
280             =head1 SYNOPSIS
281              
282             use App::newver;
283              
284             my $newver = App::newver->init(@ARGV);
285             $newver->run;
286              
287             =head1 DESCRIPTION
288              
289             B is the main backend module for L. This is a private
290             module, please consult the L manual for user documentation.
291              
292             =head1 METHODS
293              
294             =head2 $newver = App::newver->init(@argv)
295              
296             Initializes B object, reading command-line arguments from
297             C<@argv>.
298              
299             =head2 $new->run
300              
301             Runs L based on the arguments processed during C.
302              
303             =head1 AUTHOR
304              
305             Written by L
306              
307             This project's source can be found on its
308             L. Comments and pull
309             requests are welcome.
310              
311             =head1 COPYRIGHT
312              
313             Copyright (C) 2025 Samuel Young.
314              
315             This program is free software; you can redistribute it and/or modify it under
316             the terms of the Artistic License 2.0.
317              
318             =head1 SEE ALSO
319              
320             L
321              
322             =cut