runtests: prepare main test loop for multiple runners

Some variables are expanded to arrays and hashes so that multiple
runners can be used for running tests.

Ref: #10818
diff --git a/tests/runtests.pl b/tests/runtests.pl
index 87135b9..d5f4238 100755
--- a/tests/runtests.pl
+++ b/tests/runtests.pl
@@ -160,7 +160,10 @@
     ST_PREPROCESS => 3,
     ST_RUN => 4,
 };
-my $singletest_state = ST_INIT; # current state of singletest()
+my %singletest_state;  # current state of singletest() by runner ID
+my %runnerids;         # runner IDs by number
+my @runnersidle;       # runner IDs idle and ready to execute a test
+my %runnerfortest;     # runner IDs by testnum
 
 
 #######################################################################
@@ -1639,30 +1642,35 @@
 sub singletest {
     my ($runnerid, $testnum, $count, $total)=@_;
 
-    if($singletest_state == ST_INIT) {
+    if(!exists $singletest_state{$runnerid}) {
+        # First time in singletest() for this test
+        $singletest_state{$runnerid} = ST_INIT;
+    }
+
+    if($singletest_state{$runnerid} == ST_INIT) {
         my $logdir = getlogdir($testnum);
         # first, remove all lingering log files
         if(!cleardir($logdir) && $clearlocks) {
             runnerac_clearlocks($runnerid, $logdir);
-            $singletest_state = ST_CLEARLOCKS;
+            $singletest_state{$runnerid} = ST_CLEARLOCKS;
         } else {
-            $singletest_state = ST_INITED;
+            $singletest_state{$runnerid} = ST_INITED;
             # Recursively call the state machine again because there is no
             # event expected that would otherwise trigger a new call.
             return singletest(@_);
         }
 
-    } elsif($singletest_state == ST_CLEARLOCKS) {
+    } elsif($singletest_state{$runnerid} == ST_CLEARLOCKS) {
         my ($rid, $logs) = runnerar($runnerid);
         logmsg $logs;
         my $logdir = getlogdir($testnum);
         cleardir($logdir);
-        $singletest_state = ST_INITED;
+        $singletest_state{$runnerid} = ST_INITED;
         # Recursively call the state machine again because there is no
         # event expected that would otherwise trigger a new call.
         return singletest(@_);
 
-    } elsif($singletest_state == ST_INITED) {
+    } elsif($singletest_state{$runnerid} == ST_INITED) {
         ###################################################################
         # Restore environment variables that were modified in a previous run.
         # Test definition may instruct to (un)set environment vars.
@@ -1680,9 +1688,9 @@
         citest_starttest($testnum);
 
         runnerac_test_preprocess($runnerid, $testnum);
-        $singletest_state = ST_PREPROCESS;
+        $singletest_state{$runnerid} = ST_PREPROCESS;
 
-    } elsif($singletest_state == ST_PREPROCESS) {
+    } elsif($singletest_state{$runnerid} == ST_PREPROCESS) {
         my ($rid, $why, $error, $logs, $testtimings) = runnerar($runnerid);
         logmsg $logs;
         if($error == -2) {
@@ -1705,7 +1713,7 @@
         if($error) {
             # Submit the test case result with the CI environment
             citest_finishtest($testnum, $error);
-            $singletest_state = ST_INIT;
+            $singletest_state{$runnerid} = ST_INIT;
             return ($error, 0);
         }
 
@@ -1716,9 +1724,9 @@
         my $tool;
         my $usedvalgrind;
         runnerac_test_run($runnerid, $testnum);
-        $singletest_state = ST_RUN;
+        $singletest_state{$runnerid} = ST_RUN;
 
-    } elsif($singletest_state == ST_RUN) {
+    } elsif($singletest_state{$runnerid} == ST_RUN) {
         my ($rid, $error, $logs, $testtimings, $cmdres, $CURLOUT, $tool, $usedvalgrind) = runnerar($runnerid);
         logmsg $logs;
         updatetesttimings($testnum, %$testtimings);
@@ -1728,7 +1736,7 @@
             my $err = ignoreresultcode($testnum);
             # Submit the test case result with the CI environment
             citest_finishtest($testnum, $err);
-            $singletest_state = ST_INIT;
+            $singletest_state{$runnerid} = ST_INIT;
             # return a test failure, either to be reported or to be ignored
             return ($err, 0);
         }
@@ -1737,7 +1745,7 @@
             timestampskippedevents($testnum);
             # Submit the test case result with the CI environment
             citest_finishtest($testnum, $error);
-            $singletest_state = ST_INIT;
+            $singletest_state{$runnerid} = ST_INIT;
             return ($error, 0);
         }
         elsif($error > 0) {
@@ -1745,7 +1753,7 @@
             $timevrfyend{$testnum} = Time::HiRes::time();
             # Submit the test case result with the CI environment
             citest_finishtest($testnum, $error);
-            $singletest_state = ST_INIT;
+            $singletest_state{$runnerid} = ST_INIT;
             return ($error, 0);
         }
 
@@ -1762,7 +1770,7 @@
             my $err = ignoreresultcode($testnum);
             # Submit the test case result with the CI environment
             citest_finishtest($testnum, $err);
-            $singletest_state = ST_INIT;
+            $singletest_state{$runnerid} = ST_INIT;
             # return a test failure, either to be reported or to be ignored
             return ($err, 0);
         }
@@ -1771,7 +1779,7 @@
             # test success code
             # Submit the test case result with the CI environment
             citest_finishtest($testnum, $cmdres);
-            $singletest_state = ST_INIT;
+            $singletest_state{$runnerid} = ST_INIT;
             return ($cmdres, 0);
         }
 
@@ -1782,7 +1790,7 @@
 
         # Submit the test case result with the CI environment
         citest_finishtest($testnum, 0);
-        $singletest_state = ST_INIT;
+        $singletest_state{$runnerid} = ST_INIT;
 
         return (0, 0);  # state machine is finished
     }
@@ -1937,6 +1945,31 @@
     return 0;
 }
 
+#######################################################################
+# Put the given runner ID onto the queue of runners ready for a new task
+#
+sub runnerready {
+    my ($runnerid)=@_;
+    push @runnersidle, $runnerid;
+}
+
+#######################################################################
+# Pick a test runner for the given test
+#
+sub pickrunner {
+    my ($testnum)=@_;
+    if(!scalar(%runnerids)) {
+        # No runners have been created; create one now
+        my $runnernum = 1;
+        cleardir($LOGDIR);
+        mkdir($LOGDIR, 0777);
+        $runnerids{$runnernum} = runner_init($LOGDIR, $jobs);
+        runnerready($runnerids{$runnernum});
+    }
+    scalar(@runnersidle) || die "No runners available";
+
+    return pop @runnersidle;
+}
 
 #######################################################################
 # Check options to this test program
@@ -2576,6 +2609,7 @@
     $ignoretestcodes{$testnum} = $errorreturncode;
     push(@runtests, $testnum);
 }
+my $totaltests = scalar(@runtests);
 
 if($listonly) {
     exit(0);
@@ -2586,12 +2620,6 @@
 citest_starttestrun();
 
 #######################################################################
-# Initialize the runner to prepare to run tests
-cleardir($LOGDIR);
-mkdir($LOGDIR, 0777);
-my $runnerid = runner_init($LOGDIR, $jobs);
-
-#######################################################################
 # The main test-loop
 #
 # run through each candidate test and execute it
@@ -2616,8 +2644,13 @@
         }
 
         # execute one test case
+        if(!exists($runnerfortest{$testnum})) {
+            # New test; pick a runner for it
+            my $runnerid = pickrunner($testnum);
+            $runnerfortest{$testnum} = $runnerid;
+        }
         my $error;
-        ($error, $again) = singletest($runnerid, $testnum, $count, scalar(@runtests));
+        ($error, $again) = singletest($runnerfortest{$testnum}, $testnum, $count, $totaltests);
         if($again) {
             # Wait for asynchronous response
             if(!runnerar_ready(0.05)) {
@@ -2628,6 +2661,7 @@
         }
 
         # Test has completed
+        runnerready($runnerfortest{$testnum});
         if($error < 0) {
             # not a test we can run
             next nexttest;
@@ -2672,16 +2706,26 @@
 citest_finishtestrun();
 
 # Tests done, stop the servers
-runnerac_stopservers($runnerid);
-my ($rid, $unexpected, $logs) = runnerar($runnerid);
-logmsg $logs;
+foreach my $runnerid (values %runnerids) {
+    runnerac_stopservers($runnerid);
+}
 
-# Kill the runner
-# There is a race condition here since we don't know exactly when the runner
-# has finished shutting itself down
-runnerac_shutdown($runnerid);
-undef $runnerid;
-sleep 0;  # give runner a chance to run
+# Wait for servers to stop
+my $unexpected;
+foreach my $runnerid (values %runnerids) {
+    my ($rid, $unexpect, $logs) = runnerar($runnerid);
+    $unexpected ||= $unexpect;
+    logmsg $logs;
+}
+
+# Kill the runners
+# There is a race condition here since we don't know exactly when the runners
+# have each finished shutting themselves down, but we're about to exit so it
+# doesn't make much difference.
+foreach my $runnerid (values %runnerids) {
+    runnerac_shutdown($runnerid);
+    sleep 0;  # give runner a context switch so it can shut itself down
+}
 
 my $numskipped = %skipped ? sum values %skipped : 0;
 my $all = $total + $numskipped;