]> git.vpit.fr Git - perl/modules/Test-Valgrind.git/blob - lib/Test/Valgrind/Session.pm
7964761af446495c0c8d9bbfdb243e812d3671f1
[perl/modules/Test-Valgrind.git] / lib / Test / Valgrind / Session.pm
1 package Test::Valgrind::Session;
2
3 use strict;
4 use warnings;
5
6 =head1 NAME
7
8 Test::Valgrind::Session - Test::Valgrind session object.
9
10 =head1 VERSION
11
12 Version 1.13
13
14 =cut
15
16 our $VERSION = '1.13';
17
18 =head1 DESCRIPTION
19
20 This class supervises the execution of the C<valgrind> process.
21 It also acts as a dispatcher between the different components.
22
23 =cut
24
25 use File::Spec   ();
26 use Scalar::Util ();
27
28 use Fcntl (); # F_SETFD
29 use POSIX (); # SIGKILL
30
31 use version ();
32
33 use base qw<Test::Valgrind::Carp>;
34
35 =head1 METHODS
36
37 =head2 C<new>
38
39     my $tvs = Test::Valgrind::Session->new(
40      search_dirs => \@search_dirs,
41      valgrind    => $valgrind,  # One candidate
42      valgrind    => \@valgrind, # Several candidates
43      min_version => $min_version,
44      no_def_supp => $no_def_supp,
45      extra_supps => \@extra_supps,
46     );
47
48 The package constructor, which takes several options :
49
50 =over 4
51
52 =item *
53
54 All the directories from C<@search_dirs> will have F<valgrind> appended to create a list of candidates for the C<valgrind> executable.
55
56 Defaults to the current C<PATH> environment variable.
57
58 =item *
59
60 If a simple scalar C<$valgrind> is passed as the value to C<'valgrind'>, it will be the only candidate.
61 C<@search_dirs> will then be ignored.
62
63 If an array refernce C<\@valgrind> is passed, its values will be I<prepended> to the list of the candidates resulting from C<@search_dirs>.
64
65 =item *
66
67 C<$min_version> specifies the minimal C<valgrind> version required.
68 The constructor will croak if it's not able to find an adequate C<valgrind> from the supplied candidates list and search path.
69
70 Defaults to none.
71
72 =item *
73
74 If C<$no_def_supp> is false, C<valgrind> won't read the default suppression file associated with the tool and the command.
75
76 Defaults to false.
77
78 =item *
79
80 C<$extra_supps> is a reference to an array of optional suppression files that will be passed to C<valgrind>.
81
82 Defaults to none.
83
84 =back
85
86 =cut
87
88 sub new {
89  my $class = shift;
90  $class = ref($class) || $class;
91
92  my %args = @_;
93
94  my @paths;
95  my $vg = delete $args{valgrind};
96  if (defined $vg and not ref $vg) {
97   @paths = ($vg);
98  } else {
99   push @paths, @$vg if $vg and ref $vg eq 'ARRAY';
100   my $dirs = delete $args{search_dirs};
101   $dirs = [ File::Spec->path ] unless $dirs;
102   push @paths, map File::Spec->catfile($_, 'valgrind'), @$dirs
103                                                         if ref $dirs eq 'ARRAY';
104  }
105  $class->_croak('Empty valgrind candidates list') unless @paths;
106
107  my $min_version = delete $args{min_version};
108  defined and not ref and $_ = version->new($_) for $min_version;
109
110  my ($valgrind, $version);
111  for (@paths) {
112   next unless -x;
113   my $ver = qx/$_ --version/;
114   if ($ver =~ /^valgrind-(\d+(\.\d+)*)/) {
115    if ($min_version) {
116     $version = version->new($1);
117     next if $version < $min_version;
118    } else {
119     $version = $1;
120    }
121    $valgrind = $_;
122    last;
123   }
124  }
125  $class->_croak('No appropriate valgrind executable could be found')
126                                                        unless defined $valgrind;
127
128  my $extra_supps = delete $args{extra_supps};
129  $extra_supps    = [ ] unless $extra_supps and ref $extra_supps eq 'ARRAY';
130  @$extra_supps   = grep { defined && -f $_ && -r _ } @$extra_supps;
131
132  bless {
133   valgrind    => $valgrind,
134   version     => $version,
135   no_def_supp => delete($args{no_def_supp}),
136   extra_supps => $extra_supps,
137  }, $class;
138 }
139
140 =head2 C<valgrind>
141
142     my $valgrind_path = $tvs->valgrind;
143
144 The path to the selected C<valgrind> executable.
145
146 =head2 C<version>
147
148     my $valgrind_version = $tvs->version;
149
150 The L<version> object associated to the selected C<valgrind>.
151
152 =cut
153
154 sub version {
155  my ($self) = @_;
156
157  my $version = $self->{version};
158  $self->{version} = $version = version->new($version) unless ref $version;
159
160  return $version;
161 }
162
163 =head2 C<no_def_supp>
164
165     my $no_def_supp = $tvs->no_def_supp;
166
167 Read-only accessor for the C<no_def_supp> option.
168
169 =cut
170
171 eval "sub $_ { \$_[0]->{$_} }" for qw<valgrind no_def_supp>;
172
173 =head2 C<extra_supps>
174
175     my @extra_supps = $tvs->extra_supps;
176
177 Read-only accessor for the C<extra_supps> option.
178
179 =cut
180
181 sub extra_supps { @{$_[0]->{extra_supps} || []} }
182
183 =head2 C<run>
184
185     $tvs->run(
186      action  => $action,
187      tool    => $tool,
188      command => $command,
189     );
190
191 Runs the command C<$command> through C<valgrind> with the tool C<$tool>, which will report to the action C<$action>.
192
193 If the command is a L<Test::Valgrind::Command::Aggregate> object, the action and the tool will be initialized once before running all the aggregated commands.
194
195 =cut
196
197 sub run {
198  my $self = shift;
199
200  my %args = @_;
201
202  $self->start(%args);
203  my $guard = Test::Valgrind::Session::Guard->new(sub { $self->finish });
204
205  $self->_run($args{command});
206 }
207
208 sub _run {
209  my ($self, $cmd) = @_;
210
211  if ($cmd->isa('Test::Valgrind::Command::Aggregate')) {
212   $self->_run($_) for $cmd->commands;
213   return;
214  }
215
216  $self->command($cmd);
217
218  $self->report($self->report_class->new_diag(
219   'Using valgrind ' . $self->version . ' located at ' . $self->valgrind
220  ));
221
222  my $env = $self->command->env($self);
223
224  my @supp_args;
225  if ($self->do_suppressions) {
226   push @supp_args, '--gen-suppressions=all';
227  } elsif (not $self->no_def_supp) {
228   my $def_supp = $self->def_supp_file;
229   if (defined $def_supp and not -e $def_supp) {
230    $self->report($self->report_class->new_diag(
231     "Generating suppressions..."
232    ));
233    require Test::Valgrind::Suppressions;
234    Test::Valgrind::Suppressions->generate(
235     tool    => $self->tool,
236     command => $self->command,
237     target  => $def_supp,
238    );
239    $self->_croak('Couldn\'t generate suppressions') unless -e $def_supp;
240    $self->report($self->report_class->new_diag(
241     "Suppressions for this perl stored in $def_supp"
242    ));
243   }
244   for ($self->suppressions) {
245    next unless -e $_;
246    $self->report($self->report_class->new_diag("Using suppression file $_"));
247    push @supp_args, "--suppressions=$_";
248   }
249  }
250
251  pipe my $vrdr, my $vwtr or $self->_croak("pipe(\$vrdr, \$vwtr): $!");
252  {
253   my $oldfh = select $vrdr;
254   $|++;
255   select $oldfh;
256  }
257
258  my $pid = fork;
259  $self->_croak("fork(): $!") unless defined $pid;
260
261  if ($pid == 0) {
262   eval 'setpgrp 0, 0';
263   close $vrdr or $self->_croak("close(\$vrdr): $!");
264   fcntl $vwtr, Fcntl::F_SETFD(), 0
265                               or $self->_croak("fcntl(\$vwtr, F_SETFD, 0): $!");
266
267   my @args = (
268    $self->valgrind,
269    $self->tool->args($self),
270    @supp_args,
271    $self->parser->args($self, $vwtr),
272    $self->command->args($self),
273   );
274
275 #  $self->report($self->report_class->new_diag("@args"));
276
277   exec { $args[0] } @args or $self->_croak("exec @args: $!");
278  }
279
280  local $SIG{INT} = sub {
281   kill -(POSIX::SIGKILL()) => $pid;
282   waitpid $pid, 0;
283   die 'interrupted';
284  };
285
286  close $vwtr or $self->_croak("close(\$vwtr): $!");
287
288  $self->parser->parse($self, $vrdr);
289
290  $self->{exit_code} = (waitpid($pid, 0) == $pid) ? $? >> 8 : 255;
291
292  close $vrdr or $self->_croak("close(\$vrdr): $!");
293
294  return;
295 }
296
297 sub Test::Valgrind::Session::Guard::new     { bless \($_[1]), $_[0] }
298
299 sub Test::Valgrind::Session::Guard::DESTROY { ${$_[0]}->() }
300
301 =head2 C<action>
302
303 Read-only accessor for the C<action> associated to the current run.
304
305 =head2 C<tool>
306
307 Read-only accessor for the C<tool> associated to the current run.
308
309 =head2 C<parser>
310
311 Read-only accessor for the C<parser> associated to the current tool.
312
313 =head2 C<command>
314
315 Read-only accessor for the C<command> associated to the current run.
316
317 =cut
318
319 my @members;
320 BEGIN {
321  @members = qw<action tool command parser>;
322  for (@members) {
323   eval "sub $_ { \@_ <= 1 ? \$_[0]->{$_} : (\$_[0]->{$_} = \$_[1]) }";
324   die if $@;
325  }
326 }
327
328 =head2 C<do_suppressions>
329
330 Forwards to C<< ->action->do_suppressions >>.
331
332 =cut
333
334 sub do_suppressions { $_[0]->action->do_suppressions }
335
336 =head2 C<parser_class>
337
338 Calls C<< ->tool->parser_class >> with the current session object as the unique argument.
339
340 =cut
341
342 sub parser_class { $_[0]->tool->parser_class($_[0]) }
343
344 =head2 C<report_class>
345
346 Calls C<< ->tool->report_class >> with the current session object as the unique argument.
347
348 =cut
349
350 sub report_class { $_[0]->tool->report_class($_[0]) }
351
352 =head2 C<def_supp_file>
353
354 Returns an absolute path to the default suppression file associated to the current session.
355
356 C<undef> will be returned as soon as any of C<< ->command->suppressions_tag >> or C<< ->tool->suppressions_tag >> are also C<undef>.
357 Otherwise, the file part of the name is builded by joining those two together, and the directory part is roughly F<< File::HomeDir->my_home / .perl / Test-Valgrind / suppressions / $VERSION >>.
358
359 =cut
360
361 sub def_supp_file {
362  my ($self) = @_;
363
364  my $tool_tag = $self->tool->suppressions_tag($self);
365  return unless defined $tool_tag;
366
367  my $cmd_tag = $self->command->suppressions_tag($self);
368  return unless defined $cmd_tag;
369
370  require File::HomeDir; # So that it's not needed at configure time.
371
372  return File::Spec->catfile(
373   File::HomeDir->my_home,
374   '.perl',
375   'Test-Valgrind',
376   'suppressions',
377   $VERSION,
378   "$tool_tag-$cmd_tag.supp",
379  );
380 }
381
382 =head2 C<suppressions>
383
384     my @suppressions = $tvs->suppressions;
385
386 Returns the list of all the suppressions that will be passed to C<valgrind>.
387 Honors L</no_def_supp> and L</extra_supps>.
388
389 =cut
390
391 sub suppressions {
392  my ($self) = @_;
393
394  my @supps;
395  unless ($self->no_def_supp) {
396   my $def_supp = $self->def_supp_file;
397   push @supps, $def_supp if defined $def_supp;
398  }
399  push @supps, $self->extra_supps;
400
401  return @supps;
402 }
403
404 =head2 C<start>
405
406     $tvs->start(
407      action  => $action,
408      tool    => $tool,
409      command => $command,
410     );
411
412 Starts the action and tool associated to the current run.
413 It's automatically called at the beginning of L</run>.
414
415 =cut
416
417 sub start {
418  my $self = shift;
419
420  my %args = @_;
421
422  for (qw<action tool command>) {
423   my $base = 'Test::Valgrind::' . ucfirst;
424   my $value = $args{$_};
425   $self->_croak("Invalid $_") unless Scalar::Util::blessed($value)
426                                                          and $value->isa($base);
427   $self->$_($args{$_})
428  }
429
430  delete @{$self}{qw<last_status exit_code>};
431
432  $self->tool->start($self);
433  $self->parser($self->parser_class->new)->start($self);
434  $self->action->start($self);
435
436  return;
437 }
438
439 =head2 C<abort>
440
441     $tvs->abort($msg);
442
443 Forwards to C<< ->action->abort >> after unshifting the session object to the argument list.
444
445 =cut
446
447 sub abort {
448  my $self = shift;
449
450  $self->action->abort($self, @_);
451 }
452
453 =head2 C<report>
454
455     $tvs->report($report);
456
457 Forwards to C<< ->action->report >> after unshifting the session object to the argument list.
458
459 =cut
460
461 sub report {
462  my ($self, $report) = @_;
463
464  return unless defined $report;
465
466  for my $handler (qw<tool command>) {
467   $report = $self->$handler->filter($self, $report);
468   return unless defined $report;
469  }
470
471  $self->action->report($self, $report);
472 }
473
474 =head2 C<finish>
475
476     $tvs->finish;
477
478 Finishes the action and tool associated to the current run.
479 It's automatically called at the end of L</run>.
480
481 =cut
482
483 sub finish {
484  my ($self) = @_;
485
486  my $action = $self->action;
487
488  $action->finish($self);
489  $self->parser->finish($self);
490  $self->tool->finish($self);
491
492  my $status = $action->status($self);
493  $self->{last_status} = defined $status ? $status : $self->{exit_code};
494
495  $self->$_(undef) for @members;
496
497  return;
498 }
499
500 =head2 C<status>
501
502     my $status = $tvs->status;
503
504 Returns the status code of the last run of the session.
505
506 =cut
507
508 sub status { $_[0]->{last_status} }
509
510 =head1 SEE ALSO
511
512 L<Test::Valgrind>, L<Test::Valgrind::Action>, L<Test::Valgrind::Command>, L<Test::Valgrind::Tool>, L<Test::Valgrind::Parser>.
513
514 L<version>, L<File::HomeDir>.
515
516 =head1 AUTHOR
517
518 Vincent Pit, C<< <perl at profvince.com> >>, L<http://www.profvince.com>.
519
520 You can contact me by mail or on C<irc.perl.org> (vincent).
521
522 =head1 BUGS
523
524 Please report any bugs or feature requests to C<bug-test-valgrind at rt.cpan.org>, or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Test-Valgrind>.
525 I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
526
527 =head1 SUPPORT
528
529 You can find documentation for this module with the perldoc command.
530
531     perldoc Test::Valgrind::Session
532
533 =head1 COPYRIGHT & LICENSE
534
535 Copyright 2009,2010,2011 Vincent Pit, all rights reserved.
536
537 This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
538
539 =cut
540
541 1; # End of Test::Valgrind::Session