1<?php 2 3error_reporting(E_ALL | E_STRICT); 4ini_set('short_open_tag', false); 5 6if ('cli' !== php_sapi_name()) { 7 die('This script is designed for running on the command line.'); 8} 9 10function showHelp($error) { 11 die($error . "\n\n" . 12<<<OUTPUT 13This script has to be called with the following signature: 14 15 php run.php [--no-progress] testType pathToTestFiles 16 17The test type must be one of: PHP, Symfony 18 19The following options are available: 20 21 --no-progress Disables showing which file is currently tested. 22 --verbose Print more information for failures. 23 --php-version=VERSION PHP version to use for lexing/parsing. 24 25OUTPUT 26 ); 27} 28 29$allowedOptions = [ 30 '--no-progress' => true, 31 '--verbose' => true, 32 '--php-version' => true, 33]; 34 35$options = array(); 36$arguments = array(); 37 38// remove script name from argv 39array_shift($argv); 40 41foreach ($argv as $arg) { 42 if ('-' === $arg[0]) { 43 $parts = explode('=', $arg); 44 $name = $parts[0]; 45 if (!isset($allowedOptions[$name])) { 46 showHelp("Unknown option \"$name\""); 47 } 48 $options[$name] = $parts[1] ?? true; 49 } else { 50 $arguments[] = $arg; 51 } 52} 53 54if (count($arguments) !== 2) { 55 showHelp('Too few arguments passed!'); 56} 57 58$showProgress = !isset($options['--no-progress']); 59$verbose = isset($options['--verbose']); 60$phpVersion = $options['--php-version'] ?? '8.0'; 61$testType = $arguments[0]; 62$dir = $arguments[1]; 63 64require_once __DIR__ . '/../vendor/autoload.php'; 65 66switch ($testType) { 67 case 'Symfony': 68 $fileFilter = function($path) { 69 if (!preg_match('~\.php$~', $path)) { 70 return false; 71 } 72 73 if (preg_match('~(?: 74# invalid php code 75 dependency-injection.Tests.Fixtures.xml.xml_with_wrong_ext 76# difference in nop statement 77| framework-bundle.Resources.views.Form.choice_widget_options\.html 78# difference due to INF 79| yaml.Tests.InlineTest 80)\.php$~x', $path)) { 81 return false; 82 } 83 84 return true; 85 }; 86 $codeExtractor = function($file, $code) { 87 return $code; 88 }; 89 break; 90 case 'PHP': 91 $fileFilter = function($path) { 92 return preg_match('~\.phpt$~', $path); 93 }; 94 $codeExtractor = function($file, $code) { 95 if (preg_match('~(?: 96# skeleton files 97 ext.gmp.tests.001 98| ext.skeleton.tests.00\d 99# multibyte encoded files 100| ext.mbstring.tests.zend_multibyte-01 101| Zend.tests.multibyte.multibyte_encoding_001 102| Zend.tests.multibyte.multibyte_encoding_004 103| Zend.tests.multibyte.multibyte_encoding_005 104# invalid code due to missing WS after opening tag 105| tests.run-test.bug75042-3 106# contains invalid chars, which we treat as parse error 107| Zend.tests.warning_during_heredoc_scan_ahead 108# pretty print differences due to negative LNumbers 109| Zend.tests.neg_num_string 110| Zend.tests.numeric_strings.neg_num_string 111| Zend.tests.bug72918 112# pretty print difference due to nop statements 113| ext.mbstring.tests.htmlent 114| ext.standard.tests.file.fread_basic 115# its too hard to emulate these on old PHP versions 116| Zend.tests.flexible-heredoc-complex-test[1-4] 117# whitespace in namespaced name 118| Zend.tests.bug55086 119| Zend.tests.grammar.regression_010 120# not worth emulating on old PHP versions 121| Zend.tests.type_declarations.intersection_types.parsing_comment 122# comments in property fetch syntax, not emulated on old PHP versions 123| Zend.tests.gh14961 124)\.phpt$~x', $file)) { 125 return null; 126 } 127 128 if (!preg_match('~--FILE--\s*(.*?)\n--[A-Z]+--~s', $code, $matches)) { 129 return null; 130 } 131 if (preg_match('~--EXPECT(?:F|REGEX)?--\s*(?:Parse|Fatal) error~', $code)) { 132 return null; 133 } 134 135 return $matches[1]; 136 }; 137 break; 138 default: 139 showHelp('Test type must be one of: PHP or Symfony'); 140} 141 142$parser = (new PhpParser\ParserFactory())->createForVersion(PhpParser\PhpVersion::fromString($phpVersion)); 143$prettyPrinter = new PhpParser\PrettyPrinter\Standard; 144$nodeDumper = new PhpParser\NodeDumper; 145 146$cloningTraverser = new PhpParser\NodeTraverser; 147$cloningTraverser->addVisitor(new PhpParser\NodeVisitor\CloningVisitor); 148 149$parseFail = $fpppFail = $ppFail = $compareFail = $count = 0; 150 151$readTime = $parseTime = $cloneTime = 0; 152$fpppTime = $ppTime = $reparseTime = $compareTime = 0; 153$totalStartTime = microtime(true); 154 155foreach (new RecursiveIteratorIterator( 156 new RecursiveDirectoryIterator($dir), 157 RecursiveIteratorIterator::LEAVES_ONLY) 158 as $file) { 159 if (!$fileFilter($file)) { 160 continue; 161 } 162 163 $startTime = microtime(true); 164 $origCode = file_get_contents($file); 165 $readTime += microtime(true) - $startTime; 166 167 if (null === $origCode = $codeExtractor($file, $origCode)) { 168 continue; 169 } 170 171 set_time_limit(10); 172 173 ++$count; 174 175 if ($showProgress) { 176 echo substr(str_pad('Testing file ' . $count . ': ' . substr($file, strlen($dir)), 79), 0, 79), "\r"; 177 } 178 179 try { 180 $startTime = microtime(true); 181 $origStmts = $parser->parse($origCode); 182 $parseTime += microtime(true) - $startTime; 183 184 $origTokens = $parser->getTokens(); 185 186 $startTime = microtime(true); 187 $stmts = $cloningTraverser->traverse($origStmts); 188 $cloneTime += microtime(true) - $startTime; 189 190 $startTime = microtime(true); 191 $code = $prettyPrinter->printFormatPreserving($stmts, $origStmts, $origTokens); 192 $fpppTime += microtime(true) - $startTime; 193 194 if ($code !== $origCode) { 195 echo $file, ":\n Result of format-preserving pretty-print differs\n"; 196 if ($verbose) { 197 echo "FPPP output:\n=====\n$code\n=====\n\n"; 198 } 199 200 ++$fpppFail; 201 } 202 203 $startTime = microtime(true); 204 $code = "<?php\n" . $prettyPrinter->prettyPrint($stmts); 205 $ppTime += microtime(true) - $startTime; 206 207 try { 208 $startTime = microtime(true); 209 $ppStmts = $parser->parse($code); 210 $reparseTime += microtime(true) - $startTime; 211 212 $startTime = microtime(true); 213 $same = $nodeDumper->dump($stmts) == $nodeDumper->dump($ppStmts); 214 $compareTime += microtime(true) - $startTime; 215 216 if (!$same) { 217 echo $file, ":\n Result of initial parse and parse after pretty print differ\n"; 218 if ($verbose) { 219 echo "Pretty printer output:\n=====\n$code\n=====\n\n"; 220 } 221 222 ++$compareFail; 223 } 224 } catch (PhpParser\Error $e) { 225 echo $file, ":\n Parse of pretty print failed with message: {$e->getMessage()}\n"; 226 if ($verbose) { 227 echo "Pretty printer output:\n=====\n$code\n=====\n\n"; 228 } 229 230 ++$ppFail; 231 } 232 } catch (PhpParser\Error $e) { 233 echo $file, ":\n Parse failed with message: {$e->getMessage()}\n"; 234 235 ++$parseFail; 236 } catch (Throwable $e) { 237 echo $file, ":\n Unknown error occurred: $e\n"; 238 } 239} 240 241if (0 === $parseFail && 0 === $ppFail && 0 === $compareFail) { 242 $exit = 0; 243 echo "\n\n", 'All tests passed.', "\n"; 244} else { 245 $exit = 1; 246 echo "\n\n", '==========', "\n\n", 'There were: ', "\n"; 247 if (0 !== $parseFail) { 248 echo ' ', $parseFail, ' parse failures.', "\n"; 249 } 250 if (0 !== $ppFail) { 251 echo ' ', $ppFail, ' pretty print failures.', "\n"; 252 } 253 if (0 !== $fpppFail) { 254 echo ' ', $fpppFail, ' FPPP failures.', "\n"; 255 } 256 if (0 !== $compareFail) { 257 echo ' ', $compareFail, ' compare failures.', "\n"; 258 } 259} 260 261echo "\n", 262 'Tested files: ', $count, "\n", 263 "\n", 264 'Reading files took: ', $readTime, "\n", 265 'Parsing took: ', $parseTime, "\n", 266 'Cloning took: ', $cloneTime, "\n", 267 'FPPP took: ', $fpppTime, "\n", 268 'Pretty printing took: ', $ppTime, "\n", 269 'Reparsing took: ', $reparseTime, "\n", 270 'Comparing took: ', $compareTime, "\n", 271 "\n", 272 'Total time: ', microtime(true) - $totalStartTime, "\n", 273 'Maximum memory usage: ', memory_get_peak_usage(true), "\n"; 274 275exit($exit); 276