1<?php 2 3/** 4 * @file 5 * This script runs Drupal tests from command line. 6 */ 7 8use Drupal\Component\FileSystem\FileSystem; 9use Drupal\Component\Utility\Environment; 10use Drupal\Component\Utility\Html; 11use Drupal\Component\Utility\Timer; 12use Drupal\Core\Composer\Composer; 13use Drupal\Core\Database\Database; 14use Drupal\Core\File\Exception\FileException; 15use Drupal\Core\Test\EnvironmentCleaner; 16use Drupal\Core\Test\PhpUnitTestRunner; 17use Drupal\Core\Test\RunTests\TestFileParser; 18use Drupal\Core\Test\TestDatabase; 19use Drupal\Core\Test\TestRunnerKernel; 20use Drupal\Core\Test\TestDiscovery; 21use Drupal\TestTools\PhpUnitCompatibility\PhpUnit8\ClassWriter; 22use PHPUnit\Framework\TestCase; 23use PHPUnit\Runner\Version; 24use Symfony\Component\Console\Output\ConsoleOutput; 25use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem; 26use Symfony\Component\HttpFoundation\Request; 27 28// Define some colors for display. 29// A nice calming green. 30const SIMPLETEST_SCRIPT_COLOR_PASS = 32; 31// An alerting Red. 32const SIMPLETEST_SCRIPT_COLOR_FAIL = 31; 33// An annoying brown. 34const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33; 35 36// Restricting the chunk of queries prevents memory exhaustion. 37const SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT = 350; 38 39const SIMPLETEST_SCRIPT_EXIT_SUCCESS = 0; 40const SIMPLETEST_SCRIPT_EXIT_FAILURE = 1; 41const SIMPLETEST_SCRIPT_EXIT_EXCEPTION = 2; 42 43// Set defaults and get overrides. 44list($args, $count) = simpletest_script_parse_args(); 45 46if ($args['help'] || $count == 0) { 47 simpletest_script_help(); 48 exit(($count == 0) ? SIMPLETEST_SCRIPT_EXIT_FAILURE : SIMPLETEST_SCRIPT_EXIT_SUCCESS); 49} 50 51simpletest_script_init(); 52 53if (!class_exists(TestCase::class)) { 54 echo "\nrun-tests.sh requires the PHPUnit testing framework. Please use 'composer install' to ensure that it is present.\n\n"; 55 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); 56} 57 58if ($args['execute-test']) { 59 simpletest_script_setup_database(); 60 simpletest_script_run_one_test($args['test-id'], $args['execute-test']); 61 // Sub-process exited already; this is just for clarity. 62 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS); 63} 64 65if ($args['list']) { 66 // Display all available tests organized by one @group annotation. 67 echo "\nAvailable test groups & classes\n"; 68 echo "-------------------------------\n\n"; 69 $test_discovery = new TestDiscovery( 70 \Drupal::service('app.root'), 71 \Drupal::service('class_loader') 72 ); 73 try { 74 $groups = $test_discovery->getTestClasses($args['module']); 75 } 76 catch (Exception $e) { 77 error_log((string) $e); 78 echo (string) $e; 79 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 80 } 81 82 // A given class can appear in multiple groups. For historical reasons, we 83 // need to present each test only once. The test is shown in the group that is 84 // printed first. 85 $printed_tests = []; 86 foreach ($groups as $group => $tests) { 87 echo $group . "\n"; 88 $tests = array_diff(array_keys($tests), $printed_tests); 89 foreach ($tests as $test) { 90 echo " - $test\n"; 91 } 92 $printed_tests = array_merge($printed_tests, $tests); 93 } 94 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS); 95} 96 97// List-files and list-files-json provide a way for external tools such as the 98// testbot to prioritize running changed tests. 99// @see https://www.drupal.org/node/2569585 100if ($args['list-files'] || $args['list-files-json']) { 101 // List all files which could be run as tests. 102 $test_discovery = new TestDiscovery( 103 \Drupal::service('app.root'), 104 \Drupal::service('class_loader') 105 ); 106 // TestDiscovery::findAllClassFiles() gives us a classmap similar to a 107 // Composer 'classmap' array. 108 $test_classes = $test_discovery->findAllClassFiles(); 109 // JSON output is the easiest. 110 if ($args['list-files-json']) { 111 echo json_encode($test_classes); 112 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS); 113 } 114 // Output the list of files. 115 else { 116 foreach (array_values($test_classes) as $test_class) { 117 echo $test_class . "\n"; 118 } 119 } 120 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS); 121} 122 123simpletest_script_setup_database(TRUE); 124 125if ($args['clean']) { 126 // Clean up left-over tables and directories. 127 $cleaner = new EnvironmentCleaner( 128 DRUPAL_ROOT, 129 Database::getConnection(), 130 TestDatabase::getConnection(), 131 new ConsoleOutput(), 132 \Drupal::service('file_system') 133 ); 134 try { 135 $cleaner->cleanEnvironment(); 136 } 137 catch (Exception $e) { 138 echo (string) $e; 139 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 140 } 141 echo "\nEnvironment cleaned.\n"; 142 143 // Get the status messages and print them. 144 $messages = \Drupal::messenger()->messagesByType('status'); 145 foreach ($messages as $text) { 146 echo " - " . $text . "\n"; 147 } 148 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS); 149} 150 151if (!Composer::upgradePHPUnitCheck(Version::id())) { 152 simpletest_script_print_error("PHPUnit testing framework version 9 or greater is required when running on PHP 7.4 or greater. Run the command 'composer run-script drupal-phpunit-upgrade' in order to fix this."); 153 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); 154} 155 156$test_list = simpletest_script_get_test_list(); 157 158// Try to allocate unlimited time to run the tests. 159Environment::setTimeLimit(0); 160simpletest_script_reporter_init(); 161 162$tests_to_run = []; 163for ($i = 0; $i < $args['repeat']; $i++) { 164 $tests_to_run = array_merge($tests_to_run, $test_list); 165} 166 167// Execute tests. 168$status = simpletest_script_execute_batch($tests_to_run); 169 170// Stop the timer. 171simpletest_script_reporter_timer_stop(); 172 173// Ensure all test locks are released once finished. If tests are run with a 174// concurrency of 1 the each test will clean up its own lock. Test locks are 175// not released if using a higher concurrency to ensure each test method has 176// unique fixtures. 177TestDatabase::releaseAllTestLocks(); 178 179// Display results before database is cleared. 180simpletest_script_reporter_display_results(); 181 182if ($args['xml']) { 183 simpletest_script_reporter_write_xml_results(); 184} 185 186// Clean up all test results. 187if (!$args['keep-results']) { 188 try { 189 $cleaner = new EnvironmentCleaner( 190 DRUPAL_ROOT, 191 Database::getConnection(), 192 TestDatabase::getConnection(), 193 new ConsoleOutput(), 194 \Drupal::service('file_system') 195 ); 196 $cleaner->cleanResultsTable(); 197 } 198 catch (Exception $e) { 199 echo (string) $e; 200 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 201 } 202} 203 204// Test complete, exit. 205exit($status); 206 207/** 208 * Print help text. 209 */ 210function simpletest_script_help() { 211 global $args; 212 213 echo <<<EOF 214 215Run Drupal tests from the shell. 216 217Usage: {$args['script']} [OPTIONS] <tests> 218Example: {$args['script']} Profile 219 220All arguments are long options. 221 222 --help Print this page. 223 224 --list Display all available test groups. 225 226 --list-files 227 Display all discoverable test file paths. 228 229 --list-files-json 230 Display all discoverable test files as JSON. The array key will be 231 the test class name, and the value will be the file path of the 232 test. 233 234 --clean Cleans up database tables or directories from previous, failed, 235 tests and then exits (no tests are run). 236 237 --url The base URL of the root directory of this Drupal checkout; e.g.: 238 http://drupal.test/ 239 Required unless the Drupal root directory maps exactly to: 240 http://localhost:80/ 241 Use a https:// URL to force all tests to be run under SSL. 242 243 --sqlite A pathname to use for the SQLite database of the test runner. 244 Required unless this script is executed with a working Drupal 245 installation that has Simpletest module installed. 246 A relative pathname is interpreted relative to the Drupal root 247 directory. 248 Note that ':memory:' cannot be used, because this script spawns 249 sub-processes. However, you may use e.g. '/tmpfs/test.sqlite' 250 251 --keep-results-table 252 253 Boolean flag to indicate to not cleanup the simpletest result 254 table. For testbots or repeated execution of a single test it can 255 be helpful to not cleanup the simpletest result table. 256 257 --dburl A URI denoting the database driver, credentials, server hostname, 258 and database name to use in tests. 259 Required when running tests without a Drupal installation that 260 contains default database connection info in settings.php. 261 Examples: 262 mysql://username:password@localhost/databasename#table_prefix 263 sqlite://localhost/relative/path/db.sqlite 264 sqlite://localhost//absolute/path/db.sqlite 265 266 --php The absolute path to the PHP executable. Usually not needed. 267 268 --concurrency [num] 269 270 Run tests in parallel, up to [num] tests at a time. 271 272 --all Run all available tests. 273 274 --module Run all tests belonging to the specified module name. 275 (e.g., 'node') 276 277 --class Run tests identified by specific class names, instead of group names. 278 A specific test method can be added, for example, 279 'Drupal\book\Tests\BookTest::testBookExport'. This argument must 280 be last on the command line. 281 282 --file Run tests identified by specific file names, instead of group names. 283 Specify the path and the extension 284 (i.e. 'core/modules/user/user.test'). This argument must be last 285 on the command line. 286 287 --types 288 289 Runs just tests from the specified test type, for example 290 run-tests.sh 291 (i.e. --types "Simpletest,PHPUnit-Functional") 292 293 --directory Run all tests found within the specified file directory. 294 295 --xml <path> 296 297 If provided, test results will be written as xml files to this path. 298 299 --color Output text format results with color highlighting. 300 301 --verbose Output detailed assertion messages in addition to summary. 302 303 --keep-results 304 305 Keeps detailed assertion results (in the database) after tests 306 have completed. By default, assertion results are cleared. 307 308 --repeat Number of times to repeat the test. 309 310 --die-on-fail 311 312 Exit test execution immediately upon any failed assertion. This 313 allows to access the test site by changing settings.php to use the 314 test database and configuration directories. Use in combination 315 with --repeat for debugging random test failures. 316 317 --non-html Removes escaping from output. Useful for reading results on the 318 CLI. 319 320 --suppress-deprecations 321 322 Stops tests from failing if deprecation errors are triggered. If 323 this is not set the value specified in the 324 SYMFONY_DEPRECATIONS_HELPER environment variable, or the value 325 specified in core/phpunit.xml (if it exists), or the default value 326 will be used. The default is that any unexpected silenced 327 deprecation error will fail tests. 328 329 <test1>[,<test2>[,<test3> ...]] 330 331 One or more tests to be run. By default, these are interpreted 332 as the names of test groups as shown at 333 admin/config/development/testing. 334 These group names typically correspond to module names like "User" 335 or "Profile" or "System", but there is also a group "Database". 336 If --class is specified then these are interpreted as the names of 337 specific test classes whose test methods will be run. Tests must 338 be separated by commas. Ignored if --all is specified. 339 340To run this script you will normally invoke it from the root directory of your 341Drupal installation as the webserver user (differs per configuration), or root: 342 343sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']} 344 --url http://example.com/ --all 345sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']} 346 --url http://example.com/ --class "Drupal\block\Tests\BlockTest" 347 348Without a preinstalled Drupal site and enabled Simpletest module, specify a 349SQLite database pathname to create and the default database connection info to 350use in tests: 351 352sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']} 353 --sqlite /tmpfs/drupal/test.sqlite 354 --dburl mysql://username:password@localhost/database 355 --url http://example.com/ --all 356 357EOF; 358} 359 360/** 361 * Parse execution argument and ensure that all are valid. 362 * 363 * @return array 364 * The list of arguments. 365 */ 366function simpletest_script_parse_args() { 367 // Set default values. 368 $args = [ 369 'script' => '', 370 'help' => FALSE, 371 'list' => FALSE, 372 'list-files' => FALSE, 373 'list-files-json' => FALSE, 374 'clean' => FALSE, 375 'url' => '', 376 'sqlite' => NULL, 377 'dburl' => NULL, 378 'php' => '', 379 'concurrency' => 1, 380 'all' => FALSE, 381 'module' => NULL, 382 'class' => FALSE, 383 'file' => FALSE, 384 'types' => [], 385 'directory' => NULL, 386 'color' => FALSE, 387 'verbose' => FALSE, 388 'keep-results' => FALSE, 389 'keep-results-table' => FALSE, 390 'test_names' => [], 391 'repeat' => 1, 392 'die-on-fail' => FALSE, 393 'suppress-deprecations' => FALSE, 394 // Used internally. 395 'test-id' => 0, 396 'execute-test' => '', 397 'xml' => '', 398 'non-html' => FALSE, 399 ]; 400 401 // Override with set values. 402 $args['script'] = basename(array_shift($_SERVER['argv'])); 403 404 $count = 0; 405 while ($arg = array_shift($_SERVER['argv'])) { 406 if (preg_match('/--(\S+)/', $arg, $matches)) { 407 // Argument found. 408 if (array_key_exists($matches[1], $args)) { 409 // Argument found in list. 410 $previous_arg = $matches[1]; 411 if (is_bool($args[$previous_arg])) { 412 $args[$matches[1]] = TRUE; 413 } 414 elseif (is_array($args[$previous_arg])) { 415 $value = array_shift($_SERVER['argv']); 416 $args[$matches[1]] = array_map('trim', explode(',', $value)); 417 } 418 else { 419 $args[$matches[1]] = array_shift($_SERVER['argv']); 420 } 421 // Clear extraneous values. 422 $args['test_names'] = []; 423 $count++; 424 } 425 else { 426 // Argument not found in list. 427 simpletest_script_print_error("Unknown argument '$arg'."); 428 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); 429 } 430 } 431 else { 432 // Values found without an argument should be test names. 433 $args['test_names'] += explode(',', $arg); 434 $count++; 435 } 436 } 437 438 // Validate the concurrency argument. 439 if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) { 440 simpletest_script_print_error("--concurrency must be a strictly positive integer."); 441 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); 442 } 443 444 return [$args, $count]; 445} 446 447/** 448 * Initialize script variables and perform general setup requirements. 449 */ 450function simpletest_script_init() { 451 global $args, $php; 452 453 $host = 'localhost'; 454 $path = ''; 455 $port = '80'; 456 457 // Determine location of php command automatically, unless a command line 458 // argument is supplied. 459 if (!empty($args['php'])) { 460 $php = $args['php']; 461 } 462 elseif ($php_env = getenv('_')) { 463 // '_' is an environment variable set by the shell. It contains the command 464 // that was executed. 465 $php = $php_env; 466 } 467 elseif ($sudo = getenv('SUDO_COMMAND')) { 468 // 'SUDO_COMMAND' is an environment variable set by the sudo program. 469 // Extract only the PHP interpreter, not the rest of the command. 470 list($php) = explode(' ', $sudo, 2); 471 } 472 else { 473 simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.'); 474 simpletest_script_help(); 475 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); 476 } 477 478 // Detect if we're in the top-level process using the private 'execute-test' 479 // argument. Determine if being run on drupal.org's testing infrastructure 480 // using the presence of 'drupalci' in the sqlite argument. 481 // @todo https://www.drupal.org/project/drupalci_testbot/issues/2860941 Use 482 // better environment variable to detect DrupalCI. 483 if (!$args['execute-test'] && preg_match('/drupalci/', $args['sqlite'])) { 484 // Update PHPUnit if needed and possible. There is a later check once the 485 // autoloader is in place to ensure we're on the correct version. We need to 486 // do this before the autoloader is in place to ensure that it is correct. 487 $composer = ($composer = rtrim('\\' === DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer.phar`) : `which composer.phar`)) 488 ? $php . ' ' . escapeshellarg($composer) 489 : 'composer'; 490 passthru("$composer run-script drupal-phpunit-upgrade-check"); 491 } 492 493 $autoloader = require_once __DIR__ . '/../../autoload.php'; 494 // The PHPUnit compatibility layer needs to be available to autoload tests. 495 $autoloader->add('Drupal\\TestTools', __DIR__ . '/../tests'); 496 ClassWriter::mutateTestBase($autoloader); 497 498 // Get URL from arguments. 499 if (!empty($args['url'])) { 500 $parsed_url = parse_url($args['url']); 501 $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''); 502 $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : ''; 503 $port = (isset($parsed_url['port']) ? $parsed_url['port'] : $port); 504 if ($path == '/') { 505 $path = ''; 506 } 507 // If the passed URL schema is 'https' then setup the $_SERVER variables 508 // properly so that testing will run under HTTPS. 509 if ($parsed_url['scheme'] == 'https') { 510 $_SERVER['HTTPS'] = 'on'; 511 } 512 } 513 514 if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { 515 $base_url = 'https://'; 516 } 517 else { 518 $base_url = 'http://'; 519 } 520 $base_url .= $host; 521 if ($path !== '') { 522 $base_url .= $path; 523 } 524 putenv('SIMPLETEST_BASE_URL=' . $base_url); 525 $_SERVER['HTTP_HOST'] = $host; 526 $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; 527 $_SERVER['SERVER_ADDR'] = '127.0.0.1'; 528 $_SERVER['SERVER_PORT'] = $port; 529 $_SERVER['SERVER_SOFTWARE'] = NULL; 530 $_SERVER['SERVER_NAME'] = 'localhost'; 531 $_SERVER['REQUEST_URI'] = $path . '/'; 532 $_SERVER['REQUEST_METHOD'] = 'GET'; 533 $_SERVER['SCRIPT_NAME'] = $path . '/index.php'; 534 $_SERVER['SCRIPT_FILENAME'] = $path . '/index.php'; 535 $_SERVER['PHP_SELF'] = $path . '/index.php'; 536 $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line'; 537 538 if ($args['concurrency'] > 1) { 539 $directory = FileSystem::getOsTemporaryDirectory(); 540 $test_symlink = @symlink(__FILE__, $directory . '/test_symlink'); 541 if (!$test_symlink) { 542 throw new \RuntimeException('In order to use a concurrency higher than 1 the test system needs to be able to create symlinks in ' . $directory); 543 } 544 unlink($directory . '/test_symlink'); 545 putenv('RUN_TESTS_CONCURRENCY=' . $args['concurrency']); 546 } 547 548 if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') { 549 // Ensure that any and all environment variables are changed to https://. 550 foreach ($_SERVER as $key => $value) { 551 $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]); 552 } 553 } 554 555 chdir(realpath(__DIR__ . '/../..')); 556 557 // Prepare the kernel. 558 try { 559 $request = Request::createFromGlobals(); 560 $kernel = TestRunnerKernel::createFromRequest($request, $autoloader); 561 $kernel->boot(); 562 $kernel->preHandle($request); 563 } 564 catch (Exception $e) { 565 echo (string) $e; 566 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 567 } 568} 569 570/** 571 * Sets up database connection info for running tests. 572 * 573 * If this script is executed from within a real Drupal installation, then this 574 * function essentially performs nothing (unless the --sqlite or --dburl 575 * parameters were passed). 576 * 577 * Otherwise, there are three database connections of concern: 578 * - --sqlite: The test runner connection, providing access to Simpletest 579 * database tables for recording test IDs and assertion results. 580 * - --dburl: A database connection that is used as base connection info for all 581 * tests; i.e., every test will spawn from this connection. In case this 582 * connection uses e.g. SQLite, then all tests will run against SQLite. This 583 * is exposed as $databases['default']['default'] to Drupal. 584 * - The actual database connection used within a test. This is the same as 585 * --dburl, but uses an additional database table prefix. This is 586 * $databases['default']['default'] within a test environment. The original 587 * connection is retained in 588 * $databases['simpletest_original_default']['default'] and restored after 589 * each test. 590 * 591 * @param bool $new 592 * Whether this process is a run-tests.sh master process. If TRUE, the SQLite 593 * database file specified by --sqlite (if any) is set up. Otherwise, database 594 * connections are prepared only. 595 */ 596function simpletest_script_setup_database($new = FALSE) { 597 global $args; 598 599 // If there is an existing Drupal installation that contains a database 600 // connection info in settings.php, then $databases['default']['default'] will 601 // hold the default database connection already. This connection is assumed to 602 // be valid, and this connection will be used in tests, so that they run 603 // against e.g. MySQL instead of SQLite. 604 // However, in case no Drupal installation exists, this default database 605 // connection can be set and/or overridden with the --dburl parameter. 606 if (!empty($args['dburl'])) { 607 // Remove a possibly existing default connection (from settings.php). 608 Database::removeConnection('default'); 609 try { 610 $databases['default']['default'] = Database::convertDbUrlToConnectionInfo($args['dburl'], DRUPAL_ROOT); 611 } 612 catch (\InvalidArgumentException $e) { 613 simpletest_script_print_error('Invalid --dburl. Reason: ' . $e->getMessage()); 614 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); 615 } 616 } 617 // Otherwise, use the default database connection from settings.php. 618 else { 619 $databases['default'] = Database::getConnectionInfo('default'); 620 } 621 622 // If there is no default database connection for tests, we cannot continue. 623 if (!isset($databases['default']['default'])) { 624 simpletest_script_print_error('Missing default database connection for tests. Use --dburl to specify one.'); 625 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); 626 } 627 Database::addConnectionInfo('default', 'default', $databases['default']['default']); 628 629 // If no --sqlite parameter has been passed, then Simpletest module is assumed 630 // to be installed, so the test runner database connection is the default 631 // database connection. 632 if (empty($args['sqlite'])) { 633 $sqlite = FALSE; 634 $databases['test-runner']['default'] = $databases['default']['default']; 635 } 636 // Otherwise, set up a SQLite connection for the test runner. 637 else { 638 if ($args['sqlite'][0] === '/') { 639 $sqlite = $args['sqlite']; 640 } 641 else { 642 $sqlite = DRUPAL_ROOT . '/' . $args['sqlite']; 643 } 644 $databases['test-runner']['default'] = [ 645 'driver' => 'sqlite', 646 'database' => $sqlite, 647 'prefix' => [ 648 'default' => '', 649 ], 650 ]; 651 // Create the test runner SQLite database, unless it exists already. 652 if ($new && !file_exists($sqlite)) { 653 if (!is_dir(dirname($sqlite))) { 654 mkdir(dirname($sqlite)); 655 } 656 touch($sqlite); 657 } 658 } 659 660 // Add the test runner database connection. 661 Database::addConnectionInfo('test-runner', 'default', $databases['test-runner']['default']); 662 663 // Create the Simpletest schema. 664 try { 665 $connection = Database::getConnection('default', 'test-runner'); 666 $schema = $connection->schema(); 667 } 668 catch (\PDOException $e) { 669 simpletest_script_print_error($databases['test-runner']['default']['driver'] . ': ' . $e->getMessage()); 670 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); 671 } 672 if ($new && $sqlite) { 673 foreach (TestDatabase::testingSchema() as $name => $table_spec) { 674 try { 675 $table_exists = $schema->tableExists($name); 676 if (empty($args['keep-results-table']) && $table_exists) { 677 $connection->truncate($name)->execute(); 678 } 679 if (!$table_exists) { 680 $schema->createTable($name, $table_spec); 681 } 682 } 683 catch (Exception $e) { 684 echo (string) $e; 685 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 686 } 687 } 688 } 689 // Verify that the Simpletest database schema exists by checking one table. 690 try { 691 if (!$schema->tableExists('simpletest')) { 692 simpletest_script_print_error('Missing Simpletest database schema. Either install Simpletest module or use the --sqlite parameter.'); 693 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); 694 } 695 } 696 catch (Exception $e) { 697 echo (string) $e; 698 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 699 } 700} 701 702/** 703 * Execute a batch of tests. 704 */ 705function simpletest_script_execute_batch($test_classes) { 706 global $args, $test_ids; 707 708 $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS; 709 710 // Multi-process execution. 711 $children = []; 712 while (!empty($test_classes) || !empty($children)) { 713 while (count($children) < $args['concurrency']) { 714 if (empty($test_classes)) { 715 break; 716 } 717 718 try { 719 $test_id = Database::getConnection('default', 'test-runner') 720 ->insert('simpletest_test_id') 721 ->useDefaults(['test_id']) 722 ->execute(); 723 } 724 catch (Exception $e) { 725 echo (string) $e; 726 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 727 } 728 $test_ids[] = $test_id; 729 730 $test_class = array_shift($test_classes); 731 // Fork a child process. 732 $command = simpletest_script_command($test_id, $test_class); 733 $process = proc_open($command, [], $pipes, NULL, NULL, ['bypass_shell' => TRUE]); 734 735 if (!is_resource($process)) { 736 echo "Unable to fork test process. Aborting.\n"; 737 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS); 738 } 739 740 // Register our new child. 741 $children[] = [ 742 'process' => $process, 743 'test_id' => $test_id, 744 'class' => $test_class, 745 'pipes' => $pipes, 746 ]; 747 } 748 749 // Wait for children every 200ms. 750 usleep(200000); 751 752 // Check if some children finished. 753 foreach ($children as $cid => $child) { 754 $status = proc_get_status($child['process']); 755 if (empty($status['running'])) { 756 // The child exited, unregister it. 757 proc_close($child['process']); 758 if ($status['exitcode'] === SIMPLETEST_SCRIPT_EXIT_FAILURE) { 759 $total_status = max($status['exitcode'], $total_status); 760 } 761 elseif ($status['exitcode']) { 762 $message = 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').'; 763 echo $message . "\n"; 764 // @todo Return SIMPLETEST_SCRIPT_EXIT_EXCEPTION instead, when 765 // DrupalCI supports this. 766 // @see https://www.drupal.org/node/2780087 767 $total_status = max(SIMPLETEST_SCRIPT_EXIT_FAILURE, $total_status); 768 // Insert a fail for xml results. 769 TestDatabase::insertAssert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check'); 770 // Ensure that an error line is displayed for the class. 771 simpletest_script_reporter_display_summary( 772 $child['class'], 773 ['#pass' => 0, '#fail' => 1, '#exception' => 0, '#debug' => 0] 774 ); 775 if ($args['die-on-fail']) { 776 $db_prefix = TestDatabase::lastTestGet($child['test_id'])['last_prefix']; 777 $test_db = new TestDatabase($db_prefix); 778 $test_directory = $test_db->getTestSitePath(); 779 echo 'Simpletest database and files kept and test exited immediately on fail so should be reproducible if you change settings.php to use the database prefix ' . $db_prefix . ' and config directories in ' . $test_directory . "\n"; 780 $args['keep-results'] = TRUE; 781 // Exit repeat loop immediately. 782 $args['repeat'] = -1; 783 } 784 } 785 // Free-up space by removing any potentially created resources. 786 if (!$args['keep-results']) { 787 simpletest_script_cleanup($child['test_id'], $child['class'], $status['exitcode']); 788 } 789 790 // Remove this child. 791 unset($children[$cid]); 792 } 793 } 794 } 795 return $total_status; 796} 797 798/** 799 * Run a PHPUnit-based test. 800 */ 801function simpletest_script_run_phpunit($test_id, $class) { 802 $reflection = new \ReflectionClass($class); 803 if ($reflection->hasProperty('runLimit')) { 804 set_time_limit($reflection->getStaticPropertyValue('runLimit')); 805 } 806 807 $runner = PhpUnitTestRunner::create(\Drupal::getContainer()); 808 $results = $runner->runTests($test_id, [$class], $status); 809 TestDatabase::processPhpUnitResults($results); 810 811 $summaries = $runner->summarizeResults($results); 812 foreach ($summaries as $class => $summary) { 813 simpletest_script_reporter_display_summary($class, $summary); 814 } 815 return $status; 816} 817 818/** 819 * Run a single test, bootstrapping Drupal if needed. 820 */ 821function simpletest_script_run_one_test($test_id, $test_class) { 822 global $args; 823 824 try { 825 // Default to status = success. This could mean that we didn't discover any 826 // tests and that none ran. 827 $status = SIMPLETEST_SCRIPT_EXIT_SUCCESS; 828 if (strpos($test_class, '::') > 0) { 829 list($class_name, $method) = explode('::', $test_class, 2); 830 $methods = [$method]; 831 } 832 else { 833 $class_name = $test_class; 834 // Use empty array to run all the test methods. 835 $methods = []; 836 } 837 $test = new $class_name($test_id); 838 if ($args['suppress-deprecations']) { 839 putenv('SYMFONY_DEPRECATIONS_HELPER=disabled'); 840 } 841 if (is_subclass_of($test_class, TestCase::class)) { 842 $status = simpletest_script_run_phpunit($test_id, $test_class); 843 } 844 // If we aren't running a PHPUnit-based test, then we might have a 845 // Simpletest-based one. Ensure that: 1) The simpletest framework exists, 846 // and 2) that our test belongs to that framework. 847 elseif (class_exists('Drupal\simpletest\TestBase') && is_subclass_of($test_class, 'Drupal\simpletest\TestBase')) { 848 $test->dieOnFail = (bool) $args['die-on-fail']; 849 $test->verbose = (bool) $args['verbose']; 850 $test->run($methods); 851 simpletest_script_reporter_display_summary($test_class, $test->results); 852 853 $status = SIMPLETEST_SCRIPT_EXIT_SUCCESS; 854 // Finished, kill this runner. 855 if ($test->results['#fail'] || $test->results['#exception']) { 856 $status = SIMPLETEST_SCRIPT_EXIT_FAILURE; 857 } 858 } 859 // If the test is not a PHPUnit test, and either we don't have the 860 // Simpletest module or the \Drupal\simpletest\TestBase class available. 861 else { 862 simpletest_script_print_error(sprintf('Can not run %s. If this is a WebTestBase test the simpletest module must be installed. See https://www.drupal.org/node/3030340', $test_class)); 863 $status = SIMPLETEST_SCRIPT_EXIT_FAILURE; 864 } 865 866 exit($status); 867 } 868 // DrupalTestCase::run() catches exceptions already, so this is only reached 869 // when an exception is thrown in the wrapping test runner environment. 870 catch (Exception $e) { 871 echo (string) $e; 872 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 873 } 874} 875 876/** 877 * Return a command used to run a test in a separate process. 878 * 879 * @param int $test_id 880 * The current test ID. 881 * @param string $test_class 882 * The name of the test class to run. 883 * 884 * @return string 885 * The assembled command string. 886 */ 887function simpletest_script_command($test_id, $test_class) { 888 global $args, $php; 889 890 $command = escapeshellarg($php) . ' ' . escapeshellarg('./core/scripts/' . $args['script']); 891 $command .= ' --url ' . escapeshellarg($args['url']); 892 if (!empty($args['sqlite'])) { 893 $command .= ' --sqlite ' . escapeshellarg($args['sqlite']); 894 } 895 if (!empty($args['dburl'])) { 896 $command .= ' --dburl ' . escapeshellarg($args['dburl']); 897 } 898 $command .= ' --php ' . escapeshellarg($php); 899 $command .= " --test-id $test_id"; 900 foreach (['verbose', 'keep-results', 'color', 'die-on-fail', 'suppress-deprecations'] as $arg) { 901 if ($args[$arg]) { 902 $command .= ' --' . $arg; 903 } 904 } 905 // --execute-test and class name needs to come last. 906 $command .= ' --execute-test ' . escapeshellarg($test_class); 907 return $command; 908} 909 910/** 911 * Removes all remnants of a test runner. 912 * 913 * In case a fatal error occurs after the test site has been fully setup and 914 * the error happens in many tests, the environment that executes the tests can 915 * easily run out of memory or disk space. This function ensures that all 916 * created resources are properly cleaned up after every executed test. 917 * 918 * This clean-up only exists in this script, since SimpleTest module itself does 919 * not use isolated sub-processes for each test being run, so a fatal error 920 * halts not only the test, but also the test runner (i.e., the parent site). 921 * 922 * @param int $test_id 923 * The test ID of the test run. 924 * @param string $test_class 925 * The class name of the test run. 926 * @param int $exitcode 927 * The exit code of the test runner. 928 * 929 * @see simpletest_script_run_one_test() 930 */ 931function simpletest_script_cleanup($test_id, $test_class, $exitcode) { 932 if (is_subclass_of($test_class, TestCase::class)) { 933 // PHPUnit test, move on. 934 return; 935 } 936 // Retrieve the last database prefix used for testing. 937 try { 938 $last_test = TestDatabase::lastTestGet($test_id); 939 $db_prefix = $last_test['last_prefix']; 940 } 941 catch (Exception $e) { 942 echo (string) $e; 943 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 944 } 945 946 // If no database prefix was found, then the test was not set up correctly. 947 if (empty($db_prefix)) { 948 echo "\nFATAL $test_class: Found no database prefix for test ID $test_id. (Check whether setUp() is invoked correctly.)"; 949 return; 950 } 951 952 // Do not output verbose cleanup messages in case of a positive exitcode. 953 $output = !empty($exitcode); 954 $messages = []; 955 956 $messages[] = "- Found database prefix '$db_prefix' for test ID $test_id."; 957 958 // Read the log file in case any fatal errors caused the test to crash. 959 try { 960 (new TestDatabase($db_prefix))->logRead($test_id, $last_test['test_class']); 961 } 962 catch (Exception $e) { 963 echo (string) $e; 964 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 965 } 966 967 // Check whether a test site directory was setup already. 968 // @see \Drupal\simpletest\TestBase::prepareEnvironment() 969 $test_db = new TestDatabase($db_prefix); 970 $test_directory = DRUPAL_ROOT . '/' . $test_db->getTestSitePath(); 971 if (is_dir($test_directory)) { 972 // Output the error_log. 973 if (is_file($test_directory . '/error.log')) { 974 if ($errors = file_get_contents($test_directory . '/error.log')) { 975 $output = TRUE; 976 $messages[] = $errors; 977 } 978 } 979 // Delete the test site directory. 980 // simpletest_clean_temporary_directories() cannot be used here, since it 981 // would also delete file directories of other tests that are potentially 982 // running concurrently. 983 try { 984 \Drupal::service('file_system')->deleteRecursive($test_directory, ['\Drupal\Tests\BrowserTestBase', 'filePreDeleteCallback']); 985 $messages[] = "- Removed test site directory."; 986 } 987 catch (FileException $e) { 988 // Ignore failed deletes. 989 } 990 } 991 992 // Clear out all database tables from the test. 993 try { 994 $schema = Database::getConnection('default', 'default')->schema(); 995 $count = 0; 996 foreach ($schema->findTables($db_prefix . '%') as $table) { 997 $schema->dropTable($table); 998 $count++; 999 } 1000 } 1001 catch (Exception $e) { 1002 echo (string) $e; 1003 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 1004 } 1005 1006 if ($count) { 1007 $messages[] = "- Removed $count leftover tables."; 1008 } 1009 1010 if ($output) { 1011 echo implode("\n", $messages); 1012 echo "\n"; 1013 } 1014} 1015 1016/** 1017 * Get list of tests based on arguments. 1018 * 1019 * If --all specified then return all available tests, otherwise reads list of 1020 * tests. 1021 * 1022 * @return array 1023 * List of tests. 1024 */ 1025function simpletest_script_get_test_list() { 1026 global $args; 1027 1028 $test_discovery = new TestDiscovery( 1029 \Drupal::service('app.root'), 1030 \Drupal::service('class_loader') 1031 ); 1032 $types_processed = empty($args['types']); 1033 $test_list = []; 1034 if ($args['all'] || $args['module']) { 1035 try { 1036 $groups = $test_discovery->getTestClasses($args['module'], $args['types']); 1037 $types_processed = TRUE; 1038 } 1039 catch (Exception $e) { 1040 echo (string) $e; 1041 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 1042 } 1043 $all_tests = []; 1044 foreach ($groups as $group => $tests) { 1045 $all_tests = array_merge($all_tests, array_keys($tests)); 1046 } 1047 $test_list = array_unique($all_tests); 1048 } 1049 else { 1050 if ($args['class']) { 1051 $test_list = []; 1052 foreach ($args['test_names'] as $test_class) { 1053 list($class_name) = explode('::', $test_class, 2); 1054 if (class_exists($class_name)) { 1055 $test_list[] = $test_class; 1056 } 1057 else { 1058 try { 1059 $groups = $test_discovery->getTestClasses(NULL, $args['types']); 1060 } 1061 catch (Exception $e) { 1062 echo (string) $e; 1063 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 1064 } 1065 $all_classes = []; 1066 foreach ($groups as $group) { 1067 $all_classes = array_merge($all_classes, array_keys($group)); 1068 } 1069 simpletest_script_print_error('Test class not found: ' . $class_name); 1070 simpletest_script_print_alternatives($class_name, $all_classes, 6); 1071 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); 1072 } 1073 } 1074 } 1075 elseif ($args['file']) { 1076 // Extract test case class names from specified files. 1077 $parser = new TestFileParser(); 1078 foreach ($args['test_names'] as $file) { 1079 if (!file_exists($file)) { 1080 simpletest_script_print_error('File not found: ' . $file); 1081 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); 1082 } 1083 $test_list = array_merge($test_list, $parser->getTestListFromFile($file)); 1084 } 1085 } 1086 elseif ($args['directory']) { 1087 // Extract test case class names from specified directory. 1088 // Find all tests in the PSR-X structure; Drupal\$extension\Tests\*.php 1089 // Since we do not want to hard-code too many structural file/directory 1090 // assumptions about PSR-4 files and directories, we check for the 1091 // minimal conditions only; i.e., a '*.php' file that has '/Tests/' in 1092 // its path. 1093 // Ignore anything from third party vendors. 1094 $ignore = ['.', '..', 'vendor']; 1095 $files = []; 1096 if ($args['directory'][0] === '/') { 1097 $directory = $args['directory']; 1098 } 1099 else { 1100 $directory = DRUPAL_ROOT . "/" . $args['directory']; 1101 } 1102 foreach (\Drupal::service('file_system')->scanDirectory($directory, '/\.php$/', $ignore) as $file) { 1103 // '/Tests/' can be contained anywhere in the file's path (there can be 1104 // sub-directories below /Tests), but must be contained literally. 1105 // Case-insensitive to match all Simpletest and PHPUnit tests: 1106 // ./lib/Drupal/foo/Tests/Bar/Baz.php 1107 // ./foo/src/Tests/Bar/Baz.php 1108 // ./foo/tests/Drupal/foo/Tests/FooTest.php 1109 // ./foo/tests/src/FooTest.php 1110 // $file->filename doesn't give us a directory, so we use $file->uri 1111 // Strip the drupal root directory and trailing slash off the URI. 1112 $filename = substr($file->uri, strlen(DRUPAL_ROOT) + 1); 1113 if (stripos($filename, '/Tests/')) { 1114 $files[$filename] = $filename; 1115 } 1116 } 1117 $parser = new TestFileParser(); 1118 foreach ($files as $file) { 1119 $test_list = array_merge($test_list, $parser->getTestListFromFile($file)); 1120 } 1121 } 1122 else { 1123 try { 1124 $groups = $test_discovery->getTestClasses(NULL, $args['types']); 1125 $types_processed = TRUE; 1126 } 1127 catch (Exception $e) { 1128 echo (string) $e; 1129 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 1130 } 1131 // Store all the groups so we can suggest alternatives if we need to. 1132 $all_groups = array_keys($groups); 1133 // Verify that the groups exist. 1134 if (!empty($unknown_groups = array_diff($args['test_names'], $all_groups))) { 1135 $first_group = reset($unknown_groups); 1136 simpletest_script_print_error('Test group not found: ' . $first_group); 1137 simpletest_script_print_alternatives($first_group, $all_groups); 1138 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); 1139 } 1140 // Ensure our list of tests contains only one entry for each test. 1141 foreach ($args['test_names'] as $group_name) { 1142 $test_list = array_merge($test_list, array_flip(array_keys($groups[$group_name]))); 1143 } 1144 $test_list = array_flip($test_list); 1145 } 1146 } 1147 1148 // If the test list creation does not automatically limit by test type then 1149 // we need to do so here. 1150 if (!$types_processed) { 1151 $test_list = array_filter($test_list, function ($test_class) use ($args) { 1152 $test_info = TestDiscovery::getTestInfo($test_class); 1153 return in_array($test_info['type'], $args['types'], TRUE); 1154 }); 1155 } 1156 1157 if (empty($test_list)) { 1158 simpletest_script_print_error('No valid tests were specified.'); 1159 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); 1160 } 1161 return $test_list; 1162} 1163 1164/** 1165 * Initialize the reporter. 1166 */ 1167function simpletest_script_reporter_init() { 1168 global $args, $test_list, $results_map; 1169 1170 $results_map = [ 1171 'pass' => 'Pass', 1172 'fail' => 'Fail', 1173 'exception' => 'Exception', 1174 ]; 1175 1176 echo "\n"; 1177 echo "Drupal test run\n"; 1178 echo "---------------\n"; 1179 echo "\n"; 1180 1181 // Tell the user about what tests are to be run. 1182 if ($args['all']) { 1183 echo "All tests will run.\n\n"; 1184 } 1185 else { 1186 echo "Tests to be run:\n"; 1187 foreach ($test_list as $class_name) { 1188 echo " - $class_name\n"; 1189 } 1190 echo "\n"; 1191 } 1192 1193 echo "Test run started:\n"; 1194 echo " " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']) . "\n"; 1195 Timer::start('run-tests'); 1196 echo "\n"; 1197 1198 echo "Test summary\n"; 1199 echo "------------\n"; 1200 echo "\n"; 1201} 1202 1203/** 1204 * Displays the assertion result summary for a single test class. 1205 * 1206 * @param string $class 1207 * The test class name that was run. 1208 * @param array $results 1209 * The assertion results using #pass, #fail, #exception, #debug array keys. 1210 */ 1211function simpletest_script_reporter_display_summary($class, $results) { 1212 // Output all test results vertically aligned. 1213 // Cut off the class name after 60 chars, and pad each group with 3 digits 1214 // by default (more than 999 assertions are rare). 1215 $output = vsprintf('%-60.60s %10s %9s %14s %12s', [ 1216 $class, 1217 $results['#pass'] . ' passes', 1218 !$results['#fail'] ? '' : $results['#fail'] . ' fails', 1219 !$results['#exception'] ? '' : $results['#exception'] . ' exceptions', 1220 !$results['#debug'] ? '' : $results['#debug'] . ' messages', 1221 ]); 1222 1223 $status = ($results['#fail'] || $results['#exception'] ? 'fail' : 'pass'); 1224 simpletest_script_print($output . "\n", simpletest_script_color_code($status)); 1225} 1226 1227/** 1228 * Display jUnit XML test results. 1229 */ 1230function simpletest_script_reporter_write_xml_results() { 1231 global $args, $test_ids, $results_map; 1232 1233 try { 1234 $results = simpletest_script_load_messages_by_test_id($test_ids); 1235 } 1236 catch (Exception $e) { 1237 echo (string) $e; 1238 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 1239 } 1240 1241 $test_class = ''; 1242 $xml_files = []; 1243 1244 foreach ($results as $result) { 1245 if (isset($results_map[$result->status])) { 1246 if ($result->test_class != $test_class) { 1247 // We've moved onto a new class, so write the last classes results to a 1248 // file: 1249 if (isset($xml_files[$test_class])) { 1250 file_put_contents($args['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML()); 1251 unset($xml_files[$test_class]); 1252 } 1253 $test_class = $result->test_class; 1254 if (!isset($xml_files[$test_class])) { 1255 $doc = new DomDocument('1.0'); 1256 $root = $doc->createElement('testsuite'); 1257 $root = $doc->appendChild($root); 1258 $xml_files[$test_class] = ['doc' => $doc, 'suite' => $root]; 1259 } 1260 } 1261 1262 // For convenience: 1263 $dom_document = &$xml_files[$test_class]['doc']; 1264 1265 // Create the XML element for this test case: 1266 $case = $dom_document->createElement('testcase'); 1267 $case->setAttribute('classname', $test_class); 1268 if (strpos($result->function, '->') !== FALSE) { 1269 list($class, $name) = explode('->', $result->function, 2); 1270 } 1271 else { 1272 $name = $result->function; 1273 } 1274 $case->setAttribute('name', $name); 1275 1276 // Passes get no further attention, but failures and exceptions get to add 1277 // more detail: 1278 if ($result->status == 'fail') { 1279 $fail = $dom_document->createElement('failure'); 1280 $fail->setAttribute('type', 'failure'); 1281 $fail->setAttribute('message', $result->message_group); 1282 $text = $dom_document->createTextNode($result->message); 1283 $fail->appendChild($text); 1284 $case->appendChild($fail); 1285 } 1286 elseif ($result->status == 'exception') { 1287 // In the case of an exception the $result->function may not be a class 1288 // method so we record the full function name: 1289 $case->setAttribute('name', $result->function); 1290 1291 $fail = $dom_document->createElement('error'); 1292 $fail->setAttribute('type', 'exception'); 1293 $fail->setAttribute('message', $result->message_group); 1294 $full_message = $result->message . "\n\nline: " . $result->line . "\nfile: " . $result->file; 1295 $text = $dom_document->createTextNode($full_message); 1296 $fail->appendChild($text); 1297 $case->appendChild($fail); 1298 } 1299 // Append the test case XML to the test suite: 1300 $xml_files[$test_class]['suite']->appendChild($case); 1301 } 1302 } 1303 // The last test case hasn't been saved to a file yet, so do that now: 1304 if (isset($xml_files[$test_class])) { 1305 file_put_contents($args['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML()); 1306 unset($xml_files[$test_class]); 1307 } 1308} 1309 1310/** 1311 * Stop the test timer. 1312 */ 1313function simpletest_script_reporter_timer_stop() { 1314 echo "\n"; 1315 $end = Timer::stop('run-tests'); 1316 echo "Test run duration: " . \Drupal::service('date.formatter')->formatInterval($end['time'] / 1000); 1317 echo "\n\n"; 1318} 1319 1320/** 1321 * Display test results. 1322 */ 1323function simpletest_script_reporter_display_results() { 1324 global $args, $test_ids, $results_map; 1325 1326 if ($args['verbose']) { 1327 // Report results. 1328 echo "Detailed test results\n"; 1329 echo "---------------------\n"; 1330 1331 try { 1332 $results = simpletest_script_load_messages_by_test_id($test_ids); 1333 } 1334 catch (Exception $e) { 1335 echo (string) $e; 1336 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 1337 } 1338 $test_class = ''; 1339 foreach ($results as $result) { 1340 if (isset($results_map[$result->status])) { 1341 if ($result->test_class != $test_class) { 1342 // Display test class every time results are for new test class. 1343 echo "\n\n---- $result->test_class ----\n\n\n"; 1344 $test_class = $result->test_class; 1345 1346 // Print table header. 1347 echo "Status Group Filename Line Function \n"; 1348 echo "--------------------------------------------------------------------------------\n"; 1349 } 1350 1351 simpletest_script_format_result($result); 1352 } 1353 } 1354 } 1355} 1356 1357/** 1358 * Format the result so that it fits within 80 characters. 1359 * 1360 * @param object $result 1361 * The result object to format. 1362 */ 1363function simpletest_script_format_result($result) { 1364 global $args, $results_map, $color; 1365 1366 $summary = sprintf("%-9.9s %-10.10s %-17.17s %4.4s %-35.35s\n", 1367 $results_map[$result->status], $result->message_group, basename($result->file), $result->line, $result->function); 1368 1369 simpletest_script_print($summary, simpletest_script_color_code($result->status)); 1370 1371 $message = trim(strip_tags($result->message)); 1372 if ($args['non-html']) { 1373 $message = Html::decodeEntities($message, ENT_QUOTES, 'UTF-8'); 1374 } 1375 $lines = explode("\n", wordwrap($message), 76); 1376 foreach ($lines as $line) { 1377 echo " $line\n"; 1378 } 1379} 1380 1381/** 1382 * Print error messages so the user will notice them. 1383 * 1384 * Print error message prefixed with " ERROR: " and displayed in fail color if 1385 * color output is enabled. 1386 * 1387 * @param string $message 1388 * The message to print. 1389 */ 1390function simpletest_script_print_error($message) { 1391 simpletest_script_print(" ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL); 1392} 1393 1394/** 1395 * Print a message to the console, using a color. 1396 * 1397 * @param string $message 1398 * The message to print. 1399 * @param int $color_code 1400 * The color code to use for coloring. 1401 */ 1402function simpletest_script_print($message, $color_code) { 1403 global $args; 1404 if ($args['color']) { 1405 echo "\033[" . $color_code . "m" . $message . "\033[0m"; 1406 } 1407 else { 1408 echo $message; 1409 } 1410} 1411 1412/** 1413 * Get the color code associated with the specified status. 1414 * 1415 * @param string $status 1416 * The status string to get code for. Special cases are: 'pass', 'fail', or 1417 * 'exception'. 1418 * 1419 * @return int 1420 * Color code. Returns 0 for default case. 1421 */ 1422function simpletest_script_color_code($status) { 1423 switch ($status) { 1424 case 'pass': 1425 return SIMPLETEST_SCRIPT_COLOR_PASS; 1426 1427 case 'fail': 1428 return SIMPLETEST_SCRIPT_COLOR_FAIL; 1429 1430 case 'exception': 1431 return SIMPLETEST_SCRIPT_COLOR_EXCEPTION; 1432 } 1433 // Default formatting. 1434 return 0; 1435} 1436 1437/** 1438 * Prints alternative test names. 1439 * 1440 * Searches the provided array of string values for close matches based on the 1441 * Levenshtein algorithm. 1442 * 1443 * @param string $string 1444 * A string to test. 1445 * @param array $array 1446 * A list of strings to search. 1447 * @param int $degree 1448 * The matching strictness. Higher values return fewer matches. A value of 1449 * 4 means that the function will return strings from $array if the candidate 1450 * string in $array would be identical to $string by changing 1/4 or fewer of 1451 * its characters. 1452 * 1453 * @see http://php.net/manual/function.levenshtein.php 1454 */ 1455function simpletest_script_print_alternatives($string, $array, $degree = 4) { 1456 $alternatives = []; 1457 foreach ($array as $item) { 1458 $lev = levenshtein($string, $item); 1459 if ($lev <= strlen($item) / $degree || FALSE !== strpos($string, $item)) { 1460 $alternatives[] = $item; 1461 } 1462 } 1463 if (!empty($alternatives)) { 1464 simpletest_script_print(" Did you mean?\n", SIMPLETEST_SCRIPT_COLOR_FAIL); 1465 foreach ($alternatives as $alternative) { 1466 simpletest_script_print(" - $alternative\n", SIMPLETEST_SCRIPT_COLOR_FAIL); 1467 } 1468 } 1469} 1470 1471/** 1472 * Loads the simpletest messages from the database. 1473 * 1474 * Messages are ordered by test class and message id. 1475 * 1476 * @param array $test_ids 1477 * Array of test IDs of the messages to be loaded. 1478 * 1479 * @return array 1480 * Array of simpletest messages from the database. 1481 */ 1482function simpletest_script_load_messages_by_test_id($test_ids) { 1483 global $args; 1484 $results = []; 1485 1486 // Sqlite has a maximum number of variables per query. If required, the 1487 // database query is split into chunks. 1488 if (count($test_ids) > SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT && !empty($args['sqlite'])) { 1489 $test_id_chunks = array_chunk($test_ids, SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT); 1490 } 1491 else { 1492 $test_id_chunks = [$test_ids]; 1493 } 1494 1495 foreach ($test_id_chunks as $test_id_chunk) { 1496 try { 1497 $result_chunk = Database::getConnection('default', 'test-runner') 1498 ->query("SELECT * FROM {simpletest} WHERE [test_id] IN ( :test_ids[] ) ORDER BY [test_class], [message_id]", [ 1499 ':test_ids[]' => $test_id_chunk, 1500 ])->fetchAll(); 1501 } 1502 catch (Exception $e) { 1503 echo (string) $e; 1504 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); 1505 } 1506 if ($result_chunk) { 1507 $results = array_merge($results, $result_chunk); 1508 } 1509 } 1510 1511 return $results; 1512} 1513