autoconf-patches
[Top][All Lists]
Advanced

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

parallel autotest [2/3]: Implement 'testsuite --jobs'.


From: Ralf Wildenhues
Subject: parallel autotest [2/3]: Implement 'testsuite --jobs'.
Date: Mon, 26 May 2008 07:50:50 +0200
User-agent: Mutt/1.5.17+20080114 (2008-01-14)

There may be systems that support mknod but not mkfifo.  Not sure
whether it's useful to support them.

Cheers,
Ralf

2008-05-26  Ralf Wildenhues  <address@hidden>

        Implement parallel Autotest test execution: testsuite --jobs.
        * lib/autotest/general.m4 (AT_JOB_FIFO_FD): New macro.
        (AT_INIT): <at_jobs>: New variable.
        Accept -j, -jN, --jobs[=N], document them in --help output.
        Implement parallel driver loop using a FIFO, enabled with --jobs
        and if mkfifo works; otherwise, fall back to sequential loop.
        (AT_SETUP): Store, do not output summary progress line if
        parallel.
        * tests/autotest.at (parallel test execution, parallel truth)
        (parallel fallacy, parallel skip): New tests.
        * doc/autoconf.texi (testsuite Invocation): Document -j, --jobs,
        the mkfifo requirement, and that --errexit may cause concurrent
        jobs to finish.
        * NEWS: Update.

diff --git a/NEWS b/NEWS
index 9396ac0..fdee3e0 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,8 @@ GNU Autoconf NEWS - User visible changes.
 
 * Major changes in Autoconf 2.62a (2008-??-??)
 
+** Autotest testsuites accept an option --jobs[=N] for parallel testing.
+
 
 * Major changes in Autoconf 2.62 (2008-04-05) [stable]
   Released by Eric Blake, based on git versions 2.61a.*.
diff --git a/doc/autoconf.texi b/doc/autoconf.texi
index c22b822..82ee582 100644
--- a/doc/autoconf.texi
+++ b/doc/autoconf.texi
@@ -20325,6 +20325,24 @@ Change the current directory to @var{dir} before 
creating any files.
 Useful for running the testsuite in a subdirectory from a top-level
 Makefile.
 
address@hidden address@hidden@address@hidden
address@hidden address@hidden
address@hidden -j
+Run @var{n} tests in parallel, if possible.  If @var{n} is not given,
+run all given tests in parallel.  Note that there should be no space
+before the argument to @option{-j}, as @option{-j @var{number}} denotes
+the separate arguments @option{-j} and @address@hidden, see below.
+
+In parallel mode, the standard input device of the testsuite script is
+not available to commands inside a test group.  Furthermore, banner
+lines are not printed, and the summary line for each test group is
+output after the test group completes.  Summary lines may appear
+unordered.  If verbose and trace output are enabled (see below), they
+may appear intermixed from concurrently running tests.
+
+Parallel mode requires the @command{mkfifo} command to work, and will be
+silently disabled otherwise.
+
 @item --clean
 @itemx -c
 Remove all the files the test suite might have created and exit.  Meant
@@ -20402,6 +20420,8 @@ If any test fails, immediately abort testing.  It 
implies
 @option{--debug}: post test group clean up, and top-level logging
 are inhibited.  This option is meant for the full test
 suite, it is not really useful for generated debugging scripts.
+If the testsuite is run in parallel mode using @option{--jobs},
+then concurrently running tests will finish before exiting.
 
 @item --verbose
 @itemx -v
diff --git a/lib/autotest/general.m4 b/lib/autotest/general.m4
index cfa18fc..f620561 100644
--- a/lib/autotest/general.m4
+++ b/lib/autotest/general.m4
@@ -217,6 +217,7 @@ m4_foreach([AT_name], [_AT_DEFINE_INIT_LIST], 
[m4_popdef(m4_defn([AT_name]))])
 m4_wrap([_AT_FINISH])
 dnl Define FDs.
 m4_define([AS_MESSAGE_LOG_FD], [5])
+m4_define([AT_JOB_FIFO_FD], [6])
 AS_INIT[]dnl
 m4_divert_push([DEFAULTS])dnl
 AT_COPYRIGHT(
@@ -398,6 +399,8 @@ at_errexit_p=false
 # Shall we be verbose?  ':' means no, empty means yes.
 at_verbose=:
 at_quiet=
+# Running several jobs in parallel, 0 means as many as test groups.
+at_jobs=1
 
 # Shall we keep the debug scripts?  Must be `:' when the suite is
 # run by a debug script, so that the script doesn't remove itself.
@@ -558,6 +561,21 @@ do
        at_dir=$at_optarg
        ;;
 
+    # Parallel execution.
+    --jobs | -j )
+       at_jobs=0
+       ;;
+    --jobs=* | -j[[0-9]]* )
+       if test -n "$at_optarg"; then
+         at_jobs=$at_optarg
+       else
+         at_jobs=`expr X$at_option : 'X-j\(.*\)'`
+       fi
+       case $at_jobs in *[[!0-9]]*)
+         AS_ERROR([non-numeric argument to -j/--jobs: $at_jobs]) ;;
+       esac
+       ;;
+
     # Keywords.
     --keywords | -k )
        at_prev=--keywords
@@ -662,6 +680,8 @@ dnl extra quoting prevents emacs whitespace mode from 
putting tabs in output
 Execution tuning:
   -C, --directory=DIR
 [                 change to directory DIR before starting]
+  -j[[N]], --jobs[[=N]]
+[                 Allow N jobs at once; infinite jobs with no arg]
   -k, --keywords=KEYWORDS
 [                 select the tests matching all the comma-separated KEYWORDS]
 [                 multiple \`-k' accumulate; prefixed \`!' negates a KEYWORD]
@@ -772,6 +792,8 @@ at_suite_log=$at_dir/$as_me.log
 at_helper_dir=$at_suite_dir/at-groups
 # Stop file: if it exists, do not start new jobs.
 at_stop_file=$at_suite_dir/at-stop
+# The fifo used for the job dispatcher.
+at_job_fifo=$at_suite_dir/at-job-fifo
 
 if $at_clean; then
   test -d "$at_suite_dir" &&
@@ -952,6 +974,12 @@ BEGIN { FS="" }
   AS_ERROR([cannot create test line number cache])
 rm -f "$at_suite_dir/at-source-lines"
 
+# If parallel mode, don't output banners, don't split summary lines.
+if test $at_jobs -ne 1; then
+  at_print_banners=false
+  at_quiet=:
+fi
+
 # Set up helper dirs.
 rm -rf "$at_helper_dir" &&
 mkdir "$at_helper_dir" &&
@@ -1060,8 +1088,13 @@ _ATEOF
        ;;
   esac
   echo "$at_res" > "$at_job_dir/$at_res"
-  # Make sure there is a separator even with long titles.
-  AS_ECHO([" $at_msg"])
+  # In parallel mode, output the summary line only afterwards.
+  if test $at_jobs -ne 1 && test -n "$at_verbose"; then
+    AS_ECHO(["$at_desc_line $at_msg"])
+  else
+    # Make sure there is a separator even with long titles.
+    AS_ECHO([" $at_msg"])
+  fi
   at_log_msg="$at_group. $at_desc ($at_setup_line): $at_msg"
   case $at_status in
     0|77)
@@ -1107,20 +1140,77 @@ _ATEOF
 m4_text_box([Driver loop.])
 
 rm -f "$at_stop_file"
+trap 'exit_status=$?
+  echo "signal received, bailing out" >&2
+  echo stop > "$at_stop_file"
+  exit $exit_status' 1 2 13 15
 at_first=:
 
-for at_group in $at_groups; do
-  at_func_group_prepare
-  if cd "$at_group_dir" &&
-     at_func_test $at_group &&
-     . "$at_test_source"; then :; else
-    AS_WARN([unable to parse test group: $at_group])
-    at_failed=:
+if test $at_jobs -ne 1 &&
+     rm -f "$at_job_fifo" &&
+     ( mkfifo "$at_job_fifo" ) 2>/dev/null &&
+     exec AT_JOB_FIFO_FD<> "$at_job_fifo"
+then
+  # FIFO job dispatcher.
+  echo
+  if test $at_jobs -eq 0; then
+    set X $at_groups; shift; address@hidden:@]
   fi
-  at_func_group_postprocess
-  test -f "$at_stop_file" && break
-  at_first=false
-done
+  # Turn jobs into a list of numbers, starting from 1.
+  at_joblist=`AS_ECHO([" $at_groups_all "]) | \
+    sed -e 's/\( '$at_jobs'\) .*/\1/'`
+
+  set X $at_joblist
+  shift
+  for at_group in $at_groups; do
+    (
+      # Start one test group.
+      at_func_group_prepare
+      if cd "$at_group_dir" &&
+        at_func_test $at_group &&
+        . "$at_test_source" # AT_JOB_FIFO_FD<&-
+      then :; else
+       AS_WARN([unable to parse test group: $at_group])
+       at_failed=:
+      fi
+      at_func_group_postprocess
+      echo token >&AT_JOB_FIFO_FD
+    ) &
+    shift # Consume one token.
+    if test address@hidden:@] -gt 0; then :; else
+      read at_token <&AT_JOB_FIFO_FD || break
+      set x $[*]
+    fi
+    test -f "$at_stop_file" && break
+    at_first=false
+  done
+  # Read back the remaining ($at_jobs - 1) tokens.
+  set X $at_joblist
+  shift
+  if test address@hidden:@] -gt 0; then
+    shift
+    for at_job
+    do
+      read at_token
+    done <&AT_JOB_FIFO_FD
+  fi
+  exec AT_JOB_FIFO_FD<&-
+  wait
+else
+  # Run serially, avoid forks and other potential surprises.
+  for at_group in $at_groups; do
+    at_func_group_prepare
+    if cd "$at_group_dir" &&
+       at_func_test $at_group &&
+       . "$at_test_source"; then :; else
+      AS_WARN([unable to parse test group: $at_group])
+      at_failed=:
+    fi
+    at_func_group_postprocess
+    test -f "$at_stop_file" && break
+    at_first=false
+  done
+fi
 
 # Wrap up the test suite with summary statistics.
 cd "$at_helper_dir"
@@ -1486,8 +1576,9 @@ at_setup_line='m4_defn([AT_line])'
 m4_if(AT_banner_ordinal, [0], [], [at_func_banner AT_banner_ordinal
 ])dnl
 at_desc="AS_ESCAPE(m4_dquote(m4_defn([AT_description])))"
-$at_quiet AS_ECHO_N([m4_format(["%3d: $at_desc%*s"], AT_ordinal,
-  m4_max(0, m4_eval(47 - m4_qlen(m4_defn([AT_description])))), [])])
+at_desc_line=m4_format(["%3d: $at_desc%*s"], AT_ordinal,
+  m4_max(0, m4_eval(47 - m4_qlen(m4_defn([AT_description])))), [])
+$at_quiet AS_ECHO_N(["$at_desc_line"])
 m4_divert_push([TEST_SCRIPT])dnl
 ])
 
diff --git a/tests/autotest.at b/tests/autotest.at
index e6dc862..54399f3 100644
--- a/tests/autotest.at
+++ b/tests/autotest.at
@@ -745,6 +745,95 @@ AT_CHECK_KEYS([--list -k none -k first], [none|first], 
[2], [second|both], [0])
 AT_CLEANUP
 
 
+## ----------------------- ##
+## parallel test execution ##
+## ----------------------- ##
+
+AT_SETUP([parallel test execution])
+
+# The total number of tests for the parallel test micro-suite,
+# the number of tests to run concurrently.
+#
+# The total number should not be too high, to not slow down
+# the testsuite unnecessarily.  If the number of concurrent
+# jobs is too low, the race is lost more easily (see below),
+# if it is too high, fork may fail on tightly limited systems.
+m4_define([AT_PARALLEL_TESTS_TOTAL], [8])
+m4_define([AT_PARALLEL_TESTS_RUN], [4])
+
+
+AT_CHECK_AT_PREP([micro-suite],
+[[AT_INIT([suite to test parallel execution])
+m4_for([count], [1], ]]AT_PARALLEL_TESTS_TOTAL[[, [],
+   [AT_SETUP([test number count])
+    AT_CHECK([sleep 1])
+    AT_CLEANUP
+])
+]])
+
+AT_CHECK([$CONFIG_SHELL ./micro-suite --help | grep " --jobs"], [0], [ignore])
+AT_CHECK([$CONFIG_SHELL ./micro-suite -j2foo], [1], [], [stderr])
+AT_CHECK([grep 'non-numeric argument' stderr], [], [ignore])
+AT_CHECK([$CONFIG_SHELL ./micro-suite --jobs=foo], [1], [], [stderr])
+AT_CHECK([grep 'non-numeric argument' stderr], [], [ignore])
+AT_CHECK([$CONFIG_SHELL ./micro-suite -j[]AT_PARALLEL_TESTS_RUN], [], [stdout])
+# Ensure that all tests run, and lines are not split.
+AT_CHECK([grep -c '^.\{53\}ok' stdout], [], [AT_PARALLEL_TESTS_TOTAL
+])
+
+# Ensure we really are faster than sequential execution:
+# the testsuite should have completed before we kill it.
+# Unfortunately, the return value of wait is unreliable,
+# so we check that kill fails.
+#
+# Since the testsuite startup time can be high, we compare
+# it against a sequential testsuite run in a subdirectory,
+# where only a subset of tests is run.
+
+# The parallel scheduler requires mkfifo to work.
+AT_CHECK([mkfifo fifo || exit 77])
+mkdir serial
+AT_CHECK([$CONFIG_SHELL ./micro-suite --jobs=[]AT_PARALLEL_TESTS_RUN & ]dnl
+         [sleep 3 && ]dnl
+         [cd serial && $CONFIG_SHELL ../micro-suite -3 >/dev/null && ]dnl
+         [{ kill $! && exit 1; :; }], [], [stdout], [ignore])
+AT_CHECK([grep -c '^.\{53\}ok' stdout], [], [AT_PARALLEL_TESTS_TOTAL
+])
+AT_CHECK([grep 'AT_PARALLEL_TESTS_TOTAL tests' stdout], [], [ignore])
+
+AT_CLEANUP
+
+AT_CHECK_AT_TEST([parallel truth],
+  [AT_CHECK([:], 0, [], [])],
+  [], [], [], [],
+  [], [-j])
+
+AT_CHECK_AT_TEST([parallel fallacy],
+  [AT_CHECK([false], [], [], [])],
+  [], [1], [], [ignore],
+  [AT_CHECK([grep failed micro-suite.log], [], [ignore])], [-j])
+
+AT_CHECK_AT_TEST([parallel skip],
+  [AT_CHECK([exit 77], 0, [], [])],
+  [], [], [], [],
+  [AT_CHECK([grep skipped micro-suite.log], [], [ignore])], [-j])
+
+AT_CHECK_AT_TEST([parallel errexit],
+  [AT_CHECK([false])
+   AT_CLEANUP
+   AT_SETUP([barrier test])
+   AT_CHECK([sleep 4])
+   AT_CLEANUP
+   AT_SETUP([test that should not be run])
+   AT_CHECK([:])],
+  [], [1], [stdout], [stderr],
+  [AT_CHECK([test -f micro-suite.log], [1])
+   touch micro-suite.log # shut up AT_CAPTURE_FILE.
+   AT_CHECK([grep "should not be run" stdout], [1])
+   AT_CHECK([grep "[[12]] .* inhibited subsequent" stderr], [], [ignore])],
+  [-j2 --errexit])
+
+
 ## ------------------- ##
 ## srcdir propagation. ##
 ## ------------------- ##




reply via email to

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