| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Test::Which; |
|
2
|
|
|
|
|
|
|
|
|
3
|
6
|
|
|
6
|
|
1587421
|
use strict; |
|
|
6
|
|
|
|
|
17
|
|
|
|
6
|
|
|
|
|
265
|
|
|
4
|
6
|
|
|
6
|
|
51
|
use warnings; |
|
|
6
|
|
|
|
|
21
|
|
|
|
6
|
|
|
|
|
435
|
|
|
5
|
|
|
|
|
|
|
|
|
6
|
6
|
|
|
6
|
|
2250
|
use parent 'Exporter'; |
|
|
6
|
|
|
|
|
1487
|
|
|
|
6
|
|
|
|
|
72
|
|
|
7
|
|
|
|
|
|
|
our @ISA = qw(Exporter); |
|
8
|
|
|
|
|
|
|
|
|
9
|
6
|
|
|
6
|
|
3297
|
use File::Which qw(which); |
|
|
6
|
|
|
|
|
10162
|
|
|
|
6
|
|
|
|
|
562
|
|
|
10
|
6
|
|
|
6
|
|
2376
|
use version (); # provide version->parse |
|
|
6
|
|
|
|
|
10822
|
|
|
|
6
|
|
|
|
|
248
|
|
|
11
|
6
|
|
|
6
|
|
38
|
use Test::Builder; |
|
|
6
|
|
|
|
|
13
|
|
|
|
6
|
|
|
|
|
31332
|
|
|
12
|
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
our @EXPORT_OK = qw(which_ok); |
|
14
|
|
|
|
|
|
|
our %EXPORT_TAGS = ( all => \@EXPORT_OK ); |
|
15
|
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
my %VERSION_CACHE; |
|
17
|
|
|
|
|
|
|
my $TEST = Test::Builder->new(); |
|
18
|
|
|
|
|
|
|
our $TIMEOUT = 5; # Seconds |
|
19
|
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
=head1 NAME |
|
21
|
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
Test::Which - Skip tests if external programs are missing from PATH (with version checks) |
|
23
|
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
=head1 VERSION |
|
25
|
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
Version 0.06 |
|
27
|
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
=cut |
|
29
|
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
our $VERSION = '0.06'; |
|
31
|
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
=head1 SYNOPSIS |
|
33
|
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
use Test::Which 'ffmpeg' => '>=6.0', 'convert' => '>=7.1'; |
|
35
|
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
# At runtime in a subtest or test body |
|
37
|
|
|
|
|
|
|
use Test::Which qw(which_ok); |
|
38
|
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
subtest 'needs ffmpeg' => sub { |
|
40
|
|
|
|
|
|
|
which_ok 'ffmpeg' => '>=6.0' or return; |
|
41
|
|
|
|
|
|
|
... # tests that use ffmpeg |
|
42
|
|
|
|
|
|
|
}; |
|
43
|
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
=head1 DESCRIPTION |
|
45
|
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
C mirrors L but checks for executables in PATH. |
|
47
|
|
|
|
|
|
|
It can also check version constraints using a built-in heuristic that tries |
|
48
|
|
|
|
|
|
|
common version flags (--version, -version, -v, -V) and extracts version numbers |
|
49
|
|
|
|
|
|
|
from the output. |
|
50
|
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
If a version is requested but cannot be determined, the requirement fails. |
|
52
|
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
Key features: |
|
54
|
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
=over 4 |
|
56
|
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
=item * Compile-time and runtime checking of program availability |
|
58
|
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
=item * Version comparison with standard operators (>=, >, <, <=, ==, !=) |
|
60
|
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
=item * Regular expression matching for version strings |
|
62
|
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
=item * Custom version flag support for non-standard programs |
|
64
|
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
=item * Custom version extraction for unusual output formats |
|
66
|
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
=item * Caching to avoid repeated program execution |
|
68
|
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
=item * Cross-platform support (Unix, Linux, macOS, Windows) |
|
70
|
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
=back |
|
72
|
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
=head1 EXAMPLES |
|
74
|
|
|
|
|
|
|
|
|
75
|
|
|
|
|
|
|
=head2 Basic Usage |
|
76
|
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
Check for program availability without version constraints: |
|
78
|
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
use Test::Which qw(which_ok); |
|
80
|
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
which_ok 'perl', 'ffmpeg', 'convert'; |
|
82
|
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
=head2 Version Constraints |
|
84
|
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
Check programs with minimum version requirements: |
|
86
|
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
# String constraints with comparison operators |
|
88
|
|
|
|
|
|
|
which_ok 'perl' => '>=5.10'; |
|
89
|
|
|
|
|
|
|
which_ok 'ffmpeg' => '>=4.0', 'convert' => '>=7.1'; |
|
90
|
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
# Exact version match |
|
92
|
|
|
|
|
|
|
which_ok 'node' => '==18.0.0'; |
|
93
|
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
# Version range |
|
95
|
|
|
|
|
|
|
which_ok 'python' => '>=3.8', 'python' => '<4.0'; |
|
96
|
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
=head2 Hashref Syntax |
|
98
|
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
Use hashrefs for more complex constraints: |
|
100
|
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
# String version in hashref |
|
102
|
|
|
|
|
|
|
which_ok 'perl', { version => '>=5.10' }; |
|
103
|
|
|
|
|
|
|
|
|
104
|
|
|
|
|
|
|
# Regex matching |
|
105
|
|
|
|
|
|
|
which_ok 'perl', { version => qr/5\.\d+/ }; |
|
106
|
|
|
|
|
|
|
which_ok 'ffmpeg', { version => qr/^[4-6]\./ }; |
|
107
|
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
=head2 Custom Version Flags |
|
109
|
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
Some programs use non-standard flags to display version information: |
|
111
|
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
# Java uses -version (single dash) |
|
113
|
|
|
|
|
|
|
which_ok 'java', { |
|
114
|
|
|
|
|
|
|
version => '>=11', |
|
115
|
|
|
|
|
|
|
version_flag => '-version' |
|
116
|
|
|
|
|
|
|
}; |
|
117
|
|
|
|
|
|
|
|
|
118
|
|
|
|
|
|
|
# Try multiple flags in order |
|
119
|
|
|
|
|
|
|
which_ok 'myprogram', { |
|
120
|
|
|
|
|
|
|
version => '>=2.0', |
|
121
|
|
|
|
|
|
|
version_flag => ['--show-version', '-version', '--ver'] |
|
122
|
|
|
|
|
|
|
}; |
|
123
|
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
# Program prints version without any flag |
|
125
|
|
|
|
|
|
|
which_ok 'sometool', { |
|
126
|
|
|
|
|
|
|
version => '>=1.0', |
|
127
|
|
|
|
|
|
|
version_flag => '', |
|
128
|
|
|
|
|
|
|
timeout => 10, # seconds - the default is 5 |
|
129
|
|
|
|
|
|
|
}; |
|
130
|
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
# Windows-specific flag |
|
132
|
|
|
|
|
|
|
which_ok 'cmd', { |
|
133
|
|
|
|
|
|
|
version => qr/\d+/, |
|
134
|
|
|
|
|
|
|
version_flag => '/?' |
|
135
|
|
|
|
|
|
|
} if $^O eq 'MSWin32'; |
|
136
|
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
If C is not specified, the module tries these flags in order: |
|
138
|
|
|
|
|
|
|
C<--version>, C<-version>, C<-v>, C<-V> (and C?>, C<-?> on Windows) |
|
139
|
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
=head2 Custom Version Extraction |
|
141
|
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
For programs with unusual version output formats: |
|
143
|
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
which_ok 'myprogram', { |
|
145
|
|
|
|
|
|
|
version => '>=1.0', |
|
146
|
|
|
|
|
|
|
extractor => sub { |
|
147
|
|
|
|
|
|
|
my $output = shift; |
|
148
|
|
|
|
|
|
|
return $1 if $output =~ /Build (\d+\.\d+)/; |
|
149
|
|
|
|
|
|
|
return undef; |
|
150
|
|
|
|
|
|
|
} |
|
151
|
|
|
|
|
|
|
}; |
|
152
|
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
The extractor receives the program's output and should return the version |
|
154
|
|
|
|
|
|
|
string or undef if no version could be found. |
|
155
|
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
=head2 Mixed Usage |
|
157
|
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
Combine different constraint types: |
|
159
|
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
which_ok |
|
161
|
|
|
|
|
|
|
'perl' => '>=5.10', # String constraint |
|
162
|
|
|
|
|
|
|
'ffmpeg', # No constraint |
|
163
|
|
|
|
|
|
|
'convert', { version => qr/^7\./ }; # Regex constraint |
|
164
|
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
=head2 Compile-Time Checking |
|
166
|
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
Skip entire test files if requirements aren't met: |
|
168
|
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
use Test::Which 'ffmpeg' => '>=6.0', 'convert' => '>=7.1'; |
|
170
|
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
# Test file is skipped if either program is missing or the version is too old |
|
172
|
|
|
|
|
|
|
# No tests below this line will run if requirements aren't met |
|
173
|
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
=head2 Runtime Checking in Subtests |
|
175
|
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
Check requirements for individual subtests: |
|
177
|
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
use Test::Which qw(which_ok); |
|
179
|
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
subtest 'video conversion' => sub { |
|
181
|
|
|
|
|
|
|
which_ok 'ffmpeg' => '>=4.0' or return; |
|
182
|
|
|
|
|
|
|
# ... tests using ffmpeg |
|
183
|
|
|
|
|
|
|
}; |
|
184
|
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
subtest 'image processing' => sub { |
|
186
|
|
|
|
|
|
|
which_ok 'convert' => '>=7.0' or return; |
|
187
|
|
|
|
|
|
|
# ... tests using ImageMagick |
|
188
|
|
|
|
|
|
|
}; |
|
189
|
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
=head2 Absolute Paths |
|
191
|
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
You can specify absolute paths instead of searching PATH: |
|
193
|
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
which_ok '/usr/local/bin/myprogram' => '>=1.0'; |
|
195
|
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
The program must be executable. |
|
197
|
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
=head1 VERSION DETECTION |
|
199
|
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
The module attempts to detect version numbers using these strategies in order: |
|
201
|
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
=over 4 |
|
203
|
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
=item 1. Look for version near the word "version" (case-insensitive) |
|
205
|
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
Matches patterns like: C, C |
|
207
|
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
=item 2. Extract dotted version from first line of output |
|
209
|
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
Common for programs that print version info prominently |
|
211
|
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
=item 3. Find any dotted version number in output |
|
213
|
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
Fallback for less standard formats |
|
215
|
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
=item 4. Look for single number near "version" |
|
217
|
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
For programs that use simple integer versioning |
|
219
|
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
=item 5. Use any standalone number found |
|
221
|
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
Last resort - least reliable |
|
223
|
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
=back |
|
225
|
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
=head1 VERSION COMPARISON |
|
227
|
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
Version comparison uses Perl's L module. Versions are normalized |
|
229
|
|
|
|
|
|
|
to have the same number of components before comparison to avoid |
|
230
|
|
|
|
|
|
|
C's parsing quirks. |
|
231
|
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
For example: |
|
233
|
|
|
|
|
|
|
- C<2020.10> becomes C<2020.10.0> |
|
234
|
|
|
|
|
|
|
- C<2020.10.15> stays C<2020.10.15> |
|
235
|
|
|
|
|
|
|
- Then they're compared correctly |
|
236
|
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
Supported operators: C<< >= >>, C<< > >>, C<< <= >>, C<< < >>, C<=>, C |
|
238
|
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
=head1 CACHING |
|
240
|
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
Version detection results are cached to avoid repeated program execution. |
|
242
|
|
|
|
|
|
|
Each unique combination of program path and version flags creates a separate |
|
243
|
|
|
|
|
|
|
cache entry. |
|
244
|
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
Cache benefits: |
|
246
|
|
|
|
|
|
|
- Faster repeated checks in test suites |
|
247
|
|
|
|
|
|
|
- Reduced system load |
|
248
|
|
|
|
|
|
|
- Works across multiple test files in the same process |
|
249
|
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
The cache persists for the lifetime of the Perl process. |
|
251
|
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
=head1 VERBOSE OUTPUT |
|
253
|
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
Set environment variables to see detected versions: |
|
255
|
|
|
|
|
|
|
|
|
256
|
|
|
|
|
|
|
TEST_WHICH_VERBOSE=1 prove -v t/mytest.t |
|
257
|
|
|
|
|
|
|
TEST_VERBOSE=1 perl t/mytest.t |
|
258
|
|
|
|
|
|
|
prove -v t/mytest.t # HARNESS_IS_VERBOSE is set automatically |
|
259
|
|
|
|
|
|
|
|
|
260
|
|
|
|
|
|
|
Output includes the detected version for each checked program: |
|
261
|
|
|
|
|
|
|
|
|
262
|
|
|
|
|
|
|
# perl: version 5.38.0 |
|
263
|
|
|
|
|
|
|
# ffmpeg: version 6.1.1 |
|
264
|
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
=head1 PLATFORM SUPPORT |
|
266
|
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
=over 4 |
|
268
|
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
=item * B: Full support for all features |
|
270
|
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
=item * B: Basic functionality supported. Complex shell features |
|
272
|
|
|
|
|
|
|
(STDERR redirection, empty flags) may have limitations. |
|
273
|
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
=back |
|
275
|
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
=head1 DIAGNOSTICS |
|
277
|
|
|
|
|
|
|
|
|
278
|
|
|
|
|
|
|
Common error messages: |
|
279
|
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
=over 4 |
|
281
|
|
|
|
|
|
|
|
|
282
|
|
|
|
|
|
|
=item C |
|
283
|
|
|
|
|
|
|
|
|
284
|
|
|
|
|
|
|
The program 'foo' could not be found in PATH. |
|
285
|
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
=item C |
|
287
|
|
|
|
|
|
|
|
|
288
|
|
|
|
|
|
|
The program exists but the module couldn't extract a version number from |
|
289
|
|
|
|
|
|
|
its output. Try specifying a custom C or C. |
|
290
|
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
=item C=2.0> |
|
292
|
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
The program's version doesn't meet the constraint. |
|
294
|
|
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
=item C |
|
296
|
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
For regex constraints, the detected version didn't match the pattern. |
|
298
|
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
=item C |
|
300
|
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
When using hashref syntax, you must include a C key. |
|
302
|
|
|
|
|
|
|
|
|
303
|
|
|
|
|
|
|
=item C |
|
304
|
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
The version constraint string couldn't be parsed. Use formats like |
|
306
|
|
|
|
|
|
|
C<'>=1.2.3'>, C<'>2.0'>, or C<'1.5'>. |
|
307
|
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
=back |
|
309
|
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
=head1 FUNCTIONS/METHODS |
|
311
|
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
=head2 which_ok @programs_or_pairs |
|
313
|
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
Checks the named programs (with optional version constraints). |
|
315
|
|
|
|
|
|
|
If any requirement is not met, the current test or subtest is skipped |
|
316
|
|
|
|
|
|
|
via L. |
|
317
|
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
Returns true if all requirements are met, false otherwise. |
|
319
|
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
=cut |
|
321
|
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
# runtime function, returns true if all present & satisfy versions, otherwise calls skip |
|
323
|
|
|
|
|
|
|
sub which_ok { |
|
324
|
15
|
|
|
15
|
1
|
557975
|
my (@args) = @_; |
|
325
|
|
|
|
|
|
|
|
|
326
|
15
|
|
|
|
|
80
|
my $res = _check_requirements(@args); |
|
327
|
15
|
|
|
|
|
108
|
my @missing = @{ $res->{missing} }; |
|
|
15
|
|
|
|
|
119
|
|
|
328
|
15
|
|
|
|
|
32
|
my @bad = @{ $res->{bad_version} }; |
|
|
15
|
|
|
|
|
48
|
|
|
329
|
|
|
|
|
|
|
|
|
330
|
15
|
100
|
100
|
|
|
163
|
if (@missing || @bad) { |
|
331
|
3
|
|
|
|
|
13
|
my @msgs; |
|
332
|
3
|
|
|
|
|
12
|
push @msgs, map { "Missing required program '$_'" } @missing; |
|
|
1
|
|
|
|
|
5
|
|
|
333
|
3
|
|
|
|
|
54
|
push @msgs, map { "Version issue for $_->{name}: $_->{reason}" } @bad; |
|
|
2
|
|
|
|
|
22
|
|
|
334
|
3
|
|
|
|
|
21
|
my $msg = join('; ', @msgs); |
|
335
|
3
|
|
|
|
|
72
|
$TEST->skip($msg); |
|
336
|
3
|
|
|
|
|
5096
|
return 0; |
|
337
|
|
|
|
|
|
|
} |
|
338
|
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
# Print versions if TEST_VERBOSE is set |
|
340
|
12
|
50
|
33
|
|
|
156
|
if ($ENV{TEST_WHICH_VERBOSE} || $ENV{TEST_VERBOSE} || $ENV{HARNESS_IS_VERBOSE}) { |
|
|
|
|
33
|
|
|
|
|
|
341
|
0
|
|
|
|
|
0
|
for my $r (@{ $res->{checked} }) { |
|
|
0
|
|
|
|
|
0
|
|
|
342
|
0
|
|
|
|
|
0
|
my $name = $r->{name}; |
|
343
|
0
|
|
|
|
|
0
|
my $out = _capture_version_output(which($name), $r->{'version_flag'}); |
|
344
|
0
|
|
|
|
|
0
|
my $version = _extract_version($out); |
|
345
|
|
|
|
|
|
|
|
|
346
|
0
|
0
|
|
|
|
0
|
if (defined $version) { |
|
347
|
0
|
|
|
|
|
0
|
$TEST->diag("$name: version $version"); |
|
348
|
|
|
|
|
|
|
} else { |
|
349
|
0
|
|
|
|
|
0
|
$TEST->diag("$name: found but version unknown"); |
|
350
|
|
|
|
|
|
|
} |
|
351
|
|
|
|
|
|
|
} |
|
352
|
|
|
|
|
|
|
} |
|
353
|
|
|
|
|
|
|
|
|
354
|
|
|
|
|
|
|
# Actually run a passing test |
|
355
|
12
|
50
|
|
|
|
31
|
$TEST->ok(1, 'Required programs available: ' . join(', ', map { $_->{name} } @{ $res->{checked} || [] })); |
|
|
12
|
|
|
|
|
277
|
|
|
|
12
|
|
|
|
|
97
|
|
|
356
|
12
|
|
|
|
|
11435
|
return 1; |
|
357
|
|
|
|
|
|
|
} |
|
358
|
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
# Helper: run a program with one of the version flags and capture output |
|
360
|
|
|
|
|
|
|
sub _capture_version_output { |
|
361
|
15
|
|
|
15
|
|
2900
|
my ($path, $custom_flags) = @_; |
|
362
|
|
|
|
|
|
|
|
|
363
|
15
|
50
|
|
|
|
47
|
return undef unless defined $path; |
|
364
|
|
|
|
|
|
|
|
|
365
|
|
|
|
|
|
|
# Build cache key |
|
366
|
15
|
|
|
|
|
31
|
my $cache_key = $path; |
|
367
|
15
|
100
|
|
|
|
49
|
if (defined $custom_flags) { |
|
368
|
8
|
100
|
|
|
|
56
|
if (ref $custom_flags eq 'ARRAY') { |
|
|
|
50
|
|
|
|
|
|
|
369
|
1
|
|
|
|
|
11
|
$cache_key .= '|' . join(',', @$custom_flags); |
|
370
|
|
|
|
|
|
|
} elsif (!ref $custom_flags) { |
|
371
|
7
|
|
|
|
|
21
|
$cache_key .= '|' . $custom_flags; |
|
372
|
|
|
|
|
|
|
} |
|
373
|
|
|
|
|
|
|
} |
|
374
|
15
|
100
|
|
|
|
61
|
return $VERSION_CACHE{$cache_key} if exists $VERSION_CACHE{$cache_key}; |
|
375
|
|
|
|
|
|
|
|
|
376
|
|
|
|
|
|
|
# Determine flags to try |
|
377
|
13
|
|
|
|
|
23
|
my @flags; |
|
378
|
13
|
100
|
|
|
|
53
|
if (!defined $custom_flags) { |
|
|
|
100
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
379
|
6
|
|
|
|
|
35
|
@flags = qw(--version -version -v -V); |
|
380
|
6
|
50
|
|
|
|
26
|
push @flags, qw(/? -?) if $^O eq 'MSWin32'; |
|
381
|
|
|
|
|
|
|
} |
|
382
|
|
|
|
|
|
|
elsif (ref($custom_flags) eq 'ARRAY') { |
|
383
|
1
|
|
|
|
|
12
|
@flags = @$custom_flags; |
|
384
|
|
|
|
|
|
|
} |
|
385
|
|
|
|
|
|
|
elsif (!ref($custom_flags)) { |
|
386
|
6
|
|
|
|
|
18
|
@flags = ($custom_flags); # allow empty string '' |
|
387
|
|
|
|
|
|
|
} |
|
388
|
|
|
|
|
|
|
else { |
|
389
|
0
|
|
|
|
|
0
|
warn "Invalid version_flag type: ", ref($custom_flags); |
|
390
|
0
|
|
|
|
|
0
|
$VERSION_CACHE{$cache_key} = undef; |
|
391
|
0
|
|
|
|
|
0
|
return undef; |
|
392
|
|
|
|
|
|
|
} |
|
393
|
|
|
|
|
|
|
|
|
394
|
|
|
|
|
|
|
# timeout (default to 5 seconds if not set) |
|
395
|
13
|
50
|
|
|
|
41
|
my $timeout = defined $TIMEOUT ? $TIMEOUT : 5; |
|
396
|
|
|
|
|
|
|
|
|
397
|
13
|
50
|
|
|
|
50
|
my $is_win = ($^O eq 'MSWin32') ? 1 : 0; |
|
398
|
13
|
50
|
|
|
|
106
|
my $is_bat = ($path =~ /\.(bat|cmd)$/i) ? 1 : 0; |
|
399
|
|
|
|
|
|
|
|
|
400
|
|
|
|
|
|
|
FLAG: |
|
401
|
13
|
|
|
|
|
42
|
for my $flag (@flags) { |
|
402
|
|
|
|
|
|
|
|
|
403
|
|
|
|
|
|
|
# Build command / args |
|
404
|
17
|
|
|
|
|
60
|
my @cmd; |
|
405
|
17
|
50
|
33
|
|
|
64
|
if ($is_win && $is_bat) { |
|
406
|
|
|
|
|
|
|
# For .bat/.cmd on Windows, call cmd.exe /c "prog [flag]" |
|
407
|
|
|
|
|
|
|
# Build a single command string for cmd.exe /c; quote path if it contains spaces |
|
408
|
0
|
0
|
|
|
|
0
|
my $path_part = ($path =~ /\s/) ? qq{"$path"} : $path; |
|
409
|
0
|
|
|
|
|
0
|
my $cmdstr = $path_part; |
|
410
|
0
|
0
|
0
|
|
|
0
|
$cmdstr .= " $flag" if defined $flag && length $flag; |
|
411
|
0
|
|
|
|
|
0
|
@cmd = ('cmd.exe', '/c', $cmdstr); |
|
412
|
|
|
|
|
|
|
} else { |
|
413
|
|
|
|
|
|
|
# Normal argv-style call for binaries / scripts |
|
414
|
17
|
|
|
|
|
53
|
@cmd = ($path); |
|
415
|
17
|
100
|
66
|
|
|
194
|
push @cmd, $flag if defined $flag && length $flag; |
|
416
|
|
|
|
|
|
|
} |
|
417
|
|
|
|
|
|
|
|
|
418
|
17
|
|
|
|
|
72
|
my ($stdout, $stderr) = ('', ''); |
|
419
|
17
|
|
|
|
|
35
|
my $ok = 0; |
|
420
|
|
|
|
|
|
|
|
|
421
|
|
|
|
|
|
|
# Try IPC::Run3 (preferred) in argv-mode |
|
422
|
17
|
50
|
|
|
|
52
|
if (eval { require IPC::Run3; 1 }) { |
|
|
17
|
|
|
|
|
2582
|
|
|
|
17
|
|
|
|
|
77678
|
|
|
423
|
17
|
|
|
|
|
40
|
eval { |
|
424
|
17
|
|
|
0
|
|
363
|
local $SIG{ALRM} = sub { die 'TIMEOUT' }; |
|
|
0
|
|
|
|
|
0
|
|
|
425
|
17
|
|
|
|
|
161
|
alarm $timeout; |
|
426
|
17
|
|
|
|
|
221
|
IPC::Run3::run3(\@cmd, \undef, \$stdout, \$stderr); |
|
427
|
17
|
|
|
|
|
132599
|
alarm 0; |
|
428
|
|
|
|
|
|
|
}; |
|
429
|
17
|
50
|
|
|
|
189
|
if ($@) { |
|
430
|
0
|
0
|
|
|
|
0
|
next FLAG if $@ =~ /TIMEOUT/; |
|
431
|
0
|
|
|
|
|
0
|
next FLAG; |
|
432
|
|
|
|
|
|
|
} |
|
433
|
17
|
|
100
|
|
|
212
|
$ok = ($stdout ne '' || $stderr ne ''); |
|
434
|
|
|
|
|
|
|
} |
|
435
|
|
|
|
|
|
|
|
|
436
|
|
|
|
|
|
|
# Fallback to shell qx{} if IPC::Run3 not available or produced no output |
|
437
|
17
|
100
|
|
|
|
140
|
if (!$ok) { |
|
438
|
6
|
|
|
|
|
68
|
my $shell_cmd; |
|
439
|
6
|
50
|
33
|
|
|
154
|
if ($is_win && $is_bat) { |
|
|
|
50
|
|
|
|
|
|
|
440
|
|
|
|
|
|
|
# Use cmd.exe /c "prog [flag]" and capture stderr |
|
441
|
0
|
0
|
|
|
|
0
|
my $path_part = ($path =~ /\s/) ? qq{"$path"} : $path; |
|
442
|
0
|
|
|
|
|
0
|
my $inner = $path_part; |
|
443
|
0
|
0
|
0
|
|
|
0
|
$inner .= " $flag" if defined $flag && length $flag; |
|
444
|
0
|
|
|
|
|
0
|
$shell_cmd = qq{cmd.exe /c "$inner" 2>&1}; |
|
445
|
|
|
|
|
|
|
} |
|
446
|
|
|
|
|
|
|
elsif ($is_win) { |
|
447
|
|
|
|
|
|
|
# Non-bat on Windows — quote path and append flag |
|
448
|
0
|
0
|
0
|
|
|
0
|
my $flagpart = defined $flag && length $flag ? " $flag" : ''; |
|
449
|
0
|
|
|
|
|
0
|
$shell_cmd = qq{"$path"$flagpart 2>&1}; |
|
450
|
|
|
|
|
|
|
} |
|
451
|
|
|
|
|
|
|
else { |
|
452
|
|
|
|
|
|
|
# Unix: single-quote the path; if flag present pass it unquoted (shell will split) |
|
453
|
6
|
|
|
|
|
38
|
my $escaped = $path; |
|
454
|
6
|
|
|
|
|
60
|
$escaped =~ s/'/'\\''/g; |
|
455
|
6
|
50
|
33
|
|
|
169
|
if (defined $flag && length $flag) { |
|
456
|
|
|
|
|
|
|
# If flag contains spaces, shell will treat it as one word if quoted; use simple approach |
|
457
|
6
|
|
|
|
|
40
|
my $f = $flag; |
|
458
|
6
|
|
|
|
|
22
|
$f =~ s/'/'\\''/g; |
|
459
|
6
|
|
|
|
|
22
|
$shell_cmd = qq{'$escaped' '$f' 2>&1}; |
|
460
|
|
|
|
|
|
|
} else { |
|
461
|
0
|
|
|
|
|
0
|
$shell_cmd = qq{'$escaped' 2>&1}; |
|
462
|
|
|
|
|
|
|
} |
|
463
|
|
|
|
|
|
|
} |
|
464
|
|
|
|
|
|
|
|
|
465
|
6
|
|
|
|
|
19
|
eval { |
|
466
|
6
|
|
|
0
|
|
204
|
local $SIG{ALRM} = sub { die 'TIMEOUT' }; |
|
|
0
|
|
|
|
|
0
|
|
|
467
|
6
|
|
|
|
|
67
|
alarm $timeout; |
|
468
|
6
|
|
|
|
|
48749
|
$stdout = qx{$shell_cmd}; |
|
469
|
6
|
|
|
|
|
544
|
alarm 0; |
|
470
|
|
|
|
|
|
|
}; |
|
471
|
6
|
50
|
|
|
|
62
|
next FLAG if $@; |
|
472
|
6
|
|
|
|
|
75
|
$ok = ($stdout ne ''); |
|
473
|
|
|
|
|
|
|
} |
|
474
|
|
|
|
|
|
|
|
|
475
|
17
|
100
|
|
|
|
264
|
next FLAG unless $ok; |
|
476
|
|
|
|
|
|
|
|
|
477
|
|
|
|
|
|
|
# Merge outputs (IPC::Run3 already gave us stderr separately) |
|
478
|
11
|
|
|
|
|
91
|
my $output = $stdout . $stderr; |
|
479
|
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
# Normalize newlines on Windows |
|
481
|
11
|
50
|
|
|
|
48
|
$output =~ s/\r\n/\n/g if $is_win; |
|
482
|
|
|
|
|
|
|
|
|
483
|
|
|
|
|
|
|
# Cache and return |
|
484
|
11
|
|
|
|
|
150
|
$VERSION_CACHE{$cache_key} = $output; |
|
485
|
11
|
|
|
|
|
300
|
return $output; |
|
486
|
|
|
|
|
|
|
} |
|
487
|
|
|
|
|
|
|
|
|
488
|
|
|
|
|
|
|
# Nothing worked — cache failure |
|
489
|
2
|
|
|
|
|
47
|
$VERSION_CACHE{$cache_key} = undef; |
|
490
|
2
|
|
|
|
|
35
|
return undef; |
|
491
|
|
|
|
|
|
|
} |
|
492
|
|
|
|
|
|
|
|
|
493
|
|
|
|
|
|
|
# Extract the first version-like token from output |
|
494
|
|
|
|
|
|
|
sub _extract_version { |
|
495
|
14
|
|
|
14
|
|
1264
|
my $output = $_[0]; |
|
496
|
|
|
|
|
|
|
|
|
497
|
14
|
100
|
|
|
|
61
|
return undef unless defined $output; |
|
498
|
|
|
|
|
|
|
|
|
499
|
|
|
|
|
|
|
# Look for version near the word "version" |
|
500
|
|
|
|
|
|
|
# Handles: "ffmpeg version 4.2.7", "Version: 2.1.0", "ImageMagick 7.1.0-4" |
|
501
|
12
|
100
|
|
|
|
218
|
if ($output =~ /version[:\s]+v?(\d+(?:\.\d+)+)/i) { |
|
502
|
6
|
|
|
|
|
70
|
return $1; |
|
503
|
|
|
|
|
|
|
} |
|
504
|
|
|
|
|
|
|
|
|
505
|
|
|
|
|
|
|
# Look at first line (common pattern) |
|
506
|
6
|
|
|
|
|
49
|
my ($first_line) = split /\n/, $output; |
|
507
|
6
|
100
|
|
|
|
117
|
if ($first_line =~ /\b(\d+\.\d+(?:\.\d+)*)\b/) { |
|
508
|
4
|
|
|
|
|
38
|
return $1; |
|
509
|
|
|
|
|
|
|
} |
|
510
|
|
|
|
|
|
|
|
|
511
|
|
|
|
|
|
|
# Any dotted version number |
|
512
|
2
|
50
|
|
|
|
38
|
if ($output =~ /\b(\d+\.\d+(?:\.\d+)*)\b/) { |
|
513
|
2
|
|
|
|
|
15
|
return $1; |
|
514
|
|
|
|
|
|
|
} |
|
515
|
|
|
|
|
|
|
|
|
516
|
|
|
|
|
|
|
# Single number near "version" |
|
517
|
0
|
0
|
|
|
|
0
|
if ($output =~ /version[:\s]+v?(\d+)\b/i) { |
|
518
|
0
|
|
|
|
|
0
|
return $1; |
|
519
|
|
|
|
|
|
|
} |
|
520
|
|
|
|
|
|
|
|
|
521
|
|
|
|
|
|
|
# Just a standalone number (least reliable) |
|
522
|
0
|
0
|
|
|
|
0
|
if ($output =~ /\b(\d+)\b/) { |
|
523
|
0
|
|
|
|
|
0
|
return $1; |
|
524
|
|
|
|
|
|
|
} |
|
525
|
|
|
|
|
|
|
|
|
526
|
0
|
|
|
|
|
0
|
return undef; |
|
527
|
|
|
|
|
|
|
} |
|
528
|
|
|
|
|
|
|
|
|
529
|
|
|
|
|
|
|
# Compare two versions given an operator |
|
530
|
|
|
|
|
|
|
sub _version_satisfies { |
|
531
|
8
|
|
|
8
|
|
61
|
my ($found, $op, $required) = @_; |
|
532
|
|
|
|
|
|
|
|
|
533
|
8
|
50
|
|
|
|
37
|
return 0 unless defined $found; |
|
534
|
|
|
|
|
|
|
|
|
535
|
|
|
|
|
|
|
# Normalize version strings to have same number of components |
|
536
|
8
|
|
|
|
|
47
|
my @found_parts = split /\./, $found; |
|
537
|
8
|
|
|
|
|
59
|
my @req_parts = split /\./, $required; |
|
538
|
|
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
# Pad to same length |
|
540
|
8
|
100
|
|
|
|
67
|
my $max_len = @found_parts > @req_parts ? @found_parts : @req_parts; |
|
541
|
8
|
|
|
|
|
33
|
push @found_parts, (0) x ($max_len - @found_parts); |
|
542
|
8
|
|
|
|
|
30
|
push @req_parts, (0) x ($max_len - @req_parts); |
|
543
|
|
|
|
|
|
|
|
|
544
|
8
|
|
|
|
|
53
|
my $found_normalized = join('.', @found_parts); |
|
545
|
8
|
|
|
|
|
52
|
my $req_normalized = join('.', @req_parts); |
|
546
|
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
# Parse with version.pm |
|
548
|
8
|
|
|
|
|
22
|
my $vf = eval { version->parse($found_normalized) }; |
|
|
8
|
|
|
|
|
346
|
|
|
549
|
8
|
50
|
|
|
|
37
|
if ($@) { |
|
550
|
0
|
|
|
|
|
0
|
warn "Failed to parse found version '$found': $@"; |
|
551
|
0
|
|
|
|
|
0
|
return 0; |
|
552
|
|
|
|
|
|
|
} |
|
553
|
|
|
|
|
|
|
|
|
554
|
8
|
|
|
|
|
16
|
my $vr = eval { version->parse($req_normalized) }; |
|
|
8
|
|
|
|
|
121
|
|
|
555
|
8
|
50
|
|
|
|
34
|
if ($@) { |
|
556
|
0
|
|
|
|
|
0
|
warn "Failed to parse required version '$required': $@"; |
|
557
|
0
|
|
|
|
|
0
|
return 0; |
|
558
|
|
|
|
|
|
|
} |
|
559
|
|
|
|
|
|
|
|
|
560
|
|
|
|
|
|
|
# Return explicit 1 or 0 |
|
561
|
8
|
|
|
|
|
16
|
my $result; |
|
562
|
8
|
50
|
|
|
|
62
|
if ($op eq '>=') { $result = $vf >= $vr } |
|
|
8
|
0
|
|
|
|
155
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
563
|
0
|
|
|
|
|
0
|
elsif ($op eq '>') { $result = $vf > $vr } |
|
564
|
0
|
|
|
|
|
0
|
elsif ($op eq '<=') { $result = $vf <= $vr } |
|
565
|
0
|
|
|
|
|
0
|
elsif ($op eq '<') { $result = $vf < $vr } |
|
566
|
0
|
|
|
|
|
0
|
elsif ($op eq '==') { $result = $vf == $vr } |
|
567
|
0
|
|
|
|
|
0
|
elsif ($op eq '!=') { $result = $vf != $vr } |
|
568
|
0
|
|
|
|
|
0
|
else { $result = $vf == $vr } |
|
569
|
|
|
|
|
|
|
|
|
570
|
8
|
50
|
|
|
|
99
|
return $result ? 1 : 0; |
|
571
|
|
|
|
|
|
|
} |
|
572
|
|
|
|
|
|
|
|
|
573
|
|
|
|
|
|
|
# Parse a constraint like ">=1.2.3" into (op, ver) |
|
574
|
|
|
|
|
|
|
sub _parse_constraint { |
|
575
|
9
|
|
|
9
|
|
50
|
my $spec = $_[0]; |
|
576
|
|
|
|
|
|
|
|
|
577
|
9
|
50
|
|
|
|
31
|
return unless defined $spec; |
|
578
|
|
|
|
|
|
|
|
|
579
|
9
|
50
|
|
|
|
136
|
if ($spec =~ /^\s*(>=|<=|==|!=|>|<)\s*([0-9][\w\.\-]*)\s*$/) { |
|
580
|
9
|
|
|
|
|
109
|
return ($1, $2); |
|
581
|
|
|
|
|
|
|
} |
|
582
|
|
|
|
|
|
|
# allow bare version (implies ==) |
|
583
|
0
|
0
|
|
|
|
0
|
if ($spec =~ /^\s*(\d+(?:\.\d+)*)\s*$/) { |
|
584
|
0
|
|
|
|
|
0
|
return ('==', $1); |
|
585
|
|
|
|
|
|
|
} |
|
586
|
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
# If we get here, it's invalid |
|
588
|
|
|
|
|
|
|
# Return empty list, but caller should provide an helpful error |
|
589
|
0
|
|
|
|
|
0
|
return; |
|
590
|
|
|
|
|
|
|
} |
|
591
|
|
|
|
|
|
|
|
|
592
|
|
|
|
|
|
|
# Core check routine. Accepts a list of program => maybe_constraint pairs, |
|
593
|
|
|
|
|
|
|
# or simple program names in the list form. |
|
594
|
|
|
|
|
|
|
sub _check_requirements { |
|
595
|
15
|
|
|
15
|
|
68
|
my (@args) = @_; |
|
596
|
|
|
|
|
|
|
|
|
597
|
|
|
|
|
|
|
# Normalize into array of hashrefs: { name => ..., constraint => undef or '>=1' or {version => ...} } |
|
598
|
15
|
|
|
|
|
32
|
my @reqs; |
|
599
|
15
|
|
|
|
|
37
|
my $i = 0; |
|
600
|
|
|
|
|
|
|
|
|
601
|
15
|
|
|
|
|
350
|
while ($i < @args) { |
|
602
|
15
|
|
|
|
|
42
|
my $name = $args[$i]; |
|
603
|
|
|
|
|
|
|
|
|
604
|
|
|
|
|
|
|
# Validate program name |
|
605
|
15
|
50
|
|
|
|
55
|
unless (defined $name) { |
|
606
|
0
|
|
|
|
|
0
|
warn "Undefined program name at position $i, skipping"; |
|
607
|
0
|
|
|
|
|
0
|
$i++; |
|
608
|
0
|
|
|
|
|
0
|
next; |
|
609
|
|
|
|
|
|
|
} |
|
610
|
|
|
|
|
|
|
|
|
611
|
15
|
50
|
|
|
|
65
|
if (ref $name) { |
|
612
|
0
|
|
|
|
|
0
|
warn "Program name at position $i cannot be a reference, skipping"; |
|
613
|
0
|
|
|
|
|
0
|
$i++; |
|
614
|
0
|
|
|
|
|
0
|
next; |
|
615
|
|
|
|
|
|
|
} |
|
616
|
|
|
|
|
|
|
|
|
617
|
15
|
|
|
|
|
29
|
$i++; |
|
618
|
|
|
|
|
|
|
|
|
619
|
|
|
|
|
|
|
# Check if next argument is a constraint |
|
620
|
15
|
|
|
|
|
35
|
my $constraint = undef; |
|
621
|
15
|
100
|
|
|
|
66
|
if ($i < @args) { |
|
622
|
14
|
|
|
|
|
42
|
my $next = $args[$i]; |
|
623
|
|
|
|
|
|
|
|
|
624
|
14
|
50
|
|
|
|
55
|
if (defined $next) { |
|
625
|
|
|
|
|
|
|
# String constraint: >=1.2.3, >1.0, or bare version 1.2.3 |
|
626
|
14
|
100
|
|
|
|
105
|
if (!ref($next)) { |
|
|
|
50
|
|
|
|
|
|
|
627
|
3
|
50
|
33
|
|
|
33
|
if ($next =~ /^(?:>=|<=|==|!=|>|<)/ || $next =~ /^\d+(?:\.\d+)*$/) { |
|
628
|
3
|
|
|
|
|
12
|
$constraint = $next; |
|
629
|
3
|
|
|
|
|
12
|
$i++; |
|
630
|
|
|
|
|
|
|
} |
|
631
|
|
|
|
|
|
|
# Otherwise it's probably the next program name, don't consume it |
|
632
|
|
|
|
|
|
|
} elsif (ref($next) eq 'HASH') { |
|
633
|
|
|
|
|
|
|
# Hashref constraint: { version => qr/.../ } or similar |
|
634
|
11
|
|
|
|
|
53
|
$constraint = $next; |
|
635
|
11
|
|
|
|
|
27
|
$i++; |
|
636
|
|
|
|
|
|
|
} |
|
637
|
|
|
|
|
|
|
# Other refs (ARRAY, CODE, etc.) - treat as next program name, don't consume |
|
638
|
|
|
|
|
|
|
} |
|
639
|
|
|
|
|
|
|
} |
|
640
|
|
|
|
|
|
|
|
|
641
|
15
|
|
|
|
|
140
|
push @reqs, { name => $name, constraint => $constraint }; |
|
642
|
|
|
|
|
|
|
} |
|
643
|
|
|
|
|
|
|
|
|
644
|
15
|
|
|
|
|
74
|
my @missing; |
|
645
|
|
|
|
|
|
|
my @bad_version; |
|
646
|
15
|
|
|
|
|
0
|
my @checked; |
|
647
|
|
|
|
|
|
|
|
|
648
|
15
|
|
|
|
|
48
|
for my $r (@reqs) { |
|
649
|
15
|
|
|
|
|
36
|
my $name = $r->{name}; |
|
650
|
15
|
|
|
|
|
30
|
my $want = $r->{constraint}; |
|
651
|
|
|
|
|
|
|
|
|
652
|
15
|
|
|
|
|
29
|
my $path = $name; |
|
653
|
15
|
50
|
33
|
|
|
198
|
if ($name !~ m{^/} && $name !~ m{^[A-Za-z]:[\\/]}) { |
|
654
|
|
|
|
|
|
|
# Not an absolute path, search in PATH |
|
655
|
15
|
|
|
|
|
165
|
$path = which($name); |
|
656
|
15
|
100
|
|
|
|
3357
|
unless ($path) { |
|
657
|
1
|
|
|
|
|
4
|
push @missing, $name; |
|
658
|
1
|
|
|
|
|
3
|
next; |
|
659
|
|
|
|
|
|
|
} |
|
660
|
|
|
|
|
|
|
} |
|
661
|
|
|
|
|
|
|
|
|
662
|
|
|
|
|
|
|
# Verify it's executable |
|
663
|
14
|
50
|
|
|
|
216
|
unless (-x $path) { |
|
664
|
0
|
|
|
|
|
0
|
push @bad_version, { |
|
665
|
|
|
|
|
|
|
name => $name, |
|
666
|
|
|
|
|
|
|
reason => "found at $path but not executable" |
|
667
|
|
|
|
|
|
|
}; |
|
668
|
0
|
|
|
|
|
0
|
next; |
|
669
|
|
|
|
|
|
|
} |
|
670
|
|
|
|
|
|
|
|
|
671
|
|
|
|
|
|
|
# No version constraint - just check if it exists |
|
672
|
14
|
100
|
|
|
|
72
|
if (!defined $want) { |
|
673
|
1
|
|
|
|
|
6
|
push @checked, { name => $name, constraint => undef, version_flag => undef }; |
|
674
|
1
|
|
|
|
|
3
|
next; |
|
675
|
|
|
|
|
|
|
} |
|
676
|
|
|
|
|
|
|
|
|
677
|
|
|
|
|
|
|
# Extract custom version flags if provided |
|
678
|
13
|
|
|
|
|
33
|
my $version_flag = undef; |
|
679
|
|
|
|
|
|
|
|
|
680
|
|
|
|
|
|
|
# Handle hashref constraints |
|
681
|
13
|
100
|
|
|
|
95
|
if (ref($want) eq 'HASH') { |
|
|
|
50
|
|
|
|
|
|
|
682
|
|
|
|
|
|
|
# Currently support { version => ... } and { version_flag => ... } |
|
683
|
|
|
|
|
|
|
|
|
684
|
|
|
|
|
|
|
# Extract version_flag if present |
|
685
|
10
|
100
|
|
|
|
65
|
$version_flag = $want->{version_flag} if exists $want->{version_flag}; |
|
686
|
|
|
|
|
|
|
|
|
687
|
10
|
100
|
|
|
|
63
|
if($version_flag) { |
|
688
|
5
|
|
|
|
|
18
|
$r->{version_flag} = $version_flag; |
|
689
|
|
|
|
|
|
|
} |
|
690
|
|
|
|
|
|
|
|
|
691
|
10
|
100
|
|
|
|
44
|
if(exists($want->{'timeout'})) { |
|
692
|
1
|
|
|
|
|
3
|
$TIMEOUT = $want->{'timeout'}; |
|
693
|
|
|
|
|
|
|
} |
|
694
|
|
|
|
|
|
|
|
|
695
|
10
|
50
|
|
|
|
37
|
if (exists $want->{version}) { |
|
696
|
10
|
|
|
|
|
28
|
my $version_spec = $want->{version}; |
|
697
|
10
|
|
|
|
|
20
|
my $found; |
|
698
|
10
|
50
|
|
|
|
35
|
if (exists $want->{extractor}) { |
|
699
|
0
|
|
|
|
|
0
|
my $extractor = $want->{extractor}; |
|
700
|
0
|
0
|
|
|
|
0
|
if (ref($extractor) eq 'CODE') { |
|
701
|
0
|
|
|
|
|
0
|
my $out = _capture_version_output($path, $version_flag); |
|
702
|
0
|
|
|
|
|
0
|
$found = $extractor->($out); |
|
703
|
|
|
|
|
|
|
} |
|
704
|
|
|
|
|
|
|
} else { |
|
705
|
10
|
|
|
|
|
81
|
my $out = _capture_version_output($path, $version_flag); |
|
706
|
10
|
|
|
|
|
111
|
$found = _extract_version($out); |
|
707
|
|
|
|
|
|
|
} |
|
708
|
|
|
|
|
|
|
|
|
709
|
10
|
100
|
|
|
|
64
|
unless (defined $found) { |
|
710
|
1
|
|
|
|
|
20
|
push @bad_version, { |
|
711
|
|
|
|
|
|
|
name => $name, |
|
712
|
|
|
|
|
|
|
reason => 'no version detected for hashref constraint' |
|
713
|
|
|
|
|
|
|
}; |
|
714
|
1
|
|
|
|
|
12
|
next; |
|
715
|
|
|
|
|
|
|
} |
|
716
|
|
|
|
|
|
|
|
|
717
|
|
|
|
|
|
|
# Regex constraint |
|
718
|
9
|
100
|
|
|
|
91
|
if (ref($version_spec) eq 'Regexp') { |
|
|
|
50
|
|
|
|
|
|
|
719
|
3
|
50
|
|
|
|
90
|
unless ($found =~ $version_spec) { |
|
720
|
0
|
|
|
|
|
0
|
push @bad_version, { |
|
721
|
|
|
|
|
|
|
name => $name, |
|
722
|
|
|
|
|
|
|
reason => "found version $found but doesn't match pattern $version_spec" |
|
723
|
|
|
|
|
|
|
}; |
|
724
|
0
|
|
|
|
|
0
|
next; |
|
725
|
|
|
|
|
|
|
} |
|
726
|
|
|
|
|
|
|
} elsif (!ref($version_spec)) { |
|
727
|
|
|
|
|
|
|
# String constraint within hashref (treat like normal string constraint) |
|
728
|
6
|
|
|
|
|
104
|
my ($op, $ver) = _parse_constraint($version_spec); |
|
729
|
6
|
50
|
|
|
|
40
|
unless (defined $op) { |
|
730
|
0
|
|
|
|
|
0
|
push @bad_version, { |
|
731
|
|
|
|
|
|
|
name => $name, |
|
732
|
|
|
|
|
|
|
reason => "invalid constraint in hashref '$version_spec' (expected format: '>=1.2.3', '>2.0', '==1.5', or '1.5')" |
|
733
|
|
|
|
|
|
|
}; |
|
734
|
0
|
|
|
|
|
0
|
next; |
|
735
|
|
|
|
|
|
|
} |
|
736
|
6
|
50
|
|
|
|
35
|
unless (_version_satisfies($found, $op, $ver)) { |
|
737
|
0
|
|
|
|
|
0
|
push @bad_version, { |
|
738
|
|
|
|
|
|
|
name => $name, |
|
739
|
|
|
|
|
|
|
reason => "found $found but need $op$ver" |
|
740
|
|
|
|
|
|
|
}; |
|
741
|
0
|
|
|
|
|
0
|
next; |
|
742
|
|
|
|
|
|
|
} |
|
743
|
|
|
|
|
|
|
} else { |
|
744
|
|
|
|
|
|
|
# Unsupported type in hashref |
|
745
|
0
|
|
|
|
|
0
|
push @bad_version, { |
|
746
|
|
|
|
|
|
|
name => $name, |
|
747
|
|
|
|
|
|
|
reason => "unsupported version spec type in hashref: " . ref($version_spec) |
|
748
|
|
|
|
|
|
|
}; |
|
749
|
0
|
|
|
|
|
0
|
next; |
|
750
|
|
|
|
|
|
|
} |
|
751
|
|
|
|
|
|
|
} else { |
|
752
|
|
|
|
|
|
|
# Hashref without 'version' key |
|
753
|
0
|
|
|
|
|
0
|
push @bad_version, { |
|
754
|
|
|
|
|
|
|
name => $name, |
|
755
|
|
|
|
|
|
|
reason => "hashref constraint must contain 'version' key" |
|
756
|
|
|
|
|
|
|
}; |
|
757
|
0
|
|
|
|
|
0
|
next; |
|
758
|
|
|
|
|
|
|
} |
|
759
|
|
|
|
|
|
|
} elsif (!ref($want)) { |
|
760
|
|
|
|
|
|
|
# Handle string constraints |
|
761
|
3
|
|
|
|
|
9
|
my ($op, $ver) = _parse_constraint($want); |
|
762
|
3
|
50
|
|
|
|
11
|
unless (defined $op) { |
|
763
|
0
|
|
|
|
|
0
|
push @bad_version, { |
|
764
|
|
|
|
|
|
|
name => $name, |
|
765
|
|
|
|
|
|
|
reason => "invalid constraint '$want' (expected format: '>=1.2.3', '>2.0', '==1.5', or '1.5')" |
|
766
|
|
|
|
|
|
|
}; |
|
767
|
0
|
|
|
|
|
0
|
next; |
|
768
|
|
|
|
|
|
|
} |
|
769
|
|
|
|
|
|
|
|
|
770
|
3
|
|
|
|
|
12
|
my $out = _capture_version_output($path); |
|
771
|
3
|
|
|
|
|
31
|
my $found = _extract_version($out); |
|
772
|
|
|
|
|
|
|
|
|
773
|
3
|
100
|
|
|
|
18
|
unless (defined $found) { |
|
774
|
1
|
|
|
|
|
28
|
push @bad_version, { |
|
775
|
|
|
|
|
|
|
name => $name, |
|
776
|
|
|
|
|
|
|
reason => 'no version detected' |
|
777
|
|
|
|
|
|
|
}; |
|
778
|
1
|
|
|
|
|
18
|
next; |
|
779
|
|
|
|
|
|
|
} |
|
780
|
|
|
|
|
|
|
|
|
781
|
2
|
50
|
|
|
|
20
|
unless (_version_satisfies($found, $op, $ver)) { |
|
782
|
0
|
|
|
|
|
0
|
push @bad_version, { |
|
783
|
|
|
|
|
|
|
name => $name, |
|
784
|
|
|
|
|
|
|
reason => "found $found but need $op$ver" |
|
785
|
|
|
|
|
|
|
}; |
|
786
|
0
|
|
|
|
|
0
|
next; |
|
787
|
|
|
|
|
|
|
} |
|
788
|
|
|
|
|
|
|
} else { |
|
789
|
|
|
|
|
|
|
# Unsupported constraint type |
|
790
|
0
|
|
|
|
|
0
|
push @bad_version, { |
|
791
|
|
|
|
|
|
|
name => $name, |
|
792
|
|
|
|
|
|
|
reason => "unsupported constraint type: " . ref($want) |
|
793
|
|
|
|
|
|
|
}; |
|
794
|
0
|
|
|
|
|
0
|
next; |
|
795
|
|
|
|
|
|
|
} |
|
796
|
|
|
|
|
|
|
|
|
797
|
|
|
|
|
|
|
# If we got here, the program passed all checks |
|
798
|
11
|
|
|
|
|
166
|
push @checked, $r; |
|
799
|
|
|
|
|
|
|
} |
|
800
|
|
|
|
|
|
|
|
|
801
|
|
|
|
|
|
|
return { |
|
802
|
15
|
|
|
|
|
339
|
missing => \@missing, |
|
803
|
|
|
|
|
|
|
bad_version => \@bad_version, |
|
804
|
|
|
|
|
|
|
checked => \@checked |
|
805
|
|
|
|
|
|
|
}; |
|
806
|
|
|
|
|
|
|
} |
|
807
|
|
|
|
|
|
|
|
|
808
|
|
|
|
|
|
|
# import: allow compile-time checks like `use Test::Which 'prog' => '>=1.2';` |
|
809
|
|
|
|
|
|
|
sub import { |
|
810
|
12
|
|
|
12
|
|
41272
|
my $class = shift; |
|
811
|
12
|
|
|
|
|
2414
|
$class->export_to_level(1, $class, @EXPORT_OK); |
|
812
|
|
|
|
|
|
|
|
|
813
|
|
|
|
|
|
|
# Only run requirement checks if any args remain |
|
814
|
12
|
|
|
|
|
72
|
my @reqs = grep { $_ ne 'which_ok' } @_; |
|
|
8
|
|
|
|
|
37
|
|
|
815
|
|
|
|
|
|
|
|
|
816
|
12
|
50
|
|
|
|
6446
|
return unless @reqs; |
|
817
|
|
|
|
|
|
|
|
|
818
|
0
|
|
|
|
|
|
my $res = _check_requirements(@reqs); |
|
819
|
0
|
|
|
|
|
|
my @missing = @{ $res->{missing} }; |
|
|
0
|
|
|
|
|
|
|
|
820
|
0
|
|
|
|
|
|
my @bad = @{ $res->{bad_version} }; |
|
|
0
|
|
|
|
|
|
|
|
821
|
|
|
|
|
|
|
|
|
822
|
0
|
0
|
0
|
|
|
|
if (@missing || @bad) { |
|
823
|
0
|
|
|
|
|
|
my @msgs; |
|
824
|
0
|
|
|
|
|
|
push @msgs, map { "Missing required program '$_'" } @missing; |
|
|
0
|
|
|
|
|
|
|
|
825
|
0
|
|
|
|
|
|
push @msgs, map { "Version issue for $_->{name}: $_->{reason}" } @bad; |
|
|
0
|
|
|
|
|
|
|
|
826
|
0
|
|
|
|
|
|
my $msg = join('; ', @msgs); |
|
827
|
0
|
|
|
|
|
|
$TEST->plan(skip_all => "Test::Which requirements not met: $msg"); |
|
828
|
|
|
|
|
|
|
} |
|
829
|
|
|
|
|
|
|
|
|
830
|
|
|
|
|
|
|
# Print versions if TEST_VERBOSE is set |
|
831
|
0
|
0
|
0
|
|
|
|
if ($ENV{TEST_WHICH_VERBOSE} || $ENV{TEST_VERBOSE} || $ENV{HARNESS_IS_VERBOSE}) { |
|
|
|
|
0
|
|
|
|
|
|
832
|
0
|
|
|
|
|
|
for my $r (@{ $res->{checked} }) { |
|
|
0
|
|
|
|
|
|
|
|
833
|
0
|
|
|
|
|
|
my $name = $r->{name}; |
|
834
|
0
|
|
|
|
|
|
my $out = _capture_version_output(which($name), $r->{'version_flag'}); |
|
835
|
0
|
|
|
|
|
|
my $version = _extract_version($out); |
|
836
|
|
|
|
|
|
|
|
|
837
|
0
|
0
|
|
|
|
|
if (defined $version) { |
|
838
|
0
|
|
|
|
|
|
print STDERR "# $name: version $version\n"; |
|
839
|
|
|
|
|
|
|
} else { |
|
840
|
0
|
|
|
|
|
|
print STDERR "# $name: found but version unknown\n"; |
|
841
|
|
|
|
|
|
|
} |
|
842
|
|
|
|
|
|
|
} |
|
843
|
|
|
|
|
|
|
} |
|
844
|
|
|
|
|
|
|
} |
|
845
|
|
|
|
|
|
|
|
|
846
|
|
|
|
|
|
|
1; |
|
847
|
|
|
|
|
|
|
|
|
848
|
|
|
|
|
|
|
__END__ |