automake-patches
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[FYI] {test-protocols} tap: real (but still incomplete) awk implementati


From: Stefano Lattarini
Subject: [FYI] {test-protocols} tap: real (but still incomplete) awk implementation of TAP driver
Date: Sun, 21 Aug 2011 19:09:19 +0200

* lib/tap-driver.sh : Add an incomplete, but mostly working,
implementation of a TAP parser and driver in awk.  It doesn't yet
support colorized output, fetching of exit status from test
programs, nor a way to escape TAP directives in TAP result lines,
but passes all the tests of TAP support in the automake testsuite,
apart from the following ones:
  - tap-color.test
  - tap-escape-directive.test
  - tap-exit.test
  - tap-missing-plan-and-bad-exit.test
  - tap-passthrough-exit.test
  - tap-planskip-badexit.test
  - tap-planskip-unplanned-corner.test
  - tap-signal.test

Tested on Debian GNU/Linux with GNU awk 3.1.7 and 3.0.2, "original
awk" 2010-05-23-1, and mawk 1.3.3-15 (with which also the test
`tap-realtime.test' fails, in addition to those listed above).
---
 ChangeLog         |   21 +++
 lib/tap-driver.sh |  492 ++++++++++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 489 insertions(+), 24 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 3d5b5d9..a0326ad 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,26 @@
 2011-08-21  Stefano Lattarini  <address@hidden>
 
+       tap: real (but still incomplete) awk implementation of TAP driver
+       * lib/tap-driver.sh : Add an incomplete, but mostly working,
+       implementation of a TAP parser and driver in awk.  It doesn't yet
+       support colorized output, fetching of exit status from test
+       programs, nor a way to escape TAP directives in TAP result lines,
+       but passes all the tests of TAP support in the automake testsuite,
+       apart from the following ones:
+         - tap-color.test
+         - tap-escape-directive.test
+         - tap-exit.test
+         - tap-missing-plan-and-bad-exit.test
+         - tap-passthrough-exit.test
+         - tap-planskip-badexit.test
+         - tap-planskip-unplanned-corner.test
+         - tap-signal.test
+       Tested on Debian GNU/Linux with GNU awk 3.1.7 and 3.0.2, "original
+       awk" 2010-05-23-1, and mawk 1.3.3-15 (with which also the test
+       `tap-realtime.test' fails, in addition to those listed above).
+
+2011-08-21  Stefano Lattarini  <address@hidden>
+
        testsuite: fix spurious failure due to Solaris 'wc' incompatibility
        * tests/tap-number-wordboundary.test ($planned): Strip any extra
        whitespace from the output of `wc', to cater to Solaris 10.
diff --git a/lib/tap-driver.sh b/lib/tap-driver.sh
index 322e3c4..dc19fa0 100755
--- a/lib/tap-driver.sh
+++ b/lib/tap-driver.sh
@@ -23,21 +23,23 @@
 # bugs to <address@hidden> or send patches to
 # <address@hidden>.
 
-scriptversion=2011-08-17.14; # UTC
+scriptversion=2011-08-21.16; # UTC
 
 # Make unconditional expansion of undefined variables an error.  This
 # helps a lot in preventing typo-related bugs.
 set -u
 
+me=tap-driver.sh
+
 fatal ()
 {
-  echo "$0: fatal: $*" >&2
+  echo "$me: fatal: $*" >&2
   exit 1
 }
 
 usage_error ()
 {
-  echo "$0: $*" >&2
+  echo "$me: $*" >&2
   print_usage >&2
   exit 2
 }
@@ -46,11 +48,11 @@ print_usage ()
 {
   cat <<END
 Usage:
-  tap-driver --test-name=NAME --log-file=PATH --trs-file=PATH
-             [--expect-failure={yes|no}] [--color-tests={yes|no}]
-             [--enable-hard-errors={yes|no}] [--ignore-exit]
-             [--diagnostic-string=STRING] [--merge|--no-merge]
-             [--comments|--no-comments] [--] TEST-COMMAND
+  tap-driver.sh --test-name=NAME --log-file=PATH --trs-file=PATH
+                [--expect-failure={yes|no}] [--color-tests={yes|no}]
+                [--enable-hard-errors={yes|no}] [--ignore-exit]
+                [--diagnostic-string=STRING] [--merge|--no-merge]
+                [--comments|--no-comments] [--] TEST-COMMAND
 The \`--test-name', \`--log-file' and \`--trs-file' options are mandatory.
 END
 }
@@ -60,28 +62,28 @@ END
 test_name= # Used for reporting.
 log_file=  # Where to save the result and output of the test script.
 trs_file=  # Where to save the metadata of the test run.
-expect_failure=no
-color_tests=no
-merge=no
-ignore_exit=no
-comments=no
+expect_failure=0
+color_tests=0
+merge=0
+ignore_exit=0
+comments=0
 diag_string='#'
 while test $# -gt 0; do
   case $1 in
   --help) print_usage; exit $?;;
-  --version) echo "tap-driver $scriptversion"; exit $?;;
+  --version) echo "$me $scriptversion"; exit $?;;
   --test-name) test_name=$2; shift;;
   --log-file) log_file=$2; shift;;
   --trs-file) trs_file=$2; shift;;
   --color-tests) color_tests=$2; shift;;
   --expect-failure) expect_failure=$2; shift;;
   --enable-hard-errors) shift;; # No-op.
-  --merge) merge=yes;;
-  --no-merge) merge=no;;
-  --ignore-exit) ignore_exit=yes;;
-  --comments) comments=yes;;
-  --no-comments) comments=no;;
-  --diag-string) diag_string=$2; shift;;
+  --merge) merge=1;;
+  --no-merge) merge=0;;
+  --ignore-exit) ignore_exit=1;;
+  --comments) comments=1;;
+  --no-comments) comments=0;;
+  --diagnostic-string) diag_string=$2; shift;;
   --) shift; break;;
   -*) usage_error "invalid option: '$1'";;
   esac
@@ -95,6 +97,11 @@ case $expect_failure in
     *) expect_failure=0;;
 esac
 
+case $color_tests in
+  yes) color_tests=1;;
+    *) color_tests=0;;
+esac
+
 if test $color_tests = yes; then
   red='' # Red.
   grn='' # Green.
@@ -106,11 +113,448 @@ else
   red= grn= lgn= blu= mgn= std=
 fi
 
-# TODO: test script is run here.
-# "$@" | [our magic awk script]
+{
+  # FIXME: this usage loses the test program exit status.  We should
+  # probably rewrite the awk script to use the
+  #   expression | getline [var]
+  # idiom, which should allow us to obtain the final exit status from
+  # <expression> when closing it.
+  { test $merge -eq 0 || exec 2>&1; "$@"; } \
+    | LC_ALL=C ${AM_TAP_AWK-awk} \
+        -v me="$me" \
+        -v test_script_name="$test_name" \
+        -v log_file="$log_file" \
+        -v trs_file="$trs_file" \
+        -v color_tests="$color_tests" \
+        -v expect_failure="$expect_failure" \
+        -v merge="$merge" \
+        -v ignore_exit="$ignore_exit" \
+        -v comments="$comments" \
+        -v diag_string="$diag_string" \
+'
+# FIXME: the usages of "cat >&3" below could be optimized whne using
+# FIXME: GNU awk, and/on on systems that supports /dev/fd/.
+
+# Implementation note: in what follows, `result_obj` will be an
+# associative array that (partly) simulates a TAP result object
+# from the `TAP::Parser` perl module.
+
+## ----------- ##
+##  FUNCTIONS  ##
+## ----------- ##
+
+function fatal(msg)
+{
+  print me ": " msg | "cat >&3"
+  exit 1
+}
+
+function abort(where)
+{
+  fatal("internal error " where)
+}
+
+function close_or_die(fpath, fname)
+{
+  if (close(fpath) != 0)
+    fatal(sprintf("could not close %s \"%s\"", fname, fpath))
+}
+
+# Convert a boolean to a "yes"/"no" string.
+function yn(bool)
+{
+  return bool ? "yes" : "no";
+}
+
+function add_test_result(result)
+{
+  if (!test_results_index)
+    test_results_index = 0
+  test_results_list[test_results_index] = result
+  test_results_index += 1
+  test_results_seen[result] = 1;
+}
+
+# Whether the test script should be re-run by "make recheck".
+function must_recheck()
+{
+  for (k in test_results_seen)
+    if (k != "XFAIL" && k != "PASS" && k != "SKIP")
+      return 1
+  return 0
+}
+
+# Whether the content of the log file associated to this test should
+# be copied into the "global" test-suite.log.
+function copy_in_global_log()
+{
+  for (k in test_results_seen)
+    if (k != "PASS")
+      return 1
+  return 0
+}
+
+# FIXME: this can certainly be improved ...
+function get_global_test_result()
+{
+    if ("ERROR" in test_results_seen)
+      return "ERROR"
+    all_skipped = 1
+    for (k in test_results_seen)
+      if (k != "SKIP")
+        all_skipped = 0
+    if (all_skipped)
+      return "SKIP"
+    if ("FAIL" in test_results_seen || "XPASS" in test_results_seen)
+      return "FAIL"
+    return "PASS";
+}
+
+function stringify_result_obj(obj)
+{
+  if (obj["is_unplanned"] || obj["number"] != testno)
+    return "ERROR"
+
+  if (plan_seen == LATE_PLAN)
+    return "ERROR"
+
+  if (result_obj["directive"] == "TODO")
+    return obj["is_ok"] ? "XPASS" : "XFAIL"
+
+  if (result_obj["directive"] == "SKIP")
+    return obj["is_ok"] ? "SKIP" : COOKED_FAIL;
+
+  if (length(result_obj["directive"]))
+      abort("in function stringify_result_obj()")
+
+  return obj["is_ok"] ? COOKED_PASS : COOKED_FAIL
+}
+
+function decorate_result(result)
+{
+  return result # TODO!
+}
+
+function report(result, details)
+{
+  if (result ~ /^(X?(PASS|FAIL)|SKIP|ERROR)/)
+    {
+      msg = ": " test_script_name
+      add_test_result(result)
+    }
+  else if (result == "#")
+    {
+      msg = " " test_script_name ":"
+    }
+  else
+    {
+      abort("in function report()")
+    }
+  if (length(details))
+    msg = msg " " details
+  # Output on console might be colorized.
+  print decorate_result(result) msg | "cat >&3";
+  # Log the result in the log file too, to help debugging (this is
+  # especially true when said result is a TAP error or "Bail out!").
+  print result msg;
+}
+
+function testsuite_error(error_message)
+{
+  report("ERROR", "- " error_message)
+}
+
+function handle_tap_result()
+{
+  details = result_obj["number"];
+  if (length(result_obj["description"]))
+    details = details " " result_obj["description"]
+
+  if (plan_seen == LATE_PLAN)
+    {
+      details = details " # AFTER LATE PLAN";
+    }
+  else if (result_obj["is_unplanned"])
+    {
+       details = details " # UNPLANNED";
+    }
+  else if (result_obj["number"] != testno)
+    {
+       details = sprintf("%s # OUT-OF-ORDER (expecting %d)",
+                         details, testno);
+    }
+  else if (result_obj["directive"])
+    {
+      details = details " # " result_obj["directive"];
+      if (length(result_obj["explanation"]))
+        details = details " " result_obj["explanation"]
+    }
+
+  report(stringify_result_obj(result_obj), details)
+}
+
+# `skip_reason` should be emprty whenever planned > 0.
+function handle_tap_plan(planned, skip_reason)
+{
+  planned += 0 # Avoid getting confused if, say, `planned` is "00"
+  if (length(skip_reason) && planned > 0)
+    abort("in function handle_tap_plan()")
+  if (plan_seen)
+    {
+      # Error, only one plan per stream is acceptable.
+      testsuite_error("multiple test plans")
+      return;
+    }
+  planned_tests = planned
+  # The TAP plan can come before or after *all* the TAP results; we speak
+  # respectively of an "early" or a "late" plan.  If we see the plan line
+  # after at least one TAP result has been seen, assume we have a late
+  # plan; in this case, any further test result seen after the plan will
+  # be flagged as an error.
+  plan_seen = (testno >= 1 ? LATE_PLAN : EARLY_PLAN)
+  # If testno > 0, we have an error ("too many tests run") that will be
+  # automatically dealt with later, so do not worry about it here.  If
+  # $plan_seen is true, we have an error due to a repeated plan, and that
+  # has already been dealt with above.  Otherwise, we have a valid "plan
+  # with SKIP" specification, and should report it as a particular kind
+  # of SKIP result.
+  if (planned == 0 && testno == 0)
+    {
+      if (length(skip_reason))
+        skip_reason = "- "  skip_reason;
+      report("SKIP", skip_reason);
+    }
+}
+
+function extract_tap_comment(line)
+{
+  # FIXME: verify there is not an off-by-one bug here.
+  if (index(line, diag_string) == 1)
+    {
+      # Strip leading `diag_string` from `line`.
+      # FIXME: verify there is not an off-by-one bug here.
+      line = substr(line, length(diag_string) + 1)
+      # And strip any leading and trailing whitespace left.
+      sub("^[ \\t]*", "", line)
+      sub("[ \\t]*$", "", line)
+      # Return what is left (if any).
+      return line;
+    }
+  return "";
+}
+
+# When this function is called, we know that line is a TAP result line,
+# so that it matches the (perl) RE "^(not )?ok\b".
+function setup_result_obj(line)
+{
+  # Get the result, and remove it from the line.
+  result_obj["is_ok"] = (substr(line, 1, 2) == "ok" ? 1 : 0)
+  sub("^(not )?ok[ \\t]*", "", line)
+
+  # If the result has an explicit number, get it and strip it; otherwise,
+  # automatically assing the next progresive number to it.
+  if (line ~ /^[0-9]+$/ || line ~ /^[0-9]+[^a-zA-Z0-9_]/)
+    {
+      match(line, "^[0-9]+")
+      # The final `+ 0` is to normalize numbers with leading zeros.
+      result_obj["number"] = substr(line, 1, RLENGTH) + 0
+      line = substr(line, RLENGTH + 1)
+    }
+  else
+    {
+      result_obj["number"] = testno
+    }
+
+  if (plan_seen == LATE_PLAN)
+    # No further test results are acceptable after a "late" TAP plan
+    # has been seen.
+    result_obj["is_unplanned"] = 1
+  else if (plan_seen && testno > planned_tests)
+    result_obj["is_unplanned"] = 1
+  else
+    result_obj["is_unplanned"] = 0
+
+  # Strip trailing and leading whitespace.
+  sub("^[ \\t]*", "", line)
+  sub("[ \\t]*$", "", line)
+
+  # This will have to be corrected if we have a "TODO"/"SKIP" directive.
+  result_obj["description"] = line
+  result_obj["directive"] = ""
+  result_obj["explanation"] = ""
+
+  # TODO: maybe we should allow a way to escape "#"?
+  if (index(line, "#") == 0)
+    return # No possible directive, nothing more to do.
+
+  # Directives are case-insensitive.
+  rx = "[ \\t]*#[ \\t]*([tT][oO][dD][oO]|[sS][kK][iI][pP])[ \\t]*"
+
+  # See whether we have the directive, and if yes, where.
+  pos = match(line, rx "$")
+  if (!pos)
+    pos = match(line, rx "[^a-zA-Z0-9_]")
+
+  # If there was no TAP directive, we have nothing more to do.
+  if (!pos)
+    return
+
+  # Strip the directive and its explanation (if any) from the test
+  # description.
+  result_obj["description"] = substr(line, 1, pos - 1)
+  # Now remove the test description from the line, that has been dealt
+  # with already.
+  line = substr(line, pos)
+  # Strip the directive, and save its value (normalized to upper case).
+  sub("^[ \\t]*#[ \\t]*", "", line)
+  result_obj["directive"] = toupper(substr(line, 1, 4))
+  line = substr(line, 5)
+  # Now get the explanation for the directive (if any), with leading
+  # and trailing whitespace removed.
+  sub("^[ \\t]*", "", line)
+  sub("[ \\t]*$", "", line)
+  result_obj["explanation"] = line
+}
+
+function write_test_results()
+{
+  print ":global-test-result: " get_global_test_result() > trs_file
+  print ":recheck: "  yn(must_recheck()) > trs_file
+  print ":copy-in-global-log: " yn(copy_in_global_log()) > trs_file
+  for (i = 0; i < test_results_index; i += 1)
+    print ":test-result: " test_results_list[i] > trs_file
+  close_or_die(trs_file, "trs file");
+}
+
+## ------- ##
+##  SETUP  ##
+## ------- ##
+
+BEGIN {
+
+  # Properly initialized once the TAP plan is seen.
+  planned_tests = 0
+
+  COOKED_PASS = expect_failure ? "XPASS": "PASS";
+  COOKED_FAIL = expect_failure ? "XFAIL": "FAIL";
+
+  # Enumeration-like constants to remember which kind of plan (if any)
+  # has been seen.  It is important that NO_PLAN evaluates "false" as
+  # a boolean.
+  NO_PLAN = 0
+  EARLY_PLAN = 1
+  LATE_PLAN = 2
+
+  testno = 0     # Number of test results seen so far.
+  bailed_out = 0 # Whether a "Bail out!" directive has been seen.
+
+  # Whether the TAP plan has been seen or not, and if yes, which kind
+  # it is ("early" is seen before any test result, "late" otherwise).
+  plan_seen = NO_PLAN
+
+}
+
+## --------- ##
+##  PARSING  ##
+## --------- ##
+
+{
+  # Copy any input line verbatim into the log file.
+  print
+  # Parsing of TAP input should stop after a "Bail out!" directive.
+  if (bailed_out)
+    next
+}
+
+# TAP test result.
+($0 ~ /^(not )?ok$/ || $0 ~ /^(not )?ok[^a-zA-Z0-9_]/) {
+
+  testno += 1
+  setup_result_obj($0)
+  handle_tap_result()
+  next
+
+}
+
+# TAP plan (normal or "SKIP" without explanation).
+/^1\.\.[0-9]+[ \t]*$/ {
+
+  # The next two lines will put the number of planned tests in $0.
+  sub("^1\\.\\.", "")
+  sub("[^0-9]*$", "")
+  handle_tap_plan($0, "")
+  next
+
+}
+
+# TAP "SKIP" plan, with an explanation.
+/^1\.\.0+[ \t]*#/ {
+
+  # The next lines will put the skip explanation in $0, stripping any
+  # leading and trailing whitespace.  This is a little more tricky in
+  # thruth, since we want to also strip a potential leading "SKIP"
+  # string from the message.
+  sub("^[^#]*#[ \\t]*(SKIP[: \\t][ \\t]*)", "")
+  sub("[ \\t]*$", "");
+  handle_tap_plan(0, $0)
+  next
+
+}
+
+# "Bail out!" magic.
+/^Bail out!/ {
+
+  bailed_out = 1
+  # Get the bailout message (if any), with leading and trailing
+  # whitespace stripped.  The message remains stored in `$0`.
+  sub("^Bail out![ \\t]*", "");
+  sub("[ \\t]*$", "");
+  # Format the error message for the
+  bailout_message = "Bail out!"
+  if (length($0))
+    bailout_message = bailout_message " " $0
+  testsuite_error(bailout_message)
+  next
+
+}
+
+(comments != 0) {
+
+  comment = extract_tap_comment($0);
+  if (length(comment))
+    report("#", comment);
+
+}
+
+## -------- ##
+##  FINISH  ##
+## -------- ##
+
+END {
+
+  # A "Bail out!" directive should cause us to ignore any following TAP
+  # error, as well as a non-zero exit status from the TAP producer.
+  if (!bailed_out)
+    {
+      if (!plan_seen)
+        testsuite_error("missing test plan")
+      else if (planned_tests != testno)
+        {
+          bad_amount = testno > planned_tests ? "many" : "few"
+          testsuite_error(sprintf("too %s tests run (expected %d, got %d)",
+                                  bad_amount, planned_tests, testno))
+        }
+    }
+  write_test_results()
+
+  exit 0
+}
+'
+
+# TODO: document that we consume the file descriptor 3 :-(
+} 3>&1 >"$log_file" 2>&1
 
-echo "$0: still to be implemented, sorry" >&2
-exit 255
+test $? -eq 0 || fatal "I/O or internal error"
 
 # Local Variables:
 # mode: shell-script
-- 
1.7.2.3




reply via email to

[Prev in Thread] Current Thread [Next in Thread]