Unverified Commit 76f37294 authored by larowlan's avatar larowlan
Browse files

Issue #2641632 by Mile23, kim.pepper, Munavijayalakshmi, dawehner, klausi,...

Issue #2641632 by Mile23, kim.pepper, Munavijayalakshmi, dawehner, klausi, joachim, xjm: Refactor simpletest's *_phpunit_*() (and junit) functions etc. to a class, deprecate
parent 11c62acb
<?php
namespace Drupal\Core\Test;
/**
* Converts JUnit XML to Drupal's {simpletest} schema.
*
* This is mainly for converting PHPUnit test results.
*
* This class is @internal and not considered to be API.
*/
class JUnitConverter {
/**
* Converts PHPUnit's JUnit XML output file to {simpletest} schema.
*
* @param int $test_id
* The current test ID.
* @param string $phpunit_xml_file
* Path to the PHPUnit XML file.
*
* @return array[]
* The results as array of rows in a format that can be inserted into the
* {simpletest} table of the results database.
*
* @internal
*/
public static function xmlToRows($test_id, $phpunit_xml_file) {
$contents = @file_get_contents($phpunit_xml_file);
if (!$contents) {
return [];
}
return static::xmlElementToRows($test_id, new \SimpleXMLElement($contents));
}
/**
* Parse test cases from XML to {simpletest} schema.
*
* @param int $test_id
* The current test ID.
* @param \SimpleXMLElement $element
* The XML data from the JUnit file.
*
* @return array[]
* The results as array of rows in a format that can be inserted into the
* {simpletest} table of the results database.
*
* @internal
*/
public static function xmlElementToRows($test_id, \SimpleXMLElement $element) {
$records = [];
$test_cases = static::findTestCases($element);
foreach ($test_cases as $test_case) {
$records[] = static::convertTestCaseToSimpletestRow($test_id, $test_case);
}
return $records;
}
/**
* Finds all test cases recursively from a test suite list.
*
* @param \SimpleXMLElement $element
* The PHPUnit xml to search for test cases.
* @param \SimpleXMLElement $parent
* (Optional) The parent of the current element. Defaults to NULL.
*
* @return array
* A list of all test cases.
*
* @internal
*/
public static function findTestCases(\SimpleXMLElement $element, \SimpleXMLElement $parent = NULL) {
if (!isset($parent)) {
$parent = $element;
}
if ($element->getName() === 'testcase' && (int) $parent->attributes()->tests > 0) {
// Add the class attribute if the test case 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]);
}
return [$element];
}
$test_cases = [];
foreach ($element as $child) {
$file = (string) $parent->attributes()->file;
if ($file && !$child->attributes()->file) {
$child->addAttribute('file', $file);
}
$test_cases = array_merge($test_cases, static::findTestCases($child, $element));
}
return $test_cases;
}
/**
* Converts a PHPUnit test case result to a {simpletest} result row.
*
* @param int $test_id
* The current test ID.
* @param \SimpleXMLElement $test_case
* The PHPUnit test case represented as XML element.
*
* @return array
* An array containing the {simpletest} result row.
*
* @internal
*/
public static function convertTestCaseToSimpletestRow($test_id, \SimpleXMLElement $test_case) {
$message = '';
$pass = TRUE;
if ($test_case->failure) {
$lines = explode("\n", $test_case->failure);
$message = $lines[2];
$pass = FALSE;
}
if ($test_case->error) {
$message = $test_case->error;
$pass = FALSE;
}
$attributes = $test_case->attributes();
$record = [
'test_id' => $test_id,
'test_class' => (string) $attributes->class,
'status' => $pass ? 'pass' : 'fail',
'message' => $message,
'message_group' => 'Other',
'function' => $attributes->class . '->' . $attributes->name . '()',
'line' => (int) $attributes->line ?: 0,
'file' => (string) $attributes->file,
];
return $record;
}
}
<?php
namespace Drupal\Core\Test;
use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Tests\Listeners\SimpletestUiPrinter;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Process\PhpExecutableFinder;
/**
* Run PHPUnit-based tests.
*
* This class runs PHPUnit-based tests and converts their JUnit results to a
* format that can be stored in the {simpletest} database schema.
*
* This class is @internal and not considered to be API.
*
* @code
* $runner = PhpUnitTestRunner::create(\Drupal::getContainer());
* $results = $runner->runTests($test_id, $test_list['phpunit']);
* @endcode
*/
class PhpUnitTestRunner implements ContainerInjectionInterface {
/**
* Path to the working directory.
*
* JUnit log files will be stored in this directory.
*
* @var string
*/
protected $workingDirectory;
/**
* Path to the application root.
*
* @var string
*/
protected $appRoot;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
(string) $container->get('app.root'),
(string) $container->get('file_system')->realpath('public://simpletest')
);
}
/**
* Constructs a test runner.
*
* @param string $app_root
* Path to the application root.
* @param string $working_directory
* Path to the working directory. JUnit log files will be stored in this
* directory.
*/
public function __construct($app_root, $working_directory) {
$this->appRoot = $app_root;
$this->workingDirectory = $working_directory;
}
/**
* Returns the path to use for PHPUnit's --log-junit option.
*
* @param int $test_id
* The current test ID.
*
* @return string
* Path to the PHPUnit XML file to use for the current $test_id.
*
* @internal
*/
public function xmlLogFilePath($test_id) {
return $this->workingDirectory . '/phpunit-' . $test_id . '.xml';
}
/**
* Returns the command to run PHPUnit.
*
* @return string
* The command that can be run through exec().
*
* @internal
*/
public function phpUnitCommand() {
// Load the actual autoloader being used and determine its filename using
// reflection. We can determine the vendor directory based on that filename.
$autoloader = require $this->appRoot . '/autoload.php';
$reflector = new \ReflectionClass($autoloader);
$vendor_dir = dirname(dirname($reflector->getFileName()));
// The file in Composer's bin dir is a *nix link, which does not work when
// extracted from a tarball and generally not on Windows.
$command = $vendor_dir . '/phpunit/phpunit/phpunit';
if (substr(PHP_OS, 0, 3) == 'WIN') {
// On Windows it is necessary to run the script using the PHP executable.
$php_executable_finder = new PhpExecutableFinder();
$php = $php_executable_finder->find();
$command = $php . ' -f ' . escapeshellarg($command) . ' --';
}
return $command;
}
/**
* Executes the PHPUnit command.
*
* @param string[] $unescaped_test_classnames
* An array of test class names, including full namespaces, to be passed as
* a regular expression to PHPUnit's --filter option.
* @param string $phpunit_file
* A filepath to use for PHPUnit's --log-junit option.
* @param int $status
* (optional) The exit status code of the PHPUnit process will be assigned
* to this variable.
* @param string[] $output
* (optional) The output by running the phpunit command. If provided, this
* array will contain the lines output by the command.
*
* @return string
* The results as returned by exec().
*
* @internal
*/
public function runCommand(array $unescaped_test_classnames, $phpunit_file, &$status = NULL, &$output = NULL) {
global $base_url;
// Setup an environment variable containing the database connection so that
// functional tests can connect to the database.
putenv('SIMPLETEST_DB=' . Database::getConnectionInfoAsUrl());
// 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);
putenv('BROWSERTEST_OUTPUT_DIRECTORY=' . $this->workingDirectory);
}
$phpunit_bin = $this->phpUnitCommand();
$command = [
$phpunit_bin,
'--log-junit',
escapeshellarg($phpunit_file),
'--printer',
escapeshellarg(SimpletestUiPrinter::class),
];
// 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, [
'--filter',
escapeshellarg($filter_string),
]);
}
// Need to change directories before running the command so that we can use
// relative paths in the configuration file's exclusions.
$old_cwd = getcwd();
chdir($this->appRoot . "/core");
// exec in a subshell so that the environment is isolated when running tests
// via the simpletest UI.
$ret = exec(implode(" ", $command), $output, $status);
chdir($old_cwd);
putenv('SIMPLETEST_DB=');
if ($base_url) {
putenv('SIMPLETEST_BASE_URL=');
putenv('BROWSERTEST_OUTPUT_DIRECTORY=');
}
return $ret;
}
/**
* Executes PHPUnit tests and returns the results of the run.
*
* @param int $test_id
* The current test ID.
* @param string[] $unescaped_test_classnames
* An array of test class names, including full namespaces, to be passed as
* a regular expression to PHPUnit's --filter option.
* @param int $status
* (optional) The exit status code of the PHPUnit process will be assigned
* to this variable.
*
* @return array
* The parsed results of PHPUnit's JUnit XML output, in the format of
* {simpletest}'s schema.
*
* @internal
*/
public function runTests($test_id, array $unescaped_test_classnames, &$status = NULL) {
$phpunit_file = $this->xmlLogFilePath($test_id);
// Store ouptut from our test run.
$output = [];
$this->runCommand($unescaped_test_classnames, $phpunit_file, $status, $output);
if ($status == TestStatus::PASS) {
return JUnitConverter::xmlToRows($test_id, $phpunit_file);
}
return [
[
'test_id' => $test_id,
'test_class' => implode(",", $unescaped_test_classnames),
'status' => TestStatus::label($status),
'message' => 'PHPunit Test failed to complete; Error: ' . implode("\n", $output),
'message_group' => 'Other',
'function' => implode(",", $unescaped_test_classnames),
'line' => '0',
'file' => $phpunit_file,
],
];
}
/**
* Tallies test results per test class.
*
* @param string[][] $results
* Array of results in the {simpletest} schema. Can be the return value of
* PhpUnitTestRunner::runTests().
*
* @return int[][]
* Array of status tallies, keyed by test class name and status type.
*
* @internal
*/
public function summarizeResults(array $results) {
$summaries = [];
foreach ($results as $result) {
if (!isset($summaries[$result['test_class']])) {
$summaries[$result['test_class']] = [
'#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;
}
}
......@@ -5,18 +5,17 @@
* Provides testing functionality.
*/
use Drupal\Core\Url;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Database\Database;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Test\JUnitConverter;
use Drupal\Core\Test\PhpUnitTestRunner;
use Drupal\Core\Test\TestDatabase;
use Drupal\Core\Url;
use Drupal\simpletest\TestDiscovery;
use Drupal\Tests\Listeners\SimpletestUiPrinter;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\PhpExecutableFinder;
use Drupal\Core\Test\TestStatus;
/**
* Implements hook_help().
......@@ -119,8 +118,9 @@ function _simpletest_format_summary_line($summary) {
/**
* Runs tests.
*
* @param $test_list
* List of tests to run.
* @param array[] $test_list
* List of tests to run. The top level is keyed by type of test, either
* 'simpletest' or 'phpunit'. Under that is an array of class names to run.
*
* @return string
* The test ID.
......@@ -187,28 +187,16 @@ function simpletest_run_tests($test_list) {
* @return array
* The parsed results of PHPUnit's JUnit XML output, in the format of
* {simpletest}'s schema.
*
* @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
* \Drupal\Core\Test\PhpUnitTestRunner::runTests() instead.
*
* @see https://www.drupal.org/node/2948547
*/
function simpletest_run_phpunit_tests($test_id, array $unescaped_test_classnames, &$status = NULL) {
$phpunit_file = simpletest_phpunit_xml_filepath($test_id);
simpletest_phpunit_run_command($unescaped_test_classnames, $phpunit_file, $status, $output);
$rows = [];
if ($status == TestStatus::PASS) {
$rows = simpletest_phpunit_xml_to_rows($test_id, $phpunit_file);
}
else {
$rows[] = [
'test_id' => $test_id,
'test_class' => implode(",", $unescaped_test_classnames),
'status' => TestStatus::label($status),
'message' => 'PHPunit Test failed to complete; Error: ' . implode("\n", $output),
'message_group' => 'Other',
'function' => implode(",", $unescaped_test_classnames),
'line' => '0',
'file' => $phpunit_file,
];
}
return $rows;
$runner = PhpUnitTestRunner::create(\Drupal::getContainer());
@trigger_error(__FUNCTION__ . ' is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\PhpUnitTestRunner::runTests() instead. See https://www.drupal.org/node/2948547', E_USER_DEPRECATED);
return $runner->runTests($test_id, $unescaped_test_classnames, $status);
}
/**
......@@ -239,38 +227,16 @@ function simpletest_process_phpunit_results($phpunit_results) {
*
* @return array
* The test result summary. A row per test class.
*
* @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
* \Drupal\Core\Test\PhpUnitTestRunner::summarizeResults() instead.
*
* @see https://www.drupal.org/node/2948547
*/
function simpletest_summarize_phpunit_result($results) {
$summaries = [];
foreach ($results as $result) {
if (!isset($summaries[$result['test_class']])) {
$summaries[$result['test_class']] = [
'#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;
$runner = PhpUnitTestRunner::create(\Drupal::getContainer());
@trigger_error(__FUNCTION__ . ' is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\PhpUnitTestRunner::summarizeResults() instead. See https://www.drupal.org/node/2948547', E_USER_DEPRECATED);
return $runner->summarizeResults($results);
}
/**
......@@ -281,9 +247,16 @@ function simpletest_summarize_phpunit_result($results) {
*
* @return string
* Path to the PHPUnit XML file to use for the current $test_id.
*
* @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
* \Drupal\Core\Test\PhpUnitTestRunner::xmlLogFilepath() instead.
*
* @see https://www.drupal.org/node/2948547
*/
function simpletest_phpunit_xml_filepath($test_id) {
return \Drupal::service('file_system')->realpath('public://simpletest') . '/phpunit-' . $test_id . '.xml';
$runner = PhpUnitTestRunner::create(\Drupal::getContainer());
@trigger_error(__FUNCTION__ . ' is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\PhpUnitTestRunner::xmlLogFilepath() instead. See https://www.drupal.org/node/2948547', E_USER_DEPRECATED);
return $runner->xmlLogFilePath($test_id);
}
/**
......@@ -319,65 +292,16 @@ function simpletest_phpunit_configuration_filepath() {
*
* @return string
* The results as returned by exec().
*
* @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
* \Drupal\Core\Test\PhpUnitTestRunner::runCommand() instead.
*
* @see https://www.drupal.org/node/2948547
*/
function simpletest_phpunit_run_command(array $unescaped_test_classnames, $phpunit_file, &$status = NULL, &$output = NULL) {
global $base_url;
// Setup an environment variable containing the database connection so that
// functional tests can connect to the database.
putenv('SIMPLETEST_DB=' . Database::getConnectionInfoAsUrl());
// 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);
putenv('BROWSERTEST_OUTPUT_DIRECTORY=' . \Drupal::service('file_system')->realpath('public://simpletest'));
}
$phpunit_bin = simpletest_phpunit_command();
$command = [
$phpunit_bin,
'--log-junit',
escapeshellarg($phpunit_file),
'--printer',
escapeshellarg(SimpletestUiPrinter::class),
];
// 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, [
'--filter',
escapeshellarg($filter_string),
]);
}
// Need to change directories before running the command so that we can use
// relative paths in the configuration file's exclusions.
$old_cwd = getcwd();
chdir(\Drupal::root() . "/core");
// exec in a subshell so that the environment is isolated when running tests
// via the simpletest UI.
$ret = exec(implode(" ", $command), $output, $status);
chdir($old_cwd);