tap-driver.sh (19461B)
1#! /bin/sh 2# Copyright (C) 2011-2021 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 <https://www.gnu.org/licenses/>. 16 17# As a special exception to the GNU General Public License, if you 18# distribute this file as part of a program that contains a 19# configuration script generated by Autoconf, you may include it under 20# the same distribution terms that you use for the rest of that program. 21 22# This file is maintained in Automake, please report 23# bugs to <bug-automake@gnu.org> or send patches to 24# <automake-patches@gnu.org>. 25 26scriptversion=2013-12-23.17; # UTC 27 28# Make unconditional expansion of undefined variables an error. This 29# helps a lot in preventing typo-related bugs. 30set -u 31 32me=tap-driver.sh 33 34fatal () 35{ 36 echo "$me: fatal: $*" >&2 37 exit 1 38} 39 40usage_error () 41{ 42 echo "$me: $*" >&2 43 print_usage >&2 44 exit 2 45} 46 47print_usage () 48{ 49 cat <<END 50Usage: 51 tap-driver.sh --test-name=NAME --log-file=PATH --trs-file=PATH 52 [--expect-failure={yes|no}] [--color-tests={yes|no}] 53 [--enable-hard-errors={yes|no}] [--ignore-exit] 54 [--diagnostic-string=STRING] [--merge|--no-merge] 55 [--comments|--no-comments] [--] TEST-COMMAND 56The '--test-name', '-log-file' and '--trs-file' options are mandatory. 57END 58} 59 60# TODO: better error handling in option parsing (in particular, ensure 61# TODO: $log_file, $trs_file and $test_name are defined). 62test_name= # Used for reporting. 63log_file= # Where to save the result and output of the test script. 64trs_file= # Where to save the metadata of the test run. 65expect_failure=0 66color_tests=0 67merge=0 68ignore_exit=0 69comments=0 70diag_string='#' 71while test $# -gt 0; do 72 case $1 in 73 --help) print_usage; exit $?;; 74 --version) echo "$me $scriptversion"; exit $?;; 75 --test-name) test_name=$2; shift;; 76 --log-file) log_file=$2; shift;; 77 --trs-file) trs_file=$2; shift;; 78 --color-tests) color_tests=$2; shift;; 79 --expect-failure) expect_failure=$2; shift;; 80 --enable-hard-errors) shift;; # No-op. 81 --merge) merge=1;; 82 --no-merge) merge=0;; 83 --ignore-exit) ignore_exit=1;; 84 --comments) comments=1;; 85 --no-comments) comments=0;; 86 --diagnostic-string) diag_string=$2; shift;; 87 --) shift; break;; 88 -*) usage_error "invalid option: '$1'";; 89 esac 90 shift 91done 92 93test $# -gt 0 || usage_error "missing test command" 94 95case $expect_failure in 96 yes) expect_failure=1;; 97 *) expect_failure=0;; 98esac 99 100if test $color_tests = yes; then 101 init_colors=' 102 color_map["red"]="[0;31m" # Red. 103 color_map["grn"]="[0;32m" # Green. 104 color_map["lgn"]="[1;32m" # Light green. 105 color_map["blu"]="[1;34m" # Blue. 106 color_map["mgn"]="[0;35m" # Magenta. 107 color_map["std"]="[m" # No color. 108 color_for_result["ERROR"] = "mgn" 109 color_for_result["PASS"] = "grn" 110 color_for_result["XPASS"] = "red" 111 color_for_result["FAIL"] = "red" 112 color_for_result["XFAIL"] = "lgn" 113 color_for_result["SKIP"] = "blu"' 114else 115 init_colors='' 116fi 117 118# :; is there to work around a bug in bash 3.2 (and earlier) which 119# does not always set '$?' properly on redirection failure. 120# See the Autoconf manual for more details. 121:;{ 122 ( 123 # Ignore common signals (in this subshell only!), to avoid potential 124 # problems with Korn shells. Some Korn shells are known to propagate 125 # to themselves signals that have killed a child process they were 126 # waiting for; this is done at least for SIGINT (and usually only for 127 # it, in truth). Without the `trap' below, such a behaviour could 128 # cause a premature exit in the current subshell, e.g., in case the 129 # test command it runs gets terminated by a SIGINT. Thus, the awk 130 # script we are piping into would never seen the exit status it 131 # expects on its last input line (which is displayed below by the 132 # last `echo $?' statement), and would thus die reporting an internal 133 # error. 134 # For more information, see the Autoconf manual and the threads: 135 # <https://lists.gnu.org/archive/html/bug-autoconf/2011-09/msg00004.html> 136 # <http://mail.opensolaris.org/pipermail/ksh93-integration-discuss/2009-February/004121.html> 137 trap : 1 3 2 13 15 138 if test $merge -gt 0; then 139 exec 2>&1 140 else 141 exec 2>&3 142 fi 143 "$@" 144 echo $? 145 ) | LC_ALL=C ${AM_TAP_AWK-awk} \ 146 -v me="$me" \ 147 -v test_script_name="$test_name" \ 148 -v log_file="$log_file" \ 149 -v trs_file="$trs_file" \ 150 -v expect_failure="$expect_failure" \ 151 -v merge="$merge" \ 152 -v ignore_exit="$ignore_exit" \ 153 -v comments="$comments" \ 154 -v diag_string="$diag_string" \ 155' 156# TODO: the usages of "cat >&3" below could be optimized when using 157# GNU awk, and/on on systems that supports /dev/fd/. 158 159# Implementation note: in what follows, `result_obj` will be an 160# associative array that (partly) simulates a TAP result object 161# from the `TAP::Parser` perl module. 162 163## ----------- ## 164## FUNCTIONS ## 165## ----------- ## 166 167function fatal(msg) 168{ 169 print me ": " msg | "cat >&2" 170 exit 1 171} 172 173function abort(where) 174{ 175 fatal("internal error " where) 176} 177 178# Convert a boolean to a "yes"/"no" string. 179function yn(bool) 180{ 181 return bool ? "yes" : "no"; 182} 183 184function add_test_result(result) 185{ 186 if (!test_results_index) 187 test_results_index = 0 188 test_results_list[test_results_index] = result 189 test_results_index += 1 190 test_results_seen[result] = 1; 191} 192 193# Whether the test script should be re-run by "make recheck". 194function must_recheck() 195{ 196 for (k in test_results_seen) 197 if (k != "XFAIL" && k != "PASS" && k != "SKIP") 198 return 1 199 return 0 200} 201 202# Whether the content of the log file associated to this test should 203# be copied into the "global" test-suite.log. 204function copy_in_global_log() 205{ 206 for (k in test_results_seen) 207 if (k != "PASS") 208 return 1 209 return 0 210} 211 212function get_global_test_result() 213{ 214 if ("ERROR" in test_results_seen) 215 return "ERROR" 216 if ("FAIL" in test_results_seen || "XPASS" in test_results_seen) 217 return "FAIL" 218 all_skipped = 1 219 for (k in test_results_seen) 220 if (k != "SKIP") 221 all_skipped = 0 222 if (all_skipped) 223 return "SKIP" 224 return "PASS"; 225} 226 227function stringify_result_obj(result_obj) 228{ 229 if (result_obj["is_unplanned"] || result_obj["number"] != testno) 230 return "ERROR" 231 232 if (plan_seen == LATE_PLAN) 233 return "ERROR" 234 235 if (result_obj["directive"] == "TODO") 236 return result_obj["is_ok"] ? "XPASS" : "XFAIL" 237 238 if (result_obj["directive"] == "SKIP") 239 return result_obj["is_ok"] ? "SKIP" : COOKED_FAIL; 240 241 if (length(result_obj["directive"])) 242 abort("in function stringify_result_obj()") 243 244 return result_obj["is_ok"] ? COOKED_PASS : COOKED_FAIL 245} 246 247function decorate_result(result) 248{ 249 color_name = color_for_result[result] 250 if (color_name) 251 return color_map[color_name] "" result "" color_map["std"] 252 # If we are not using colorized output, or if we do not know how 253 # to colorize the given result, we should return it unchanged. 254 return result 255} 256 257function report(result, details) 258{ 259 if (result ~ /^(X?(PASS|FAIL)|SKIP|ERROR)/) 260 { 261 msg = ": " test_script_name 262 add_test_result(result) 263 } 264 else if (result == "#") 265 { 266 msg = " " test_script_name ":" 267 } 268 else 269 { 270 abort("in function report()") 271 } 272 if (length(details)) 273 msg = msg " " details 274 # Output on console might be colorized. 275 print decorate_result(result) msg 276 # Log the result in the log file too, to help debugging (this is 277 # especially true when said result is a TAP error or "Bail out!"). 278 print result msg | "cat >&3"; 279} 280 281function testsuite_error(error_message) 282{ 283 report("ERROR", "- " error_message) 284} 285 286function handle_tap_result() 287{ 288 details = result_obj["number"]; 289 if (length(result_obj["description"])) 290 details = details " " result_obj["description"] 291 292 if (plan_seen == LATE_PLAN) 293 { 294 details = details " # AFTER LATE PLAN"; 295 } 296 else if (result_obj["is_unplanned"]) 297 { 298 details = details " # UNPLANNED"; 299 } 300 else if (result_obj["number"] != testno) 301 { 302 details = sprintf("%s # OUT-OF-ORDER (expecting %d)", 303 details, testno); 304 } 305 else if (result_obj["directive"]) 306 { 307 details = details " # " result_obj["directive"]; 308 if (length(result_obj["explanation"])) 309 details = details " " result_obj["explanation"] 310 } 311 312 report(stringify_result_obj(result_obj), details) 313} 314 315# `skip_reason` should be empty whenever planned > 0. 316function handle_tap_plan(planned, skip_reason) 317{ 318 planned += 0 # Avoid getting confused if, say, `planned` is "00" 319 if (length(skip_reason) && planned > 0) 320 abort("in function handle_tap_plan()") 321 if (plan_seen) 322 { 323 # Error, only one plan per stream is acceptable. 324 testsuite_error("multiple test plans") 325 return; 326 } 327 planned_tests = planned 328 # The TAP plan can come before or after *all* the TAP results; we speak 329 # respectively of an "early" or a "late" plan. If we see the plan line 330 # after at least one TAP result has been seen, assume we have a late 331 # plan; in this case, any further test result seen after the plan will 332 # be flagged as an error. 333 plan_seen = (testno >= 1 ? LATE_PLAN : EARLY_PLAN) 334 # If testno > 0, we have an error ("too many tests run") that will be 335 # automatically dealt with later, so do not worry about it here. If 336 # $plan_seen is true, we have an error due to a repeated plan, and that 337 # has already been dealt with above. Otherwise, we have a valid "plan 338 # with SKIP" specification, and should report it as a particular kind 339 # of SKIP result. 340 if (planned == 0 && testno == 0) 341 { 342 if (length(skip_reason)) 343 skip_reason = "- " skip_reason; 344 report("SKIP", skip_reason); 345 } 346} 347 348function extract_tap_comment(line) 349{ 350 if (index(line, diag_string) == 1) 351 { 352 # Strip leading `diag_string` from `line`. 353 line = substr(line, length(diag_string) + 1) 354 # And strip any leading and trailing whitespace left. 355 sub("^[ \t]*", "", line) 356 sub("[ \t]*$", "", line) 357 # Return what is left (if any). 358 return line; 359 } 360 return ""; 361} 362 363# When this function is called, we know that line is a TAP result line, 364# so that it matches the (perl) RE "^(not )?ok\b". 365function setup_result_obj(line) 366{ 367 # Get the result, and remove it from the line. 368 result_obj["is_ok"] = (substr(line, 1, 2) == "ok" ? 1 : 0) 369 sub("^(not )?ok[ \t]*", "", line) 370 371 # If the result has an explicit number, get it and strip it; otherwise, 372 # automatically assign the next test number to it. 373 if (line ~ /^[0-9]+$/ || line ~ /^[0-9]+[^a-zA-Z0-9_]/) 374 { 375 match(line, "^[0-9]+") 376 # The final `+ 0` is to normalize numbers with leading zeros. 377 result_obj["number"] = substr(line, 1, RLENGTH) + 0 378 line = substr(line, RLENGTH + 1) 379 } 380 else 381 { 382 result_obj["number"] = testno 383 } 384 385 if (plan_seen == LATE_PLAN) 386 # No further test results are acceptable after a "late" TAP plan 387 # has been seen. 388 result_obj["is_unplanned"] = 1 389 else if (plan_seen && testno > planned_tests) 390 result_obj["is_unplanned"] = 1 391 else 392 result_obj["is_unplanned"] = 0 393 394 # Strip trailing and leading whitespace. 395 sub("^[ \t]*", "", line) 396 sub("[ \t]*$", "", line) 397 398 # This will have to be corrected if we have a "TODO"/"SKIP" directive. 399 result_obj["description"] = line 400 result_obj["directive"] = "" 401 result_obj["explanation"] = "" 402 403 if (index(line, "#") == 0) 404 return # No possible directive, nothing more to do. 405 406 # Directives are case-insensitive. 407 rx = "[ \t]*#[ \t]*([tT][oO][dD][oO]|[sS][kK][iI][pP])[ \t]*" 408 409 # See whether we have the directive, and if yes, where. 410 pos = match(line, rx "$") 411 if (!pos) 412 pos = match(line, rx "[^a-zA-Z0-9_]") 413 414 # If there was no TAP directive, we have nothing more to do. 415 if (!pos) 416 return 417 418 # Let`s now see if the TAP directive has been escaped. For example: 419 # escaped: ok \# SKIP 420 # not escaped: ok \\# SKIP 421 # escaped: ok \\\\\# SKIP 422 # not escaped: ok \ # SKIP 423 if (substr(line, pos, 1) == "#") 424 { 425 bslash_count = 0 426 for (i = pos; i > 1 && substr(line, i - 1, 1) == "\\"; i--) 427 bslash_count += 1 428 if (bslash_count % 2) 429 return # Directive was escaped. 430 } 431 432 # Strip the directive and its explanation (if any) from the test 433 # description. 434 result_obj["description"] = substr(line, 1, pos - 1) 435 # Now remove the test description from the line, that has been dealt 436 # with already. 437 line = substr(line, pos) 438 # Strip the directive, and save its value (normalized to upper case). 439 sub("^[ \t]*#[ \t]*", "", line) 440 result_obj["directive"] = toupper(substr(line, 1, 4)) 441 line = substr(line, 5) 442 # Now get the explanation for the directive (if any), with leading 443 # and trailing whitespace removed. 444 sub("^[ \t]*", "", line) 445 sub("[ \t]*$", "", line) 446 result_obj["explanation"] = line 447} 448 449function get_test_exit_message(status) 450{ 451 if (status == 0) 452 return "" 453 if (status !~ /^[1-9][0-9]*$/) 454 abort("getting exit status") 455 if (status < 127) 456 exit_details = "" 457 else if (status == 127) 458 exit_details = " (command not found?)" 459 else if (status >= 128 && status <= 255) 460 exit_details = sprintf(" (terminated by signal %d?)", status - 128) 461 else if (status > 256 && status <= 384) 462 # We used to report an "abnormal termination" here, but some Korn 463 # shells, when a child process die due to signal number n, can leave 464 # in $? an exit status of 256+n instead of the more standard 128+n. 465 # Apparently, both behaviours are allowed by POSIX (2008), so be 466 # prepared to handle them both. See also Austing Group report ID 467 # 0000051 <http://www.austingroupbugs.net/view.php?id=51> 468 exit_details = sprintf(" (terminated by signal %d?)", status - 256) 469 else 470 # Never seen in practice. 471 exit_details = " (abnormal termination)" 472 return sprintf("exited with status %d%s", status, exit_details) 473} 474 475function write_test_results() 476{ 477 print ":global-test-result: " get_global_test_result() > trs_file 478 print ":recheck: " yn(must_recheck()) > trs_file 479 print ":copy-in-global-log: " yn(copy_in_global_log()) > trs_file 480 for (i = 0; i < test_results_index; i += 1) 481 print ":test-result: " test_results_list[i] > trs_file 482 close(trs_file); 483} 484 485BEGIN { 486 487## ------- ## 488## SETUP ## 489## ------- ## 490 491'"$init_colors"' 492 493# Properly initialized once the TAP plan is seen. 494planned_tests = 0 495 496COOKED_PASS = expect_failure ? "XPASS": "PASS"; 497COOKED_FAIL = expect_failure ? "XFAIL": "FAIL"; 498 499# Enumeration-like constants to remember which kind of plan (if any) 500# has been seen. It is important that NO_PLAN evaluates "false" as 501# a boolean. 502NO_PLAN = 0 503EARLY_PLAN = 1 504LATE_PLAN = 2 505 506testno = 0 # Number of test results seen so far. 507bailed_out = 0 # Whether a "Bail out!" directive has been seen. 508 509# Whether the TAP plan has been seen or not, and if yes, which kind 510# it is ("early" is seen before any test result, "late" otherwise). 511plan_seen = NO_PLAN 512 513## --------- ## 514## PARSING ## 515## --------- ## 516 517is_first_read = 1 518 519while (1) 520 { 521 # Involutions required so that we are able to read the exit status 522 # from the last input line. 523 st = getline 524 if (st < 0) # I/O error. 525 fatal("I/O error while reading from input stream") 526 else if (st == 0) # End-of-input 527 { 528 if (is_first_read) 529 abort("in input loop: only one input line") 530 break 531 } 532 if (is_first_read) 533 { 534 is_first_read = 0 535 nextline = $0 536 continue 537 } 538 else 539 { 540 curline = nextline 541 nextline = $0 542 $0 = curline 543 } 544 # Copy any input line verbatim into the log file. 545 print | "cat >&3" 546 # Parsing of TAP input should stop after a "Bail out!" directive. 547 if (bailed_out) 548 continue 549 550 # TAP test result. 551 if ($0 ~ /^(not )?ok$/ || $0 ~ /^(not )?ok[^a-zA-Z0-9_]/) 552 { 553 testno += 1 554 setup_result_obj($0) 555 handle_tap_result() 556 } 557 # TAP plan (normal or "SKIP" without explanation). 558 else if ($0 ~ /^1\.\.[0-9]+[ \t]*$/) 559 { 560 # The next two lines will put the number of planned tests in $0. 561 sub("^1\\.\\.", "") 562 sub("[^0-9]*$", "") 563 handle_tap_plan($0, "") 564 continue 565 } 566 # TAP "SKIP" plan, with an explanation. 567 else if ($0 ~ /^1\.\.0+[ \t]*#/) 568 { 569 # The next lines will put the skip explanation in $0, stripping 570 # any leading and trailing whitespace. This is a little more 571 # tricky in truth, since we want to also strip a potential leading 572 # "SKIP" string from the message. 573 sub("^[^#]*#[ \t]*(SKIP[: \t][ \t]*)?", "") 574 sub("[ \t]*$", ""); 575 handle_tap_plan(0, $0) 576 } 577 # "Bail out!" magic. 578 # Older versions of prove and TAP::Harness (e.g., 3.17) did not 579 # recognize a "Bail out!" directive when preceded by leading 580 # whitespace, but more modern versions (e.g., 3.23) do. So we 581 # emulate the latter, "more modern" behaviour. 582 else if ($0 ~ /^[ \t]*Bail out!/) 583 { 584 bailed_out = 1 585 # Get the bailout message (if any), with leading and trailing 586 # whitespace stripped. The message remains stored in `$0`. 587 sub("^[ \t]*Bail out![ \t]*", ""); 588 sub("[ \t]*$", ""); 589 # Format the error message for the 590 bailout_message = "Bail out!" 591 if (length($0)) 592 bailout_message = bailout_message " " $0 593 testsuite_error(bailout_message) 594 } 595 # Maybe we have too look for dianogtic comments too. 596 else if (comments != 0) 597 { 598 comment = extract_tap_comment($0); 599 if (length(comment)) 600 report("#", comment); 601 } 602 } 603 604## -------- ## 605## FINISH ## 606## -------- ## 607 608# A "Bail out!" directive should cause us to ignore any following TAP 609# error, as well as a non-zero exit status from the TAP producer. 610if (!bailed_out) 611 { 612 if (!plan_seen) 613 { 614 testsuite_error("missing test plan") 615 } 616 else if (planned_tests != testno) 617 { 618 bad_amount = testno > planned_tests ? "many" : "few" 619 testsuite_error(sprintf("too %s tests run (expected %d, got %d)", 620 bad_amount, planned_tests, testno)) 621 } 622 if (!ignore_exit) 623 { 624 # Fetch exit status from the last line. 625 exit_message = get_test_exit_message(nextline) 626 if (exit_message) 627 testsuite_error(exit_message) 628 } 629 } 630 631write_test_results() 632 633exit 0 634 635} # End of "BEGIN" block. 636' 637 638# TODO: document that we consume the file descriptor 3 :-( 639} 3>"$log_file" 640 641test $? -eq 0 || fatal "I/O or internal error" 642 643# Local Variables: 644# mode: shell-script 645# sh-indentation: 2 646# eval: (add-hook 'before-save-hook 'time-stamp) 647# time-stamp-start: "scriptversion=" 648# time-stamp-format: "%:y-%02m-%02d.%02H" 649# time-stamp-time-zone: "UTC0" 650# time-stamp-end: "; # UTC" 651# End: