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