simple-phpunit.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. // Please update when phpunit needs to be reinstalled with fresh deps:
  11. // Cache-Id: 2021-02-04 11:00 UTC
  12. error_reporting(-1);
  13. global $argv, $argc;
  14. $argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : [];
  15. $argc = isset($_SERVER['argc']) ? $_SERVER['argc'] : 0;
  16. $getEnvVar = function ($name, $default = false) use ($argv) {
  17. if (false !== $value = getenv($name)) {
  18. return $value;
  19. }
  20. static $phpunitConfig = null;
  21. if (null === $phpunitConfig) {
  22. $phpunitConfigFilename = null;
  23. $getPhpUnitConfig = function ($probableConfig) use (&$getPhpUnitConfig) {
  24. if (!$probableConfig) {
  25. return null;
  26. }
  27. if (is_dir($probableConfig)) {
  28. return $getPhpUnitConfig($probableConfig.\DIRECTORY_SEPARATOR.'phpunit.xml');
  29. }
  30. if (file_exists($probableConfig)) {
  31. return $probableConfig;
  32. }
  33. if (file_exists($probableConfig.'.dist')) {
  34. return $probableConfig.'.dist';
  35. }
  36. return null;
  37. };
  38. foreach ($argv as $cliArgumentIndex => $cliArgument) {
  39. if ('--' === $cliArgument) {
  40. break;
  41. }
  42. // long option
  43. if ('--configuration' === $cliArgument && array_key_exists($cliArgumentIndex + 1, $argv)) {
  44. $phpunitConfigFilename = $getPhpUnitConfig($argv[$cliArgumentIndex + 1]);
  45. break;
  46. }
  47. // short option
  48. if (0 === strpos($cliArgument, '-c')) {
  49. if ('-c' === $cliArgument && array_key_exists($cliArgumentIndex + 1, $argv)) {
  50. $phpunitConfigFilename = $getPhpUnitConfig($argv[$cliArgumentIndex + 1]);
  51. } else {
  52. $phpunitConfigFilename = $getPhpUnitConfig(substr($cliArgument, 2));
  53. }
  54. break;
  55. }
  56. }
  57. $phpunitConfigFilename = $phpunitConfigFilename ?: $getPhpUnitConfig('phpunit.xml');
  58. if ($phpunitConfigFilename) {
  59. $phpunitConfig = new DomDocument();
  60. $phpunitConfig->load($phpunitConfigFilename);
  61. } else {
  62. $phpunitConfig = false;
  63. }
  64. }
  65. if (false !== $phpunitConfig) {
  66. $var = new DOMXpath($phpunitConfig);
  67. foreach ($var->query('//php/server[@name="'.$name.'"]') as $var) {
  68. return $var->getAttribute('value');
  69. }
  70. foreach ($var->query('//php/env[@name="'.$name.'"]') as $var) {
  71. return $var->getAttribute('value');
  72. }
  73. }
  74. return $default;
  75. };
  76. $passthruOrFail = function ($command) {
  77. passthru($command, $status);
  78. if ($status) {
  79. exit($status);
  80. }
  81. };
  82. if (\PHP_VERSION_ID >= 80000) {
  83. // PHP 8 requires PHPUnit 9.3+
  84. $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '9.4') ?: '9.4';
  85. } elseif (\PHP_VERSION_ID >= 70200) {
  86. // PHPUnit 8 requires PHP 7.2+
  87. $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '8.3') ?: '8.3';
  88. } elseif (\PHP_VERSION_ID >= 70100) {
  89. // PHPUnit 7 requires PHP 7.1+
  90. $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '7.5') ?: '7.5';
  91. } elseif (\PHP_VERSION_ID >= 70000) {
  92. // PHPUnit 6 requires PHP 7.0+
  93. $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '6.5') ?: '6.5';
  94. } elseif (\PHP_VERSION_ID >= 50600) {
  95. // PHPUnit 4 does not support PHP 7
  96. $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '5.7') ?: '5.7';
  97. } else {
  98. // PHPUnit 5.1 requires PHP 5.6+
  99. $PHPUNIT_VERSION = '4.8';
  100. }
  101. $MAX_PHPUNIT_VERSION = $getEnvVar('SYMFONY_MAX_PHPUNIT_VERSION', false);
  102. if ($MAX_PHPUNIT_VERSION && version_compare($MAX_PHPUNIT_VERSION, $PHPUNIT_VERSION, '<')) {
  103. $PHPUNIT_VERSION = $MAX_PHPUNIT_VERSION;
  104. }
  105. $PHPUNIT_REMOVE_RETURN_TYPEHINT = filter_var($getEnvVar('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT', '0'), \FILTER_VALIDATE_BOOLEAN);
  106. $COMPOSER_JSON = getenv('COMPOSER') ?: 'composer.json';
  107. $root = __DIR__;
  108. while (!file_exists($root.'/'.$COMPOSER_JSON) || file_exists($root.'/DeprecationErrorHandler.php')) {
  109. if ($root === dirname($root)) {
  110. break;
  111. }
  112. $root = dirname($root);
  113. }
  114. $oldPwd = getcwd();
  115. $PHPUNIT_DIR = $getEnvVar('SYMFONY_PHPUNIT_DIR', $root.'/vendor/bin/.phpunit');
  116. $PHP = defined('PHP_BINARY') ? \PHP_BINARY : 'php';
  117. $PHP = escapeshellarg($PHP);
  118. if ('phpdbg' === \PHP_SAPI) {
  119. $PHP .= ' -qrr';
  120. }
  121. $defaultEnvs = [
  122. 'COMPOSER' => 'composer.json',
  123. 'COMPOSER_VENDOR_DIR' => 'vendor',
  124. 'COMPOSER_BIN_DIR' => 'bin',
  125. 'SYMFONY_SIMPLE_PHPUNIT_BIN_DIR' => __DIR__,
  126. ];
  127. foreach ($defaultEnvs as $envName => $envValue) {
  128. if ($envValue !== getenv($envName)) {
  129. putenv("$envName=$envValue");
  130. $_SERVER[$envName] = $_ENV[$envName] = $envValue;
  131. }
  132. }
  133. if ('disabled' === $getEnvVar('SYMFONY_DEPRECATIONS_HELPER')) {
  134. putenv('SYMFONY_DEPRECATIONS_HELPER=disabled');
  135. }
  136. $COMPOSER = file_exists($COMPOSER = $oldPwd.'/composer.phar')
  137. || ($COMPOSER = rtrim('\\' === \DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer.phar`) : `which composer.phar 2> /dev/null`))
  138. || ($COMPOSER = rtrim('\\' === \DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer`) : `which composer 2> /dev/null`))
  139. || file_exists($COMPOSER = rtrim('\\' === \DIRECTORY_SEPARATOR ? `git rev-parse --show-toplevel 2> NUL` : `git rev-parse --show-toplevel 2> /dev/null`).\DIRECTORY_SEPARATOR.'composer.phar')
  140. ? ('#!/usr/bin/env php' === file_get_contents($COMPOSER, false, null, 0, 18) ? $PHP : '').' '.escapeshellarg($COMPOSER) // detect shell wrappers by looking at the shebang
  141. : 'composer';
  142. $prevCacheDir = getenv('COMPOSER_CACHE_DIR');
  143. if ($prevCacheDir) {
  144. if (false === $absoluteCacheDir = realpath($prevCacheDir)) {
  145. @mkdir($prevCacheDir, 0777, true);
  146. $absoluteCacheDir = realpath($prevCacheDir);
  147. }
  148. if ($absoluteCacheDir) {
  149. putenv("COMPOSER_CACHE_DIR=$absoluteCacheDir");
  150. } else {
  151. $prevCacheDir = false;
  152. }
  153. }
  154. $SYMFONY_PHPUNIT_REMOVE = $getEnvVar('SYMFONY_PHPUNIT_REMOVE', 'phpspec/prophecy'.($PHPUNIT_VERSION < 6.0 ? ' symfony/yaml' : ''));
  155. $configurationHash = md5(implode(\PHP_EOL, [md5_file(__FILE__), $SYMFONY_PHPUNIT_REMOVE, (int) $PHPUNIT_REMOVE_RETURN_TYPEHINT]));
  156. $PHPUNIT_VERSION_DIR = sprintf('phpunit-%s-%d', $PHPUNIT_VERSION, $PHPUNIT_REMOVE_RETURN_TYPEHINT);
  157. if (!file_exists("$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit") || $configurationHash !== @file_get_contents("$PHPUNIT_DIR/.$PHPUNIT_VERSION_DIR.md5")) {
  158. // Build a standalone phpunit without symfony/yaml nor prophecy by default
  159. @mkdir($PHPUNIT_DIR, 0777, true);
  160. chdir($PHPUNIT_DIR);
  161. if (file_exists("$PHPUNIT_VERSION_DIR")) {
  162. passthru(sprintf('\\' === \DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s > NUL' : 'rm -rf %s', "$PHPUNIT_VERSION_DIR.old"));
  163. rename("$PHPUNIT_VERSION_DIR", "$PHPUNIT_VERSION_DIR.old");
  164. passthru(sprintf('\\' === \DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s' : 'rm -rf %s', "$PHPUNIT_VERSION_DIR.old"));
  165. }
  166. $info = [];
  167. foreach (explode("\n", `$COMPOSER info --no-ansi -a -n phpunit/phpunit "$PHPUNIT_VERSION.*"`) as $line) {
  168. $line = rtrim($line);
  169. if (!$info && preg_match('/^versions +: /', $line)) {
  170. $info['versions'] = explode(', ', ltrim(substr($line, 9), ': '));
  171. } elseif (isset($info['requires'])) {
  172. if ('' === $line) {
  173. break;
  174. }
  175. $line = explode(' ', $line, 2);
  176. $info['requires'][$line[0]] = $line[1];
  177. } elseif ($info && 'requires' === $line) {
  178. $info['requires'] = [];
  179. }
  180. }
  181. if (in_array('--colors=never', $argv, true) || (isset($argv[$i = array_search('never', $argv, true) - 1]) && '--colors' === $argv[$i])) {
  182. $COMPOSER .= ' --no-ansi';
  183. } else {
  184. $COMPOSER .= ' --ansi';
  185. }
  186. $info += [
  187. 'versions' => [],
  188. 'requires' => ['php' => '*'],
  189. ];
  190. $stableVersions = array_filter($info['versions'], function ($v) {
  191. return !preg_match('/-dev$|^dev-/', $v);
  192. });
  193. if (!$stableVersions) {
  194. $passthruOrFail("$COMPOSER create-project --ignore-platform-reqs --no-install --prefer-dist --no-scripts --no-plugins --no-progress -s dev phpunit/phpunit $PHPUNIT_VERSION_DIR \"$PHPUNIT_VERSION.*\"");
  195. } else {
  196. $passthruOrFail("$COMPOSER create-project --ignore-platform-reqs --no-install --prefer-dist --no-scripts --no-plugins --no-progress phpunit/phpunit $PHPUNIT_VERSION_DIR \"$PHPUNIT_VERSION.*\"");
  197. }
  198. @copy("$PHPUNIT_VERSION_DIR/phpunit.xsd", 'phpunit.xsd');
  199. chdir("$PHPUNIT_VERSION_DIR");
  200. if ($SYMFONY_PHPUNIT_REMOVE) {
  201. $passthruOrFail("$COMPOSER remove --no-update ".$SYMFONY_PHPUNIT_REMOVE);
  202. }
  203. if (5.1 <= $PHPUNIT_VERSION && $PHPUNIT_VERSION < 5.4) {
  204. $passthruOrFail("$COMPOSER require --no-update phpunit/phpunit-mock-objects \"~3.1.0\"");
  205. }
  206. if (preg_match('{\^((\d++\.)\d++)[\d\.]*$}', $info['requires']['php'], $phpVersion) && version_compare($phpVersion[2].'99', \PHP_VERSION, '<')) {
  207. $passthruOrFail("$COMPOSER config platform.php \"$phpVersion[1].99\"");
  208. } else {
  209. $passthruOrFail("$COMPOSER config --unset platform.php");
  210. }
  211. if (file_exists($path = $root.'/vendor/symfony/phpunit-bridge')) {
  212. $passthruOrFail("$COMPOSER require --no-update symfony/phpunit-bridge \"*@dev\"");
  213. $passthruOrFail("$COMPOSER config repositories.phpunit-bridge path ".escapeshellarg(str_replace('/', \DIRECTORY_SEPARATOR, $path)));
  214. if ('\\' === \DIRECTORY_SEPARATOR) {
  215. file_put_contents('composer.json', preg_replace('/^( {8})"phpunit-bridge": \{$/m', "$0\n$1 ".'"options": {"symlink": false},', file_get_contents('composer.json')));
  216. }
  217. } else {
  218. $passthruOrFail("$COMPOSER require --no-update symfony/phpunit-bridge \"*\"");
  219. }
  220. $prevRoot = getenv('COMPOSER_ROOT_VERSION');
  221. putenv("COMPOSER_ROOT_VERSION=$PHPUNIT_VERSION.99");
  222. $q = '\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID < 80000 ? '"' : '';
  223. // --no-suggest is not in the list to keep compat with composer 1.0, which is shipped with Ubuntu 16.04LTS
  224. $exit = proc_close(proc_open("$q$COMPOSER install --no-dev --prefer-dist --no-progress $q", [], $p, getcwd()));
  225. putenv('COMPOSER_ROOT_VERSION'.(false !== $prevRoot ? '='.$prevRoot : ''));
  226. if ($prevCacheDir) {
  227. putenv("COMPOSER_CACHE_DIR=$prevCacheDir");
  228. }
  229. if ($exit) {
  230. exit($exit);
  231. }
  232. // Mutate TestCase code
  233. $alteredCode = file_get_contents($alteredFile = './src/Framework/TestCase.php');
  234. if ($PHPUNIT_REMOVE_RETURN_TYPEHINT) {
  235. $alteredCode = preg_replace('/^ ((?:protected|public)(?: static)? function \w+\(\)): void/m', ' $1', $alteredCode);
  236. }
  237. $alteredCode = preg_replace('/abstract class (?:TestCase|PHPUnit_Framework_TestCase)[^\{]+\{/', '$0 '.\PHP_EOL." use \Symfony\Bridge\PhpUnit\Legacy\PolyfillTestCaseTrait;", $alteredCode, 1);
  238. file_put_contents($alteredFile, $alteredCode);
  239. // Mutate Assert code
  240. $alteredCode = file_get_contents($alteredFile = './src/Framework/Assert.php');
  241. $alteredCode = preg_replace('/abstract class (?:Assert|PHPUnit_Framework_Assert)[^\{]+\{/', '$0 '.\PHP_EOL." use \Symfony\Bridge\PhpUnit\Legacy\PolyfillAssertTrait;", $alteredCode, 1);
  242. file_put_contents($alteredFile, $alteredCode);
  243. file_put_contents('phpunit', <<<'EOPHP'
  244. <?php
  245. define('PHPUNIT_COMPOSER_INSTALL', __DIR__.'/vendor/autoload.php');
  246. require PHPUNIT_COMPOSER_INSTALL;
  247. if (!class_exists(\SymfonyExcludeListPhpunit::class, false)) {
  248. class SymfonyExcludeListPhpunit {}
  249. }
  250. if (method_exists(\PHPUnit\Util\ExcludeList::class, 'addDirectory')) {
  251. (new PHPUnit\Util\Excludelist())->getExcludedDirectories();
  252. PHPUnit\Util\ExcludeList::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListPhpunit::class))->getFileName()));
  253. class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\ExcludeList::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListSimplePhpunit::class))->getFileName()));
  254. } elseif (method_exists(\PHPUnit\Util\Blacklist::class, 'addDirectory')) {
  255. (new PHPUnit\Util\BlackList())->getBlacklistedDirectories();
  256. PHPUnit\Util\Blacklist::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListPhpunit::class))->getFileName()));
  257. class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\Blacklist::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListSimplePhpunit::class))->getFileName()));
  258. } else {
  259. PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyExcludeListPhpunit'] = 1;
  260. PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyExcludeListSimplePhpunit'] = 1;
  261. }
  262. Symfony\Bridge\PhpUnit\TextUI\Command::main();
  263. EOPHP
  264. );
  265. chdir('..');
  266. file_put_contents(".$PHPUNIT_VERSION_DIR.md5", $configurationHash);
  267. chdir($oldPwd);
  268. }
  269. // Create a symlink with a predictable path pointing to the currently used version.
  270. // This is useful for static analytics tools such as PHPStan having to load PHPUnit's classes
  271. // and for other testing libraries such as Behat using PHPUnit's assertions.
  272. chdir($PHPUNIT_DIR);
  273. if (file_exists('phpunit')) {
  274. @unlink('phpunit');
  275. }
  276. @symlink($PHPUNIT_VERSION_DIR, 'phpunit');
  277. chdir($oldPwd);
  278. if ($PHPUNIT_VERSION < 8.0) {
  279. $argv = array_filter($argv, function ($v) use (&$argc) {
  280. if ('--do-not-cache-result' !== $v) {
  281. return true;
  282. }
  283. --$argc;
  284. return false;
  285. });
  286. } elseif (filter_var(getenv('SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE'), \FILTER_VALIDATE_BOOLEAN)) {
  287. $argv[] = '--do-not-cache-result';
  288. ++$argc;
  289. }
  290. $components = [];
  291. $cmd = array_map('escapeshellarg', $argv);
  292. $exit = 0;
  293. if (isset($argv[1]) && 'symfony' === $argv[1] && !file_exists('symfony') && file_exists('src/Symfony')) {
  294. $argv[1] = 'src/Symfony';
  295. }
  296. if (isset($argv[1]) && is_dir($argv[1]) && !file_exists($argv[1].'/phpunit.xml.dist')) {
  297. // Find Symfony components in plain php for Windows portability
  298. $finder = new RecursiveDirectoryIterator($argv[1], FilesystemIterator::KEY_AS_FILENAME | FilesystemIterator::UNIX_PATHS);
  299. $finder = new RecursiveIteratorIterator($finder);
  300. $finder->setMaxDepth(getenv('SYMFONY_PHPUNIT_MAX_DEPTH') ?: 3);
  301. foreach ($finder as $file => $fileInfo) {
  302. if ('phpunit.xml.dist' === $file) {
  303. $components[] = dirname($fileInfo->getPathname());
  304. }
  305. }
  306. if ($components) {
  307. array_shift($cmd);
  308. }
  309. }
  310. $cmd[0] = sprintf('%s %s --colors=always', $PHP, escapeshellarg("$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit"));
  311. $cmd = str_replace('%', '%%', implode(' ', $cmd)).' %1$s';
  312. if ('\\' === \DIRECTORY_SEPARATOR) {
  313. $cmd = 'cmd /v:on /d /c "('.$cmd.')%2$s"';
  314. } else {
  315. $cmd .= '%2$s';
  316. }
  317. if ($components) {
  318. $skippedTests = isset($_SERVER['SYMFONY_PHPUNIT_SKIPPED_TESTS']) ? $_SERVER['SYMFONY_PHPUNIT_SKIPPED_TESTS'] : false;
  319. $runningProcs = [];
  320. foreach ($components as $component) {
  321. // Run phpunit tests in parallel
  322. if ($skippedTests) {
  323. putenv("SYMFONY_PHPUNIT_SKIPPED_TESTS=$component/$skippedTests");
  324. }
  325. $c = escapeshellarg($component);
  326. if ($proc = proc_open(sprintf($cmd, $c, " > $c/phpunit.stdout 2> $c/phpunit.stderr"), [], $pipes)) {
  327. $runningProcs[$component] = $proc;
  328. } else {
  329. $exit = 1;
  330. echo "\033[41mKO\033[0m $component\n\n";
  331. }
  332. }
  333. while ($runningProcs) {
  334. usleep(300000);
  335. $terminatedProcs = [];
  336. foreach ($runningProcs as $component => $proc) {
  337. $procStatus = proc_get_status($proc);
  338. if (!$procStatus['running']) {
  339. $terminatedProcs[$component] = $procStatus['exitcode'];
  340. unset($runningProcs[$component]);
  341. proc_close($proc);
  342. }
  343. }
  344. foreach ($terminatedProcs as $component => $procStatus) {
  345. foreach (['out', 'err'] as $file) {
  346. $file = "$component/phpunit.std$file";
  347. readfile($file);
  348. unlink($file);
  349. }
  350. // Fail on any individual component failures but ignore some error codes on Windows when APCu is enabled:
  351. // STATUS_STACK_BUFFER_OVERRUN (-1073740791/0xC0000409)
  352. // STATUS_ACCESS_VIOLATION (-1073741819/0xC0000005)
  353. // STATUS_HEAP_CORRUPTION (-1073740940/0xC0000374)
  354. if ($procStatus && ('\\' !== \DIRECTORY_SEPARATOR || !extension_loaded('apcu') || !filter_var(ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN) || !in_array($procStatus, [-1073740791, -1073741819, -1073740940]))) {
  355. $exit = $procStatus;
  356. echo "\033[41mKO\033[0m $component\n\n";
  357. } else {
  358. echo "\033[32mOK\033[0m $component\n\n";
  359. }
  360. }
  361. }
  362. } elseif (!isset($argv[1]) || 'install' !== $argv[1] || file_exists('install')) {
  363. if (!class_exists(\SymfonyExcludeListSimplePhpunit::class, false)) {
  364. class SymfonyExcludeListSimplePhpunit
  365. {
  366. }
  367. }
  368. array_splice($argv, 1, 0, ['--colors=always']);
  369. $_SERVER['argv'] = $argv;
  370. $_SERVER['argc'] = ++$argc;
  371. include "$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit";
  372. }
  373. exit($exit);