run-tests.sh

Same filename and directory in other branches
  1. 10 core/scripts/run-tests.sh
  2. 11.x core/scripts/run-tests.sh
  3. 9 core/scripts/run-tests.sh
  4. 8.9.x core/scripts/run-tests.sh
  5. 7.x scripts/run-tests.sh

Script for running tests on DrupalCI.

This script is intended for use only by drupal.org testing. In general, tests should be run directly with phpunit.

@internal

File

core/scripts/run-tests.sh

View source
  1. <?php
  2. /**
  3. * @file
  4. * Script for running tests on DrupalCI.
  5. *
  6. * This script is intended for use only by drupal.org testing. In general,
  7. * tests should be run directly with phpunit.
  8. *
  9. * @internal
  10. */
  11. use Composer\Autoload\ClassLoader;
  12. use Drupal\Component\FileSystem\FileSystem;
  13. use Drupal\Component\Utility\Environment;
  14. use Drupal\Component\Utility\Html;
  15. use Drupal\Component\Utility\Timer;
  16. use Drupal\Core\Composer\Composer;
  17. use Drupal\Core\Database\Database;
  18. use Drupal\Core\Test\EnvironmentCleaner;
  19. use Drupal\Core\Test\PhpUnitTestDiscovery;
  20. use Drupal\Core\Test\PhpUnitTestRunner;
  21. use Drupal\Core\Test\SimpletestTestRunResultsStorage;
  22. use Drupal\Core\Test\TestDatabase;
  23. use Drupal\Core\Test\TestRun;
  24. use Drupal\Core\Test\TestRunnerKernel;
  25. use Drupal\Core\Test\TestRunResultsStorageInterface;
  26. use Drupal\TestTools\TestRunner\Configuration as Config;
  27. use Drupal\TestTools\TestRunner\WorkAllocator;
  28. use PHPUnit\Framework\TestCase;
  29. use PHPUnit\Runner\Version;
  30. use Symfony\Component\Console\Helper\DescriptorHelper;
  31. use Symfony\Component\Console\Input\InputDefinition;
  32. use Symfony\Component\Console\Output\ConsoleOutput;
  33. use Symfony\Component\HttpFoundation\Request;
  34. use Symfony\Component\Process\PhpExecutableFinder;
  35. // cspell:ignore exitcode testbots wwwrun
  36. // Define some colors for display.
  37. // A nice calming green.
  38. const SIMPLETEST_SCRIPT_COLOR_PASS = 32;
  39. // An alerting Red.
  40. const SIMPLETEST_SCRIPT_COLOR_FAIL = 31;
  41. // An annoying brown.
  42. const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33;
  43. // An appeasing yellow.
  44. const SIMPLETEST_SCRIPT_COLOR_YELLOW = 33;
  45. // A refreshing cyan.
  46. const SIMPLETEST_SCRIPT_COLOR_CYAN = 36;
  47. // A fainting gray.
  48. const SIMPLETEST_SCRIPT_COLOR_GRAY = 90;
  49. // A notable white.
  50. const SIMPLETEST_SCRIPT_COLOR_BRIGHT_WHITE = "1;97";
  51. // Restricting the chunk of queries prevents memory exhaustion.
  52. const SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT = 350;
  53. const SIMPLETEST_SCRIPT_EXIT_SUCCESS = 0;
  54. const SIMPLETEST_SCRIPT_EXIT_FAILURE = 1;
  55. const SIMPLETEST_SCRIPT_EXIT_ERROR = 2;
  56. const SIMPLETEST_SCRIPT_EXIT_EXCEPTION = 3;
  57. // Setup class autoloading.
  58. $autoloader = require_once __DIR__ . '/../../autoload.php';
  59. $autoloader->addPsr4('Drupal\\TestTools\\', __DIR__ . '/../tests/Drupal/TestTools');
  60. // Setup console output.
  61. $console_output = new ConsoleOutput();
  62. // Get the configuration from the command line.
  63. $script_basename = basename($_SERVER['argv'][0]);
  64. try {
  65. Config::createFromCommandLine($_SERVER['argv']);
  66. }
  67. catch (\RuntimeException $e) {
  68. simpletest_script_print_error($e->getMessage() . ' ' . "Use the --help option for the list and usage of the options available.\n");
  69. simpletest_script_print(Config::commandLineDefinition()->getSynopsis(), SIMPLETEST_SCRIPT_COLOR_PASS);
  70. exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
  71. }
  72. // If --help requested, show it and exit.
  73. if (Config::get('help')) {
  74. simpletest_script_help(Config::commandLineDefinition(), $script_basename, $console_output);
  75. exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
  76. }
  77. // Initialize script variables and bootstrap Drupal kernel.
  78. simpletest_script_init($autoloader);
  79. if (!class_exists(TestCase::class)) {
  80. echo "\nrun-tests.sh requires the PHPUnit testing framework. Use 'composer install' to ensure that it is present.\n\n";
  81. exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
  82. }
  83. // Defaults the PHPUnit configuration file path.
  84. if (empty(Config::get('phpunit-configuration'))) {
  85. Config::set('phpunit-configuration', \Drupal::root() . \DIRECTORY_SEPARATOR . 'core');
  86. }
  87. if (!Composer::upgradePHPUnitCheck(Version::id())) {
  88. simpletest_script_print_error("PHPUnit testing framework version 11 or greater is required when running on PHP 8.4 or greater. Run the command 'composer run-script drupal-phpunit-upgrade' in order to fix this.");
  89. exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
  90. }
  91. if (Config::get('list')) {
  92. // Display all available tests organized by one #[Group()] attribute.
  93. echo "\nAvailable test groups & classes\n";
  94. echo "-------------------------------\n\n";
  95. $testDiscovery = PhpUnitTestDiscovery::instance()->setConfigurationFilePath(Config::get('phpunit-configuration'));
  96. try {
  97. $groupedTestClassInfoList = $testDiscovery->getTestClasses(Config::get('module'), Config::get('types'), Config::get('directory'), Config::getTests());
  98. dump_discovery_warnings();
  99. }
  100. catch (Exception $e) {
  101. error_log((string) $e);
  102. echo (string) $e;
  103. exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  104. }
  105. // A given class can appear in multiple groups. For historical reasons, we
  106. // need to present each test only once. The test is shown in the group that is
  107. // printed first.
  108. $printed_tests = [];
  109. foreach ($groupedTestClassInfoList as $group => $tests) {
  110. echo $group . "\n";
  111. $tests = array_diff(array_keys($tests), $printed_tests);
  112. foreach ($tests as $test) {
  113. echo " - $test\n";
  114. }
  115. $printed_tests = array_merge($printed_tests, $tests);
  116. }
  117. exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
  118. }
  119. // List-files and list-files-json provide a way for external tools such as the
  120. // testbot to prioritize running changed tests.
  121. // @see https://www.drupal.org/node/2569585
  122. if (Config::get('list-files') || Config::get('list-files-json')) {
  123. // List all files which could be run as tests.
  124. $testDiscovery = PhpUnitTestDiscovery::instance()->setConfigurationFilePath(Config::get('phpunit-configuration'));
  125. // PhpUnitTestDiscovery::findAllClassFiles() gives us a classmap similar to a
  126. // Composer 'classmap' array.
  127. $test_classes = $testDiscovery->findAllClassFiles(Config::get('module'), Config::get('types'), Config::get('directory'), Config::getTests());
  128. // JSON output is the easiest.
  129. if (Config::get('list-files-json')) {
  130. echo json_encode($test_classes);
  131. exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
  132. }
  133. // Output the list of files.
  134. else {
  135. foreach (array_values($test_classes) as $test_class) {
  136. echo $test_class . "\n";
  137. }
  138. }
  139. exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
  140. }
  141. simpletest_script_setup_database();
  142. // Setup the test run results storage environment. Currently, this coincides
  143. // with the simpletest database schema.
  144. $test_run_results_storage = simpletest_script_setup_test_run_results_storage();
  145. if (Config::get('clean')) {
  146. // Clean up left-over tables and directories.
  147. $cleaner = new EnvironmentCleaner(
  148. DRUPAL_ROOT,
  149. Database::getConnection(),
  150. $test_run_results_storage,
  151. $console_output,
  152. \Drupal::service('file_system')
  153. );
  154. try {
  155. $cleaner->cleanEnvironment();
  156. }
  157. catch (Exception $e) {
  158. echo (string) $e;
  159. exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  160. }
  161. echo "\nEnvironment cleaned.\n";
  162. // Get the status messages and print them.
  163. $messages = \Drupal::messenger()->messagesByType('status');
  164. foreach ($messages as $text) {
  165. echo " - " . $text . "\n";
  166. }
  167. exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
  168. }
  169. echo "\n";
  170. echo "Drupal test run\n\n";
  171. echo "--------------------------------------------------------------\n";
  172. echo sprintf("Drupal Version.......: %s\n", \Drupal::VERSION);
  173. echo sprintf("PHP Version..........: %s\n", \PHP_VERSION);
  174. echo sprintf("PHP Binary...........: %s\n", (new PhpExecutableFinder())->find());
  175. echo sprintf("PHPUnit Version......: %s\n", Version::id());
  176. echo sprintf("PHPUnit configuration: %s\n", Config::get('phpunit-configuration'));
  177. if (Config::get('dburl')) {
  178. $sut_connection_info = Database::getConnectionInfo();
  179. $sut_tasks_class = $sut_connection_info['default']['namespace'] . "\\Install\\Tasks";
  180. $sut_installer = new $sut_tasks_class();
  181. $sut_connection = Database::getConnection();
  182. echo sprintf("Database.............: %s\n", (string) $sut_installer->name());
  183. echo sprintf("Database Version.....: %s\n", $sut_connection->version());
  184. }
  185. echo sprintf("Working directory....: %s\n", getcwd());
  186. echo "--------------------------------------------------------------\n";
  187. $groupedTestClassInfoList = simpletest_script_get_test_list();
  188. $workAllocator = new WorkAllocator(
  189. $groupedTestClassInfoList,
  190. (int) Config::get('ci-parallel-node-total'),
  191. (int) Config::get('ci-parallel-node-index'),
  192. );
  193. $test_list = array_keys($workAllocator->getAllocatedList());
  194. if (Config::get('debug-discovery')) {
  195. if ((int) Config::get('ci-parallel-node-total') > 1) {
  196. dump_bin_tests_sequence((int) Config::get('ci-parallel-node-index'), $workAllocator->getSortedList(), $workAllocator->getAllocatedList());
  197. }
  198. else {
  199. dump_tests_sequence($workAllocator->getAllocatedList());
  200. }
  201. }
  202. // Try to allocate unlimited time to run the tests.
  203. Environment::setTimeLimit(0);
  204. simpletest_script_reporter_init();
  205. $tests_to_run = [];
  206. for ($i = 0; $i < Config::get('repeat'); $i++) {
  207. $tests_to_run = array_merge($tests_to_run, $test_list);
  208. }
  209. // Execute tests.
  210. $status = simpletest_script_execute_batch($test_run_results_storage, $tests_to_run);
  211. // Stop the timer.
  212. simpletest_script_reporter_timer_stop();
  213. // Ensure all test locks are released once finished. If tests are run with a
  214. // concurrency of 1 the each test will clean up its own lock. Test locks are
  215. // not released if using a higher concurrency to ensure each test has unique
  216. // fixtures.
  217. TestDatabase::releaseAllTestLocks();
  218. // Display results before database is cleared.
  219. simpletest_script_reporter_display_results($test_run_results_storage);
  220. if (Config::get('xml')) {
  221. simpletest_script_reporter_write_xml_results($test_run_results_storage);
  222. }
  223. // Clean up all test results.
  224. if (!Config::get('keep-results')) {
  225. try {
  226. $cleaner = new EnvironmentCleaner(
  227. DRUPAL_ROOT,
  228. Database::getConnection(),
  229. $test_run_results_storage,
  230. $console_output,
  231. \Drupal::service('file_system')
  232. );
  233. $cleaner->cleanResults();
  234. }
  235. catch (Exception $e) {
  236. echo (string) $e;
  237. exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  238. }
  239. }
  240. // Test complete, exit.
  241. exit($status);
  242. /**
  243. * Print help text.
  244. */
  245. function simpletest_script_help(InputDefinition $input_definition, string $script_basename, ConsoleOutput $console_output): void {
  246. echo <<
  247. Run Drupal tests from the shell.
  248. Usage: {$script_basename} [OPTIONS]
  249. Example: {$script_basename} Profile
  250. EOF;
  251. $helper = new DescriptorHelper();
  252. $helper->describe($console_output, $input_definition);
  253. echo <<
  254. To run this script you will normally invoke it from the root directory of your
  255. Drupal installation as the webserver user (differs per configuration), or root:
  256. sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$script_basename} --url http://example.com/ --all
  257. sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$script_basename} --url http://example.com/ --class Drupal\\\\Tests\\\\block\\\\Functional\\\\BlockTest
  258. Without a preinstalled Drupal site, specify a SQLite database pathname to create
  259. (for the test runner) and the default database connection info (for Drupal) to
  260. use in tests:
  261. sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$script_basename}
  262. --sqlite /tmpfs/drupal/test.sqlite
  263. --dburl mysql://username:password@localhost/database
  264. --url http://example.com/ --all
  265. EOF;
  266. }
  267. /**
  268. * Initialize script variables and perform general setup requirements.
  269. *
  270. * @param \Drupal\Core\Composer\Composer $autoloader
  271. * The Composer provided PHP class loader.
  272. */
  273. function simpletest_script_init(ClassLoader $autoloader): void {
  274. // Get URL from arguments.
  275. $parsed_url = parse_url(Config::get('url'));
  276. $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
  277. $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
  278. $port = $parsed_url['port'] ?? '80';
  279. // If the passed URL schema is 'https' then setup the $_SERVER variables
  280. // properly so that testing will run under HTTPS.
  281. if ($parsed_url['scheme'] == 'https') {
  282. $_SERVER['HTTPS'] = 'on';
  283. }
  284. $base_url = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://';
  285. $base_url .= $host;
  286. if ($path !== '') {
  287. $base_url .= $path;
  288. }
  289. putenv('SIMPLETEST_BASE_URL=' . $base_url);
  290. $_SERVER['HTTP_HOST'] = $host;
  291. $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
  292. $_SERVER['SERVER_ADDR'] = '127.0.0.1';
  293. $_SERVER['SERVER_PORT'] = $port;
  294. $_SERVER['SERVER_SOFTWARE'] = NULL;
  295. $_SERVER['SERVER_NAME'] = 'localhost';
  296. $_SERVER['REQUEST_URI'] = $path . '/';
  297. $_SERVER['REQUEST_METHOD'] = 'GET';
  298. $_SERVER['SCRIPT_NAME'] = $path . '/index.php';
  299. $_SERVER['SCRIPT_FILENAME'] = $path . '/index.php';
  300. $_SERVER['PHP_SELF'] = $path . '/index.php';
  301. $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
  302. if (Config::get('concurrency') > 1) {
  303. $directory = FileSystem::getOsTemporaryDirectory();
  304. $test_symlink = @symlink(__FILE__, $directory . '/test_symlink');
  305. if (!$test_symlink) {
  306. throw new \RuntimeException('In order to use a concurrency higher than 1 the test system needs to be able to create symlinks in ' . $directory);
  307. }
  308. unlink($directory . '/test_symlink');
  309. putenv('RUN_TESTS_CONCURRENCY=' . Config::get('concurrency'));
  310. }
  311. if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
  312. // Ensure that any and all environment variables are changed to https://.
  313. foreach ($_SERVER as $key => $value) {
  314. // Some values are NULL. Non-NULL values which are falsy will not contain
  315. // text to replace.
  316. if ($value) {
  317. $_SERVER[$key] = str_replace('http://', 'https://', $value);
  318. }
  319. }
  320. }
  321. chdir(realpath(__DIR__ . '/../..'));
  322. // Prepare the kernel.
  323. try {
  324. $request = Request::createFromGlobals();
  325. $kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
  326. $kernel->boot();
  327. $kernel->preHandle($request);
  328. }
  329. catch (Exception $e) {
  330. echo (string) $e;
  331. exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  332. }
  333. }
  334. /**
  335. * Sets up database connection info for running tests.
  336. *
  337. * If this script is executed from within a real Drupal installation, then this
  338. * function essentially performs nothing (unless the --sqlite or --dburl
  339. * parameters were passed).
  340. *
  341. * Otherwise, there are three database connections of concern:
  342. * - --sqlite: The test runner connection, providing access to database tables
  343. * for recording test IDs and assertion results.
  344. * - --dburl: A database connection that is used as base connection info for all
  345. * tests; i.e., every test will spawn from this connection. In case this
  346. * connection uses e.g. SQLite, then all tests will run against SQLite. This
  347. * is exposed as $databases['default']['default'] to Drupal.
  348. * - The actual database connection used within a test. This is the same as
  349. * --dburl, but uses an additional database table prefix. This is
  350. * $databases['default']['default'] within a test environment. The original
  351. * connection is retained in
  352. * $databases['simpletest_original_default']['default'] and restored after
  353. * each test.
  354. */
  355. function simpletest_script_setup_database(): void {
  356. // If there is an existing Drupal installation that contains a database
  357. // connection info in settings.php, then $databases['default']['default'] will
  358. // hold the default database connection already. This connection is assumed to
  359. // be valid, and this connection will be used in tests, so that they run
  360. // against e.g. MySQL instead of SQLite.
  361. // However, in case no Drupal installation exists, this default database
  362. // connection can be set and/or overridden with the --dburl parameter.
  363. if (Config::get('dburl')) {
  364. // Remove a possibly existing default connection (from settings.php).
  365. Database::removeConnection('default');
  366. try {
  367. $databases['default']['default'] = Database::convertDbUrlToConnectionInfo(Config::get('dburl'), TRUE);
  368. }
  369. catch (\InvalidArgumentException $e) {
  370. simpletest_script_print_error('Invalid --dburl. Reason: ' . $e->getMessage());
  371. exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
  372. }
  373. }
  374. // Otherwise, use the default database connection from settings.php.
  375. else {
  376. $databases['default'] = Database::getConnectionInfo('default');
  377. }
  378. if (isset($databases['default']['default'])) {
  379. Database::addConnectionInfo('default', 'default', $databases['default']['default']);
  380. }
  381. }
  382. /**
  383. * Sets up the test runs results storage.
  384. */
  385. function simpletest_script_setup_test_run_results_storage() {
  386. $databases['default'] = Database::getConnectionInfo('default');
  387. // If no --sqlite parameter has been passed, then the test runner database
  388. // connection is the default database connection.
  389. $sqlite = Config::get('sqlite');
  390. if (!$sqlite) {
  391. $sqlite = FALSE;
  392. $databases['test-runner']['default'] = $databases['default']['default'];
  393. }
  394. // Otherwise, set up a SQLite connection for the test runner.
  395. else {
  396. if ($sqlite === ':memory:') {
  397. $sqlite = ':memory:';
  398. }
  399. elseif (is_string($sqlite) && !str_starts_with($sqlite, '/')) {
  400. $sqlite = DRUPAL_ROOT . '/' . $sqlite;
  401. }
  402. $databases['test-runner']['default'] = [
  403. 'driver' => 'sqlite',
  404. 'database' => $sqlite,
  405. 'prefix' => '',
  406. ];
  407. // Create the test runner SQLite database, unless it exists already.
  408. if ($sqlite !== ':memory:' && !file_exists($sqlite)) {
  409. if (!is_dir(dirname($sqlite))) {
  410. mkdir(dirname($sqlite));
  411. }
  412. touch($sqlite);
  413. }
  414. }
  415. // Add the test runner database connection.
  416. Database::addConnectionInfo('test-runner', 'default', $databases['test-runner']['default']);
  417. // Create the test result schema.
  418. try {
  419. $test_run_results_storage = new SimpletestTestRunResultsStorage(Database::getConnection('default', 'test-runner'));
  420. }
  421. catch (\PDOException $e) {
  422. simpletest_script_print_error($databases['test-runner']['default']['driver'] . ': ' . $e->getMessage());
  423. exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
  424. }
  425. if ($sqlite) {
  426. try {
  427. $test_run_results_storage->buildTestingResultsEnvironment(Config::get('keep-results-table'));
  428. }
  429. catch (Exception $e) {
  430. echo (string) $e;
  431. exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  432. }
  433. }
  434. // Verify that the test result database schema exists by checking one table.
  435. try {
  436. if (!$test_run_results_storage->validateTestingResultsEnvironment()) {
  437. simpletest_script_print_error('Missing test result database schema. Use the --sqlite parameter.');
  438. exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
  439. }
  440. }
  441. catch (Exception $e) {
  442. echo (string) $e;
  443. exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  444. }
  445. return $test_run_results_storage;
  446. }
  447. /**
  448. * Execute a batch of tests.
  449. */
  450. function simpletest_script_execute_batch(TestRunResultsStorageInterface $test_run_results_storage, $test_classes) {
  451. global $test_ids, $total_time;
  452. $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
  453. $total_time = 0;
  454. $process_runner = PhpUnitTestRunner::create(\Drupal::getContainer())
  455. ->setConfigurationFilePath(Config::get('phpunit-configuration'));
  456. // Multi-process execution.
  457. $children = [];
  458. while (!empty($test_classes) || !empty($children)) {
  459. while (count($children) < Config::get('concurrency')) {
  460. if (empty($test_classes)) {
  461. break;
  462. }
  463. try {
  464. $test_run = TestRun::createNew($test_run_results_storage);
  465. }
  466. catch (Exception $e) {
  467. echo (string) $e;
  468. exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  469. }
  470. $test_ids[] = $test_run->id();
  471. $test_class = array_shift($test_classes);
  472. // Fork a child process.
  473. try {
  474. $process = $process_runner->startPhpUnitOnSingleTestClass(
  475. $test_run,
  476. $test_class,
  477. Config::get('color'),
  478. Config::get('suppress-deprecations'),
  479. );
  480. }
  481. catch (\Throwable $e) {
  482. // PHPUnit catches exceptions already, so this is only reached when an
  483. // exception is thrown in the wrapped test runner environment.
  484. echo (string) $e;
  485. exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  486. }
  487. // Register our new child.
  488. $children[] = [
  489. 'process' => $process,
  490. 'test_run' => $test_run,
  491. 'class' => $test_class,
  492. ];
  493. }
  494. // Wait for children every 2ms.
  495. usleep(2000);
  496. // Check if some children finished.
  497. foreach ($children as $cid => $child) {
  498. if ($child['process']->isTerminated()) {
  499. // The child exited.
  500. $child['test_run']->end(microtime(TRUE));
  501. $total_time += $child['test_run']->duration();
  502. $process_outcome = $process_runner->processPhpUnitOnSingleTestClassOutcome(
  503. $child['process'],
  504. $child['test_run'],
  505. $child['class'],
  506. );
  507. simpletest_script_reporter_display_summary(
  508. $child['class'],
  509. $process_outcome['summaries'][$child['class']],
  510. $child['test_run']->duration()
  511. );
  512. if ($process_outcome['error_output']) {
  513. echo 'ERROR: ' . implode("\n", $process_outcome['error_output']);
  514. }
  515. if (in_array($process_outcome['status'], [SIMPLETEST_SCRIPT_EXIT_FAILURE, SIMPLETEST_SCRIPT_EXIT_ERROR])) {
  516. $total_status = max($process_outcome['status'], $total_status);
  517. }
  518. elseif ($process_outcome['status']) {
  519. $message = 'FATAL ' . $child['class'] . ': test runner returned an unexpected error code (' . $process_outcome['status'] . ').';
  520. echo $message . "\n";
  521. $total_status = max(SIMPLETEST_SCRIPT_EXIT_EXCEPTION, $total_status);
  522. if (Config::get('die-on-fail')) {
  523. $test_db = new TestDatabase($child['test_run']->getDatabasePrefix());
  524. $test_directory = $test_db->getTestSitePath();
  525. echo 'Test database and files kept and test exited immediately on fail so should be reproducible if you change settings.php to use the database prefix ' . $child['test_run']->getDatabasePrefix() . ' and config directories in ' . $test_directory . "\n";
  526. Config::set('keep-results', TRUE);
  527. // Exit repeat loop immediately.
  528. Config::set('repeat', -1);
  529. }
  530. }
  531. // Remove this child.
  532. unset($children[$cid]);
  533. }
  534. }
  535. }
  536. return $total_status;
  537. }
  538. /**
  539. * Get list of tests based on arguments.
  540. *
  541. * If --all specified then return all available tests, otherwise reads list of
  542. * tests.
  543. *
  544. * @return array
  545. * List of tests.
  546. */
  547. function simpletest_script_get_test_list() {
  548. $testDiscovery = PhpUnitTestDiscovery::instance()->setConfigurationFilePath(Config::get('phpunit-configuration'));
  549. echo "Test discovery\n";
  550. try {
  551. if (Config::get('all') || Config::get('module') || Config::get('directory')) {
  552. if (Config::get('types')) {
  553. echo sprintf("PHPUnit test suite(s): %s\n", implode(', ', Config::get('types')));
  554. }
  555. if (Config::get('module')) {
  556. echo sprintf("Drupal module........: %s\n", Config::get('module'));
  557. }
  558. if (Config::get('directory')) {
  559. echo sprintf("Directory............: %s\n", Config::get('directory'));
  560. }
  561. if (Config::getTests()) {
  562. echo sprintf("PHPUnit test group(s): %s\n", implode(', ', Config::getTests()));
  563. }
  564. echo "--------------------------------------------------------------\n";
  565. $groupedTestClassInfoList = $testDiscovery->getTestClasses(Config::get('module'), Config::get('types'), Config::get('directory'), Config::getTests());
  566. }
  567. elseif (Config::get('class')) {
  568. // When --class is specified, we have to find the file of each of the
  569. // classes indicated as argument and run test discovery for it, then
  570. // merge the results.
  571. $classesArg = Config::getTests();
  572. echo sprintf("Test class(es).......: %s\n", array_shift($classesArg));
  573. foreach ($classesArg as $arg) {
  574. echo sprintf(" : %s\n", $arg);
  575. }
  576. echo "--------------------------------------------------------------\n";
  577. $groupedTestClassInfoList = [];
  578. foreach (Config::getTests() as $test_class) {
  579. [$class_name] = explode('::', $test_class, 2);
  580. if (class_exists($class_name)) {
  581. $fileName = (new \ReflectionClass($class_name))->getFileName();
  582. $groupedClassInfo = $testDiscovery->getTestClasses(NULL, [], $fileName);
  583. foreach (array_keys($groupedClassInfo) as $classGroupKey) {
  584. if (array_key_exists($classGroupKey, $groupedTestClassInfoList)) {
  585. $groupedTestClassInfoList[$classGroupKey] = array_merge($groupedTestClassInfoList[$classGroupKey], $groupedClassInfo[$classGroupKey]);
  586. }
  587. else {
  588. $groupedTestClassInfoList[$classGroupKey] = $groupedClassInfo[$classGroupKey];
  589. }
  590. }
  591. }
  592. else {
  593. // The class does not exist: we discover all the test classes and
  594. // suggest a possible alternative.
  595. $groupedTestClassInfoList = $testDiscovery->getTestClasses(NULL, Config::get('types'));
  596. dump_discovery_warnings();
  597. $all_classes = [];
  598. foreach ($groupedTestClassInfoList as $group) {
  599. $all_classes = array_merge($all_classes, array_keys($group));
  600. }
  601. simpletest_script_print_error('Test class not found: ' . $class_name);
  602. simpletest_script_print_alternatives($class_name, $all_classes, 6);
  603. exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
  604. }
  605. }
  606. }
  607. elseif (Config::get('file')) {
  608. // When --file is specified, we have to run test discovery for each of
  609. // the files indicated, then merge the results.
  610. $filesArg = Config::getTests();
  611. echo sprintf("Test file(s).........: %s\n", array_shift($filesArg));
  612. foreach ($filesArg as $arg) {
  613. echo sprintf(" : %s\n", $arg);
  614. }
  615. echo "--------------------------------------------------------------\n";
  616. $groupedTestClassInfoList = [];
  617. foreach (Config::getTests() as $file) {
  618. if (!file_exists($file) || is_dir($file)) {
  619. simpletest_script_print_error('File not found: ' . $file);
  620. exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
  621. }
  622. $groupedClassInfo = $testDiscovery->getTestClasses(NULL, [], $file);
  623. foreach (array_keys($groupedClassInfo) as $classGroupKey) {
  624. if (array_key_exists($classGroupKey, $groupedTestClassInfoList)) {
  625. $groupedTestClassInfoList[$classGroupKey] = array_merge($groupedTestClassInfoList[$classGroupKey], $groupedClassInfo[$classGroupKey]);
  626. }
  627. else {
  628. $groupedTestClassInfoList[$classGroupKey] = $groupedClassInfo[$classGroupKey];
  629. }
  630. }
  631. }
  632. }
  633. else {
  634. // When no restriction options are specified, we consider the argument as
  635. // a list of groups of tests to be executed.
  636. if (Config::get('types')) {
  637. echo sprintf("PHPUnit test suite(s): %s\n", implode(', ', Config::get('types')));
  638. }
  639. if (Config::getTests()) {
  640. echo sprintf("PHPUnit test group(s): %s\n", implode(', ', Config::getTests()));
  641. }
  642. echo "--------------------------------------------------------------\n";
  643. $groupedTestClassInfoList = [];
  644. try {
  645. $groupedTestClassInfoFullSuiteList = $testDiscovery->getTestClasses(NULL, Config::get('types'));
  646. }
  647. catch (\Exception $e) {
  648. echo (string) $e;
  649. exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  650. }
  651. // Store all the groups so we can suggest alternatives if we need to.
  652. $all_groups = array_keys($groupedTestClassInfoFullSuiteList);
  653. // Verify that the groups exist.
  654. if (!empty($unknown_groups = array_diff(Config::getTests(), $all_groups))) {
  655. $first_group = reset($unknown_groups);
  656. simpletest_script_print_error('Test group not found: ' . $first_group);
  657. simpletest_script_print_alternatives($first_group, $all_groups);
  658. exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
  659. }
  660. foreach (Config::getTests() as $group_name) {
  661. $groupedTestClassInfoList[$group_name] = $groupedTestClassInfoFullSuiteList[$group_name];
  662. }
  663. // The '#slow' group is a special case, because it may not be selected in
  664. // the argument, but it must be present if any test class indicates it in
  665. // metadata, for the work allocator to prioritize its execution.
  666. foreach ($groupedTestClassInfoList as $testClassInfoList) {
  667. foreach ($testClassInfoList as $testClass => $testClassInfo) {
  668. if (in_array('#slow', $testClassInfo['groups'])) {
  669. $groupedTestClassInfoList['#slow'][$testClass] = $testClassInfo;
  670. }
  671. }
  672. }
  673. }
  674. }
  675. catch (\Exception $e) {
  676. echo (string) $e;
  677. exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  678. }
  679. echo "\n";
  680. dump_discovery_warnings();
  681. if (empty($groupedTestClassInfoList)) {
  682. simpletest_script_print_error('No valid tests were specified.');
  683. exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
  684. }
  685. return $groupedTestClassInfoList;
  686. }
  687. /**
  688. * Dumps the list of tests in order of execution after sorting.
  689. *
  690. * @param array $tests
  691. * The array of test class info.
  692. */
  693. function dump_tests_sequence(array $tests): void {
  694. if (!Config::get('debug-discovery')) {
  695. return;
  696. }
  697. echo "Test execution sequence\n";
  698. echo "-----------------------\n\n";
  699. echo " Seq Slow? Group Cnt Class\n";
  700. echo "-----------------------------------------\n";
  701. foreach ($tests as $testInfo) {
  702. echo sprintf(
  703. "%4d %5s %15s %4d %s\n",
  704. $testInfo['worker_sequence'],
  705. in_array('#slow', $testInfo['groups']) ? '#slow' : '',
  706. trim_with_ellipsis($testInfo['group'], 15, \STR_PAD_RIGHT),
  707. $testInfo['tests_count'],
  708. trim_with_ellipsis($testInfo['name'], 60, \STR_PAD_LEFT),
  709. );
  710. }
  711. echo "-----------------------------------------\n\n";
  712. }
  713. /**
  714. * Dumps the list of tests in order of execution for a bin.
  715. *
  716. * @param int $bin
  717. * The bin.
  718. * @param array $allTests
  719. * The list of all test classes discovered.
  720. * @param array $tests
  721. * The list of test class to run for this bin.
  722. */
  723. function dump_bin_tests_sequence(int $bin, array $allTests, array $tests): void {
  724. echo "Test execution sequence. ";
  725. echo "Tests marked *** will be executed in this PARALLEL BIN #{$bin}.\n";
  726. echo "-------------------------------------------------------------------------------------\n\n";
  727. echo " Sort Bin \n";
  728. echo "Bin Seq Seq Slow? Group Cnt Class\n";
  729. echo "-------------------------------------------------------------------------------------\n";
  730. foreach ($allTests as $testInfo) {
  731. $inBin = isset($tests[$testInfo['name']]);
  732. $message = sprintf(
  733. "%s %4d %s %5s %15s %4d %s\n",
  734. $inBin ? "***" : " ",
  735. $testInfo['sorted_sequence'],
  736. $inBin ? sprintf('%4d', $tests[$testInfo['name']]['worker_sequence']) : " ",
  737. in_array('#slow', $testInfo['groups']) ? '#slow' : '',
  738. trim_with_ellipsis($testInfo['group'], 15, \STR_PAD_RIGHT),
  739. $testInfo['tests_count'],
  740. trim_with_ellipsis($testInfo['name'], 60, \STR_PAD_LEFT),
  741. );
  742. simpletest_script_print($message, $inBin ? SIMPLETEST_SCRIPT_COLOR_BRIGHT_WHITE : SIMPLETEST_SCRIPT_COLOR_GRAY);
  743. }
  744. echo "-------------------------------------------------------------------------------------\n\n";
  745. }
  746. /**
  747. * Initialize the reporter.
  748. */
  749. function simpletest_script_reporter_init(): void {
  750. global $test_list, $results_map;
  751. $results_map = [
  752. 'pass' => 'Pass',
  753. 'fail' => 'Fail',
  754. 'error' => 'Error',
  755. 'skipped' => 'Skipped',
  756. 'cli_fail' => 'Failure',
  757. 'exception' => 'Exception',
  758. 'debug' => 'Log',
  759. ];
  760. // Tell the user about what tests are to be run.
  761. if (Config::get('all')) {
  762. echo "All tests will run.\n\n";
  763. }
  764. else {
  765. echo "Tests to be run:\n";
  766. foreach ($test_list as $class_name) {
  767. echo " - $class_name\n";
  768. }
  769. echo "\n";
  770. }
  771. echo "Test run started:\n";
  772. echo " " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']) . "\n";
  773. Timer::start('run-tests');
  774. echo "\n";
  775. echo "Test summary\n";
  776. echo "------------\n";
  777. echo "\n";
  778. }
  779. /**
  780. * Displays the assertion result summary for a single test class.
  781. *
  782. * @param string $class
  783. * The test class name that was run.
  784. * @param array $results
  785. * The assertion results using #pass, #fail, #exception, #debug array keys.
  786. * @param float|null $duration
  787. * The time taken for the test to complete.
  788. */
  789. function simpletest_script_reporter_display_summary($class, $results, $duration = NULL): void {
  790. // Output all test results vertically aligned.
  791. $summary = [str_pad($results['#pass'], 4, " ", STR_PAD_LEFT) . ' passed'];
  792. if ($results['#fail']) {
  793. $summary[] = $results['#fail'] . ' failed';
  794. }
  795. if ($results['#error']) {
  796. $summary[] = $results['#error'] . ' errored';
  797. }
  798. if ($results['#skipped']) {
  799. $summary[] = $results['#skipped'] . ' skipped';
  800. }
  801. if ($results['#exception']) {
  802. $summary[] = $results['#exception'] . ' exception(s)';
  803. }
  804. if ($results['#debug']) {
  805. $summary[] = $results['#debug'] . ' log(s)';
  806. }
  807. if ($results['#cli_fail']) {
  808. $summary[] = 'exit code ' . $results['#exit_code'];
  809. }
  810. // The key $results['#time'] holds the sum of the tests execution times,
  811. // without taking into account the process spawning time and the setup
  812. // times of the tests themselves. So for reporting to be consistent with
  813. // PHPUnit CLI reported execution time, we report here the overall time of
  814. // execution of the spawned process.
  815. $time = sprintf('%8.3fs', $duration);
  816. $output = vsprintf('%s %s %s', [$time, trim_with_ellipsis($class, 70, STR_PAD_LEFT), implode(', ', $summary)]);
  817. $status = ($results['#fail'] || $results['#cli_fail'] || $results['#exception'] || $results['#error'] ? 'fail' : 'pass');
  818. simpletest_script_print($output . "\n", simpletest_script_color_code($status));
  819. }
  820. /**
  821. * Display jUnit XML test results.
  822. */
  823. function simpletest_script_reporter_write_xml_results(TestRunResultsStorageInterface $test_run_results_storage): void {
  824. global $test_ids, $results_map;
  825. try {
  826. $results = simpletest_script_load_messages_by_test_id($test_run_results_storage, $test_ids);
  827. }
  828. catch (Exception $e) {
  829. echo (string) $e;
  830. exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  831. }
  832. $test_class = '';
  833. $xml_files = [];
  834. foreach ($results as $result) {
  835. if (isset($results_map[$result->status])) {
  836. if ($result->test_class != $test_class) {
  837. // We've moved onto a new class, so write the last classes results to a
  838. // file:
  839. if (isset($xml_files[$test_class])) {
  840. file_put_contents(Config::get('xml') . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());
  841. unset($xml_files[$test_class]);
  842. }
  843. $test_class = $result->test_class;
  844. if (!isset($xml_files[$test_class])) {
  845. $doc = new DOMDocument('1.0', 'utf-8');
  846. $root = $doc->createElement('testsuite');
  847. $root = $doc->appendChild($root);
  848. $xml_files[$test_class] = ['doc' => $doc, 'suite' => $root];
  849. }
  850. }
  851. // For convenience:
  852. $dom_document = &$xml_files[$test_class]['doc'];
  853. // Create the XML element for this test case:
  854. $case = $dom_document->createElement('testcase');
  855. $case->setAttribute('classname', $test_class);
  856. if (str_contains($result->function, '->')) {
  857. [, $name] = explode('->', $result->function, 2);
  858. }
  859. else {
  860. $name = $result->function;
  861. }
  862. $case->setAttribute('name', $name);
  863. // Passes get no further attention, but failures and exceptions get to add
  864. // more detail:
  865. if ($result->status == 'fail') {
  866. $fail = $dom_document->createElement('failure');
  867. $fail->setAttribute('type', 'failure');
  868. $fail->setAttribute('message', $result->message_group);
  869. $text = $dom_document->createTextNode($result->message);
  870. $fail->appendChild($text);
  871. $case->appendChild($fail);
  872. }
  873. elseif ($result->status == 'exception') {
  874. // In the case of an exception the $result->function may not be a class
  875. // method so we record the full function name:
  876. $case->setAttribute('name', $result->function);
  877. $fail = $dom_document->createElement('error');
  878. $fail->setAttribute('type', 'exception');
  879. $fail->setAttribute('message', $result->message_group);
  880. $full_message = $result->message . "\n\nline: " . $result->line . "\nfile: " . $result->file;
  881. $text = $dom_document->createTextNode($full_message);
  882. $fail->appendChild($text);
  883. $case->appendChild($fail);
  884. }
  885. // Append the test case XML to the test suite:
  886. $xml_files[$test_class]['suite']->appendChild($case);
  887. }
  888. }
  889. // The last test case hasn't been saved to a file yet, so do that now:
  890. if (isset($xml_files[$test_class])) {
  891. file_put_contents(Config::get('xml') . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());
  892. unset($xml_files[$test_class]);
  893. }
  894. }
  895. /**
  896. * Stop the test timer.
  897. */
  898. function simpletest_script_reporter_timer_stop(): void {
  899. global $total_time;
  900. echo "\n";
  901. $end = Timer::stop('run-tests');
  902. $wall_seconds = $end['time'] / 1000;
  903. $formatter = \Drupal::service('date.formatter');
  904. echo "Wall time: " . $formatter->formatInterval((int) $wall_seconds) . "\n";
  905. echo "Total time: " . $formatter->formatInterval((int) $total_time) . "\n";
  906. if ($wall_seconds > 0) {
  907. echo sprintf("Speedup: %.2fx (concurrency %d)\n", $total_time / $wall_seconds, Config::get('concurrency'));
  908. }
  909. echo "\n";
  910. }
  911. /**
  912. * Display test results.
  913. */
  914. function simpletest_script_reporter_display_results(TestRunResultsStorageInterface $test_run_results_storage): void {
  915. global $test_ids, $results_map;
  916. if (Config::get('verbose')) {
  917. // Report results.
  918. echo "Detailed test results\n";
  919. echo "---------------------\n";
  920. try {
  921. $results = simpletest_script_load_messages_by_test_id($test_run_results_storage, $test_ids);
  922. }
  923. catch (Exception $e) {
  924. echo (string) $e;
  925. exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  926. }
  927. $test_class = '';
  928. foreach ($results as $result) {
  929. if (isset($results_map[$result->status])) {
  930. if ($result->test_class != $test_class) {
  931. // Display test class every time results are for new test class.
  932. echo "\n\n---- $result->test_class ----\n\n\n";
  933. $test_class = $result->test_class;
  934. // Print table header.
  935. echo "Status Duration Info \n";
  936. echo "--------------------------------------------------------------------------------------------------------\n";
  937. }
  938. simpletest_script_format_result($result);
  939. }
  940. }
  941. }
  942. }
  943. /**
  944. * Format the result so that it fits within 80 characters.
  945. *
  946. * @param object $result
  947. * The result object to format.
  948. */
  949. function simpletest_script_format_result($result): void {
  950. global $results_map;
  951. if ($result->time == 0) {
  952. $duration = " ";
  953. }
  954. elseif ($result->time < 0.001) {
  955. $duration = " <1 ms";
  956. }
  957. else {
  958. $duration = sprintf("%9.3fs", $result->time);
  959. }
  960. $summary = sprintf("%-9.9s %s %s\n", $results_map[$result->status], $duration, trim_with_ellipsis($result->function, 80, STR_PAD_LEFT));
  961. simpletest_script_print($summary, simpletest_script_color_code($result->status));
  962. if ($result->message === '' || in_array($result->status, ['pass', 'fail', 'error'])) {
  963. return;
  964. }
  965. $message = trim(strip_tags($result->message));
  966. if (Config::get('non-html')) {
  967. $message = Html::decodeEntities($message);
  968. }
  969. $lines = explode("\n", $message);
  970. foreach ($lines as $line) {
  971. echo " $line\n";
  972. }
  973. }
  974. /**
  975. * Print error messages so the user will notice them.
  976. *
  977. * Print error message prefixed with " ERROR: " and displayed in fail color if
  978. * color output is enabled.
  979. *
  980. * @param string $message
  981. * The message to print.
  982. */
  983. function simpletest_script_print_error($message): void {
  984. simpletest_script_print(" ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
  985. }
  986. /**
  987. * Print a message to the console, using a color.
  988. *
  989. * @param string $message
  990. * The message to print.
  991. * @param int|string $color_code
  992. * The color code to use for coloring.
  993. */
  994. function simpletest_script_print($message, $color_code): void {
  995. try {
  996. if (Config::get('color')) {
  997. echo "\033[" . $color_code . "m" . $message . "\033[0m";
  998. }
  999. else {
  1000. echo $message;
  1001. }
  1002. }
  1003. catch (\RuntimeException) {
  1004. echo $message;
  1005. }
  1006. }
  1007. /**
  1008. * Get the color code associated with the specified status.
  1009. *
  1010. * @param string $status
  1011. * The status string to get code for. Special cases are: 'pass', 'fail', or
  1012. * 'exception'.
  1013. *
  1014. * @return int
  1015. * Color code. Returns 0 for default case.
  1016. */
  1017. function simpletest_script_color_code($status) {
  1018. return match ($status) {
  1019. 'pass' => SIMPLETEST_SCRIPT_COLOR_PASS,
  1020. 'fail', 'cli_fail', 'error', 'exception' => SIMPLETEST_SCRIPT_COLOR_FAIL,
  1021. 'skipped' => SIMPLETEST_SCRIPT_COLOR_YELLOW,
  1022. 'debug' => SIMPLETEST_SCRIPT_COLOR_CYAN,
  1023. default => 0,
  1024. };
  1025. }
  1026. /**
  1027. * Prints alternative test names.
  1028. *
  1029. * Searches the provided array of string values for close matches based on the
  1030. * Levenshtein algorithm.
  1031. *
  1032. * @param string $string
  1033. * A string to test.
  1034. * @param array $array
  1035. * A list of strings to search.
  1036. * @param int $degree
  1037. * The matching strictness. Higher values return fewer matches. A value of
  1038. * 4 means that the function will return strings from $array if the candidate
  1039. * string in $array would be identical to $string by changing 1/4 or fewer of
  1040. * its characters.
  1041. *
  1042. * @see http://php.net/manual/function.levenshtein.php
  1043. */
  1044. function simpletest_script_print_alternatives($string, $array, $degree = 4): void {
  1045. $alternatives = [];
  1046. foreach ($array as $item) {
  1047. $lev = levenshtein($string, $item);
  1048. if ($lev <= strlen($item) / $degree || str_contains($string, $item)) {
  1049. $alternatives[] = $item;
  1050. }
  1051. }
  1052. if (!empty($alternatives)) {
  1053. simpletest_script_print(" Did you mean?\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
  1054. foreach ($alternatives as $alternative) {
  1055. simpletest_script_print(" - $alternative\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
  1056. }
  1057. }
  1058. }
  1059. /**
  1060. * Loads test result messages from the database.
  1061. *
  1062. * Messages are ordered by test class and message id.
  1063. *
  1064. * @param array $test_ids
  1065. * Array of test IDs of the messages to be loaded.
  1066. *
  1067. * @return array
  1068. * Array of test result messages from the database.
  1069. */
  1070. function simpletest_script_load_messages_by_test_id(TestRunResultsStorageInterface $test_run_results_storage, $test_ids) {
  1071. $results = [];
  1072. // Sqlite has a maximum number of variables per query. If required, the
  1073. // database query is split into chunks.
  1074. if (count($test_ids) > SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT && Config::get('sqlite')) {
  1075. $test_id_chunks = array_chunk($test_ids, SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT);
  1076. }
  1077. else {
  1078. $test_id_chunks = [$test_ids];
  1079. }
  1080. foreach ($test_id_chunks as $test_id_chunk) {
  1081. try {
  1082. $result_chunk = [];
  1083. foreach ($test_id_chunk as $test_id) {
  1084. $test_run = TestRun::get($test_run_results_storage, $test_id);
  1085. $result_chunk = array_merge($result_chunk, $test_run->getLogEntriesByTestClass());
  1086. }
  1087. }
  1088. catch (Exception $e) {
  1089. echo (string) $e;
  1090. exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  1091. }
  1092. if ($result_chunk) {
  1093. $results = array_merge($results, $result_chunk);
  1094. }
  1095. }
  1096. return $results;
  1097. }
  1098. /**
  1099. * Trims a string adding a leading or trailing ellipsis.
  1100. *
  1101. * @param string $input
  1102. * The input string.
  1103. * @param int $length
  1104. * The exact trimmed string length.
  1105. * @param int $side
  1106. * Leading or trailing ellipsis.
  1107. *
  1108. * @return string
  1109. * The trimmed string.
  1110. */
  1111. function trim_with_ellipsis(string $input, int $length, int $side): string {
  1112. if (strlen($input) < $length) {
  1113. return str_pad($input, $length, ' ', \STR_PAD_RIGHT);
  1114. }
  1115. elseif (strlen($input) > $length) {
  1116. return match($side) {
  1117. \STR_PAD_RIGHT => substr($input, 0, $length - 1) . '…',
  1118. default => '…' . substr($input, -$length + 1),
  1119. };
  1120. }
  1121. return $input;
  1122. }
  1123. /**
  1124. * Outputs the discovery warning messages.
  1125. */
  1126. function dump_discovery_warnings(): void {
  1127. $warnings = PhpUnitTestDiscovery::instance()->getWarnings();
  1128. if (!empty($warnings)) {
  1129. simpletest_script_print("Test discovery warnings\n", SIMPLETEST_SCRIPT_COLOR_BRIGHT_WHITE);
  1130. simpletest_script_print("-----------------------\n", SIMPLETEST_SCRIPT_COLOR_BRIGHT_WHITE);
  1131. foreach ($warnings as $warning) {
  1132. $tmp = explode("\n", $warning);
  1133. simpletest_script_print('* ' . array_shift($tmp) . "\n", SIMPLETEST_SCRIPT_COLOR_EXCEPTION);
  1134. foreach ($tmp as $sub) {
  1135. simpletest_script_print(' ' . $sub . "\n", SIMPLETEST_SCRIPT_COLOR_EXCEPTION);
  1136. }
  1137. echo "\n";
  1138. }
  1139. }
  1140. }

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.