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