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