simpletest.module 28.5 KB
Newer Older
1 2
<?php

3 4 5 6 7
/**
 * @file
 * Provides testing functionality.
 */

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 Symfony\Component\Process\PhpExecutableFinder;
16
use Drupal\Core\Test\TestStatus;
17

18
/**
19
 * Implements hook_help().
20
 */
21
function simpletest_help($route_name, RouteMatchInterface $route_match) {
22 23
  switch ($route_name) {
    case 'help.page.simpletest':
24 25
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
26
      $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>.', array(':simpletest' => 'https://www.drupal.org/documentation/modules/simpletest')) . '</p>';
27 28 29
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Running tests') . '</dt>';
30
      $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.', array(':admin-simpletest' => \Drupal::url('simpletest.test_form'))) . '</p>';
31
      $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>';
32
      $output .= '</dl>';
33
      return $output;
34

35
    case 'simpletest.test_form':
36 37
      $output = t('Select the test(s) or test group(s) you would like to run, and click <em>Run tests</em>.');
      return $output;
38 39 40 41
  }
}

/**
42
 * Implements hook_theme().
43 44 45
 */
function simpletest_theme() {
  return array(
46
    'simpletest_result_summary' => array(
47
      'variables' => array('label' => NULL, 'items' => array(), 'pass' => 0, 'fail' => 0, 'exception' => 0, 'debug' => 0),
48 49 50 51
    ),
  );
}

52
/**
53
 * Implements hook_js_alter().
54
 */
55
function simpletest_js_alter(&$javascript, AttachedAssetsInterface $assets) {
56 57 58
  // Since SimpleTest is a special use case for the table select, stick the
  // SimpleTest JavaScript above the table select.
  $simpletest = drupal_get_path('module', 'simpletest') . '/simpletest.js';
59 60
  if (array_key_exists($simpletest, $javascript) && array_key_exists('core/misc/tableselect.js', $javascript)) {
    $javascript[$simpletest]['weight'] = $javascript['core/misc/tableselect.js']['weight'] - 1;
61 62 63
  }
}

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
/**
 * Prepares variables for simpletest result summary templates.
 *
 * Default template: simpletest-result-summary.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - label: An optional label to be rendered before the results.
 *   - ok: The overall group result pass or fail.
 *   - pass: The number of passes.
 *   - fail: The number of fails.
 *   - exception: The number of exceptions.
 *   - debug: The number of debug messages.
 */
function template_preprocess_simpletest_result_summary(&$variables) {
  $variables['items'] = _simpletest_build_summary_line($variables);
}

/**
 * Formats each test result type pluralized summary.
 *
 * @param array $summary
 *   A summary of the test results.
 *
 * @return array
 *   The pluralized test summary items.
 */
function _simpletest_build_summary_line($summary) {
  $translation = \Drupal::translation();
  $items['pass'] = $translation->formatPlural($summary['pass'], '1 pass', '@count passes');
  $items['fail'] = $translation->formatPlural($summary['fail'], '1 fail', '@count fails');
  $items['exception'] = $translation->formatPlural($summary['exception'], '1 exception', '@count exceptions');
  if ($summary['debug']) {
    $items['debug'] = $translation->formatPlural($summary['debug'], '1 debug message', '@count debug messages');
98
  }
99 100 101 102 103 104 105 106 107 108 109 110 111 112
  return $items;
}

/**
 * Formats test result summaries into a comma separated string for run-tests.sh.
 *
 * @param array $summary
 *   A summary of the test results.
 *
 * @return string
 *   A concatenated string of the formatted test results.
 */
function _simpletest_format_summary_line($summary) {
  $parts = _simpletest_build_summary_line($summary);
113
  return implode(', ', $parts);
114 115
}

116
/**
117
 * Runs tests.
118
 *
119 120
 * @param $test_list
 *   List of tests to run.
121 122 123
 *
 * @return string
 *   The test ID.
124
 */
125
function simpletest_run_tests($test_list) {
126 127 128 129 130 131 132
  // We used to separate PHPUnit and Simpletest tests for a performance
  // optimization. In order to support backwards compatibility check if these
  // keys are set and create a single test list.
  // @todo https://www.drupal.org/node/2748967 Remove BC support in Drupal 9.
  if (isset($test_list['simpletest'])) {
    $test_list = array_merge($test_list, $test_list['simpletest']);
    unset($test_list['simpletest']);
133
  }
134 135 136
  if (isset($test_list['phpunit'])) {
    $test_list = array_merge($test_list, $test_list['phpunit']);
    unset($test_list['phpunit']);
137 138
  }

139 140 141
  $test_id = db_insert('simpletest_test_id')
    ->useDefaults(array('test_id'))
    ->execute();
142

143
  // Clear out the previous verbose files.
144
  file_unmanaged_delete_recursive('public://simpletest/verbose');
145

146
  // Get the info for the first test being run.
147
  $first_test = reset($test_list);
148
  $info = TestDiscovery::getTestInfo($first_test);
149

150
  $batch = array(
151
    'title' => t('Running tests'),
152 153
    'operations' => array(
      array('_simpletest_batch_operation', array($test_list, $test_id)),
154
    ),
155
    'finished' => '_simpletest_batch_finished',
156
    'progress_message' => '',
157
    'library' => array('simpletest/drupal.simpletest'),
158
    'init_message' => t('Processing test @num of @max - %test.', array('%test' => $info['name'], '@num' => '1', '@max' => count($test_list))),
159 160
  );
  batch_set($batch);
161

162
  \Drupal::moduleHandler()->invokeAll('test_group_started');
163

164
  return $test_id;
165 166
}

167
/**
168
 * Executes PHPUnit tests and returns the results of the run.
169 170 171 172 173
 *
 * @param $test_id
 *   The current test ID.
 * @param $unescaped_test_classnames
 *   An array of test class names, including full namespaces, to be passed as
174
 *   a regular expression to PHPUnit's --filter option.
175 176 177
 * @param int $status
 *   (optional) The exit status code of the PHPUnit process will be assigned to
 *   this variable.
178 179
 *
 * @return array
180 181
 *   The parsed results of PHPUnit's JUnit XML output, in the format of
 *   {simpletest}'s schema.
182
 */
183
function simpletest_run_phpunit_tests($test_id, array $unescaped_test_classnames, &$status = NULL) {
184
  $phpunit_file = simpletest_phpunit_xml_filepath($test_id);
185
  simpletest_phpunit_run_command($unescaped_test_classnames, $phpunit_file, $status, $output);
186

187 188 189
  $rows = [];
  if ($status == TestStatus::PASS) {
    $rows = simpletest_phpunit_xml_to_rows($test_id, $phpunit_file);
190
  }
191
  else {
192 193 194
    $rows[] = [
      'test_id' => $test_id,
      'test_class' => implode(",", $unescaped_test_classnames),
195
      'status' => TestStatus::label($status),
196 197 198 199 200 201 202
      'message' => 'PHPunit Test failed to complete; Error: ' . implode("\n", $output),
      'message_group' => 'Other',
      'function' => implode(",", $unescaped_test_classnames),
      'line' => '0',
      'file' => $phpunit_file,
    ];
  }
203
  return $rows;
204 205 206
}

/**
207
 * Inserts the parsed PHPUnit results into {simpletest}.
208
 *
209 210
 * @param array[] $phpunit_results
 *   An array of test results returned from simpletest_phpunit_xml_to_rows().
211 212
 */
function simpletest_process_phpunit_results($phpunit_results) {
213 214
  // Insert the results of the PHPUnit test run into the database so the results
  // are displayed along with Simpletest's results.
215
  if (!empty($phpunit_results)) {
216
    $query = TestDatabase::getConnection()
217 218
      ->insert('simpletest')
      ->fields(array_keys($phpunit_results[0]));
219 220 221 222 223 224 225
    foreach ($phpunit_results as $result) {
      $query->values($result);
    }
    $query->execute();
  }
}

226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
/**
 * Maps phpunit results to a data structure for batch messages and run-tests.sh.
 *
 * @param array $results
 *   The output from simpletest_run_phpunit_tests().
 *
 * @return array
 *   The test result summary. A row per test class.
 */
function simpletest_summarize_phpunit_result($results) {
  $summaries = [];
  foreach ($results as $result) {
    if (!isset($summaries[$result['test_class']])) {
      $summaries[$result['test_class']] = array(
        '#pass' => 0,
        '#fail' => 0,
        '#exception' => 0,
        '#debug' => 0,
      );
    }

    switch ($result['status']) {
      case 'pass':
        $summaries[$result['test_class']]['#pass']++;
        break;

      case 'fail':
        $summaries[$result['test_class']]['#fail']++;
        break;

      case 'exception':
        $summaries[$result['test_class']]['#exception']++;
        break;

      case 'debug':
        $summaries[$result['test_class']]['#debug']++;
        break;
    }
  }
  return $summaries;
}

268
/**
269
 * Returns the path to use for PHPUnit's --log-junit option.
270 271 272
 *
 * @param $test_id
 *   The current test ID.
273
 *
274
 * @return string
275
 *   Path to the PHPUnit XML file to use for the current $test_id.
276 277
 */
function simpletest_phpunit_xml_filepath($test_id) {
278
  return \Drupal::service('file_system')->realpath('public://simpletest') . '/phpunit-' . $test_id . '.xml';
279 280 281 282 283 284
}

/**
 * Returns the path to core's phpunit.xml.dist configuration file.
 *
 * @return string
285
 *   The path to core's phpunit.xml.dist configuration file.
286 287
 */
function simpletest_phpunit_configuration_filepath() {
288
  return \Drupal::root() . '/core/phpunit.xml.dist';
289 290 291
}

/**
292
 * Executes the PHPUnit command.
293 294 295
 *
 * @param array $unescaped_test_classnames
 *   An array of test class names, including full namespaces, to be passed as
296
 *   a regular expression to PHPUnit's --filter option.
297
 * @param string $phpunit_file
298
 *   A filepath to use for PHPUnit's --log-junit option.
299 300 301
 * @param int $status
 *   (optional) The exit status code of the PHPUnit process will be assigned to
 *   this variable.
302 303
 * @param string $output
 *   (optional) The output by running the phpunit command.
304 305
 *
 * @return string
306
 *   The results as returned by exec().
307
 */
308
function simpletest_phpunit_run_command(array $unescaped_test_classnames, $phpunit_file, &$status = NULL, &$output = NULL) {
309
  global $base_url;
310 311 312
  // Setup an environment variable containing the database connection so that
  // functional tests can connect to the database.
  putenv('SIMPLETEST_DB=' . Database::getConnectionInfoAsUrl());
313 314 315 316 317 318 319

  // Setup an environment variable containing the base URL, if it is available.
  // This allows functional tests to browse the site under test. When running
  // tests via CLI, core/phpunit.xml.dist or core/scripts/run-tests.sh can set
  // this variable.
  if ($base_url) {
    putenv('SIMPLETEST_BASE_URL=' . $base_url);
320
    putenv('BROWSERTEST_OUTPUT_DIRECTORY=' . \Drupal::service('file_system')->realpath('public://simpletest'));
321
  }
322
  $phpunit_bin = simpletest_phpunit_command();
323 324 325 326 327

  $command = array(
    $phpunit_bin,
    '--log-junit',
    escapeshellarg($phpunit_file),
328 329
    '--printer',
    '\\\Drupal\\\Tests\\\Listeners\\\SimpletestUiPrinter',
330 331
  );

332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
  // Optimized for running a single test.
  if (count($unescaped_test_classnames) == 1) {
    $class = new \ReflectionClass($unescaped_test_classnames[0]);
    $command[] = escapeshellarg($class->getFileName());
  }
  else {
    // Double escape namespaces so they'll work in a regexp.
    $escaped_test_classnames = array_map(function($class) {
      return addslashes($class);
    }, $unescaped_test_classnames);

    $filter_string = implode("|", $escaped_test_classnames);
    $command = array_merge($command, array(
      '--filter',
      escapeshellarg($filter_string),
    ));
  }

350 351 352
  // Need to change directories before running the command so that we can use
  // relative paths in the configuration file's exclusions.
  $old_cwd = getcwd();
353
  chdir(\Drupal::root() . "/core");
354 355 356

  // exec in a subshell so that the environment is isolated when running tests
  // via the simpletest UI.
357 358
  $ret = exec(join($command, " "), $output, $status);

359
  chdir($old_cwd);
360
  putenv('SIMPLETEST_DB=');
361 362
  if ($base_url) {
    putenv('SIMPLETEST_BASE_URL=');
363
    putenv('BROWSERTEST_OUTPUT_DIRECTORY=');
364
  }
365 366 367
  return $ret;
}

368 369
/**
 * Returns the command to run PHPUnit.
370 371 372
 *
 * @return string
 *   The command that can be run through exec().
373 374
 */
function simpletest_phpunit_command() {
375 376 377 378 379 380
  // Load the actual autoloader being used and determine its filename using
  // reflection. We can determine the vendor directory based on that filename.
  $autoloader = require \Drupal::root() . '/autoload.php';
  $reflector = new ReflectionClass($autoloader);
  $vendor_dir = dirname(dirname($reflector->getFileName()));

381 382 383 384 385
  // Don't use the committed version in composer's bin dir if running on
  // windows.
  if (substr(PHP_OS, 0, 3) == 'WIN') {
    $php_executable_finder = new PhpExecutableFinder();
    $php = $php_executable_finder->find();
386
    $phpunit_bin = escapeshellarg($php) . ' -f ' . escapeshellarg($vendor_dir . '/phpunit/phpunit/composer/bin/phpunit') . ' --';
387 388
  }
  else {
389
    $phpunit_bin = $vendor_dir . '/phpunit/phpunit/phpunit';
390 391 392 393
  }
  return $phpunit_bin;
}

394
/**
395
 * Implements callback_batch_operation().
396 397
 */
function _simpletest_batch_operation($test_list_init, $test_id, &$context) {
398
  simpletest_classloader_register();
399 400 401 402 403
  // Get working values.
  if (!isset($context['sandbox']['max'])) {
    // First iteration: initialize working values.
    $test_list = $test_list_init;
    $context['sandbox']['max'] = count($test_list);
404
    $test_results = array('#pass' => 0, '#fail' => 0, '#exception' => 0, '#debug' => 0);
405 406 407 408 409 410 411 412 413 414
  }
  else {
    // Nth iteration: get the current values where we last stored them.
    $test_list = $context['sandbox']['tests'];
    $test_results = $context['sandbox']['test_results'];
  }
  $max = $context['sandbox']['max'];

  // Perform the next test.
  $test_class = array_shift($test_list);
415 416 417 418 419 420 421 422 423 424 425
  if (is_subclass_of($test_class, \PHPUnit_Framework_TestCase::class)) {
    $phpunit_results = simpletest_run_phpunit_tests($test_id, [$test_class]);
    simpletest_process_phpunit_results($phpunit_results);
    $test_results[$test_class] = simpletest_summarize_phpunit_result($phpunit_results)[$test_class];
  }
  else {
    $test = new $test_class($test_id);
    $test->run();
    \Drupal::moduleHandler()->invokeAll('test_finished', array($test->results));
    $test_results[$test_class] = $test->results;
  }
426
  $size = count($test_list);
427
  $info = TestDiscovery::getTestInfo($test_class);
428 429 430 431 432 433 434

  // Gather results and compose the report.
  foreach ($test_results[$test_class] as $key => $value) {
    $test_results[$key] += $value;
  }
  $test_results[$test_class]['#name'] = $info['name'];
  $items = array();
435
  foreach (Element::children($test_results) as $class) {
436 437 438 439 440
    $class_test_result = $test_results[$class] + array(
      '#theme' => 'simpletest_result_summary',
      '#label' => t($test_results[$class]['#name'] . ':'),
    );
    array_unshift($items, drupal_render($class_test_result));
441
  }
442
  $context['message'] = t('Processed test @num of @max - %test.', array('%test' => $info['name'], '@num' => $max - $size, '@max' => $max));
443 444 445 446 447 448
  $overall_results = $test_results + array(
    '#theme' => 'simpletest_result_summary',
    '#label' => t('Overall results:'),
  );
  $context['message'] .= drupal_render($overall_results);

449 450 451 452 453
  $item_list = array(
    '#theme' => 'item_list',
    '#items' => $items,
  );
  $context['message'] .= drupal_render($item_list);
454 455 456 457 458 459 460 461 462 463 464

  // Save working values for the next iteration.
  $context['sandbox']['tests'] = $test_list;
  $context['sandbox']['test_results'] = $test_results;
  // The test_id is the only thing we need to save for the report page.
  $context['results']['test_id'] = $test_id;

  // Multistep processing: report progress.
  $context['finished'] = 1 - $size / $max;
}

465 466 467
/**
 * Implements callback_batch_finished().
 */
468
function _simpletest_batch_finished($success, $results, $operations, $elapsed) {
469
  if ($success) {
470
    drupal_set_message(t('The test run finished in @elapsed.', array('@elapsed' => $elapsed)));
471 472
  }
  else {
473
    // Use the test_id passed as a parameter to _simpletest_batch_operation().
474 475 476 477 478 479 480 481
    $test_id = $operations[0][1][1];

    // Retrieve the last database prefix used for testing and the last test
    // class that was run from. Use the information to read the lgo file
    // in case any fatal errors caused the test to crash.
    list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id);
    simpletest_log_read($test_id, $last_prefix, $last_test_class);

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 490 491 492
 * Get information about the last test that ran given a test ID.
 *
 * @param $test_id
 *   The test ID to get the last test from.
493
 * @return array
494 495 496 497
 *   Array containing the last database prefix used and the last test class
 *   that ran.
 */
function simpletest_last_test_get($test_id) {
498
  $last_prefix = TestDatabase::getConnection()
499 500 501 502
    ->queryRange('SELECT last_prefix FROM {simpletest_test_id} WHERE test_id = :test_id', 0, 1, array(
      ':test_id' => $test_id,
    ))
    ->fetchField();
503
  $last_test_class = TestDatabase::getConnection()
504 505 506 507
    ->queryRange('SELECT test_class FROM {simpletest} WHERE test_id = :test_id ORDER BY message_id DESC', 0, 1, array(
      ':test_id' => $test_id,
    ))
    ->fetchField();
508 509 510
  return array($last_prefix, $last_test_class);
}

511
/**
512
 * Reads the error log and reports any errors as assertion failures.
513 514 515 516 517
 *
 * The errors in the log should only be fatal errors since any other errors
 * will have been recorded by the error handler.
 *
 * @param $test_id
518
 *   The test ID to which the log relates.
519
 * @param $database_prefix
520 521 522
 *   The database prefix to which the log relates.
 * @param $test_class
 *   The test class to which the log relates.
523
 *
524 525
 * @return bool
 *   Whether any fatal errors were found.
526
 */
527
function simpletest_log_read($test_id, $database_prefix, $test_class) {
528 529
  $test_db = new TestDatabase($database_prefix);
  $log = DRUPAL_ROOT . '/' . $test_db->getTestSitePath() . '/error.log';
530
  $found = FALSE;
531 532
  if (file_exists($log)) {
    foreach (file($log) as $line) {
533 534 535
      if (preg_match('/\[.*?\] (.*?): (.*?) in (.*) on line (\d+)/', $line, $match)) {
        // Parse PHP fatal errors for example: PHP Fatal error: Call to
        // undefined function break_me() in /path/to/file.php on line 17
536
        $caller = array(
537 538
          'line' => $match[4],
          '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 556 557
 * The list of test classes is loaded by searching the designated directory for
 * each module for files matching the PSR-0 standard. Once loaded the test list
 * is cached and stored in a static variable.
558
 *
559 560 561 562
 * @param string $extension
 *   (optional) The name of an extension to limit discovery to; e.g., 'node'.
 * @param string[] $types
 *   An array of included test types.
563
 *
564
 * @return array[]
565 566
 *   An array of tests keyed with the groups, and then keyed by test classes.
 *   For example:
567
 *   @code
568 569 570
 *     $groups['Block'] => array(
 *       'BlockTestCase' => array(
 *         'name' => 'Block functionality',
571
 *         'description' => 'Add, edit and delete custom block.',
572
 *         'group' => 'Block',
573 574 575
 *       ),
 *     );
 *   @endcode
576 577 578 579
 *
 * @deprecated in Drupal 8.3.x, for removal before 9.0.0 release. Use
 *   \Drupal::service('test_discovery')->getTestClasses($extension, $types)
 *   instead.
580
 */
581 582
function simpletest_test_get_all($extension = NULL, array $types = []) {
  return \Drupal::service('test_discovery')->getTestClasses($extension, $types);
583
}
584

585
/**
586 587 588 589
 * Registers test namespaces of all extensions and core test classes.
 *
 * @deprecated in Drupal 8.3.x for removal before 9.0.0 release. Use
 *   \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 600 601
 *   The name of the file, including the path. The suffix '.txt' is appended to
 *   the supplied file name and the file is put into the public:// files
 *   directory.
602 603 604 605 606
 * @param int $width
 *   The number of characters on one line.
 * @param int $lines
 *   The number of lines in the file.
 * @param string $type
607 608 609 610 611 612
 *   (optional) The type, one of:
 *   - text: The generated file contains random ASCII characters.
 *   - binary: The generated file contains random characters whose codes are in
 *     the range of 0 to 31.
 *   - binary-text: The generated file contains random sequence of '0' and '1'
 *     values.
613 614 615
 *
 * @return string
 *   The name of the file, including the path.
616 617 618
 */
function simpletest_generate_file($filename, $width, $lines, $type = 'binary-text') {
  $text = '';
619 620 621 622 623 624 625 626 627 628 629 630 631 632
  for ($i = 0; $i < $lines; $i++) {
    // Generate $width - 1 characters to leave space for the "\n" character.
    for ($j = 0; $j < $width - 1; $j++) {
      switch ($type) {
        case 'text':
          $text .= chr(rand(32, 126));
          break;
        case 'binary':
          $text .= chr(rand(0, 31));
          break;
        case 'binary-text':
        default:
          $text .= rand(0, 1);
          break;
633
      }
634
    }
635 636
    $text .= "\n";
  }
637 638

  // Create filename.
639
  file_put_contents('public://' . $filename . '.txt', $text);
640 641 642
  return $filename;
}

643
/**
644
 * Removes all temporary database tables and directories.
645 646 647 648
 */
function simpletest_clean_environment() {
  simpletest_clean_database();
  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 654 655
  }
  else {
    drupal_set_message(t('Clear results is disabled and the test results table will not be cleared.'), 'warning');
  }
656 657

  // Detect test classes that have been added, renamed or deleted.
658 659
  \Drupal::cache()->delete('simpletest');
  \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 669 670 671
  foreach ($tables as $table) {
    // Only drop tables which begin wih 'test' followed by digits, for example,
    // {test12345678node__body}.
    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 689
 */
function simpletest_clean_temporary_directories() {
  $count = 0;
690 691
  if (is_dir(DRUPAL_ROOT . '/sites/simpletest')) {
    $files = scandir(DRUPAL_ROOT . '/sites/simpletest');
692
    foreach ($files as $file) {
693 694
      if ($file[0] != '.') {
        $path = DRUPAL_ROOT . '/sites/simpletest/' . $file;
695
        file_unmanaged_delete_recursive($path, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback'));
696 697
        $count++;
      }
698 699 700 701
    }
  }

  if ($count > 0) {
702
    drupal_set_message(\Drupal::translation()->formatPlural($count, 'Removed 1 temporary directory.', 'Removed @count temporary directories.'));
703 704 705 706 707 708
  }
  else {
    drupal_set_message(t('No temporary directories to remove.'));
  }
}

709
/**
710
 * Clears the test result tables.
711 712 713
 *
 * @param $test_id
 *   Test ID to remove results for, or NULL to remove all results.
714 715 716
 *
 * @return int
 *   The number of results that were removed.
717
 */
718
function simpletest_clean_results_table($test_id = NULL) {
719
  if (\Drupal::config('simpletest.settings')->get('clear_results')) {
720
    $connection = TestDatabase::getConnection();
721
    if ($test_id) {
722
      $count = $connection->query('SELECT COUNT(test_id) FROM {simpletest_test_id} WHERE test_id = :test_id', array(':test_id' => $test_id))->fetchField();
723

724
      $connection->delete('simpletest')
725 726
        ->condition('test_id', $test_id)
        ->execute();
727
      $connection->delete('simpletest_test_id')
728 729 730 731
        ->condition('test_id', $test_id)
        ->execute();
    }
    else {
732
      $count = $connection->query('SELECT COUNT(test_id) FROM {simpletest_test_id}')->fetchField();
733

734
      // Clear test results.
735 736
      $connection->delete('simpletest')->execute();
      $connection->delete('simpletest_test_id')->execute();
737
    }
738

739
    return $count;
740
  }
741
  return 0;
742
}
743 744 745 746 747 748 749 750 751 752 753 754

/**
 * Implements hook_mail_alter().
 *
 * Aborts sending of messages with ID 'simpletest_cancel_test'.
 *
 * @see MailTestCase::testCancelMessage()
 */
function simpletest_mail_alter(&$message) {
  if ($message['id'] == 'simpletest_cancel_test') {
    $message['send'] = FALSE;
  }
755 756 757
}

/**
758
 * Converts PHPUnit's JUnit XML output to an array.
759 760 761 762
 *
 * @param $test_id
 *   The current test ID.
 * @param $phpunit_xml_file
763 764 765 766 767
 *   Path to the PHPUnit XML file.
 *
 * @return array[]
 *   The results as array of rows in a format that can be inserted into
 *   {simpletest}.
768 769
 */
function simpletest_phpunit_xml_to_rows($test_id, $phpunit_xml_file) {
770 771 772 773
  $contents = @file_get_contents($phpunit_xml_file);
  if (!$contents) {
    return;
  }
774
  $records = array();
775 776 777
  $testcases = simpletest_phpunit_find_testcases(new SimpleXMLElement($contents));
  foreach ($testcases as $testcase) {
    $records[] = simpletest_phpunit_testcase_to_row($test_id, $testcase);
778 779 780
  }
  return $records;
}
781 782

/**
783
 * Finds all test cases recursively from a test suite list.
784
 *
785 786 787 788
 * @param \SimpleXMLElement $element
 *   The PHPUnit xml to search for test cases.
 * @param \SimpleXMLElement $suite
 *   (Optional) The parent of the current element. Defaults to NULL.
789 790
 *
 * @return array
791
 *   A list of all test cases.
792
 */
793
function simpletest_phpunit_find_testcases(\SimpleXMLElement $element, \SimpleXMLElement $parent = NULL) {
794 795
  $testcases = array();

796 797 798 799 800 801 802 803 804 805 806
  if (!isset($parent)) {
    $parent = $element;
  }

  if ($element->getName() === 'testcase' && (int) $parent->attributes()->tests > 0) {
    // Add the class attribute if the testcase does not have one. This is the
    // case for tests using a data provider. The name of the parent testsuite
    // will be in the format class::method.
    if (!$element->attributes()->class) {
      $name = explode('::', $parent->attributes()->name, 2);
      $element->addAttribute('class', $name[0]);
807
    }
808 809 810 811 812 813 814
    $testcases[] = $element;
  }
  else {
    foreach ($element as $child) {
      $file = (string) $parent->attributes()->file;
      if ($file && !$child->attributes()->file) {
        $child->addAttribute('file', $file);
815
      }
816
      $testcases = array_merge($testcases, simpletest_phpunit_find_testcases($child, $element));
817 818 819 820 821 822
    }
  }
  return $testcases;
}

/**
823
 * Converts a PHPUnit test case result to a {simpletest} result row.
824 825 826 827
 *
 * @param int $test_id
 *   The current test ID.
 * @param \SimpleXMLElement $testcase
828
 *   The PHPUnit test case represented as XML element.
829 830
 *
 * @return array
831
 *   An array containing the {simpletest} result row.
832
 */
833
function simpletest_phpunit_testcase_to_row($test_id, \SimpleXMLElement $testcase) {
834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856
  $message = '';
  $pass = TRUE;
  if ($testcase->failure) {
    $lines = explode("\n", $testcase->failure);
    $message = $lines[2];
    $pass = FALSE;
  }
  if ($testcase->error) {
    $message = $testcase->error;
    $pass = FALSE;
  }

  $attributes = $testcase->attributes();

  $record = array(
    'test_id' => $test_id,
    'test_class' => (string) $attributes->class,
    'status' => $pass ? 'pass' : 'fail',
    'message' => $message,
    // @todo: Check on the proper values for this.
    'message_group' => 'Other',
    'function' => $attributes->class . '->' . $attributes->name . '()',
    'line' => $attributes->line ?: 0,
857
    'file' => $attributes->file,
858 859 860
  );
  return $record;
}