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