Skip to content
Snippets Groups Projects
run-tests.sh 49.7 KiB
Newer Older
          simpletest_script_print_error('Test class not found: ' . $class_name);
          simpletest_script_print_alternatives($class_name, $all_classes, 6);
      // Extract test case class names from specified files.
      $parser = new TestFileParser();
      foreach ($args['test_names'] as $file) {
        if (!file_exists($file)) {
          simpletest_script_print_error('File not found: ' . $file);
        $test_list = array_merge($test_list, $parser->getTestListFromFile($file));
        $groups = $test_discovery->getTestClasses(NULL, $args['types']);
      }
      catch (Exception $e) {
        echo (string) $e;
        exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
      }
      // Store all the groups so we can suggest alternatives if we need to.
      $all_groups = array_keys($groups);
      // Verify that the groups exist.
      if (!empty($unknown_groups = array_diff($args['test_names'], $all_groups))) {
        $first_group = reset($unknown_groups);
        simpletest_script_print_error('Test group not found: ' . $first_group);
        simpletest_script_print_alternatives($first_group, $all_groups);
        exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
      }
      // Merge the tests from the groups together.
      foreach ($args['test_names'] as $group_name) {
        $test_list = array_merge($test_list, array_keys($groups[$group_name]));
      // Ensure our list of tests contains only one entry for each test.
      $test_list = array_unique($test_list);
  // If the test list creation does not automatically limit by test type then
  // we need to do so here.
  if (!$types_processed) {
    $test_list = array_filter($test_list, function ($test_class) use ($args) {
      $test_info = TestDiscovery::getTestInfo($test_class);
      return in_array($test_info['type'], $args['types'], TRUE);
    });
  }

  if (empty($test_list)) {
    simpletest_script_print_error('No valid tests were specified.');
/**
 * Sort tests by test type and number of public methods.
 */
function sort_tests_by_type_and_methods(array &$tests) {
  usort($tests, function ($a, $b) {
    if (get_test_type_weight($a) === get_test_type_weight($b)) {
      return get_test_class_method_count($b) <=> get_test_class_method_count($a);
    }
    return get_test_type_weight($b) <=> get_test_type_weight($a);
  });
}

/**
 * Sort tests by the number of public methods in the test class.
 *
 * Tests with several methods take longer to run than tests with a single
 * method all else being equal, so this allows tests runs to be sorted by
 * approximately the slowest to fastest tests. Tests that are exceptionally
 * slow can be added to the '#slow' group so they are placed first in each
 * test run regardless of the number of methods.
 *
 * @param string[] $tests
 *   An array of test class names.
 */
function sort_tests_by_public_method_count(array &$tests): void {
  usort($tests, function ($a, $b) {
    return get_test_class_method_count($b) <=> get_test_class_method_count($a);
/**
 * Weights a test class based on which test base class it extends.
 *
 * @param string $class
 *   The test class name.
 */
function get_test_type_weight(string $class): int {
  return match(TRUE) {
    is_subclass_of($class, WebDriverTestBase::class) => 3,
    is_subclass_of($class, BrowserTestBase::class) => 2,
    is_subclass_of($class, BuildTestBase::class) => 2,
    is_subclass_of($class, KernelTestBase::class) => 1,
    default => 0,
  };
}

/**
 * Get an approximate test method count for a test class.
 *
 * @param string $class
 *   The test class name.
 */
function get_test_class_method_count(string $class): int {
  $reflection = new \ReflectionClass($class);
  $count = 0;
  foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
    // If a method uses a dataProvider, increase the count by 20 since data
    // providers result in a single method running multiple times.
    $comments = $method->getDocComment();
    preg_match_all('#@(.*?)\n#s', $comments, $annotations);
    foreach ($annotations[1] as $annotation) {
      if (str_starts_with($annotation, 'dataProvider')) {
        $count = $count + 20;
        continue;
      }
    }
    $count++;
  }
  return $count;
}

/**
 * Distribute tests into bins.
 *
 * The given array of tests is split into the available bins. The distribution
 * starts with the first test, placing the first test in the first bin, the
 * second test in the second bin and so on. This results each bin having a
 * similar number of test methods to run in total.
 *
 * @param string[] $tests
 *   An array of test class names.
 * @param int $bin_count
 *   The number of bins available.
 *
 * @return array
 *   An associative array of bins and the test class names in each bin.
 */
function place_tests_into_bins(array $tests, int $bin_count) {
  // Create a bin corresponding to each parallel test job.
  $bins = array_fill(0, $bin_count, []);
  // Go through each test and add them to one bin at a time.
  foreach ($tests as $key => $test) {
    $bins[($key % $bin_count)][] = $test;
  }
  return $bins;
}

/**
 * Initialize the reporter.
 */
function simpletest_script_reporter_init() {
  echo "\n";
  echo "Drupal test run\n";
  echo "---------------\n";
  echo "\n";
  // Tell the user about what tests are to be run.
  if ($args['all']) {
    echo "All tests will run.\n\n";
  }
  else {
    echo "Tests to be run:\n";
  echo "  " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']) . "\n";
  Timer::start('run-tests');
/**
 * Displays the assertion result summary for a single test class.
 *
 * @param string $class
 *   The test class name that was run.
 * @param array $results
 *   The assertion results using #pass, #fail, #exception, #debug array keys.
 * @param int|null $duration
 *   The time taken for the test to complete.
function simpletest_script_reporter_display_summary($class, $results, $duration = NULL) {
  // Output all test results vertically aligned.
  // Cut off the class name after 60 chars, and pad each group with 3 digits
  // by default (more than 999 assertions are rare).
  $output = vsprintf('%-60.60s %10s %5s %9s %14s %12s', [
    !$results['#fail'] ? '' : $results['#fail'] . ' fails',
    !$results['#exception'] ? '' : $results['#exception'] . ' exceptions',
    !$results['#debug'] ? '' : $results['#debug'] . ' messages',

  $status = ($results['#fail'] || $results['#exception'] ? 'fail' : 'pass');
  simpletest_script_print($output . "\n", simpletest_script_color_code($status));
}

function simpletest_script_reporter_write_xml_results(TestRunResultsStorageInterface $test_run_results_storage) {
    $results = simpletest_script_load_messages_by_test_id($test_run_results_storage, $test_ids);
  }
  catch (Exception $e) {
    echo (string) $e;
    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  }

  foreach ($results as $result) {
    if (isset($results_map[$result->status])) {
      if ($result->test_class != $test_class) {
        // We've moved onto a new class, so write the last classes results to a
        // file:
          file_put_contents($args['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());
          unset($xml_files[$test_class]);
        }
        $test_class = $result->test_class;
        if (!isset($xml_files[$test_class])) {
          $doc = new DomDocument('1.0');
          $root = $doc->createElement('testsuite');
          $root = $doc->appendChild($root);
          $xml_files[$test_class] = ['doc' => $doc, 'suite' => $root];
        }
      }

      // For convenience:
      $dom_document = &$xml_files[$test_class]['doc'];

      // Create the XML element for this test case:
      $case = $dom_document->createElement('testcase');
      $case->setAttribute('classname', $test_class);
      if (str_contains($result->function, '->')) {
        [$class, $name] = explode('->', $result->function, 2);
      // Passes get no further attention, but failures and exceptions get to add
      // more detail:
      if ($result->status == 'fail') {
        $fail = $dom_document->createElement('failure');
        $fail->setAttribute('type', 'failure');
        $fail->setAttribute('message', $result->message_group);
        $text = $dom_document->createTextNode($result->message);
        $fail->appendChild($text);
        $case->appendChild($fail);
      }
      elseif ($result->status == 'exception') {
        // In the case of an exception the $result->function may not be a class
        // method so we record the full function name:
        $case->setAttribute('name', $result->function);

        $fail = $dom_document->createElement('error');
        $fail->setAttribute('type', 'exception');
        $fail->setAttribute('message', $result->message_group);
        $full_message = $result->message . "\n\nline: " . $result->line . "\nfile: " . $result->file;
        $text = $dom_document->createTextNode($full_message);
        $fail->appendChild($text);
        $case->appendChild($fail);
      }
      // Append the test case XML to the test suite:
      $xml_files[$test_class]['suite']->appendChild($case);
    }
  }
  // The last test case hasn't been saved to a file yet, so do that now:
  if (isset($xml_files[$test_class])) {
    file_put_contents($args['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());
    unset($xml_files[$test_class]);
  }
}

/**
 * Stop the test timer.
 */
function simpletest_script_reporter_timer_stop() {
  $end = Timer::stop('run-tests');
catch's avatar
catch committed
  echo "Test run duration: " . \Drupal::service('date.formatter')->formatInterval((int) ($end['time'] / 1000));
function simpletest_script_reporter_display_results(TestRunResultsStorageInterface $test_run_results_storage) {
    echo "Detailed test results\n";
    echo "---------------------\n";
      $results = simpletest_script_load_messages_by_test_id($test_run_results_storage, $test_ids);
    }
    catch (Exception $e) {
      echo (string) $e;
      exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
    }
      if (isset($results_map[$result->status])) {
        if ($result->test_class != $test_class) {
          // Display test class every time results are for new test class.
          echo "\n\n---- $result->test_class ----\n\n\n";
          $test_class = $result->test_class;
          // Print table header.
          echo "Status    Group      Filename          Line Function                            \n";
          echo "--------------------------------------------------------------------------------\n";
 * Format the result so that it fits within 80 characters.
 * @param object $result
 *   The result object to format.
 */
function simpletest_script_format_result($result) {
  global $args, $results_map, $color;
  $summary = sprintf("%-9.9s %-10.10s %-17.17s %4.4s %-35.35s\n",
    $results_map[$result->status], $result->message_group, basename($result->file), $result->line, $result->function);

  simpletest_script_print($summary, simpletest_script_color_code($result->status));
  $message = trim(strip_tags($result->message));
  if ($args['non-html']) {
    $message = Html::decodeEntities($message);
 * Print error messages so the user will notice them.
 * Print error message prefixed with "  ERROR: " and displayed in fail color if
 * color output is enabled.
 *
 * @param string $message
 *   The message to print.
function simpletest_script_print_error($message) {
  simpletest_script_print("  ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
 * Print a message to the console, using a color.
 * @param string $message
 *   The message to print.
 * @param int $color_code
 *   The color code to use for coloring.
 */
function simpletest_script_print($message, $color_code) {
  global $args;
  if ($args['color']) {
    echo "\033[" . $color_code . "m" . $message . "\033[0m";
  }
  else {
    echo $message;
  }
}

/**
 * Get the color code associated with the specified status.
 *
 * @param string $status
 *   The status string to get code for. Special cases are: 'pass', 'fail', or
 *   'exception'.
 *
 * @return int
 *   Color code. Returns 0 for default case.
 */
function simpletest_script_color_code($status) {
  switch ($status) {
    case 'pass':
      return SIMPLETEST_SCRIPT_COLOR_PASS;
    case 'fail':
      return SIMPLETEST_SCRIPT_COLOR_FAIL;
    case 'exception':
      return SIMPLETEST_SCRIPT_COLOR_EXCEPTION;
  }

/**
 * Prints alternative test names.
 *
 * Searches the provided array of string values for close matches based on the
 * Levenshtein algorithm.
 *
 * @param string $string
 *   A string to test.
 * @param array $array
 *   A list of strings to search.
 * @param int $degree
 *   The matching strictness. Higher values return fewer matches. A value of
 *   4 means that the function will return strings from $array if the candidate
 *   string in $array would be identical to $string by changing 1/4 or fewer of
 *   its characters.
 * @see http://php.net/manual/function.levenshtein.php
 */
function simpletest_script_print_alternatives($string, $array, $degree = 4) {
  foreach ($array as $item) {
    $lev = levenshtein($string, $item);
    if ($lev <= strlen($item) / $degree || str_contains($string, $item)) {
      $alternatives[] = $item;
    }
  }
  if (!empty($alternatives)) {
    simpletest_script_print("  Did you mean?\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
    foreach ($alternatives as $alternative) {
      simpletest_script_print("  - $alternative\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
    }
  }
}
 * Loads test result messages from the database.
 *
 * Messages are ordered by test class and message id.
 *
 * @param array $test_ids
 *   Array of test IDs of the messages to be loaded.
 *
 * @return array
 *   Array of test result messages from the database.
function simpletest_script_load_messages_by_test_id(TestRunResultsStorageInterface $test_run_results_storage, $test_ids) {

  // Sqlite has a maximum number of variables per query. If required, the
  // database query is split into chunks.
  if (count($test_ids) > SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT && !empty($args['sqlite'])) {
    $test_id_chunks = array_chunk($test_ids, SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT);
  }
  else {
      $result_chunk = [];
      foreach ($test_id_chunk as $test_id) {
        $test_run = TestRun::get($test_run_results_storage, $test_id);
        $result_chunk = array_merge($result_chunk, $test_run->getLogEntriesByTestClass());
      }
    }
    catch (Exception $e) {
      echo (string) $e;
      exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
    }
    if ($result_chunk) {
      $results = array_merge($results, $result_chunk);
    }
  }

  return $results;
}