File Coverage

blib/lib/Test/Alien.pm
Criterion Covered Total %
statement 224 288 77.7
branch 64 108 59.2
condition 16 35 45.7
subroutine 33 36 91.6
pod 5 6 83.3
total 342 473 72.3


line stmt bran cond sub pod time code
1             package Test::Alien;
2              
3 7     7   1103976 use strict;
  7         13  
  7         170  
4 7     7   23 use warnings;
  7         9  
  7         146  
5 7     7   163 use 5.008001;
  7         19  
6 7     7   5375 use Env qw( @PATH );
  7         14394  
  7         37  
7 7     7   3480 use File::Which 1.10 qw( which );
  7         4615  
  7         398  
8 7     7   3835 use if $^O ne 'MSWin32', 'Capture::Tiny' => 'capture_merged';
  7         57  
  7         36  
9 7     7   103006 use Capture::Tiny qw( capture );
  7         13  
  7         226  
10 7     7   30 use File::Temp qw( tempdir );
  7         10  
  7         240  
11 7     7   28 use Carp qw( croak );
  7         10  
  7         226  
12 7     7   60 use File::Spec;
  7         11  
  7         126  
13 7     7   24 use File::Basename qw( dirname );
  7         9  
  7         329  
14 7     7   26 use File::Path qw( mkpath );
  7         8  
  7         275  
15 7     7   3342 use File::Copy qw( move );
  7         12292  
  7         345  
16 7     7   2220 use Text::ParseWords qw( shellwords );
  7         5037  
  7         358  
17 7     7   32 use Test2::API qw( context run_subtest );
  7         10  
  7         318  
18 7     7   25 use base qw( Exporter );
  7         9  
  7         806  
19              
20             BEGIN {
21             *capture_merged = sub (&;@)
22             {
23             # TODO: fix this error properly:
24             #Error in tempfile() using template C:\Users\ollisg\AppData\Local\Temp\XXXXXXXXXX: Could not create temp file C:\Users\ollisg\AppData\Local\Temp\eysiq7e9w5: Permission denied at N:/home/ollisg/perl5/straw
25             # rry/x86/5.22.1/lib/perl5/Capture/Tiny.pm line 360.
26            
27             # this seems to work more reliably on windows, at the cost of being much noisier.
28 0         0 my $code = shift;
29 0 0       0 wantarray ? ('', $code->(@_)) : '';
30 7 50   7   14958 } if $^O eq 'MSWin32';
31             }
32              
33             our @EXPORT = qw( alien_ok run_ok xs_ok ffi_ok with_subtest synthetic );
34              
35             # ABSTRACT: Testing tools for Alien modules
36             our $VERSION = '0.14'; # VERSION
37              
38              
39             our @aliens;
40              
41             sub alien_ok ($;$)
42             {
43 5     5 1 12138 my($alien, $message) = @_;
44              
45 5 100       19 my $name = ref $alien ? ref($alien) . '[instance]' : $alien;
46            
47 5         10 my @methods = qw( cflags libs dynamic_libs bin_dir );
48 5   33     40 $message ||= "$name responds to: @methods";
49 5         9 my @missing = grep { ! $alien->can($_) } @methods;
  20         116  
50            
51 5         9 my $ok = !@missing;
52 5         12 my $ctx = context();
53 5         223 $ctx->ok($ok, $message);
54 5         633 $ctx->diag(" missing method $_") for @missing;
55 5         132 $ctx->release;
56            
57 5 100       78 if($ok)
58             {
59 4         5 push @aliens, $alien;
60 4         13 unshift @PATH, $alien->bin_dir;
61             }
62            
63 5         137 $ok;
64             }
65              
66              
67             sub synthetic
68             {
69 7     7 1 2526 my($opt) = @_;
70 7   100     19 $opt ||= {};
71 7         19 my %alien = %$opt;
72 7         469 require Test::Alien::Synthetic;
73 7         50 bless \%alien, 'Test::Alien::Synthetic',
74             }
75              
76              
77             sub run_ok
78             {
79 5     5 1 24036 my($command, $message) = @_;
80            
81 5 50       26 my(@command) = ref $command ? @$command : ($command);
82 5   66     26 $message ||= "run @command";
83            
84 5         439 require Test::Alien::Run;
85 5         58 my $run = bless {
86             out => '',
87             err => '',
88             exit => 0,
89             sig => 0,
90             cmd => [@command],
91             }, 'Test::Alien::Run';
92            
93 5         17 my $ctx = context();
94 5         313 my $exe = which $command[0];
95 5 100       164 if(defined $exe)
96             {
97 4         9 shift @command;
98 4         16 $run->{cmd} = [$exe, @command];
99 4         8 my @diag;
100 4         5 my $ok = 1;
101 4         4 my($exit, $errno);
102 4     4   168 ($run->{out}, $run->{err}, $exit, $errno) = capture { system $exe, @command; ($?,$!); };
  4         3648  
  4         18339  
103            
104 4 100       3134 if($exit == -1)
    100          
105             {
106 1         9 $ok = 0;
107 1         5 $run->{fail} = "failed to execute: $errno";
108 1         64 push @diag, " failed to execute: $errno";
109             }
110             elsif($exit & 127)
111             {
112 1         6 $ok = 0;
113 1         4 push @diag, " killed with signal: @{[ $exit & 127 ]}";
  1         9  
114 1         6 $run->{sig} = $exit & 127;
115             }
116             else
117             {
118 2         7 $run->{exit} = $exit >> 8;
119             }
120              
121 4         37 $ctx->ok($ok, $message);
122 4 100       635 $ok
123             ? $ctx->note(" using $exe")
124             : $ctx->diag(" using $exe");
125 4         267 $ctx->diag(@diag) for @diag;
126              
127             }
128             else
129             {
130 1         9 $ctx->ok(0, $message);
131 1         131 $ctx->diag(" command not found");
132 1         52 $run->{fail} = 'command not found';
133             }
134            
135 5         118 $ctx->release;
136            
137 5         126 $run;
138             }
139              
140              
141             sub _flags
142             {
143 0     0   0 my($class, $method) = @_;
144 0         0 my $static = "${method}_static";
145 0 0 0     0 $class->can($static) && $class->can('install_type') && $class->install_type eq 'share'
146             ? $class->$static
147             : $class->$method;
148             }
149              
150             sub xs_ok
151             {
152 9     9 1 19198 my $cb;
153 9 100 66     102 $cb = pop if defined $_[-1] && ref $_[-1] eq 'CODE';
154 9         21 my($xs, $message) = @_;
155 9   100     49 $message ||= 'xs';
156              
157 9         98 require ExtUtils::CBuilder;
158 9         133 my $skip = !ExtUtils::CBuilder->new->have_compiler;
159              
160 9 100       433665 if($skip)
161             {
162 4         923 my $ctx = context();
163 4         461 $ctx->skip($message, 'test requires a compiler');
164 4 100       1281 $ctx->skip("$message subtest", 'test requires a compiler') if $cb;
165 4         274 $ctx->release;
166 4         100 return;
167             }
168            
169 5 100       3541 $xs = { xs => $xs } unless ref $xs;
170             # make sure this is a copy because we may
171             # modify it.
172 5         17 $xs->{xs} = "@{[ $xs->{xs} ]}";
  5         50  
173 5   50     60 $xs->{pxs} ||= {};
174 5         13 my $verbose = $xs->{verbose};
175 5         11 my $ok = 1;
176 5         8 my @diag;
177 5         50 my $dir = tempdir( CLEANUP => 1 );
178 5         2768 my $xs_filename = File::Spec->catfile($dir, 'test.xs');
179 5         34 my $c_filename = File::Spec->catfile($dir, 'test.c');
180            
181 5         49 my $ctx = context();
182 5         756 my $module;
183              
184 5 100       50 if($xs->{xs} =~ /\bTA_MODULE\b/)
185             {
186 1         6 our $count;
187 1 50       8 $count = 0 unless defined $count;
188 1         11 my $name = sprintf "Test::Alien::XS::Mod%s", $count++;
189 1         5 my $code = $xs->{xs};
190 1         17 $code =~ s{\bTA_MODULE\b}{$name}g;
191 1         7 $xs->{xs} = $code;
192             }
193              
194             # this regex copied shamefully from ExtUtils::ParseXS
195             # in part because we need the module name to do the bootstrap
196             # and also because if this regex doesn't match then ParseXS
197             # does an exit() which we don't want.
198 5 100       69 if($xs->{xs} =~ /^MODULE\s*=\s*([\w:]+)(?:\s+PACKAGE\s*=\s*([\w:]+))?(?:\s+PREFIX\s*=\s*(\S+))?\s*$/m)
199             {
200 3         16 $module = $1;
201 3 100       25 $ctx->note("detect module name $module") if $verbose;
202             }
203             else
204             {
205 2         4 $ok = 0;
206 2         10 push @diag, ' XS does not have a module decleration that we could find';
207             }
208              
209 5 100       660 if($ok)
210             {
211 3         301 open my $fh, '>', $xs_filename;
212 3         110 print $fh $xs->{xs};
213 3         158 close $fh;
214            
215 3         838 require ExtUtils::ParseXS;
216 3         17113 my $pxs = ExtUtils::ParseXS->new;
217            
218             my($out, $err) = capture_merged {
219 3     3   3673 eval {
220             $pxs->process_file(
221             filename => $xs_filename,
222             output => $c_filename,
223             versioncheck => 0,
224             prototypes => 0,
225 3         12 %{ $xs->{pxs} },
  3         26  
226             );
227             };
228 3         141931 $@;
229 3         211 };
230            
231 3 100       3051 $ctx->note("parse xs $xs_filename => $c_filename") if $verbose;
232 3 100       629 $ctx->note($out) if $verbose;
233 3 50 66     249 $ctx->note("error: $err") if $verbose && $err;
234              
235 3 50       27 unless($pxs->report_error_count == 0)
236             {
237 0         0 $ok = 0;
238 0         0 push @diag, ' ExtUtils::ParseXS failed:';
239 0 0       0 push @diag, " $err" if $err;
240 0         0 push @diag, " $_" for split /\r?\n/, $out;
241             }
242             }
243              
244 5 100       702 if($ok)
245             {
246 3         60 my $cb = ExtUtils::CBuilder->new;
247              
248             my($out, $obj, $err) = capture_merged {
249 3     3   4643 my $obj = eval {
250             $cb->compile(
251             source => $c_filename,
252 3         40 extra_compiler_flags => [shellwords map { _flags $_, 'cflags' } @aliens],
  0         0  
253             );
254             };
255 3         554230 ($obj, $@);
256 3         34367 };
257            
258 3 100       4391 $ctx->note("compile $c_filename") if $verbose;
259 3 100       592 $ctx->note($out) if $verbose;
260 3 50 66     283 $ctx->note($err) if $verbose && $err;
261            
262 3 100       14 unless($obj)
263             {
264 1         2 $ok = 0;
265 1         6 push @diag, ' ExtUtils::CBuilder->compile failed';
266 1 50       7 push @diag, " $err" if $err;
267 1         155 push @diag, " $_" for split /\r?\n/, $out;
268             }
269            
270 3 100       27 if($ok)
271             {
272            
273             my($out, $lib, $err) = capture_merged {
274 2     2   4069 my $lib = eval {
275             $cb->link(
276             objects => [$obj],
277             module_name => $module,
278 2         33 extra_linker_flags => [shellwords map { _flags $_, 'libs' } @aliens],
  0         0  
279             );
280             };
281 2         85644 ($lib, $@);
282 2         1161 };
283            
284 2 100       2777 $ctx->note("link $obj") if $verbose;
285 2 100       591 $ctx->note($out) if $verbose;
286 2 50 66     1520 $ctx->note($err) if $verbose && $err;
287            
288 2 50       11 if($lib)
289             {
290 2 100       24 $ctx->note("created lib $lib") if $xs->{verbose};
291             }
292             else
293             {
294 0         0 $ok = 0;
295 0         0 push @diag, ' ExtUtils::CBuilder->link failed';
296 0 0       0 push @diag, " $err" if $err;
297 0         0 push @diag, " $_" for split /\r?\n/, $out;
298             }
299            
300 2 50       429 if($ok)
301             {
302 2         35 require Config;
303 2         30 my @modparts = split(/::/,$module);
304 2         193 my $dl_dlext = $Config::Config{dlext};
305 2         15 my $modfname = $modparts[-1];
306              
307 2         64 my $libpath = File::Spec->catfile($dir, 'auto', @modparts, "$modfname.$dl_dlext");
308 2         1698 mkpath(dirname($libpath), 0, 0700);
309 2 50       29 move($lib, $libpath) || die "unable to copy $lib => $libpath $!";
310            
311 2         439 pop @modparts;
312 2         42 my $pmpath = File::Spec->catfile($dir, @modparts, "$modfname.pm");
313 2         618 mkpath(dirname($pmpath), 0, 0700);
314 2         237 open my $fh, '>', $pmpath;
315 2         35 print $fh '# line '. __LINE__ . ' "' . __FILE__ . qq("\n) . qq{
316             package $module;
317            
318 1     1   8 use strict;
  1     1   6  
  1         102  
  1         8  
  1         2  
  1         46  
319 1     1   7 use warnings;
  1     1   1  
  1         124  
  1         8  
  1         75  
  1         109  
320             require XSLoader;
321             our \$VERSION = '0.01';
322             XSLoader::load('$module','\$VERSION');
323            
324             1;
325             };
326 2         89 close $fh;
327              
328             {
329 2         5 local @INC = @INC;
  2         39  
330 2         12 unshift @INC, $dir;
331 2         209 eval '# line '. __LINE__ . ' "' . __FILE__ . qq("\n) . qq{
332 1     1   296 use $module;
  1     1   3  
  1         34  
  1         137  
  1         2  
  1         31  
333             };
334             }
335            
336 2 50       92 if(my $error = $@)
337             {
338 0         0 $ok = 0;
339 0         0 push @diag, ' DynaLoader failed';
340 0         0 push @diag, " $error";
341             }
342             }
343             }
344             }
345              
346 5         2325 $ctx->ok($ok, $message);
347 5         2211 $ctx->diag($_) for @diag;
348 5         3168 $ctx->release;
349            
350 5 100       185 if($cb)
351             {
352             $cb = sub {
353 1     1   280 my $ctx = context();
354 1         58 $ctx->plan(0, 'SKIP', "subtest requires xs success");
355 0         0 $ctx->release;
356 3 100       33 } unless $ok;
357              
358 3         30 @_ = ("$message subtest", $cb, 1, $module);
359              
360 3         33 goto \&Test2::API::run_subtest;
361             }
362              
363 2         13 $ok;
364             }
365              
366 2     2 0 3938 sub with_subtest (&) { $_[0]; }
367              
368              
369             sub ffi_ok
370             {
371 0     0 1 0 my $cb;
372 0 0 0     0 $cb = pop if defined $_[-1] && ref $_[-1] eq 'CODE';
373 0         0 my($opt, $message) = @_;
374            
375 0   0     0 $message ||= 'ffi';
376            
377 0         0 my $ok = 1;
378 0         0 my $skip;
379             my $ffi;
380 0         0 my @diag;
381            
382             {
383 0         0 my $min = '0.12'; # the first CPAN release
  0         0  
384 0 0       0 $min = '0.15' if $opt->{ignore_not_found};
385 0 0       0 $min = '0.18' if $opt->{lang};
386 0         0 eval qq{ use FFI::Platypus $min };
387 0 0       0 if($@)
388             {
389 0         0 $ok = 0;
390 0         0 $skip = "Test requires FFI::Platypus $min";
391             }
392             }
393            
394 0 0 0     0 if($ok && $opt->{lang})
395             {
396 0         0 my $class = "FFI::Platypus::Lang::@{[ $opt->{lang} ]}";
  0         0  
397 0         0 eval qq{ use $class () };
398 0 0       0 if($@)
399             {
400 0         0 $ok = 0;
401 0         0 $skip = "Test requires FFI::Platypus::Lang::@{[ $opt->{lang} ]}";
  0         0  
402             }
403             }
404            
405 0 0       0 if($ok)
406             {
407             $ffi = FFI::Platypus->new(
408 0         0 lib => [map { $_->dynamic_libs } @aliens],
409             ignore_not_found => $opt->{ignore_not_found},
410             lang => $opt->{lang},
411 0         0 );
412 0 0       0 foreach my $symbol (@{ $opt->{symbols} || [] })
  0         0  
413             {
414 0 0       0 unless($ffi->find_symbol($symbol))
415             {
416 0         0 $ok = 0;
417 0         0 push @diag, " $symbol not found"
418             }
419             }
420             }
421            
422 0         0 my $ctx = context();
423            
424 0 0       0 if($skip)
425             {
426 0         0 $ctx->skip($message, $skip);
427             }
428             else
429             {
430 0         0 $ctx->ok($ok, $message);
431             }
432 0         0 $ctx->diag($_) for @diag;
433            
434 0         0 $ctx->release;
435              
436 0 0       0 if($cb)
437             {
438             $cb = sub {
439 0     0   0 my $ctx = context();
440 0         0 $ctx->plan(0, 'SKIP', "subtest requires ffi success");
441 0         0 $ctx->release;
442 0 0       0 } unless $ok;
443              
444 0         0 @_ = ("$message subtest", $cb, 1, $ffi);
445              
446 0         0 goto \&Test2::API::run_subtest;
447             }
448            
449 0         0 $ok;
450             }
451              
452             1;
453              
454             __END__