7d72aa44bcd55fa88bb096e5ec29da8356d07bfa
[yaffs-website] / web / core / modules / simpletest / simpletest.module
1 <?php
2
3 /**
4  * @file
5  * Provides testing functionality.
6  */
7
8 use Drupal\Core\Asset\AttachedAssetsInterface;
9 use Drupal\Core\Database\Database;
10 use Drupal\Core\Render\Element;
11 use Drupal\Core\Routing\RouteMatchInterface;
12 use Drupal\simpletest\TestBase;
13 use Drupal\Core\Test\TestDatabase;
14 use Drupal\simpletest\TestDiscovery;
15 use Drupal\Tests\Listeners\SimpletestUiPrinter;
16 use Symfony\Component\Process\PhpExecutableFinder;
17 use Drupal\Core\Test\TestStatus;
18
19 /**
20  * Implements hook_help().
21  */
22 function simpletest_help($route_name, RouteMatchInterface $route_match) {
23   switch ($route_name) {
24     case 'help.page.simpletest':
25       $output = '';
26       $output .= '<h3>' . t('About') . '</h3>';
27       $output .= '<p>' . t('The Testing module provides a framework for running automated tests. It can be used to verify a working state of Drupal before and after any code changes, or as a means for developers to write and execute tests for their modules. For more information, see the <a href=":simpletest">online documentation for the Testing module</a>.', [':simpletest' => 'https://www.drupal.org/documentation/modules/simpletest']) . '</p>';
28       $output .= '<h3>' . t('Uses') . '</h3>';
29       $output .= '<dl>';
30       $output .= '<dt>' . t('Running tests') . '</dt>';
31       $output .= '<dd><p>' . t('Visit the <a href=":admin-simpletest">Testing page</a> to display a list of available tests. For comprehensive testing, select <em>all</em> tests, or individually select tests for more targeted testing. Note that it might take several minutes for all tests to complete.', [':admin-simpletest' => \Drupal::url('simpletest.test_form')]) . '</p>';
32       $output .= '<p>' . t('After the tests run, a message will be displayed next to each test group indicating whether tests within it passed, failed, or had exceptions. A pass means that the test returned the expected results, while fail means that it did not. An exception normally indicates an error outside of the test, such as a PHP warning or notice. If there were failures or exceptions, the results will be expanded to show details, and the tests that had failures or exceptions will be indicated in red or pink rows. You can then use these results to refine your code and tests, until all tests pass.') . '</p></dd>';
33       $output .= '</dl>';
34       return $output;
35
36     case 'simpletest.test_form':
37       $output = t('Select the test(s) or test group(s) you would like to run, and click <em>Run tests</em>.');
38       return $output;
39   }
40 }
41
42 /**
43  * Implements hook_theme().
44  */
45 function simpletest_theme() {
46   return [
47     'simpletest_result_summary' => [
48       'variables' => ['label' => NULL, 'items' => [], 'pass' => 0, 'fail' => 0, 'exception' => 0, 'debug' => 0],
49     ],
50   ];
51 }
52
53 /**
54  * Implements hook_js_alter().
55  */
56 function simpletest_js_alter(&$javascript, AttachedAssetsInterface $assets) {
57   // Since SimpleTest is a special use case for the table select, stick the
58   // SimpleTest JavaScript above the table select.
59   $simpletest = drupal_get_path('module', 'simpletest') . '/simpletest.js';
60   if (array_key_exists($simpletest, $javascript) && array_key_exists('core/misc/tableselect.js', $javascript)) {
61     $javascript[$simpletest]['weight'] = $javascript['core/misc/tableselect.js']['weight'] - 1;
62   }
63 }
64
65 /**
66  * Prepares variables for simpletest result summary templates.
67  *
68  * Default template: simpletest-result-summary.html.twig.
69  *
70  * @param array $variables
71  *   An associative array containing:
72  *   - label: An optional label to be rendered before the results.
73  *   - ok: The overall group result pass or fail.
74  *   - pass: The number of passes.
75  *   - fail: The number of fails.
76  *   - exception: The number of exceptions.
77  *   - debug: The number of debug messages.
78  */
79 function template_preprocess_simpletest_result_summary(&$variables) {
80   $variables['items'] = _simpletest_build_summary_line($variables);
81 }
82
83 /**
84  * Formats each test result type pluralized summary.
85  *
86  * @param array $summary
87  *   A summary of the test results.
88  *
89  * @return array
90  *   The pluralized test summary items.
91  */
92 function _simpletest_build_summary_line($summary) {
93   $translation = \Drupal::translation();
94   $items['pass'] = $translation->formatPlural($summary['pass'], '1 pass', '@count passes');
95   $items['fail'] = $translation->formatPlural($summary['fail'], '1 fail', '@count fails');
96   $items['exception'] = $translation->formatPlural($summary['exception'], '1 exception', '@count exceptions');
97   if ($summary['debug']) {
98     $items['debug'] = $translation->formatPlural($summary['debug'], '1 debug message', '@count debug messages');
99   }
100   return $items;
101 }
102
103 /**
104  * Formats test result summaries into a comma separated string for run-tests.sh.
105  *
106  * @param array $summary
107  *   A summary of the test results.
108  *
109  * @return string
110  *   A concatenated string of the formatted test results.
111  */
112 function _simpletest_format_summary_line($summary) {
113   $parts = _simpletest_build_summary_line($summary);
114   return implode(', ', $parts);
115 }
116
117 /**
118  * Runs tests.
119  *
120  * @param $test_list
121  *   List of tests to run.
122  *
123  * @return string
124  *   The test ID.
125  */
126 function simpletest_run_tests($test_list) {
127   // We used to separate PHPUnit and Simpletest tests for a performance
128   // optimization. In order to support backwards compatibility check if these
129   // keys are set and create a single test list.
130   // @todo https://www.drupal.org/node/2748967 Remove BC support in Drupal 9.
131   if (isset($test_list['simpletest'])) {
132     $test_list = array_merge($test_list, $test_list['simpletest']);
133     unset($test_list['simpletest']);
134   }
135   if (isset($test_list['phpunit'])) {
136     $test_list = array_merge($test_list, $test_list['phpunit']);
137     unset($test_list['phpunit']);
138   }
139
140   $test_id = db_insert('simpletest_test_id')
141     ->useDefaults(['test_id'])
142     ->execute();
143
144   // Clear out the previous verbose files.
145   file_unmanaged_delete_recursive('public://simpletest/verbose');
146
147   // Get the info for the first test being run.
148   $first_test = reset($test_list);
149   $info = TestDiscovery::getTestInfo($first_test);
150
151   $batch = [
152     'title' => t('Running tests'),
153     'operations' => [
154       ['_simpletest_batch_operation', [$test_list, $test_id]],
155     ],
156     'finished' => '_simpletest_batch_finished',
157     'progress_message' => '',
158     'library' => ['simpletest/drupal.simpletest'],
159     'init_message' => t('Processing test @num of @max - %test.', ['%test' => $info['name'], '@num' => '1', '@max' => count($test_list)]),
160   ];
161   batch_set($batch);
162
163   \Drupal::moduleHandler()->invokeAll('test_group_started');
164
165   return $test_id;
166 }
167
168 /**
169  * Executes PHPUnit tests and returns the results of the run.
170  *
171  * @param $test_id
172  *   The current test ID.
173  * @param $unescaped_test_classnames
174  *   An array of test class names, including full namespaces, to be passed as
175  *   a regular expression to PHPUnit's --filter option.
176  * @param int $status
177  *   (optional) The exit status code of the PHPUnit process will be assigned to
178  *   this variable.
179  *
180  * @return array
181  *   The parsed results of PHPUnit's JUnit XML output, in the format of
182  *   {simpletest}'s schema.
183  */
184 function simpletest_run_phpunit_tests($test_id, array $unescaped_test_classnames, &$status = NULL) {
185   $phpunit_file = simpletest_phpunit_xml_filepath($test_id);
186   simpletest_phpunit_run_command($unescaped_test_classnames, $phpunit_file, $status, $output);
187
188   $rows = [];
189   if ($status == TestStatus::PASS) {
190     $rows = simpletest_phpunit_xml_to_rows($test_id, $phpunit_file);
191   }
192   else {
193     $rows[] = [
194       'test_id' => $test_id,
195       'test_class' => implode(",", $unescaped_test_classnames),
196       'status' => TestStatus::label($status),
197       'message' => 'PHPunit Test failed to complete; Error: ' . implode("\n", $output),
198       'message_group' => 'Other',
199       'function' => implode(",", $unescaped_test_classnames),
200       'line' => '0',
201       'file' => $phpunit_file,
202     ];
203   }
204   return $rows;
205 }
206
207 /**
208  * Inserts the parsed PHPUnit results into {simpletest}.
209  *
210  * @param array[] $phpunit_results
211  *   An array of test results returned from simpletest_phpunit_xml_to_rows().
212  */
213 function simpletest_process_phpunit_results($phpunit_results) {
214   // Insert the results of the PHPUnit test run into the database so the results
215   // are displayed along with Simpletest's results.
216   if (!empty($phpunit_results)) {
217     $query = TestDatabase::getConnection()
218       ->insert('simpletest')
219       ->fields(array_keys($phpunit_results[0]));
220     foreach ($phpunit_results as $result) {
221       $query->values($result);
222     }
223     $query->execute();
224   }
225 }
226
227 /**
228  * Maps phpunit results to a data structure for batch messages and run-tests.sh.
229  *
230  * @param array $results
231  *   The output from simpletest_run_phpunit_tests().
232  *
233  * @return array
234  *   The test result summary. A row per test class.
235  */
236 function simpletest_summarize_phpunit_result($results) {
237   $summaries = [];
238   foreach ($results as $result) {
239     if (!isset($summaries[$result['test_class']])) {
240       $summaries[$result['test_class']] = [
241         '#pass' => 0,
242         '#fail' => 0,
243         '#exception' => 0,
244         '#debug' => 0,
245       ];
246     }
247
248     switch ($result['status']) {
249       case 'pass':
250         $summaries[$result['test_class']]['#pass']++;
251         break;
252
253       case 'fail':
254         $summaries[$result['test_class']]['#fail']++;
255         break;
256
257       case 'exception':
258         $summaries[$result['test_class']]['#exception']++;
259         break;
260
261       case 'debug':
262         $summaries[$result['test_class']]['#debug']++;
263         break;
264     }
265   }
266   return $summaries;
267 }
268
269 /**
270  * Returns the path to use for PHPUnit's --log-junit option.
271  *
272  * @param $test_id
273  *   The current test ID.
274  *
275  * @return string
276  *   Path to the PHPUnit XML file to use for the current $test_id.
277  */
278 function simpletest_phpunit_xml_filepath($test_id) {
279   return \Drupal::service('file_system')->realpath('public://simpletest') . '/phpunit-' . $test_id . '.xml';
280 }
281
282 /**
283  * Returns the path to core's phpunit.xml.dist configuration file.
284  *
285  * @return string
286  *   The path to core's phpunit.xml.dist configuration file.
287  */
288 function simpletest_phpunit_configuration_filepath() {
289   return \Drupal::root() . '/core/phpunit.xml.dist';
290 }
291
292 /**
293  * Executes the PHPUnit command.
294  *
295  * @param array $unescaped_test_classnames
296  *   An array of test class names, including full namespaces, to be passed as
297  *   a regular expression to PHPUnit's --filter option.
298  * @param string $phpunit_file
299  *   A filepath to use for PHPUnit's --log-junit option.
300  * @param int $status
301  *   (optional) The exit status code of the PHPUnit process will be assigned to
302  *   this variable.
303  * @param string $output
304  *   (optional) The output by running the phpunit command.
305  *
306  * @return string
307  *   The results as returned by exec().
308  */
309 function simpletest_phpunit_run_command(array $unescaped_test_classnames, $phpunit_file, &$status = NULL, &$output = NULL) {
310   global $base_url;
311   // Setup an environment variable containing the database connection so that
312   // functional tests can connect to the database.
313   putenv('SIMPLETEST_DB=' . Database::getConnectionInfoAsUrl());
314
315   // Setup an environment variable containing the base URL, if it is available.
316   // This allows functional tests to browse the site under test. When running
317   // tests via CLI, core/phpunit.xml.dist or core/scripts/run-tests.sh can set
318   // this variable.
319   if ($base_url) {
320     putenv('SIMPLETEST_BASE_URL=' . $base_url);
321     putenv('BROWSERTEST_OUTPUT_DIRECTORY=' . \Drupal::service('file_system')->realpath('public://simpletest'));
322   }
323   $phpunit_bin = simpletest_phpunit_command();
324
325   $command = [
326     $phpunit_bin,
327     '--log-junit',
328     escapeshellarg($phpunit_file),
329     '--printer',
330     escapeshellarg(SimpletestUiPrinter::class),
331   ];
332
333   // Optimized for running a single test.
334   if (count($unescaped_test_classnames) == 1) {
335     $class = new \ReflectionClass($unescaped_test_classnames[0]);
336     $command[] = escapeshellarg($class->getFileName());
337   }
338   else {
339     // Double escape namespaces so they'll work in a regexp.
340     $escaped_test_classnames = array_map(function($class) {
341       return addslashes($class);
342     }, $unescaped_test_classnames);
343
344     $filter_string = implode("|", $escaped_test_classnames);
345     $command = array_merge($command, [
346       '--filter',
347       escapeshellarg($filter_string),
348     ]);
349   }
350
351   // Need to change directories before running the command so that we can use
352   // relative paths in the configuration file's exclusions.
353   $old_cwd = getcwd();
354   chdir(\Drupal::root() . "/core");
355
356   // exec in a subshell so that the environment is isolated when running tests
357   // via the simpletest UI.
358   $ret = exec(join($command, " "), $output, $status);
359
360   chdir($old_cwd);
361   putenv('SIMPLETEST_DB=');
362   if ($base_url) {
363     putenv('SIMPLETEST_BASE_URL=');
364     putenv('BROWSERTEST_OUTPUT_DIRECTORY=');
365   }
366   return $ret;
367 }
368
369 /**
370  * Returns the command to run PHPUnit.
371  *
372  * @return string
373  *   The command that can be run through exec().
374  */
375 function simpletest_phpunit_command() {
376   // Load the actual autoloader being used and determine its filename using
377   // reflection. We can determine the vendor directory based on that filename.
378   $autoloader = require \Drupal::root() . '/autoload.php';
379   $reflector = new ReflectionClass($autoloader);
380   $vendor_dir = dirname(dirname($reflector->getFileName()));
381
382   // The file in Composer's bin dir is a *nix link, which does not work when
383   // extracted from a tarball and generally not on Windows.
384   $command = $vendor_dir . '/phpunit/phpunit/phpunit';
385   if (substr(PHP_OS, 0, 3) == 'WIN') {
386     // On Windows it is necessary to run the script using the PHP executable.
387     $php_executable_finder = new PhpExecutableFinder();
388     $php = $php_executable_finder->find();
389     $command = $php . ' -f ' . escapeshellarg($command) . ' --';
390   }
391   return $command;
392 }
393
394 /**
395  * Implements callback_batch_operation().
396  */
397 function _simpletest_batch_operation($test_list_init, $test_id, &$context) {
398   simpletest_classloader_register();
399   // Get working values.
400   if (!isset($context['sandbox']['max'])) {
401     // First iteration: initialize working values.
402     $test_list = $test_list_init;
403     $context['sandbox']['max'] = count($test_list);
404     $test_results = ['#pass' => 0, '#fail' => 0, '#exception' => 0, '#debug' => 0];
405   }
406   else {
407     // Nth iteration: get the current values where we last stored them.
408     $test_list = $context['sandbox']['tests'];
409     $test_results = $context['sandbox']['test_results'];
410   }
411   $max = $context['sandbox']['max'];
412
413   // Perform the next test.
414   $test_class = array_shift($test_list);
415   if (is_subclass_of($test_class, \PHPUnit_Framework_TestCase::class)) {
416     $phpunit_results = simpletest_run_phpunit_tests($test_id, [$test_class]);
417     simpletest_process_phpunit_results($phpunit_results);
418     $test_results[$test_class] = simpletest_summarize_phpunit_result($phpunit_results)[$test_class];
419   }
420   else {
421     $test = new $test_class($test_id);
422     $test->run();
423     \Drupal::moduleHandler()->invokeAll('test_finished', [$test->results]);
424     $test_results[$test_class] = $test->results;
425   }
426   $size = count($test_list);
427   $info = TestDiscovery::getTestInfo($test_class);
428
429   // Gather results and compose the report.
430   foreach ($test_results[$test_class] as $key => $value) {
431     $test_results[$key] += $value;
432   }
433   $test_results[$test_class]['#name'] = $info['name'];
434   $items = [];
435   foreach (Element::children($test_results) as $class) {
436     $class_test_result = $test_results[$class] + [
437       '#theme' => 'simpletest_result_summary',
438       '#label' => t($test_results[$class]['#name'] . ':'),
439     ];
440     array_unshift($items, drupal_render($class_test_result));
441   }
442   $context['message'] = t('Processed test @num of @max - %test.', ['%test' => $info['name'], '@num' => $max - $size, '@max' => $max]);
443   $overall_results = $test_results + [
444     '#theme' => 'simpletest_result_summary',
445     '#label' => t('Overall results:'),
446   ];
447   $context['message'] .= drupal_render($overall_results);
448
449   $item_list = [
450     '#theme' => 'item_list',
451     '#items' => $items,
452   ];
453   $context['message'] .= drupal_render($item_list);
454
455   // Save working values for the next iteration.
456   $context['sandbox']['tests'] = $test_list;
457   $context['sandbox']['test_results'] = $test_results;
458   // The test_id is the only thing we need to save for the report page.
459   $context['results']['test_id'] = $test_id;
460
461   // Multistep processing: report progress.
462   $context['finished'] = 1 - $size / $max;
463 }
464
465 /**
466  * Implements callback_batch_finished().
467  */
468 function _simpletest_batch_finished($success, $results, $operations, $elapsed) {
469   if ($success) {
470     drupal_set_message(t('The test run finished in @elapsed.', ['@elapsed' => $elapsed]));
471   }
472   else {
473     // Use the test_id passed as a parameter to _simpletest_batch_operation().
474     $test_id = $operations[0][1][1];
475
476     // Retrieve the last database prefix used for testing and the last test
477     // class that was run from. Use the information to read the lgo file
478     // in case any fatal errors caused the test to crash.
479     list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id);
480     simpletest_log_read($test_id, $last_prefix, $last_test_class);
481
482     drupal_set_message(t('The test run did not successfully finish.'), 'error');
483     drupal_set_message(t('Use the <em>Clean environment</em> button to clean-up temporary files and tables.'), 'warning');
484   }
485   \Drupal::moduleHandler()->invokeAll('test_group_finished');
486 }
487
488 /**
489  * Get information about the last test that ran given a test ID.
490  *
491  * @param $test_id
492  *   The test ID to get the last test from.
493  * @return array
494  *   Array containing the last database prefix used and the last test class
495  *   that ran.
496  */
497 function simpletest_last_test_get($test_id) {
498   $last_prefix = TestDatabase::getConnection()
499     ->queryRange('SELECT last_prefix FROM {simpletest_test_id} WHERE test_id = :test_id', 0, 1, [
500       ':test_id' => $test_id,
501     ])
502     ->fetchField();
503   $last_test_class = TestDatabase::getConnection()
504     ->queryRange('SELECT test_class FROM {simpletest} WHERE test_id = :test_id ORDER BY message_id DESC', 0, 1, [
505       ':test_id' => $test_id,
506     ])
507     ->fetchField();
508   return [$last_prefix, $last_test_class];
509 }
510
511 /**
512  * Reads the error log and reports any errors as assertion failures.
513  *
514  * The errors in the log should only be fatal errors since any other errors
515  * will have been recorded by the error handler.
516  *
517  * @param $test_id
518  *   The test ID to which the log relates.
519  * @param $database_prefix
520  *   The database prefix to which the log relates.
521  * @param $test_class
522  *   The test class to which the log relates.
523  *
524  * @return bool
525  *   Whether any fatal errors were found.
526  */
527 function simpletest_log_read($test_id, $database_prefix, $test_class) {
528   $test_db = new TestDatabase($database_prefix);
529   $log = DRUPAL_ROOT . '/' . $test_db->getTestSitePath() . '/error.log';
530   $found = FALSE;
531   if (file_exists($log)) {
532     foreach (file($log) as $line) {
533       if (preg_match('/\[.*?\] (.*?): (.*?) in (.*) on line (\d+)/', $line, $match)) {
534         // Parse PHP fatal errors for example: PHP Fatal error: Call to
535         // undefined function break_me() in /path/to/file.php on line 17
536         $caller = [
537           'line' => $match[4],
538           'file' => $match[3],
539         ];
540         TestBase::insertAssert($test_id, $test_class, FALSE, $match[2], $match[1], $caller);
541       }
542       else {
543         // Unknown format, place the entire message in the log.
544         TestBase::insertAssert($test_id, $test_class, FALSE, $line, 'Fatal error');
545       }
546       $found = TRUE;
547     }
548   }
549   return $found;
550 }
551
552 /**
553  * Gets a list of all of the tests provided by the system.
554  *
555  * The list of test classes is loaded by searching the designated directory for
556  * each module for files matching the PSR-0 standard. Once loaded the test list
557  * is cached and stored in a static variable.
558  *
559  * @param string $extension
560  *   (optional) The name of an extension to limit discovery to; e.g., 'node'.
561  * @param string[] $types
562  *   An array of included test types.
563  *
564  * @return array[]
565  *   An array of tests keyed with the groups, and then keyed by test classes.
566  *   For example:
567  *   @code
568  *     $groups['Block'] => array(
569  *       'BlockTestCase' => array(
570  *         'name' => 'Block functionality',
571  *         'description' => 'Add, edit and delete custom block.',
572  *         'group' => 'Block',
573  *       ),
574  *     );
575  *   @endcode
576  *
577  * @deprecated in Drupal 8.3.x, for removal before 9.0.0 release. Use
578  *   \Drupal::service('test_discovery')->getTestClasses($extension, $types)
579  *   instead.
580  */
581 function simpletest_test_get_all($extension = NULL, array $types = []) {
582   return \Drupal::service('test_discovery')->getTestClasses($extension, $types);
583 }
584
585 /**
586  * Registers test namespaces of all extensions and core test classes.
587  *
588  * @deprecated in Drupal 8.3.x for removal before 9.0.0 release. Use
589  *   \Drupal::service('test_discovery')->registerTestNamespaces() instead.
590  */
591 function simpletest_classloader_register() {
592   \Drupal::service('test_discovery')->registerTestNamespaces();
593 }
594
595 /**
596  * Generates a test file.
597  *
598  * @param string $filename
599  *   The name of the file, including the path. The suffix '.txt' is appended to
600  *   the supplied file name and the file is put into the public:// files
601  *   directory.
602  * @param int $width
603  *   The number of characters on one line.
604  * @param int $lines
605  *   The number of lines in the file.
606  * @param string $type
607  *   (optional) The type, one of:
608  *   - text: The generated file contains random ASCII characters.
609  *   - binary: The generated file contains random characters whose codes are in
610  *     the range of 0 to 31.
611  *   - binary-text: The generated file contains random sequence of '0' and '1'
612  *     values.
613  *
614  * @return string
615  *   The name of the file, including the path.
616  */
617 function simpletest_generate_file($filename, $width, $lines, $type = 'binary-text') {
618   $text = '';
619   for ($i = 0; $i < $lines; $i++) {
620     // Generate $width - 1 characters to leave space for the "\n" character.
621     for ($j = 0; $j < $width - 1; $j++) {
622       switch ($type) {
623         case 'text':
624           $text .= chr(rand(32, 126));
625           break;
626         case 'binary':
627           $text .= chr(rand(0, 31));
628           break;
629         case 'binary-text':
630         default:
631           $text .= rand(0, 1);
632           break;
633       }
634     }
635     $text .= "\n";
636   }
637
638   // Create filename.
639   file_put_contents('public://' . $filename . '.txt', $text);
640   return $filename;
641 }
642
643 /**
644  * Removes all temporary database tables and directories.
645  */
646 function simpletest_clean_environment() {
647   simpletest_clean_database();
648   simpletest_clean_temporary_directories();
649   if (\Drupal::config('simpletest.settings')->get('clear_results')) {
650     $count = simpletest_clean_results_table();
651     drupal_set_message(\Drupal::translation()->formatPlural($count, 'Removed 1 test result.', 'Removed @count test results.'));
652   }
653   else {
654     drupal_set_message(t('Clear results is disabled and the test results table will not be cleared.'), 'warning');
655   }
656
657   // Detect test classes that have been added, renamed or deleted.
658   \Drupal::cache()->delete('simpletest');
659   \Drupal::cache()->delete('simpletest_phpunit');
660 }
661
662 /**
663  * Removes prefixed tables from the database from crashed tests.
664  */
665 function simpletest_clean_database() {
666   $tables = db_find_tables('test%');
667   $count = 0;
668   foreach ($tables as $table) {
669     // Only drop tables which begin wih 'test' followed by digits, for example,
670     // {test12345678node__body}.
671     if (preg_match('/^test\d+.*/', $table, $matches)) {
672       db_drop_table($matches[0]);
673       $count++;
674     }
675   }
676
677   if ($count > 0) {
678     drupal_set_message(\Drupal::translation()->formatPlural($count, 'Removed 1 leftover table.', 'Removed @count leftover tables.'));
679   }
680   else {
681     drupal_set_message(t('No leftover tables to remove.'));
682   }
683 }
684
685 /**
686  * Finds all leftover temporary directories and removes them.
687  */
688 function simpletest_clean_temporary_directories() {
689   $count = 0;
690   if (is_dir(DRUPAL_ROOT . '/sites/simpletest')) {
691     $files = scandir(DRUPAL_ROOT . '/sites/simpletest');
692     foreach ($files as $file) {
693       if ($file[0] != '.') {
694         $path = DRUPAL_ROOT . '/sites/simpletest/' . $file;
695         file_unmanaged_delete_recursive($path, function ($any_path) {
696           @chmod($any_path, 0700);
697         });
698         $count++;
699       }
700     }
701   }
702
703   if ($count > 0) {
704     drupal_set_message(\Drupal::translation()->formatPlural($count, 'Removed 1 temporary directory.', 'Removed @count temporary directories.'));
705   }
706   else {
707     drupal_set_message(t('No temporary directories to remove.'));
708   }
709 }
710
711 /**
712  * Clears the test result tables.
713  *
714  * @param $test_id
715  *   Test ID to remove results for, or NULL to remove all results.
716  *
717  * @return int
718  *   The number of results that were removed.
719  */
720 function simpletest_clean_results_table($test_id = NULL) {
721   if (\Drupal::config('simpletest.settings')->get('clear_results')) {
722     $connection = TestDatabase::getConnection();
723     if ($test_id) {
724       $count = $connection->query('SELECT COUNT(test_id) FROM {simpletest_test_id} WHERE test_id = :test_id', [':test_id' => $test_id])->fetchField();
725
726       $connection->delete('simpletest')
727         ->condition('test_id', $test_id)
728         ->execute();
729       $connection->delete('simpletest_test_id')
730         ->condition('test_id', $test_id)
731         ->execute();
732     }
733     else {
734       $count = $connection->query('SELECT COUNT(test_id) FROM {simpletest_test_id}')->fetchField();
735
736       // Clear test results.
737       $connection->delete('simpletest')->execute();
738       $connection->delete('simpletest_test_id')->execute();
739     }
740
741     return $count;
742   }
743   return 0;
744 }
745
746 /**
747  * Implements hook_mail_alter().
748  *
749  * Aborts sending of messages with ID 'simpletest_cancel_test'.
750  *
751  * @see MailTestCase::testCancelMessage()
752  */
753 function simpletest_mail_alter(&$message) {
754   if ($message['id'] == 'simpletest_cancel_test') {
755     $message['send'] = FALSE;
756   }
757 }
758
759 /**
760  * Converts PHPUnit's JUnit XML output to an array.
761  *
762  * @param $test_id
763  *   The current test ID.
764  * @param $phpunit_xml_file
765  *   Path to the PHPUnit XML file.
766  *
767  * @return array[]
768  *   The results as array of rows in a format that can be inserted into
769  *   {simpletest}.
770  */
771 function simpletest_phpunit_xml_to_rows($test_id, $phpunit_xml_file) {
772   $contents = @file_get_contents($phpunit_xml_file);
773   if (!$contents) {
774     return;
775   }
776   $records = [];
777   $testcases = simpletest_phpunit_find_testcases(new SimpleXMLElement($contents));
778   foreach ($testcases as $testcase) {
779     $records[] = simpletest_phpunit_testcase_to_row($test_id, $testcase);
780   }
781   return $records;
782 }
783
784 /**
785  * Finds all test cases recursively from a test suite list.
786  *
787  * @param \SimpleXMLElement $element
788  *   The PHPUnit xml to search for test cases.
789  * @param \SimpleXMLElement $suite
790  *   (Optional) The parent of the current element. Defaults to NULL.
791  *
792  * @return array
793  *   A list of all test cases.
794  */
795 function simpletest_phpunit_find_testcases(\SimpleXMLElement $element, \SimpleXMLElement $parent = NULL) {
796   $testcases = [];
797
798   if (!isset($parent)) {
799     $parent = $element;
800   }
801
802   if ($element->getName() === 'testcase' && (int) $parent->attributes()->tests > 0) {
803     // Add the class attribute if the testcase does not have one. This is the
804     // case for tests using a data provider. The name of the parent testsuite
805     // will be in the format class::method.
806     if (!$element->attributes()->class) {
807       $name = explode('::', $parent->attributes()->name, 2);
808       $element->addAttribute('class', $name[0]);
809     }
810     $testcases[] = $element;
811   }
812   else {
813     foreach ($element as $child) {
814       $file = (string) $parent->attributes()->file;
815       if ($file && !$child->attributes()->file) {
816         $child->addAttribute('file', $file);
817       }
818       $testcases = array_merge($testcases, simpletest_phpunit_find_testcases($child, $element));
819     }
820   }
821   return $testcases;
822 }
823
824 /**
825  * Converts a PHPUnit test case result to a {simpletest} result row.
826  *
827  * @param int $test_id
828  *   The current test ID.
829  * @param \SimpleXMLElement $testcase
830  *   The PHPUnit test case represented as XML element.
831  *
832  * @return array
833  *   An array containing the {simpletest} result row.
834  */
835 function simpletest_phpunit_testcase_to_row($test_id, \SimpleXMLElement $testcase) {
836   $message = '';
837   $pass = TRUE;
838   if ($testcase->failure) {
839     $lines = explode("\n", $testcase->failure);
840     $message = $lines[2];
841     $pass = FALSE;
842   }
843   if ($testcase->error) {
844     $message = $testcase->error;
845     $pass = FALSE;
846   }
847
848   $attributes = $testcase->attributes();
849
850   $record = [
851     'test_id' => $test_id,
852     'test_class' => (string) $attributes->class,
853     'status' => $pass ? 'pass' : 'fail',
854     'message' => $message,
855     // @todo: Check on the proper values for this.
856     'message_group' => 'Other',
857     'function' => $attributes->class . '->' . $attributes->name . '()',
858     'line' => $attributes->line ?: 0,
859     'file' => $attributes->file,
860   ];
861   return $record;
862 }