diff --git a/.github/jit_check.php b/.github/jit_check.php new file mode 100644 index 000000000000..50f3ac9ec383 --- /dev/null +++ b/.github/jit_check.php @@ -0,0 +1,30 @@ +&1"), $result, $code); + $result = implode("\n", $result); + if ($code) { + printMutex("An error occurred while executing $cmd (status $code, extra info $extra): $result"); + die(1); + } + return $result; +} + +$parallel = (int) ($argv[1] ?? 0); +$parallel = $parallel ?: ((int)`nproc`); +$parallel = $parallel ?: 8; + +$repos = []; + +$repos["psalm"] = [ + "https://github.com/vimeo/psalm", + "master", + null, + function (): iterable { + $it = new RecursiveDirectoryIterator("tests"); + /** @var SplFileInfo $file */ + foreach(new RecursiveIteratorIterator($it) as $file) { + if ($file->getExtension() == 'php' && ctype_upper($file->getBasename()[0])) { + yield [ + getcwd()."/vendor/bin/phpunit", + $file->getRealPath(), + ]; + } + } + }, + 2 +]; + +$repos["phpseclib"] = [ + "https://github.com/phpseclib/phpseclib", + "master", + null, + function (): iterable { + $it = new RecursiveDirectoryIterator("tests"); + /** @var SplFileInfo $file */ + foreach(new RecursiveIteratorIterator($it) as $file) { + if ($file->getExtension() == 'php' && ctype_upper($file->getBasename()[0])) { + yield [ + getcwd()."/vendor/bin/phpunit", + '-c', + getcwd()."/tests/phpunit.xml", + $file->getRealPath(), + ]; + } + } + }, + 2 +]; + +$repos["phpunit"] = [ + "https://github.com/sebastianbergmann/phpunit.git", + "main", + null, + ["./phpunit"], + 2 +]; + +$repos["infection"] = [ + "https://github.com/infection/infection", + "master", + null, + ["vendor/bin/phpunit"], + 2 +]; + +$repos["wordpress"] = [ + "https://github.com/WordPress/wordpress-develop.git", + "", + function (): void { + $f = file_get_contents('wp-tests-config-sample.php'); + $f = str_replace('youremptytestdbnamehere', 'test', $f); + $f = str_replace('yourusernamehere', 'root', $f); + $f = str_replace('yourpasswordhere', 'root', $f); + file_put_contents('wp-tests-config.php', $f); + }, + ["vendor/bin/phpunit"], + 2 +]; + +foreach (['amp', 'cache', 'dns', 'file', 'http', 'parallel', 'parser', 'pipeline', 'process', 'serialization', 'socket', 'sync', 'websocket-client', 'websocket-server'] as $repo) { + $repos["amphp-$repo"] = ["https://github.com/amphp/$repo.git", "", null, ["vendor/bin/phpunit"], 2]; +} + +$repos["laravel"] = [ + "https://github.com/laravel/framework.git", + "master", + function (): void { + $c = file_get_contents("tests/Filesystem/FilesystemTest.php"); + $c = str_replace("public function testSharedGet()", "#[\\PHPUnit\\Framework\\Attributes\\Group('skip')]\n public function testSharedGet()", $c); + file_put_contents("tests/Filesystem/FilesystemTest.php", $c); + }, + ["vendor/bin/phpunit", "--exclude-group", "skip"], + 2 +]; + +foreach (['async', 'cache', 'child-process', 'datagram', 'dns', 'event-loop', 'promise', 'promise-stream', 'promise-timer', 'stream'] as $repo) { + $repos["reactphp-$repo"] = ["https://github.com/reactphp/$repo.git", "", null, ["vendor/bin/phpunit"], 2]; +} + +$repos["revolt"] = ["https://github.com/revoltphp/event-loop.git", "", null, ["vendor/bin/phpunit"], 2]; + +$repos["symfony"] = [ + "https://github.com/symfony/symfony.git", + "", + function (): void { + e("php ./phpunit install"); + }, + function (): iterable { + $it = new RecursiveDirectoryIterator("src/Symfony"); + /** @var SplFileInfo $file */ + foreach(new RecursiveIteratorIterator($it) as $file) { + if ($file->getBasename() == 'phpunit.xml.dist') { + yield [ + getcwd()."/phpunit", + dirname($file->getRealPath()), + "--exclude-group", + "tty,benchmark,intl-data,transient", + "--exclude-group", + "skip" + ]; + } + } + }, + 2 +]; + +$finalStatus = 0; +$parentPids = []; + +$waitOne = function () use (&$finalStatus, &$parentPids): void { + $res = pcntl_wait($status); + if ($res === -1) { + printMutex("An error occurred while waiting with waitpid!"); + $finalStatus = $finalStatus ?: 1; + return; + } + if (!isset($parentPids[$res])) { + printMutex("Unknown PID $res returned!"); + $finalStatus = $finalStatus ?: 1; + return; + } + $desc = $parentPids[$res]; + unset($parentPids[$res]); + if (pcntl_wifexited($status)) { + $status = pcntl_wexitstatus($status); + printMutex("Child task $desc exited with status $status"); + if ($status !== 0) { + $finalStatus = $status; + } + } elseif (pcntl_wifstopped($status)) { + $status = pcntl_wstopsig($status); + printMutex("Child task $desc stopped by signal $status"); + $finalStatus = 1; + } elseif (pcntl_wifsignaled($status)) { + $status = pcntl_wtermsig($status); + printMutex("Child task $desc terminated by signal $status"); + $finalStatus = 1; + } +}; + +$waitAll = function () use ($waitOne, &$parentPids): void { + while ($parentPids) { + $waitOne(); + } +}; + +printMutex("Cloning repos..."); + +foreach ($repos as $dir => [$repo, $branch, $prepare, $command, $repeat]) { + $pid = pcntl_fork(); + if ($pid) { + $parentPids[$pid] = "clone $dir"; + continue; + } + + chdir(sys_get_temp_dir()); + if ($branch) { + $branch = "--branch $branch"; + } + e("git clone $repo $branch --depth 1 $dir"); + + exit(0); +} + +$waitAll(); + +printMutex("Done cloning repos!"); + +printMutex("Preparing repos (max $parallel processes)..."); +foreach ($repos as $dir => [$repo, $branch, $prepare, $command, $repeat]) { + chdir(sys_get_temp_dir()."/$dir"); + $rev = e("git rev-parse HEAD", $dir); + + $pid = pcntl_fork(); + if ($pid) { + $parentPids[$pid] = "prepare $dir ($rev)"; + if (count($parentPids) >= $parallel) { + $waitOne(); + } + continue; + } + + e("composer i --ignore-platform-reqs", $dir); + if ($prepare) { + $prepare(); + } + + exit(0); +} +$waitAll(); + +printMutex("Done preparing repos!"); + +printMutex("Running tests (max $parallel processes)..."); +foreach ($repos as $dir => [$repo, $branch, $prepare, $command, $repeat]) { + chdir(sys_get_temp_dir()."/$dir"); + $rev = e("git rev-parse HEAD", $dir); + + if ($command instanceof Closure) { + $commands = iterator_to_array($command()); + } else { + $commands = [$command]; + } + + foreach ($commands as $idx => $cmd) { + $cmd = array_merge([ + PHP_BINARY, + '--repeat', + $repeat, + '-f', + __DIR__.'/jit_check.php', + ], $cmd); + + $cmdStr = implode(" ", $cmd); + + $pid = pcntl_fork(); + if ($pid) { + $parentPids[$pid] = "test $dir ($rev): $cmdStr"; + if (count($parentPids) >= $parallel) { + $waitOne(); + } + continue; + } + + $output = sys_get_temp_dir()."/out_{$dir}_$idx.txt"; + + $p = proc_open($cmd, [ + ["pipe", "r"], + ["file", $output, "a"], + ["file", $output, "a"] + ], $pipes, sys_get_temp_dir()."/$dir"); + + if ($p === false) { + printMutex("Failure starting $cmdStr"); + exit(1); + } + + $final = 0; + $status = proc_close($p); + if ($status !== 0) { + if ($status > 128) { + $final = $status; + } + printMutex( + "$dir ($rev): $cmdStr terminated with status $status:".PHP_EOL + .file_get_contents($output).PHP_EOL + ); + } + + exit($final); + } +} + +$waitAll(); + +printMutex("All done!"); + +die($finalStatus); diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 1b1532af7f79..58e48ff19136 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -174,8 +174,8 @@ jobs: configuration_parameters: >- CFLAGS="-fsanitize=undefined,address -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-fsanitize=undefined,address" - run_tests_parameters: '--asan' - test_function_jit: false + run_tests_parameters: '--asan --repeat 2' + test_function_jit: true asan: true - name: _REPEAT debug: true @@ -535,106 +535,34 @@ jobs: run: | echo opcache.jit=tracing >> /etc/php.d/opcache.ini echo opcache.jit_buffer_size=1G >> /etc/php.d/opcache.ini + echo opcache.jit_prof_threshold=0.000000001 >> /etc/php.d/opcache.ini echo opcache.jit_max_root_traces=100000 >> /etc/php.d/opcache.ini echo opcache.jit_max_side_traces=100000 >> /etc/php.d/opcache.ini echo opcache.jit_max_exit_counters=100000 >> /etc/php.d/opcache.ini + echo opcache.jit_blacklist_root_trace=255 >> /etc/php.d/opcache.ini + echo opcache.jit_blacklist_side_trace=255 >> /etc/php.d/opcache.ini echo opcache.jit_hot_loop=1 >> /etc/php.d/opcache.ini echo opcache.jit_hot_func=1 >> /etc/php.d/opcache.ini echo opcache.jit_hot_return=1 >> /etc/php.d/opcache.ini echo opcache.jit_hot_side_exit=1 >> /etc/php.d/opcache.ini php -v - - name: Test AMPHP - if: ${{ !cancelled() }} - run: | - repositories="amp cache dns file http parallel parser pipeline process serialization socket sync websocket-client websocket-server" - X=0 - for repository in $repositories; do - printf "Testing amp/%s\n" "$repository" - git clone "https://github.com/amphp/$repository.git" "amphp-$repository" --depth 1 - cd "amphp-$repository" - git rev-parse HEAD - php /usr/bin/composer install --no-progress --ignore-platform-req=php+ - vendor/bin/phpunit || EXIT_CODE=$? - if [ ${EXIT_CODE:-0} -gt 128 ]; then - X=1; - fi - cd .. - done - exit $X - - name: Test Laravel - if: ${{ !cancelled() && !inputs.skip_laravel }} - run: | - git clone https://github.com/laravel/framework.git --depth=1 - cd framework - git rev-parse HEAD - php /usr/bin/composer install --no-progress --ignore-platform-req=php+ - # Hack to disable a test that hangs - php -r '$c = file_get_contents("tests/Filesystem/FilesystemTest.php"); $c = str_replace("public function testSharedGet()", "#[\\PHPUnit\\Framework\\Attributes\\Group('"'"'skip'"'"')]\n public function testSharedGet()", $c); file_put_contents("tests/Filesystem/FilesystemTest.php", $c);' - php vendor/bin/phpunit --exclude-group skip || EXIT_CODE=$? - if [ ${EXIT_CODE:-0} -gt 128 ]; then - exit 1 - fi - - name: Test ReactPHP - if: ${{ !cancelled() }} - run: | - repositories="async cache child-process datagram dns event-loop promise promise-stream promise-timer stream" - X=0 - for repository in $repositories; do - printf "Testing reactphp/%s\n" "$repository" - git clone "https://github.com/reactphp/$repository.git" "reactphp-$repository" --depth 1 - cd "reactphp-$repository" - git rev-parse HEAD - php /usr/bin/composer install --no-progress --ignore-platform-req=php+ - vendor/bin/phpunit || EXIT_CODE=$? - if [ $[EXIT_CODE:-0} -gt 128 ]; then - X=1; - fi - cd .. - done - exit $X - - name: Test Revolt PHP - if: ${{ !cancelled() }} - run: | - git clone https://github.com/revoltphp/event-loop.git --depth=1 - cd event-loop - git rev-parse HEAD - php /usr/bin/composer install --no-progress --ignore-platform-req=php+ - vendor/bin/phpunit || EXIT_CODE=$? - if [ ${EXIT_CODE:-0} -gt 128 ]; then - exit 1 - fi - - name: Test Symfony - if: ${{ !cancelled() && !inputs.skip_symfony }} + + - name: Test multiple libraries and frameworks in parallel run: | - git clone https://github.com/symfony/symfony.git --depth=1 - cd symfony - git rev-parse HEAD - php /usr/bin/composer install --no-progress --ignore-platform-req=php+ - php ./phpunit install - # Test causes a heap-buffer-overflow but I cannot reproduce it locally... - php -r '$c = file_get_contents("src/Symfony/Component/HtmlSanitizer/Tests/HtmlSanitizerCustomTest.php"); $c = str_replace("public function testSanitizeDeepNestedString()", "/** @group skip */\n public function testSanitizeDeepNestedString()", $c); file_put_contents("src/Symfony/Component/HtmlSanitizer/Tests/HtmlSanitizerCustomTest.php", $c);' - # Buggy FFI test in Symfony, see https://github.com/symfony/symfony/issues/47668 - php -r '$c = file_get_contents("src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php"); $c = str_replace("public function testCastNonTrailingCharPointer()", "/** @group skip */\n public function testCastNonTrailingCharPointer()", $c); file_put_contents("src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php", $c);' - export SYMFONY_DEPRECATIONS_HELPER=max[total]=999 - X=0 - for component in $(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -printf '%h\n'); do - php ./phpunit $component --exclude-group tty,benchmark,intl-data,transient --exclude-group skip || EXIT_CODE=$? - if [ ${EXIT_CODE:-0} -gt 128 ]; then - X=1; - fi - done - exit $X - - name: Test PHPUnit - if: ${{ !cancelled() }} + sudo prlimit --pid $$ --nofile=1048576:1048576 + + php $GITHUB_WORKSPACE/.github/nightly.php || exit $? + + - name: Test PHPSeclib + if: always() run: | - git clone https://github.com/sebastianbergmann/phpunit.git --branch=main --depth=1 - cd phpunit - git rev-parse HEAD - php /usr/bin/composer install --no-progress --ignore-platform-req=php+ - php ./phpunit || EXIT_CODE=$? - if [ ${EXIT_CODE:-0} -gt 128 ]; then - exit 1 - fi + git clone https://github.com/phpseclib/phpseclib --branch=master + cd phpseclib + export ASAN_OPTIONS=exitcode=139 + export PHPSECLIB_ALLOW_JIT=1 + php /usr/bin/composer install --no-progress --ignore-platform-reqs + php $GITHUB_WORKSPACE/.github/jit_check.php vendor/bin/paratest --verbose --configuration=tests/phpunit.xml --runner=WrapperRunner || exit $? + - name: 'Symfony Preloading' if: ${{ !cancelled() && !inputs.skip_symfony }} run: | @@ -643,21 +571,6 @@ jobs: git rev-parse HEAD sed -i 's/PHP_SAPI/"cli-server"/g' var/cache/dev/App_KernelDevDebugContainer.preload.php php -d opcache.preload=var/cache/dev/App_KernelDevDebugContainer.preload.php public/index.php - - name: Test Wordpress - if: ${{ !cancelled() && !inputs.skip_wordpress }} - run: | - git clone https://github.com/WordPress/wordpress-develop.git wordpress --depth=1 - cd wordpress - git rev-parse HEAD - php /usr/bin/composer install --no-progress --ignore-platform-req=php+ - cp wp-tests-config-sample.php wp-tests-config.php - sed -i 's/youremptytestdbnamehere/test/g' wp-tests-config.php - sed -i 's/yourusernamehere/root/g' wp-tests-config.php - sed -i 's/yourpasswordhere/root/g' wp-tests-config.php - php vendor/bin/phpunit || EXIT_CODE=$? - if [ $EXIT_CODE -gt 128 ]; then - exit 1 - fi - name: Notify Slack if: failure() uses: ./.github/actions/notify-slack