Newer
Older

Angie Byron
committed
}

Angie Byron
committed
simpletest_script_print_error('Test class not found: ' . $class_name);
simpletest_script_print_alternatives($class_name, $all_classes, 6);
exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);

Angie Byron
committed
}

Dries Buytaert
committed
}
}

Angie Byron
committed
elseif ($args['file']) {

Dries Buytaert
committed
// Extract test case class names from specified files.
$parser = new TestFileParser();
foreach ($args['test_names'] as $file) {

Dries Buytaert
committed
if (!file_exists($file)) {
simpletest_script_print_error('File not found: ' . $file);
exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);

Dries Buytaert
committed
}
$test_list = array_merge($test_list, $parser->getTestListFromFile($file));
}
}

Dries Buytaert
committed
else {
try {
$groups = $test_discovery->getTestClasses(NULL, $args['types']);
$types_processed = TRUE;
}
catch (Exception $e) {
echo (string) $e;
exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
}

Alex Pott
committed
// 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.

Dries Buytaert
committed
foreach ($args['test_names'] as $group_name) {
$test_list = array_merge($test_list, array_keys($groups[$group_name]));

Dries Buytaert
committed
}
// Ensure our list of tests contains only one entry for each test.
$test_list = array_unique($test_list);

Dries Buytaert
committed
}

Dries Buytaert
committed
// 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);
});
}

Dries Buytaert
committed
if (empty($test_list)) {
simpletest_script_print_error('No valid tests were specified.');
exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);

Dries Buytaert
committed
}

Dries Buytaert
committed
return $test_list;
/**
* 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);
});
}

Théodore Biadala
committed
/**
* 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);

Théodore Biadala
committed
});
}
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
/**
* 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;
}

Théodore Biadala
committed
/**
* 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.
*/

Lee Rowlands
committed
function place_tests_into_bins(array $tests, int $bin_count) {

Théodore Biadala
committed
// 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;
}

Dries Buytaert
committed
/**
* Initialize the reporter.
*/
function simpletest_script_reporter_init() {

Dries Buytaert
committed
global $args, $test_list, $results_map;

Dries Buytaert
committed

Lee Rowlands
committed
$results_map = [

Dries Buytaert
committed
'pass' => 'Pass',
'fail' => 'Fail',

Alex Pott
committed
'exception' => 'Exception',

Lee Rowlands
committed
];

Dries Buytaert
committed
echo "\n";
echo "Drupal test run\n";
echo "---------------\n";
echo "\n";

Dries Buytaert
committed

Dries Buytaert
committed
// 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";

Dries Buytaert
committed
foreach ($test_list as $class_name) {

Angie Byron
committed
echo " - $class_name\n";

Dries Buytaert
committed
}
echo "\n";

Dries Buytaert
committed

Dries Buytaert
committed
echo "Test run started:\n";

Angie Byron
committed
echo " " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']) . "\n";
Timer::start('run-tests');

Dries Buytaert
committed
echo "\n";

Dries Buytaert
committed
echo "Test summary\n";
echo "------------\n";

Dries Buytaert
committed
echo "\n";

Angie Byron
committed
/**
* 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.

Théodore Biadala
committed
* @param int|null $duration
* The time taken for the test to complete.

Angie Byron
committed
*/

Théodore Biadala
committed
function simpletest_script_reporter_display_summary($class, $results, $duration = NULL) {

Angie Byron
committed
// 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).

Théodore Biadala
committed
$output = vsprintf('%-60.60s %10s %5s %9s %14s %12s', [

Angie Byron
committed
$class,

Alex Pott
committed
$results['#pass'] . ' passes',

Théodore Biadala
committed
isset($duration) ? ceil($duration) . 's' : '',

Alex Pott
committed
!$results['#fail'] ? '' : $results['#fail'] . ' fails',

Angie Byron
committed
!$results['#exception'] ? '' : $results['#exception'] . ' exceptions',

Alex Pott
committed
!$results['#debug'] ? '' : $results['#debug'] . ' messages',

Lee Rowlands
committed
]);

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

Dries Buytaert
committed
/**

Dries Buytaert
committed
* Display jUnit XML test results.

Dries Buytaert
committed
*/
function simpletest_script_reporter_write_xml_results(TestRunResultsStorageInterface $test_run_results_storage) {

Dries Buytaert
committed
global $args, $test_ids, $results_map;

Dries Buytaert
committed
try {
$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);
}

Dries Buytaert
committed
$test_class = '';

Lee Rowlands
committed
$xml_files = [];

Dries Buytaert
committed
foreach ($results as $result) {
if (isset($results_map[$result->status])) {
if ($result->test_class != $test_class) {

Alex Pott
committed
// We've moved onto a new class, so write the last classes results to a
// file:

Dries Buytaert
committed
if (isset($xml_files[$test_class])) {
file_put_contents($args['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());

Dries Buytaert
committed
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);

Lee Rowlands
committed
$xml_files[$test_class] = ['doc' => $doc, 'suite' => $root];

Dries Buytaert
committed
}
}
// 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, '->')) {

Lee Rowlands
committed
[$class, $name] = explode('->', $result->function, 2);

Alex Pott
committed
}
else {
$name = $result->function;
}

Dries Buytaert
committed
$case->setAttribute('name', $name);

Alex Pott
committed
// Passes get no further attention, but failures and exceptions get to add
// more detail:

Dries Buytaert
committed
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
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());

Dries Buytaert
committed
unset($xml_files[$test_class]);
}
}
/**
* Stop the test timer.
*/
function simpletest_script_reporter_timer_stop() {

Dries Buytaert
committed
echo "\n";
$end = Timer::stop('run-tests');
echo "Test run duration: " . \Drupal::service('date.formatter')->formatInterval((int) ($end['time'] / 1000));

Dries Buytaert
committed
echo "\n\n";

Dries Buytaert
committed
}
/**
* Display test results.
*/
function simpletest_script_reporter_display_results(TestRunResultsStorageInterface $test_run_results_storage) {

Dries Buytaert
committed
global $args, $test_ids, $results_map;

Dries Buytaert
committed

Dries Buytaert
committed
if ($args['verbose']) {
// Report results.

Dries Buytaert
committed
echo "Detailed test results\n";
echo "---------------------\n";

Dries Buytaert
committed
try {
$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);
}

Dries Buytaert
committed
$test_class = '';

Dries Buytaert
committed
foreach ($results as $result) {

Dries Buytaert
committed
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;

Dries Buytaert
committed

catch
committed
// Print table header.
echo "Status Group Filename Line Function \n";
echo "--------------------------------------------------------------------------------\n";

Dries Buytaert
committed
}
simpletest_script_format_result($result);
}
}

Dries Buytaert
committed
/**

Alex Pott
committed
* Format the result so that it fits within 80 characters.

Dries Buytaert
committed
*

Alex Pott
committed
* @param object $result
* The result object to format.

Dries Buytaert
committed
*/
function simpletest_script_format_result($result) {
global $args, $results_map, $color;

Dries Buytaert
committed

Dries Buytaert
committed
$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);

Dries Buytaert
committed
simpletest_script_print($summary, simpletest_script_color_code($result->status));

Dries Buytaert
committed
$message = trim(strip_tags($result->message));
if ($args['non-html']) {
$message = Html::decodeEntities($message);
}

Dave Long
committed
$lines = explode("\n", $message);

Dries Buytaert
committed
foreach ($lines as $line) {
echo " $line\n";
}
}

Dries Buytaert
committed

Dries Buytaert
committed
/**

Alex Pott
committed
* Print error messages so the user will notice them.

Dries Buytaert
committed
*

Alex Pott
committed
* Print error message prefixed with " ERROR: " and displayed in fail color if
* color output is enabled.
*
* @param string $message
* The message to print.

Dries Buytaert
committed
*/

Dries Buytaert
committed
function simpletest_script_print_error($message) {
simpletest_script_print(" ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);

Dries Buytaert
committed
}

Dries Buytaert
committed

Dries Buytaert
committed
/**

Alex Pott
committed
* Print a message to the console, using a color.

Dries Buytaert
committed
*

Alex Pott
committed
* @param string $message
* The message to print.
* @param int $color_code
* The color code to use for coloring.

Dries Buytaert
committed
*/
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.
*

Alex Pott
committed
* @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.

Dries Buytaert
committed
*/
function simpletest_script_color_code($status) {
switch ($status) {
case 'pass':
return SIMPLETEST_SCRIPT_COLOR_PASS;

Alex Pott
committed

Dries Buytaert
committed
case 'fail':
return SIMPLETEST_SCRIPT_COLOR_FAIL;

Alex Pott
committed

Dries Buytaert
committed
case 'exception':
return SIMPLETEST_SCRIPT_COLOR_EXCEPTION;
}

Alex Pott
committed
// Default formatting.
return 0;

Dries Buytaert
committed
}

Angie Byron
committed
/**
* 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.

Alex Pott
committed
*
* @see http://php.net/manual/function.levenshtein.php

Angie Byron
committed
*/
function simpletest_script_print_alternatives($string, $array, $degree = 4) {

Lee Rowlands
committed
$alternatives = [];

Angie Byron
committed
foreach ($array as $item) {
$lev = levenshtein($string, $item);
if ($lev <= strlen($item) / $degree || str_contains($string, $item)) {

Angie Byron
committed
$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);
}
}
}

Angie Byron
committed
/**

catch
committed
* Loads test result messages from the database.

Angie Byron
committed
*
* 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

catch
committed
* Array of test result messages from the database.

Angie Byron
committed
*/
function simpletest_script_load_messages_by_test_id(TestRunResultsStorageInterface $test_run_results_storage, $test_ids) {

Angie Byron
committed
global $args;

Lee Rowlands
committed
$results = [];

Angie Byron
committed
// 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 {

Lee Rowlands
committed
$test_id_chunks = [$test_ids];

Angie Byron
committed
}
foreach ($test_id_chunks as $test_id_chunk) {
try {
$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);
}

Angie Byron
committed
if ($result_chunk) {
$results = array_merge($results, $result_chunk);
}
}
return $results;
}