2
0

tap-driver.pl 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. #! /usr/bin/env perl
  2. # Copyright (C) 2011-2017 Free Software Foundation, Inc.
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2, or (at your option)
  7. # any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. # As a special exception to the GNU General Public License, if you
  17. # distribute this file as part of a program that contains a
  18. # configuration script generated by Autoconf, you may include it under
  19. # the same distribution terms that you use for the rest of that program.
  20. # This file is maintained in Automake, please report
  21. # bugs to <bug-automake@gnu.org> or send patches to
  22. # <automake-patches@gnu.org>.
  23. # ---------------------------------- #
  24. # Imports, static data, and setup. #
  25. # ---------------------------------- #
  26. use warnings FATAL => 'all';
  27. use strict;
  28. use Getopt::Long ();
  29. use TAP::Parser;
  30. my $VERSION = '2013-12-24.15'; # UTC
  31. my $ME = "tap-driver.pl";
  32. my $USAGE = <<'END';
  33. Usage:
  34. tap-driver --test-name=NAME --log-file=PATH --trs-file=PATH
  35. [--expect-failure={yes|no}] [--color-tests={yes|no}]
  36. [--enable-hard-errors={yes|no}] [--ignore-exit]
  37. [--diagnostic-string=STRING] [--merge|--no-merge]
  38. [--comments|--no-comments] [--] TEST-COMMAND
  39. The '--test-name', '--log-file' and '--trs-file' options are mandatory.
  40. END
  41. my $HELP = "$ME: TAP-aware test driver for Automake testsuite harness." .
  42. "\n" . $USAGE;
  43. # Keep this in sync with 'lib/am/check.am:$(am__tty_colors)'.
  44. my %COLOR = (
  45. red => "\e[0;31m",
  46. grn => "\e[0;32m",
  47. lgn => "\e[1;32m",
  48. blu => "\e[1;34m",
  49. mgn => "\e[0;35m",
  50. brg => "\e[1m",
  51. std => "\e[m",
  52. );
  53. # It's important that NO_PLAN evaluates "false" as a boolean.
  54. use constant NO_PLAN => 0;
  55. use constant EARLY_PLAN => 1;
  56. use constant LATE_PLAN => 2;
  57. # ------------------- #
  58. # Global variables. #
  59. # ------------------- #
  60. my $testno = 0; # Number of test results seen so far.
  61. my $bailed_out = 0; # Whether a "Bail out!" directive has been seen.
  62. my $parser; # TAP parser object (will be initialized later).
  63. # Whether the TAP plan has been seen or not, and if yes, which kind
  64. # it is ("early" is seen before any test result, "late" otherwise).
  65. my $plan_seen = NO_PLAN;
  66. # ----------------- #
  67. # Option parsing. #
  68. # ----------------- #
  69. my %cfg = (
  70. "color-tests" => 0,
  71. "expect-failure" => 0,
  72. "merge" => 0,
  73. "comments" => 0,
  74. "ignore-exit" => 0,
  75. );
  76. my $test_script_name = undef;
  77. my $log_file = undef;
  78. my $trs_file = undef;
  79. my $diag_string = "#";
  80. Getopt::Long::GetOptions
  81. (
  82. 'help' => sub { print $HELP; exit 0; },
  83. 'version' => sub { print "$ME $VERSION\n"; exit 0; },
  84. 'test-name=s' => \$test_script_name,
  85. 'log-file=s' => \$log_file,
  86. 'trs-file=s' => \$trs_file,
  87. 'color-tests=s' => \&bool_opt,
  88. 'expect-failure=s' => \&bool_opt,
  89. 'enable-hard-errors=s' => sub {}, # No-op.
  90. 'diagnostic-string=s' => \$diag_string,
  91. 'comments' => sub { $cfg{"comments"} = 1; },
  92. 'no-comments' => sub { $cfg{"comments"} = 0; },
  93. 'merge' => sub { $cfg{"merge"} = 1; },
  94. 'no-merge' => sub { $cfg{"merge"} = 0; },
  95. 'ignore-exit' => sub { $cfg{"ignore-exit"} = 1; },
  96. ) or exit 1;
  97. # ------------- #
  98. # Prototypes. #
  99. # ------------- #
  100. sub add_test_result ($);
  101. sub bool_opt ($$);
  102. sub colored ($$);
  103. sub copy_in_global_log ();
  104. sub decorate_result ($);
  105. sub extract_tap_comment ($);
  106. sub finish ();
  107. sub get_global_test_result ();
  108. sub get_test_exit_message ();
  109. sub get_test_results ();
  110. sub handle_tap_bailout ($);
  111. sub handle_tap_plan ($);
  112. sub handle_tap_result ($);
  113. sub is_null_string ($);
  114. sub main (@);
  115. sub must_recheck ();
  116. sub report ($;$);
  117. sub setup_io ();
  118. sub setup_parser (@);
  119. sub stringify_result_obj ($);
  120. sub testsuite_error ($);
  121. sub trap_perl_warnings_and_errors ();
  122. sub write_test_results ();
  123. sub yn ($);
  124. # -------------- #
  125. # Subroutines. #
  126. # -------------- #
  127. sub bool_opt ($$)
  128. {
  129. my ($opt, $val) = @_;
  130. if ($val =~ /^(?:y|yes)\z/i)
  131. {
  132. $cfg{$opt} = 1;
  133. }
  134. elsif ($val =~ /^(?:n|no)\z/i)
  135. {
  136. $cfg{$opt} = 0;
  137. }
  138. else
  139. {
  140. die "$ME: invalid argument '$val' for option '$opt'\n";
  141. }
  142. }
  143. # If the given string is undefined or empty, return true, otherwise
  144. # return false. This function is useful to avoid pitfalls like:
  145. # if ($message) { print "$message\n"; }
  146. # which wouldn't print anything if $message is the literal "0".
  147. sub is_null_string ($)
  148. {
  149. my $str = shift;
  150. return ! (defined $str and length $str);
  151. }
  152. # Convert a boolean to a "yes"/"no" string.
  153. sub yn ($)
  154. {
  155. my $bool = shift;
  156. return $bool ? "yes" : "no";
  157. }
  158. TEST_RESULTS :
  159. {
  160. my (@test_results_list, %test_results_seen);
  161. sub add_test_result ($)
  162. {
  163. my $res = shift;
  164. push @test_results_list, $res;
  165. $test_results_seen{$res} = 1;
  166. }
  167. sub get_test_results ()
  168. {
  169. return @test_results_list;
  170. }
  171. # Whether the test script should be re-run by "make recheck".
  172. sub must_recheck ()
  173. {
  174. return grep { !/^(?:XFAIL|PASS|SKIP)$/ } (keys %test_results_seen);
  175. }
  176. # Whether the content of the log file associated to this test should
  177. # be copied into the "global" test-suite.log.
  178. sub copy_in_global_log ()
  179. {
  180. return grep { not $_ eq "PASS" } (keys %test_results_seen);
  181. }
  182. sub get_global_test_result ()
  183. {
  184. return "ERROR"
  185. if $test_results_seen{"ERROR"};
  186. return "FAIL"
  187. if $test_results_seen{"FAIL"} || $test_results_seen{"XPASS"};
  188. return "SKIP"
  189. if scalar keys %test_results_seen == 1 && $test_results_seen{"SKIP"};
  190. return "PASS";
  191. }
  192. }
  193. sub write_test_results ()
  194. {
  195. open RES, ">", $trs_file or die "$ME: opening $trs_file: $!\n";
  196. print RES ":global-test-result: " . get_global_test_result . "\n";
  197. print RES ":recheck: " . yn (must_recheck) . "\n";
  198. print RES ":copy-in-global-log: " . yn (copy_in_global_log) . "\n";
  199. foreach my $result (get_test_results)
  200. {
  201. print RES ":test-result: $result\n";
  202. }
  203. close RES or die "$ME: closing $trs_file: $!\n";
  204. }
  205. sub trap_perl_warnings_and_errors ()
  206. {
  207. $SIG{__WARN__} = $SIG{__DIE__} = sub
  208. {
  209. # Be sure to send the warning/error message to the original stderr
  210. # (presumably the console), not into the log file.
  211. open STDERR, ">&OLDERR";
  212. die @_;
  213. }
  214. }
  215. sub setup_io ()
  216. {
  217. # Redirect stderr and stdout to a temporary log file. Save the
  218. # original stdout stream, since we need it to print testsuite
  219. # progress output. Save original stderr stream, so that we can
  220. # redirect warning and error messages from perl there.
  221. open LOG, ">", $log_file or die "$ME: opening $log_file: $!\n";
  222. open OLDOUT, ">&STDOUT" or die "$ME: duplicating stdout: $!\n";
  223. open OLDERR, ">&STDERR" or die "$ME: duplicating stdout: $!\n";
  224. *OLDERR = *OLDERR; # To pacify a "used only once" warning.
  225. trap_perl_warnings_and_errors;
  226. open STDOUT, ">&LOG" or die "$ME: redirecting stdout: $!\n";
  227. open STDERR, ">&LOG" or die "$ME: redirecting stderr: $!\n";
  228. }
  229. sub setup_parser (@)
  230. {
  231. local $@ = '';
  232. eval { $parser = TAP::Parser->new ({exec => \@_, merge => $cfg{merge}}) };
  233. if ($@ ne '')
  234. {
  235. # Don't use the error message in $@ as set by TAP::Parser, since
  236. # currently it's both too generic (at the point of being basically
  237. # useless) and quite long.
  238. report "ERROR", "- couldn't execute test script";
  239. finish;
  240. }
  241. }
  242. sub get_test_exit_message ()
  243. {
  244. my $wstatus = $parser->wait;
  245. # Watch out for possible internal errors.
  246. die "$ME: couldn't get the exit status of the TAP producer"
  247. unless defined $wstatus;
  248. # Return an undefined value if the producer exited with success.
  249. return unless $wstatus;
  250. # Otherwise, determine whether it exited with error or was terminated
  251. # by a signal.
  252. use POSIX qw (WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG);
  253. if (WIFEXITED ($wstatus))
  254. {
  255. return sprintf "exited with status %d", WEXITSTATUS ($wstatus);
  256. }
  257. elsif (WIFSIGNALED ($wstatus))
  258. {
  259. return sprintf "terminated by signal %d", WTERMSIG ($wstatus);
  260. }
  261. else
  262. {
  263. return "terminated abnormally";
  264. }
  265. }
  266. sub stringify_result_obj ($)
  267. {
  268. my $result_obj = shift;
  269. my $COOKED_PASS = $cfg{"expect-failure"} ? "XPASS": "PASS";
  270. my $COOKED_FAIL = $cfg{"expect-failure"} ? "XFAIL": "FAIL";
  271. if ($result_obj->is_unplanned || $result_obj->number != $testno)
  272. {
  273. return "ERROR";
  274. }
  275. elsif ($plan_seen == LATE_PLAN)
  276. {
  277. return "ERROR";
  278. }
  279. elsif (!$result_obj->directive)
  280. {
  281. return $result_obj->is_ok ? $COOKED_PASS: $COOKED_FAIL;
  282. }
  283. elsif ($result_obj->has_todo)
  284. {
  285. return $result_obj->is_actual_ok ? "XPASS" : "XFAIL";
  286. }
  287. elsif ($result_obj->has_skip)
  288. {
  289. return $result_obj->is_ok ? "SKIP" : $COOKED_FAIL;
  290. }
  291. die "$ME: INTERNAL ERROR"; # NOTREACHED
  292. }
  293. sub colored ($$)
  294. {
  295. my ($color_name, $text) = @_;
  296. return $COLOR{$color_name} . $text . $COLOR{'std'};
  297. }
  298. sub decorate_result ($)
  299. {
  300. my $result = shift;
  301. return $result unless $cfg{"color-tests"};
  302. my %color_for_result =
  303. (
  304. "ERROR" => 'mgn',
  305. "PASS" => 'grn',
  306. "XPASS" => 'red',
  307. "FAIL" => 'red',
  308. "XFAIL" => 'lgn',
  309. "SKIP" => 'blu',
  310. );
  311. if (my $color = $color_for_result{$result})
  312. {
  313. return colored ($color, $result);
  314. }
  315. else
  316. {
  317. return $result; # Don't colorize unknown stuff.
  318. }
  319. }
  320. sub report ($;$)
  321. {
  322. my ($msg, $result, $explanation) = (undef, @_);
  323. if ($result =~ /^(?:X?(?:PASS|FAIL)|SKIP|ERROR)/)
  324. {
  325. $msg = ": $test_script_name";
  326. add_test_result $result;
  327. }
  328. elsif ($result eq "#")
  329. {
  330. $msg = " $test_script_name:";
  331. }
  332. else
  333. {
  334. die "$ME: INTERNAL ERROR"; # NOTREACHED
  335. }
  336. $msg .= " $explanation" if defined $explanation;
  337. $msg .= "\n";
  338. # Output on console might be colorized.
  339. print OLDOUT decorate_result ($result) . $msg;
  340. # Log the result in the log file too, to help debugging (this is
  341. # especially true when said result is a TAP error or "Bail out!").
  342. print $result . $msg;
  343. }
  344. sub testsuite_error ($)
  345. {
  346. report "ERROR", "- $_[0]";
  347. }
  348. sub handle_tap_result ($)
  349. {
  350. $testno++;
  351. my $result_obj = shift;
  352. my $test_result = stringify_result_obj $result_obj;
  353. my $string = $result_obj->number;
  354. my $description = $result_obj->description;
  355. $string .= " $description"
  356. unless is_null_string $description;
  357. if ($plan_seen == LATE_PLAN)
  358. {
  359. $string .= " # AFTER LATE PLAN";
  360. }
  361. elsif ($result_obj->is_unplanned)
  362. {
  363. $string .= " # UNPLANNED";
  364. }
  365. elsif ($result_obj->number != $testno)
  366. {
  367. $string .= " # OUT-OF-ORDER (expecting $testno)";
  368. }
  369. elsif (my $directive = $result_obj->directive)
  370. {
  371. $string .= " # $directive";
  372. my $explanation = $result_obj->explanation;
  373. $string .= " $explanation"
  374. unless is_null_string $explanation;
  375. }
  376. report $test_result, $string;
  377. }
  378. sub handle_tap_plan ($)
  379. {
  380. my $plan = shift;
  381. if ($plan_seen)
  382. {
  383. # Error, only one plan per stream is acceptable.
  384. testsuite_error "multiple test plans";
  385. return;
  386. }
  387. # The TAP plan can come before or after *all* the TAP results; we speak
  388. # respectively of an "early" or a "late" plan. If we see the plan line
  389. # after at least one TAP result has been seen, assume we have a late
  390. # plan; in this case, any further test result seen after the plan will
  391. # be flagged as an error.
  392. $plan_seen = ($testno >= 1 ? LATE_PLAN : EARLY_PLAN);
  393. # If $testno > 0, we have an error ("too many tests run") that will be
  394. # automatically dealt with later, so don't worry about it here. If
  395. # $plan_seen is true, we have an error due to a repeated plan, and that
  396. # has already been dealt with above. Otherwise, we have a valid "plan
  397. # with SKIP" specification, and should report it as a particular kind
  398. # of SKIP result.
  399. if ($plan->directive && $testno == 0)
  400. {
  401. my $explanation = is_null_string ($plan->explanation) ?
  402. undef : "- " . $plan->explanation;
  403. report "SKIP", $explanation;
  404. }
  405. }
  406. sub handle_tap_bailout ($)
  407. {
  408. my ($bailout, $msg) = ($_[0], "Bail out!");
  409. $bailed_out = 1;
  410. $msg .= " " . $bailout->explanation
  411. unless is_null_string $bailout->explanation;
  412. testsuite_error $msg;
  413. }
  414. sub extract_tap_comment ($)
  415. {
  416. my $line = shift;
  417. if (index ($line, $diag_string) == 0)
  418. {
  419. # Strip leading '$diag_string' from '$line'.
  420. $line = substr ($line, length ($diag_string));
  421. # And strip any leading and trailing whitespace left.
  422. $line =~ s/(?:^\s*|\s*$)//g;
  423. # Return what is left (if any).
  424. return $line;
  425. }
  426. return "";
  427. }
  428. sub finish ()
  429. {
  430. write_test_results;
  431. close LOG or die "$ME: closing $log_file: $!\n";
  432. exit 0;
  433. }
  434. sub main (@)
  435. {
  436. setup_io;
  437. setup_parser @_;
  438. while (defined (my $cur = $parser->next))
  439. {
  440. # Verbatim copy any input line into the log file.
  441. print $cur->raw . "\n";
  442. # Parsing of TAP input should stop after a "Bail out!" directive.
  443. next if $bailed_out;
  444. if ($cur->is_plan)
  445. {
  446. handle_tap_plan ($cur);
  447. }
  448. elsif ($cur->is_test)
  449. {
  450. handle_tap_result ($cur);
  451. }
  452. elsif ($cur->is_bailout)
  453. {
  454. handle_tap_bailout ($cur);
  455. }
  456. elsif ($cfg{comments})
  457. {
  458. my $comment = extract_tap_comment ($cur->raw);
  459. report "#", "$comment" if length $comment;
  460. }
  461. }
  462. # A "Bail out!" directive should cause us to ignore any following TAP
  463. # error, as well as a non-zero exit status from the TAP producer.
  464. if (!$bailed_out)
  465. {
  466. if (!$plan_seen)
  467. {
  468. testsuite_error "missing test plan";
  469. }
  470. elsif ($parser->tests_planned != $parser->tests_run)
  471. {
  472. my ($planned, $run) = ($parser->tests_planned, $parser->tests_run);
  473. my $bad_amount = $run > $planned ? "many" : "few";
  474. testsuite_error (sprintf "too %s tests run (expected %d, got %d)",
  475. $bad_amount, $planned, $run);
  476. }
  477. if (!$cfg{"ignore-exit"})
  478. {
  479. my $msg = get_test_exit_message ();
  480. testsuite_error $msg if $msg;
  481. }
  482. }
  483. finish;
  484. }
  485. # ----------- #
  486. # Main code. #
  487. # ----------- #
  488. main @ARGV;
  489. # Local Variables:
  490. # perl-indent-level: 2
  491. # perl-continued-statement-offset: 2
  492. # perl-continued-brace-offset: 0
  493. # perl-brace-offset: 0
  494. # perl-brace-imaginary-offset: 0
  495. # perl-label-offset: -2
  496. # cperl-indent-level: 2
  497. # cperl-brace-offset: 0
  498. # cperl-continued-brace-offset: 0
  499. # cperl-label-offset: -2
  500. # cperl-extra-newline-before-brace: t
  501. # cperl-merge-trailing-else: nil
  502. # cperl-continued-statement-offset: 2
  503. # eval: (add-hook 'write-file-hooks 'time-stamp)
  504. # time-stamp-start: "my $VERSION = "
  505. # time-stamp-format: "'%:y-%02m-%02d.%02H'"
  506. # time-stamp-time-zone: "UTC0"
  507. # time-stamp-end: "; # UTC"
  508. # End: