#771448: rewrite the test runner to be able to run tests concurrently on environments that doesn't support PCTNL.

From: damz <damz@damz.dev.local.dev.local>


---
 run-tests.sh |  129 +++++++++++++++++++++++++++-------------------------------
 1 files changed, 60 insertions(+), 69 deletions(-)

diff --git scripts/run-tests.sh scripts/run-tests.sh
index 748fbbe..6cddf7f 100755
--- scripts/run-tests.sh
+++ scripts/run-tests.sh
@@ -17,10 +17,10 @@ if ($args['help'] || $count == 0) {
   exit;
 }
 
-if ($args['execute-batch']) {
-  // Masquerade as Apache for running tests.
+if ($args['execute-test']) {
+  // Execute a single test case, masquerading as Apache.
   simpletest_script_init("Apache");
-  simpletest_script_execute_batch();
+  simpletest_script_run_one_test($args['test-id'], $args['execute-test']);
 }
 else {
   // Run administrative functions as CLI.
@@ -68,8 +68,6 @@ if ($args['list']) {
   exit;
 }
 
-$test_list = simpletest_script_get_test_list();
-
 // Try to allocate unlimited time to run the tests.
 drupal_set_time_limit(0);
 
@@ -79,7 +77,7 @@ simpletest_script_reporter_init();
 $test_id = db_insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();
 
 // Execute tests.
-simpletest_script_command($args['concurrency'], $test_id, implode(",", $test_list));
+simpletest_script_execute_batch($test_id, simpletest_script_get_test_list());
 
 // Retrieve the last database prefix used for testing and the last test class
 // that was run from. Use the information to read the lgo file in case any
@@ -93,6 +91,9 @@ simpletest_script_reporter_display_results();
 // Cleanup our test results.
 simpletest_clean_results_table($test_id);
 
+// Test complete, exit.
+exit;
+
 /**
  * Print help text.
  */
@@ -183,8 +184,8 @@ function simpletest_script_parse_args() {
     'verbose' => FALSE,
     'test_names' => array(),
     // Used internally.
-    'test-id' => NULL,
-    'execute-batch' => FALSE
+    'test-id' => 0,
+    'execute-test' => '',
   );
 
   // Override with set values.
@@ -225,10 +226,6 @@ function simpletest_script_parse_args() {
     simpletest_script_print_error("--concurrency must be a strictly positive integer.");
     exit;
   }
-  elseif ($args['concurrency'] > 1 && !function_exists('pcntl_fork')) {
-    simpletest_script_print_error("Parallel test execution requires the Process Control extension to be compiled in PHP. See http://php.net/manual/en/intro.pcntl.php for more information.");
-    exit;
-  }
 
   return array($args, $count);
 }
@@ -299,72 +296,58 @@ function simpletest_script_init($server_software) {
 /**
  * Execute a batch of tests.
  */
-function simpletest_script_execute_batch() {
+function simpletest_script_execute_batch($test_id, $test_classes) {
   global $args;
 
-  if (is_null($args['test-id'])) {
-    simpletest_script_print_error("--execute-batch should not be called interactively.");
-    exit;
-  }
-  if ($args['concurrency'] == 1) {
-    // Fallback to mono-threaded execution.
-    if (count($args['test_names']) > 1) {
-      foreach ($args['test_names'] as $test_class) {
-        // Execute each test in its separate Drupal environment.
-        simpletest_script_command(1, $args['test-id'], $test_class);
-      }
-      exit;
-    }
-    else {
-      // Execute an individual test.
-      $test_class = array_shift($args['test_names']);
-      drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
-      simpletest_script_run_one_test($args['test-id'], $test_class);
-      exit;
-    }
-  }
-  else {
-    // Multi-threaded execution.
-    $children = array();
-    while (!empty($args['test_names']) || !empty($children)) {
-      // Fork children safely since Drupal is not bootstrapped yet.
-      while (count($children) < $args['concurrency']) {
-        if (empty($args['test_names'])) break;
-
-        $child = array();
-        $child['test_class'] = $test_class = array_shift($args['test_names']);
-        $child['pid'] = pcntl_fork();
-        if (!$child['pid']) {
-          // This is the child process, bootstrap and execute the test.
-          drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
-          simpletest_script_run_one_test($args['test-id'], $test_class);
-          exit;
-        }
-        else {
-          // Register our new child.
-          $children[] = $child;
-        }
+  // Multi-process execution.
+  $children = array();
+  while (!empty($test_classes) || !empty($children)) {
+    while (count($children) < $args['concurrency']) {
+      if (empty($test_classes)) break;
+
+      // Fork a child process.
+      $test_class = array_shift($test_classes);
+      $command = simpletest_script_command($test_id, $test_class);
+      $process = proc_open($command, array(), $pipes, NULL, NULL, array('bypass_shell' => TRUE));
+
+      if (!is_resource($process)) {
+        echo "Unable to fork test process. Aborting.\n";
+        exit;
       }
 
-      // Wait for children every 200ms.
-      usleep(200000);
+      // Register our new child.
+      $children[] = array(
+        'process' => $process,
+        'class' => $test_class,
+        'pipes' => $pipes,
+      );
+    }
 
-      // Check if some children finished.
-      foreach ($children as $cid => $child) {
-        if (pcntl_waitpid($child['pid'], $status, WUNTRACED | WNOHANG)) {
-          // This particular child exited.
-          unset($children[$cid]);
+    // Wait for children every 200ms.
+    usleep(200000);
+
+    // Check if some children finished.
+    foreach ($children as $cid => $child) {
+      $status = proc_get_status($child['process']);
+      if (empty($status['running'])) {
+        // The child exited, unregister it.
+        proc_close($child['process']);
+        if ($status['exitcode']) {
+          echo 'FATAL ' . $test_class . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n";
         }
+        unset($children[$cid]);
       }
     }
-    exit;
   }
 }
 
 /**
- * Run a single test (assume a Drupal bootstrapped environment).
+ * Run a single test.
  */
 function simpletest_script_run_one_test($test_id, $test_class) {
+  // Bootstrap Drupal.
+  drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
+
   $test = new $test_class($test_id);
   $test->run();
   $info = $test->getInfo();
@@ -372,20 +355,28 @@ function simpletest_script_run_one_test($test_id, $test_class) {
   $status = ((isset($test->results['#fail']) && $test->results['#fail'] > 0)
            || (isset($test->results['#exception']) && $test->results['#exception'] > 0) ? 'fail' : 'pass');
   simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($test->results) . "\n", simpletest_script_color_code($status));
+
+  // Finished, kill this runner.
+  exit(0);
 }
 
 /**
- * Execute a command to run batch of tests in separate process.
+ * Return a command used to run a test in a separate process.
+ *
+ * @param $test_id
+ *  The current test ID.
+ * @param $test_class
+ *  The name of the test class to run.
  */
-function simpletest_script_command($concurrency, $test_id, $tests) {
+function simpletest_script_command($test_id, $test_class) {
   global $args, $php;
 
-  $command = "$php ./scripts/{$args['script']} --url {$args['url']}";
+  $command = escapeshellarg($php) . ' ' . escapeshellarg('./scripts/' . $args['script']) . ' --url ' . escapeshellarg($args['url']);
   if ($args['color']) {
     $command .= ' --color';
   }
-  $command .= " --php " . escapeshellarg($php) . " --concurrency $concurrency --test-id $test_id --execute-batch $tests";
-  passthru($command);
+  $command .= " --php " . escapeshellarg($php) . " --test-id $test_id --execute-test $test_class";
+  return $command;
 }
 
 /**
