File Coverage

blib/lib/Test/Alien.pm
Criterion Covered Total %
statement 235 300 78.3
branch 66 112 58.9
condition 16 35 45.7
subroutine 34 37 91.8
pod 5 6 83.3
total 356 490 72.6


line stmt bran cond sub pod time code
1             package Test::Alien;
2              
3 7     7   965892 use strict;
  7         17  
  7         180  
4 7     7   31 use warnings;
  7         13  
  7         138  
5 7     7   123 use 5.008001;
  7         22  
6 7     7   4852 use Env qw( @PATH );
  7         13194  
  7         44  
7 7     7   3212 use File::Which 1.10 qw( which );
  7         4489  
  7         367  
8 7     7   3498 use if $^O ne 'MSWin32', 'Capture::Tiny' => 'capture_merged';
  7         87  
  7         36  
9 7     7   89601 use Capture::Tiny qw( capture );
  7         16  
  7         196  
10 7     7   34 use File::Temp ();
  7         14  
  7         95  
11 7     7   31 use Carp qw( croak );
  7         64  
  7         241  
12 7     7   50 use File::Spec;
  7         15  
  7         125  
13 7     7   30 use File::Basename qw( dirname );
  7         15  
  7         327  
14 7     7   34 use File::Path qw( mkpath );
  7         13  
  7         239  
15 7     7   2854 use File::Copy qw( move );
  7         11596  
  7         325  
16 7     7   1934 use Text::ParseWords qw( shellwords );
  7         4738  
  7         370  
17 7     7   46 use Test2::API qw( context run_subtest );
  7         14  
  7         309  
18 7     7   38 use base qw( Exporter );
  7         12  
  7         708  
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   14643 } 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.15'; # VERSION
37              
38              
39             our @aliens;
40              
41             sub alien_ok ($;$)
42             {
43 5     5 1 13610 my($alien, $message) = @_;
44              
45 5 100       20 my $name = ref $alien ? ref($alien) . '[instance]' : $alien;
46            
47 5         17 my @methods = qw( cflags libs dynamic_libs bin_dir );
48 5   33     47 $message ||= "$name responds to: @methods";
49 5         13 my @missing = grep { ! $alien->can($_) } @methods;
  20         127  
50            
51 5         12 my $ok = !@missing;
52 5         16 my $ctx = context();
53 5         334 $ctx->ok($ok, $message);
54 5         817 $ctx->diag(" missing method $_") for @missing;
55 5         250 $ctx->release;
56            
57 5 100       124 if($ok)
58             {
59 4         10 push @aliens, $alien;
60 4         18 unshift @PATH, $alien->bin_dir;
61             }
62            
63 5         141 $ok;
64             }
65              
66              
67             sub synthetic
68             {
69 7     7 1 3348 my($opt) = @_;
70 7   100     27 $opt ||= {};
71 7         25 my %alien = %$opt;
72 7         484 require Test::Alien::Synthetic;
73 7         58 bless \%alien, 'Test::Alien::Synthetic',
74             }
75              
76              
77             sub run_ok
78             {
79 5     5 1 22178 my($command, $message) = @_;
80            
81 5 50       23 my(@command) = ref $command ? @$command : ($command);
82 5   66     25 $message ||= "run @command";
83            
84 5         503 require Test::Alien::Run;
85 5         45 my $run = bless {
86             out => '',
87             err => '',
88             exit => 0,
89             sig => 0,
90             cmd => [@command],
91             }, 'Test::Alien::Run';
92            
93 5         20 my $ctx = context();
94 5         365 my $exe = which $command[0];
95 5 100       170 if(defined $exe)
96             {
97 4         8 shift @command;
98 4         17 $run->{cmd} = [$exe, @command];
99 4         9 my @diag;
100 4         9 my $ok = 1;
101 4         8 my($exit, $errno);
102 4     4   164 ($run->{out}, $run->{err}, $exit, $errno) = capture { system $exe, @command; ($?,$!); };
  4         4074  
  4         23782  
103            
104 4 100       3278 if($exit == -1)
    100          
105             {
106 1         6 $ok = 0;
107 1         4 $run->{fail} = "failed to execute: $errno";
108 1         43 push @diag, " failed to execute: $errno";
109             }
110             elsif($exit & 127)
111             {
112 1         5 $ok = 0;
113 1         4 push @diag, " killed with signal: @{[ $exit & 127 ]}";
  1         7  
114 1         4 $run->{sig} = $exit & 127;
115             }
116             else
117             {
118 2         8 $run->{exit} = $exit >> 8;
119             }
120              
121 4         33 $ctx->ok($ok, $message);
122 4 100       644 $ok
123             ? $ctx->note(" using $exe")
124             : $ctx->diag(" using $exe");
125 4         329 $ctx->diag(@diag) for @diag;
126              
127             }
128             else
129             {
130 1         8 $ctx->ok(0, $message);
131 1         147 $ctx->diag(" command not found");
132 1         62 $run->{fail} = 'command not found';
133             }
134            
135 5         144 $ctx->release;
136            
137 5         166 $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 21618 my $cb;
153 9 100 66     79 $cb = pop if defined $_[-1] && ref $_[-1] eq 'CODE';
154 9         26 my($xs, $message) = @_;
155 9   100     47 $message ||= 'xs';
156              
157 9         66 require ExtUtils::CBuilder;
158 9         101 my $skip = !ExtUtils::CBuilder->new->have_compiler;
159              
160 9 100       357973 if($skip)
161             {
162 4         480 my $ctx = context();
163 4         321 $ctx->skip($message, 'test requires a compiler');
164 4 100       1008 $ctx->skip("$message subtest", 'test requires a compiler') if $cb;
165 4         454 $ctx->release;
166 4         112 return;
167             }
168            
169 5 100       2865 $xs = { xs => $xs } unless ref $xs;
170             # make sure this is a copy because we may
171             # modify it.
172 5         16 $xs->{xs} = "@{[ $xs->{xs} ]}";
  5         47  
173 5   50     62 $xs->{pxs} ||= {};
174 5         14 my $verbose = $xs->{verbose};
175 5         16 my $ok = 1;
176 5         10 my @diag;
177 5         37 my $dir = _tempdir( CLEANUP => 1, TEMPLATE => 'testalienXXXXX' );
178 5         143 my $xs_filename = File::Spec->catfile($dir, 'test.xs');
179 5         45 my $c_filename = File::Spec->catfile($dir, 'test.c');
180            
181 5         62 my $ctx = context();
182 5         857 my $module;
183              
184 5 100       51 if($xs->{xs} =~ /\bTA_MODULE\b/)
185             {
186 1         4 our $count;
187 1 50       10 $count = 0 unless defined $count;
188 1         8 my $name = sprintf "Test::Alien::XS::Mod%s", $count++;
189 1         4 my $code = $xs->{xs};
190 1         18 $code =~ s{\bTA_MODULE\b}{$name}g;
191 1         5 $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       71 if($xs->{xs} =~ /^MODULE\s*=\s*([\w:]+)(?:\s+PACKAGE\s*=\s*([\w:]+))?(?:\s+PREFIX\s*=\s*(\S+))?\s*$/m)
199             {
200 3         22 $module = $1;
201 3 100       28 $ctx->note("detect module name $module") if $verbose;
202             }
203             else
204             {
205 2         6 $ok = 0;
206 2         11 push @diag, ' XS does not have a module decleration that we could find';
207             }
208              
209 5 100       355 if($ok)
210             {
211 3         198 open my $fh, '>', $xs_filename;
212 3         32 print $fh $xs->{xs};
213 3         84 close $fh;
214            
215 3         773 require ExtUtils::ParseXS;
216 3         16190 my $pxs = ExtUtils::ParseXS->new;
217            
218             my($out, $err) = capture_merged {
219 3     3   3550 eval {
220             $pxs->process_file(
221             filename => $xs_filename,
222             output => $c_filename,
223             versioncheck => 0,
224             prototypes => 0,
225 3         13 %{ $xs->{pxs} },
  3         21  
226             );
227             };
228 3         112089 $@;
229 3         191 };
230            
231 3 100       2399 $ctx->note("parse xs $xs_filename => $c_filename") if $verbose;
232 3 100       241 $ctx->note($out) if $verbose;
233 3 50 66     132 $ctx->note("error: $err") if $verbose && $err;
234              
235 3 50       18 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       278 if($ok)
245             {
246 3         42 my $cb = ExtUtils::CBuilder->new;
247              
248             my($out, $obj, $err) = capture_merged {
249 3     3   3266 my $obj = eval {
250             $cb->compile(
251             source => $c_filename,
252 3         32 extra_compiler_flags => [shellwords map { _flags $_, 'cflags' } @aliens],
  0         0  
253             );
254             };
255 3         339325 ($obj, $@);
256 3         23522 };
257            
258 3 100       3054 $ctx->note("compile $c_filename") if $verbose;
259 3 100       493 $ctx->note($out) if $verbose;
260 3 50 66     237 $ctx->note($err) if $verbose && $err;
261            
262 3 100       15 unless($obj)
263             {
264 1         3 $ok = 0;
265 1         5 push @diag, ' ExtUtils::CBuilder->compile failed';
266 1 50       9 push @diag, " $err" if $err;
267 1         105 push @diag, " $_" for split /\r?\n/, $out;
268             }
269            
270 3 100       67 if($ok)
271             {
272            
273             my($out, $lib, $err) = capture_merged {
274 2     2   2247 my $lib = eval {
275             $cb->link(
276             objects => [$obj],
277             module_name => $module,
278 2         22 extra_linker_flags => [shellwords map { _flags $_, 'libs' } @aliens],
  0         0  
279             );
280             };
281 2         46187 ($lib, $@);
282 2         138 };
283            
284 2 100       2029 $ctx->note("link $obj") if $verbose;
285 2 100       551 $ctx->note($out) if $verbose;
286 2 50 66     285 $ctx->note($err) if $verbose && $err;
287            
288 2 50       11 if($lib)
289             {
290 2 100       21 $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       306 if($ok)
301             {
302 2         27 require Config;
303 2         29 my @modparts = split(/::/,$module);
304 2         40 my $dl_dlext = $Config::Config{dlext};
305 2         8 my $modfname = $modparts[-1];
306              
307 2         54 my $libpath = File::Spec->catfile($dir, 'auto', @modparts, "$modfname.$dl_dlext");
308 2         902 mkpath(dirname($libpath), 0, 0700);
309 2 50       24 move($lib, $libpath) || die "unable to copy $lib => $libpath $!";
310            
311 2         191 pop @modparts;
312 2         29 my $pmpath = File::Spec->catfile($dir, @modparts, "$modfname.pm");
313 2         350 mkpath(dirname($pmpath), 0, 0700);
314 2         116 open my $fh, '>', $pmpath;
315 2         33 print $fh '# line '. __LINE__ . ' "' . __FILE__ . qq("\n) . qq{
316             package $module;
317            
318 1     1   7 use strict;
  1     1   5  
  1         37  
  1         6  
  1         3  
  1         28  
319 1     1   6 use warnings;
  1     1   4  
  1         96  
  1         6  
  1         2  
  1         77  
320             require XSLoader;
321             our \$VERSION = '0.01';
322             XSLoader::load('$module','\$VERSION');
323            
324             1;
325             };
326 2         53 close $fh;
327              
328             {
329 2         7 local @INC = @INC;
  2         26  
330 2         23 unshift @INC, $dir;
331 2         159 eval '# line '. __LINE__ . ' "' . __FILE__ . qq("\n) . qq{
332 1     1   121 use $module;
  1     1   4  
  1         23  
  1         97  
  1         5  
  1         21  
333             };
334             }
335            
336 2 50       33 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         1591 $ctx->ok($ok, $message);
347 5         1738 $ctx->diag($_) for @diag;
348 5         4310 $ctx->release;
349            
350 5 100       193 if($cb)
351             {
352             $cb = sub {
353 1     1   316 my $ctx = context();
354 1         100 $ctx->plan(0, 'SKIP', "subtest requires xs success");
355 0         0 $ctx->release;
356 3 100       23 } unless $ok;
357              
358 3         23 @_ = ("$message subtest", $cb, 1, $module);
359              
360 3         26 goto \&Test2::API::run_subtest;
361             }
362              
363 2         11 $ok;
364             }
365              
366 2     2 0 3039 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             sub _tempdir
453             {
454             # makes sure /tmp or whatever isn't mounted noexec,
455             # which will cause xs_ok tests to fail.
456              
457 5     5   55 my $dir = File::Temp::tempdir(@_);
458              
459 5 50       2164 if($^O ne 'MSWin32')
460             {
461 5         75 my $filename = File::Spec->catfile($dir, 'foo.pl');
462 5         16 my $fh;
463 5         340 open $fh, '>', $filename;
464 5         110 print $fh "#!$^X";
465 5         133 close $fh;
466 5         77 chmod 0755, $filename;
467 5         15381 system $filename, 'foo';
468 5 50       173 if($?)
469             {
470 0         0 $dir = File::Temp::tempdir( DIR => File::Spec->curdir );
471             }
472             }
473            
474 5         60 $dir;
475             }
476              
477             1;
478              
479             __END__