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