PhpUnitTestRunner.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. <?php
  2. namespace Drupal\Core\Test;
  3. use Drupal\Core\Database\Database;
  4. use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
  5. use Drupal\Tests\Listeners\SimpletestUiPrinter;
  6. use Symfony\Component\DependencyInjection\ContainerInterface;
  7. use Symfony\Component\Process\PhpExecutableFinder;
  8. /**
  9. * Run PHPUnit-based tests.
  10. *
  11. * This class runs PHPUnit-based tests and converts their JUnit results to a
  12. * format that can be stored in the {simpletest} database schema.
  13. *
  14. * This class is @internal and not considered to be API.
  15. *
  16. * @code
  17. * $runner = PhpUnitTestRunner::create(\Drupal::getContainer());
  18. * $results = $runner->runTests($test_id, $test_list['phpunit']);
  19. * @endcode
  20. */
  21. class PhpUnitTestRunner implements ContainerInjectionInterface {
  22. /**
  23. * Path to the working directory.
  24. *
  25. * JUnit log files will be stored in this directory.
  26. *
  27. * @var string
  28. */
  29. protected $workingDirectory;
  30. /**
  31. * Path to the application root.
  32. *
  33. * @var string
  34. */
  35. protected $appRoot;
  36. /**
  37. * {@inheritdoc}
  38. */
  39. public static function create(ContainerInterface $container) {
  40. return new static(
  41. (string) $container->get('app.root'),
  42. (string) $container->get('file_system')->realpath('public://simpletest')
  43. );
  44. }
  45. /**
  46. * Constructs a test runner.
  47. *
  48. * @param string $app_root
  49. * Path to the application root.
  50. * @param string $working_directory
  51. * Path to the working directory. JUnit log files will be stored in this
  52. * directory.
  53. */
  54. public function __construct($app_root, $working_directory) {
  55. $this->appRoot = $app_root;
  56. $this->workingDirectory = $working_directory;
  57. }
  58. /**
  59. * Returns the path to use for PHPUnit's --log-junit option.
  60. *
  61. * @param int $test_id
  62. * The current test ID.
  63. *
  64. * @return string
  65. * Path to the PHPUnit XML file to use for the current $test_id.
  66. *
  67. * @internal
  68. */
  69. public function xmlLogFilePath($test_id) {
  70. return $this->workingDirectory . '/phpunit-' . $test_id . '.xml';
  71. }
  72. /**
  73. * Returns the command to run PHPUnit.
  74. *
  75. * @return string
  76. * The command that can be run through exec().
  77. *
  78. * @internal
  79. */
  80. public function phpUnitCommand() {
  81. // Load the actual autoloader being used and determine its filename using
  82. // reflection. We can determine the vendor directory based on that filename.
  83. $autoloader = require $this->appRoot . '/autoload.php';
  84. $reflector = new \ReflectionClass($autoloader);
  85. $vendor_dir = dirname(dirname($reflector->getFileName()));
  86. // The file in Composer's bin dir is a *nix link, which does not work when
  87. // extracted from a tarball and generally not on Windows.
  88. $command = $vendor_dir . '/phpunit/phpunit/phpunit';
  89. if (substr(PHP_OS, 0, 3) == 'WIN') {
  90. // On Windows it is necessary to run the script using the PHP executable.
  91. $php_executable_finder = new PhpExecutableFinder();
  92. $php = $php_executable_finder->find();
  93. $command = $php . ' -f ' . escapeshellarg($command) . ' --';
  94. }
  95. return $command;
  96. }
  97. /**
  98. * Executes the PHPUnit command.
  99. *
  100. * @param string[] $unescaped_test_classnames
  101. * An array of test class names, including full namespaces, to be passed as
  102. * a regular expression to PHPUnit's --filter option.
  103. * @param string $phpunit_file
  104. * A filepath to use for PHPUnit's --log-junit option.
  105. * @param int $status
  106. * (optional) The exit status code of the PHPUnit process will be assigned
  107. * to this variable.
  108. * @param string[] $output
  109. * (optional) The output by running the phpunit command. If provided, this
  110. * array will contain the lines output by the command.
  111. *
  112. * @return string
  113. * The results as returned by exec().
  114. *
  115. * @internal
  116. */
  117. public function runCommand(array $unescaped_test_classnames, $phpunit_file, &$status = NULL, &$output = NULL) {
  118. global $base_url;
  119. // Setup an environment variable containing the database connection so that
  120. // functional tests can connect to the database.
  121. putenv('SIMPLETEST_DB=' . Database::getConnectionInfoAsUrl());
  122. // Setup an environment variable containing the base URL, if it is available.
  123. // This allows functional tests to browse the site under test. When running
  124. // tests via CLI, core/phpunit.xml.dist or core/scripts/run-tests.sh can set
  125. // this variable.
  126. if ($base_url) {
  127. putenv('SIMPLETEST_BASE_URL=' . $base_url);
  128. putenv('BROWSERTEST_OUTPUT_DIRECTORY=' . $this->workingDirectory);
  129. }
  130. $phpunit_bin = $this->phpUnitCommand();
  131. $command = [
  132. $phpunit_bin,
  133. '--log-junit',
  134. escapeshellarg($phpunit_file),
  135. '--printer',
  136. escapeshellarg(SimpletestUiPrinter::class),
  137. ];
  138. // Optimized for running a single test.
  139. if (count($unescaped_test_classnames) == 1) {
  140. $class = new \ReflectionClass($unescaped_test_classnames[0]);
  141. $command[] = escapeshellarg($class->getFileName());
  142. }
  143. else {
  144. // Double escape namespaces so they'll work in a regexp.
  145. $escaped_test_classnames = array_map(function ($class) {
  146. return addslashes($class);
  147. }, $unescaped_test_classnames);
  148. $filter_string = implode("|", $escaped_test_classnames);
  149. $command = array_merge($command, [
  150. '--filter',
  151. escapeshellarg($filter_string),
  152. ]);
  153. }
  154. // Need to change directories before running the command so that we can use
  155. // relative paths in the configuration file's exclusions.
  156. $old_cwd = getcwd();
  157. chdir($this->appRoot . "/core");
  158. // exec in a subshell so that the environment is isolated when running tests
  159. // via the simpletest UI.
  160. $ret = exec(implode(" ", $command), $output, $status);
  161. chdir($old_cwd);
  162. putenv('SIMPLETEST_DB=');
  163. if ($base_url) {
  164. putenv('SIMPLETEST_BASE_URL=');
  165. putenv('BROWSERTEST_OUTPUT_DIRECTORY=');
  166. }
  167. return $ret;
  168. }
  169. /**
  170. * Executes PHPUnit tests and returns the results of the run.
  171. *
  172. * @param int $test_id
  173. * The current test ID.
  174. * @param string[] $unescaped_test_classnames
  175. * An array of test class names, including full namespaces, to be passed as
  176. * a regular expression to PHPUnit's --filter option.
  177. * @param int $status
  178. * (optional) The exit status code of the PHPUnit process will be assigned
  179. * to this variable.
  180. *
  181. * @return array
  182. * The parsed results of PHPUnit's JUnit XML output, in the format of
  183. * {simpletest}'s schema.
  184. *
  185. * @internal
  186. */
  187. public function runTests($test_id, array $unescaped_test_classnames, &$status = NULL) {
  188. $phpunit_file = $this->xmlLogFilePath($test_id);
  189. // Store output from our test run.
  190. $output = [];
  191. $this->runCommand($unescaped_test_classnames, $phpunit_file, $status, $output);
  192. if ($status == TestStatus::PASS) {
  193. return JUnitConverter::xmlToRows($test_id, $phpunit_file);
  194. }
  195. return [
  196. [
  197. 'test_id' => $test_id,
  198. 'test_class' => implode(",", $unescaped_test_classnames),
  199. 'status' => TestStatus::label($status),
  200. 'message' => 'PHPUnit Test failed to complete; Error: ' . implode("\n", $output),
  201. 'message_group' => 'Other',
  202. 'function' => implode(",", $unescaped_test_classnames),
  203. 'line' => '0',
  204. 'file' => $phpunit_file,
  205. ],
  206. ];
  207. }
  208. /**
  209. * Tallies test results per test class.
  210. *
  211. * @param string[][] $results
  212. * Array of results in the {simpletest} schema. Can be the return value of
  213. * PhpUnitTestRunner::runTests().
  214. *
  215. * @return int[][]
  216. * Array of status tallies, keyed by test class name and status type.
  217. *
  218. * @internal
  219. */
  220. public function summarizeResults(array $results) {
  221. $summaries = [];
  222. foreach ($results as $result) {
  223. if (!isset($summaries[$result['test_class']])) {
  224. $summaries[$result['test_class']] = [
  225. '#pass' => 0,
  226. '#fail' => 0,
  227. '#exception' => 0,
  228. '#debug' => 0,
  229. ];
  230. }
  231. switch ($result['status']) {
  232. case 'pass':
  233. $summaries[$result['test_class']]['#pass']++;
  234. break;
  235. case 'fail':
  236. $summaries[$result['test_class']]['#fail']++;
  237. break;
  238. case 'exception':
  239. $summaries[$result['test_class']]['#exception']++;
  240. break;
  241. case 'debug':
  242. $summaries[$result['test_class']]['#debug']++;
  243. break;
  244. }
  245. }
  246. return $summaries;
  247. }
  248. }