]> git.vpit.fr Git - perl/modules/Test-Leaner.git/blob - lib/Test/Leaner.pm
Add tap_stream()/diag_stream()
[perl/modules/Test-Leaner.git] / lib / Test / Leaner.pm
1 package Test::Leaner;
2
3 use 5.006;
4 use strict;
5 use warnings;
6
7 =head1 NAME
8
9 Test::Leaner - A slimmer Test::More for when you favor performance over completeness.
10
11 =head1 VERSION
12
13 Version 0.01
14
15 =cut
16
17 our $VERSION = '0.01';
18
19 =head1 SYNOPSIS
20
21     use Test::Leaner tests => 10_000;
22     for (1 .. 10_000) {
23      ...
24      is $one, 1, "checking situation $_";
25     }
26
27
28 =head1 DESCRIPTION
29
30 When profiling some L<Test::More>-based test script that contained about 10 000 unit tests, I realized that 60% of the time was spent in L<Test::Builder> itself, even though every single test actually involved a costly C<eval STRING>.
31
32 This module aims to be a partial replacement to L<Test::More> in those situations where you want to run a large number of simple tests.
33
34 =cut
35
36 use Exporter ();
37
38 BEGIN {
39  if ($] >= 5.008 and $INC{'threads.pm'}) {
40   my $use_ithreads = do {
41    require Config;
42    no warnings 'once';
43    $Config::Config{useithreads};
44   };
45   if ($use_ithreads) {
46    require threads::shared;
47    *THREADSAFE = sub () { 1 };
48   }
49  }
50  unless (defined &Test::Leaner::THREADSAFE) {
51   *THREADSAFE = sub () { 0 }
52  }
53 }
54
55 my ($TAP_STREAM, $DIAG_STREAM);
56
57 my ($plan, $test, $failed, $no_diag, $done_testing);
58
59 sub NO_PLAN  () { -1 }
60 sub SKIP_ALL () { -2 }
61
62 BEGIN {
63  if (THREADSAFE) {
64   threads::shared::share($_) for $plan, $test, $failed, $no_diag, $done_testing;
65  }
66
67  lock $plan if THREADSAFE;
68
69  $plan   = undef;
70  $test   = 0;
71  $failed = 0;
72 }
73
74 sub carp {
75  my $level = 1 + ($Test::Builder::Level || 0);
76  my ($file, $line) = (caller $level)[1, 2];
77  warn @_, " at $file line $line.\n";
78 }
79
80 sub croak {
81  my $level = 1 + ($Test::Builder::Level || 0);
82  my ($file, $line) = (caller $level)[1, 2];
83  die @_, " at $file line $line.\n";
84 }
85
86 sub sanitize_comment {
87  $_[0] =~ s/\n+\z//;
88  $_[0] =~ s/#/\\#/g;
89  $_[0] =~ s/\n/\n# /g;
90 }
91
92 =head1 FUNCTIONS
93
94 The following functions from L<Test::More> are implemented and exported by default.
95
96 =head2 C<< plan [ tests => $count | 'no_plan' | skip_all => $reason ] >>
97
98 =cut
99
100 sub plan {
101  my ($key, $value) = @_;
102
103  return unless $key;
104
105  lock $plan if THREADSAFE;
106
107  croak("You tried to plan twice") if defined $plan;
108
109  my $plan_str;
110
111  if ($key eq 'no_plan') {
112   croak("no_plan takes no arguments") if $value;
113   $plan       = NO_PLAN;
114  } elsif ($key eq 'tests') {
115   croak("Got an undefined number of tests") unless defined $value;
116   croak("You said to run 0 tests")          unless $value;
117   croak("Number of tests must be a positive integer.  You gave it '$value'")
118                                             unless $value =~ /^\+?[0-9]+$/;
119   $plan       = $value;
120   $plan_str   = "1..$value";
121  } elsif ($key eq 'skip_all') {
122   $plan       = SKIP_ALL;
123   $plan_str   = '1..0 # SKIP';
124   if (defined $value) {
125    sanitize_comment($value);
126    $plan_str .= " $value" if length $value;
127   }
128  } else {
129   my @args = grep defined, $key, $value;
130   croak("plan() doesn't understand @args");
131  }
132
133  if (defined $plan_str) {
134   local $\;
135   print $TAP_STREAM "$plan_str\n";
136  }
137
138  exit 0 if $plan == SKIP_ALL;
139
140  return 1;
141 }
142
143 our @EXPORT = qw<
144  plan
145  skip_all
146  skip
147  done_testing
148  pass
149  fail
150  ok
151  is
152  isnt
153  cmp_ok
154  like
155  unlike
156  diag
157  note
158  BAIL_OUT
159 >;
160
161 sub import {
162  my $class = shift;
163
164  my @imports;
165  my $i = 0;
166  while ($i <= $#_) {
167   my $item = $_[$i];
168   my $splice;
169   if (defined $item) {
170    if ($item eq 'import') {
171     push @imports, @{ $_[$i+1] };
172     $splice  = 2;
173    } elsif ($item eq 'no_diag') {
174     lock $plan if THREADSAFE;
175     $no_diag = 1;
176     $splice  = 1;
177    }
178   }
179   if ($splice) {
180    splice @_, $i, $splice;
181   } else {
182    ++$i;
183   }
184  }
185
186  if (@_) {
187   local $Test::Builder::Level = ($Test::Builder::Level || 0) + 1;
188   &plan;
189  }
190
191  @_ = ($class, @imports);
192  goto &Exporter::import;
193 }
194
195 =head2 C<skip_all $reason>
196
197 =cut
198
199 sub skip_all {
200  @_ = (skip_all => $_[0]);
201  goto &plan;
202 }
203
204 =head2 C<< skip $reason => $count >>
205
206 =cut
207
208 sub skip {
209  my ($reason, $count) = @_;
210
211  lock $plan if THREADSAFE;
212
213  if (not defined $count) {
214   carp("skip() needs to know \$how_many tests are in the block")
215                                       unless defined $plan and $plan == NO_PLAN;
216   $count = 1;
217  } elsif ($count =~ /[^0-9]/) {
218   carp('skip() was passed a non-numeric number of tests.  Did you get the arguments backwards?');
219   $count = 1;
220  }
221
222  for (1 .. $count) {
223   ++$test;
224
225   my $skip_str = "ok $test # skip";
226   if (defined $reason) {
227    sanitize_comment($reason);
228    $skip_str  .= " $reason" if length $reason;
229   }
230
231   local $\;
232   print $TAP_STREAM "$skip_str\n";
233  }
234
235  no warnings 'exiting';
236  last SKIP;
237 }
238
239 =head2 C<done_testing [ $count ]>
240
241 =cut
242
243 sub done_testing {
244  my ($count) = @_;
245
246  lock $plan if THREADSAFE;
247
248  $count = $test unless defined $count;
249  croak("Number of tests must be a positive integer.  You gave it '$count'")
250                                                  unless $count =~ /^\+?[0-9]+$/;
251
252  if (not defined $plan or $plan == NO_PLAN) {
253   $plan         = $count; # $plan can't be NO_PLAN anymore
254   $done_testing = 1;
255   local $\;
256   print $TAP_STREAM "1..$plan\n";
257  } else {
258   if ($done_testing) {
259    @_ = ('done_testing() was already called');
260    goto &fail;
261   } elsif ($plan != $count) {
262    @_ = ("planned to run $plan tests but done_testing() expects $count");
263    goto &fail;
264   }
265  }
266
267  return 1;
268 }
269
270 =head2 C<ok $ok [, $desc ]>
271
272 =cut
273
274 sub ok ($;$) {
275  my ($ok, $desc) = @_;
276
277  lock $plan if THREADSAFE;
278
279  ++$test;
280
281  my $test_str = "ok $test";
282  unless ($ok) {
283   $test_str   = "not $test_str";
284   ++$failed;
285  }
286  if (defined $desc) {
287   sanitize_comment($desc);
288   $test_str .= " - $desc" if length $desc;
289  }
290
291  local $\;
292  print $TAP_STREAM "$test_str\n";
293
294  return $ok;
295 }
296
297 =head2 C<pass [ $desc ]>
298
299 =cut
300
301 sub pass (;$) {
302  unshift @_, 1;
303  goto &ok;
304 }
305
306 =head2 C<fail [ $desc ]>
307
308 =cut
309
310 sub fail (;$) {
311  unshift @_, 0;
312  goto &ok;
313 }
314
315 =head2 C<is $got, $expected [, $desc ]>
316
317 =cut
318
319 sub is ($$;$) {
320  my ($got, $expected, $desc) = @_;
321  no warnings 'uninitialized';
322  @_ = (
323   (not(defined $got xor defined $expected) and $got eq $expected),
324   $desc,
325  );
326  goto &ok;
327 }
328
329 =head2 C<isnt $got, $expected [, $desc ]>
330
331 =cut
332
333 sub isnt ($$;$) {
334  my ($got, $expected, $desc) = @_;
335  no warnings 'uninitialized';
336  @_ = (
337   ((defined $got xor defined $expected) or $got ne $expected),
338   $desc,
339  );
340  goto &ok;
341 }
342
343 my %binops = (
344  'or'  => 'or',
345  'and' => 'and',
346  'xor' => 'xor',
347
348  '||'  => 'hor',
349  '&&'  => 'hand',
350
351  'lt'  => 'lt',
352  'le'  => 'le',
353  'gt'  => 'gt',
354  'ge'  => 'ge',
355  'eq'  => 'eq',
356  'ne'  => 'ne',
357  'cmp' => 'cmp',
358
359  '<'   => 'nlt',
360  '<='  => 'nle',
361  '>'   => 'ngt',
362  '>='  => 'nge',
363  '=='  => 'neq',
364  '!='  => 'nne',
365  '<=>' => 'ncmp',
366
367  '=~'  => 'like',
368  '!~'  => 'unlike',
369  '~~'  => 'smartmatch',
370 );
371
372 my %binop_handlers;
373
374 sub _create_binop_handler {
375  my ($op) = @_;
376  my $name = $binops{$op};
377  croak("Operator $op not supported") unless defined $name;
378  {
379   local $@;
380   eval <<"IS_BINOP";
381 sub is_$name (\$\$;\$) {
382  my (\$got, \$expected, \$desc) = \@_;
383  \@_ = ((\$got $op \$expected), \$desc);
384  goto &ok;
385 }
386 IS_BINOP
387   die $@ if $@;
388  }
389  $binop_handlers{$op} = do {
390   no strict 'refs';
391   \&{__PACKAGE__."::is_$name"};
392  }
393 }
394
395 =head2 C<like $got, $regexp_expected [, $desc ]>
396
397 =head2 C<unlike $got, $regexp_expected, [, $desc ]>
398
399 =cut
400
401 {
402  no warnings 'once';
403  *like   = _create_binop_handler('=~');
404  *unlike = _create_binop_handler('!~');
405 }
406
407 =head2 C<cmp_ok $got, $op, $expected [, $desc ]>
408
409 =cut
410
411 sub cmp_ok ($$$;$) {
412  my ($got, $op, $expected, $desc) = @_;
413  my $handler = $binop_handlers{$op};
414  unless ($handler) {
415   local $Test::More::Level = ($Test::More::Level || 0) + 1;
416   $handler = _create_binop_handler($op);
417  }
418  @_ = ($got, $expected, $desc);
419  goto $handler;
420 }
421
422 sub _diag_fh {
423  my $fh = shift;
424
425  return unless @_;
426
427  lock $plan if THREADSAFE;
428  return if $no_diag;
429
430  my $msg = join '', map { defined($_) ? $_ : 'undef' } @_;
431  sanitize_comment($msg);
432  return unless length $msg;
433
434  local $\;
435  print $fh "# $msg\n";
436
437  return 0;
438 };
439
440 =head2 C<diag @text>
441
442 =cut
443
444 sub diag {
445  unshift @_, $DIAG_STREAM;
446  goto &_diag_fh;
447 }
448
449 =head2 C<note @text>
450
451 =cut
452
453 sub note {
454  unshift @_, $TAP_STREAM;
455  goto &_diag_fh;
456 }
457
458 =head2 C<BAIL_OUT [ $desc ]>
459
460 =cut
461
462 sub BAIL_OUT {
463  my ($desc) = @_;
464
465  lock $plan if THREADSAFE;
466
467  my $bail_out_str = 'Bail out!';
468  if (defined $desc) {
469   sanitize_comment($desc);
470   $bail_out_str  .= "  $desc" if length $desc; # Two spaces
471  }
472
473  local $\;
474  print $TAP_STREAM "$bail_out_str\n";
475
476  exit 255;
477 }
478
479 END {
480  unless ($?) {
481   lock $plan if THREADSAFE;
482
483   if (defined $plan) {
484    if ($failed) {
485     $? = $failed <= 254 ? $failed : 254;
486    } elsif ($plan >= 0) {
487     $? = $test == $plan ? 0 : 255;
488    } elsif ($plan == NO_PLAN) {
489     local $\;
490     print $TAP_STREAM "1..$test\n";
491    }
492   }
493  }
494 }
495
496 =pod
497
498 L</pass>, L</fail>, L</ok>, L</is>, L</isnt>, L</like>, L</unlike> and L</cmp_ok> are all guaranteed to return the truth value of the test.
499 Their L<Test::More> counterparts behave the same, but it is not documented anywhere.
500
501 L<Test::Leaner> also provides some functions of its own, which are never exported.
502
503 =head2 C<tap_stream [ $fh ]>
504
505 Read/write accessor for the filehandle to which the tests are outputted.
506 On write, it also turns autoflush on onto C<$fh>.
507
508 Note that it can only be used as a write accessor before you start any thread, as L<threads::shared> cannot reliably share filehandles.
509
510 Defaults to C<STDOUT>.
511
512 =cut
513
514 sub tap_stream (;*) {
515  if (@_) {
516   $TAP_STREAM = $_[0];
517
518   my $fh = select $TAP_STREAM;
519   $|++;
520   select $fh;
521  }
522
523  return $TAP_STREAM;
524 }
525
526 tap_stream *STDOUT;
527
528 =head2 C<diag_stream [ $fh ]>
529
530 Read/write accessor for the filehandle to which the diagnostics are printed.
531 On write, it also turns autoflush on onto C<$fh>.
532
533 Just like L</tap_stream>, it can only be used as a write accessor before you start any thread, as L<threads::shared> cannot reliably share filehandles.
534
535 Defaults to C<STDERR>.
536
537 =cut
538
539 sub diag_stream (;*) {
540  if (@_) {
541   $DIAG_STREAM = $_[0];
542
543   my $fh = select $DIAG_STREAM;
544   $|++;
545   select $fh;
546  }
547
548  return $DIAG_STREAM;
549 }
550
551 diag_stream *STDERR;
552
553 =head1 DEPENDENCIES
554
555 L<perl> 5.6.
556
557 L<Exporter>, L<Test::More>
558
559 =head1 AUTHOR
560
561 Vincent Pit, C<< <perl at profvince.com> >>, L<http://www.profvince.com>.
562
563 You can contact me by mail or on C<irc.perl.org> (vincent).
564
565 =head1 BUGS
566
567 Please report any bugs or feature requests to C<bug-test-leaner at rt.cpan.org>, or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Test-Leaner>.
568 I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
569
570 =head1 SUPPORT
571
572 You can find documentation for this module with the perldoc command.
573
574     perldoc Test::Leaner
575
576 =head1 COPYRIGHT & LICENSE
577
578 Copyright 2010 Vincent Pit, all rights reserved.
579
580 This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
581
582 =cut
583
584 1; # End of Test::Leaner