1<?php 2namespace phpdbg\testing { 3 4 /* 5 * Workaround ... 6 */ 7 if (!defined('DIR_SEP')) 8 define('DIR_SEP', '\\' . DIRECTORY_SEPARATOR); 9 10 /** 11 * TestConfigurationExceptions are thrown 12 * when the configuration prohibits tests executing 13 * 14 * @package phpdbg 15 * @subpackage testing 16 */ 17 class TestConfigurationException extends \Exception { 18 19 /** 20 * 21 * @param array Tests confguration 22 * @param message Exception message 23 * @param ... formatting parameters 24 */ 25 public function __construct() { 26 $argv = func_get_args(); 27 28 if (count($argv)) { 29 30 $this->config = array_shift($argv); 31 $this->message = vsprintf( 32 array_shift($argv), $argv); 33 } 34 } 35 } 36 37 /** 38 * 39 * @package phpdbg 40 * @subpackage testing 41 */ 42 class TestsConfiguration implements \ArrayAccess { 43 44 /** 45 * 46 * @param array basic configuration 47 * @param array argv 48 */ 49 public function __construct($config, $cmd) { 50 $this->options = $config; 51 while (($key = array_shift($cmd))) { 52 switch (substr($key, 0, 1)) { 53 case '-': switch(substr($key, 1, 1)) { 54 case '-': { 55 $arg = substr($key, 2); 56 if (($e=strpos($arg, '=')) !== false) { 57 $key = substr($arg, 0, $e); 58 $value = substr($arg, $e+1); 59 } else { 60 $key = $arg; 61 $value = array_shift($cmd); 62 } 63 64 if (isset($key) && isset($value)) { 65 switch ($key) { 66 case 'phpdbg': 67 case 'width': 68 $this->options[$key] = $value; 69 break; 70 71 default: { 72 if (isset($config[$key])) { 73 if (is_array($config[$key])) { 74 $this->options[$key][] = $value; 75 } else { 76 $this->options[$key] = array($config[$key], $value); 77 } 78 } else { 79 $this->options[$key] = $value; 80 } 81 } 82 } 83 84 } 85 } break; 86 87 default: 88 $this->flags[] = substr($key, 1); 89 } break; 90 } 91 } 92 93 if (!is_executable($this->options['phpdbg'])) { 94 throw new TestConfigurationException( 95 $this->options, 'phpdbg could not be found at the specified path (%s)', $this->options['phpdbg']); 96 } else $this->options['phpdbg'] = realpath($this->options['phpdbg']); 97 98 $this->options['width'] = (integer) $this->options['width']; 99 100 /* display properly, all the time */ 101 if ($this->options['width'] < 50) { 102 $this->options['width'] = 50; 103 } 104 105 /* calculate column widths */ 106 $this->options['lwidth'] = ceil($this->options['width'] / 3); 107 $this->options['rwidth'] = ceil($this->options['width'] - $this->options['lwidth']) - 5; 108 } 109 110 public function hasFlag($flag) { 111 return in_array( 112 $flag, $this->flags); 113 } 114 115 public function offsetExists($offset) { return isset($this->options[$offset]); } 116 public function offsetGet($offset) { return $this->options[$offset]; } 117 public function offsetUnset($offset) { unset($this->options[$offset]); } 118 public function offsetSet($offset, $data) { $this->options[$offset] = $data; } 119 120 protected $options = array(); 121 protected $flags = array(); 122 } 123 124 /** 125 * Tests is the console programming API for the test suite 126 * 127 * @package phpdbg 128 * @subpackage testing 129 */ 130 class Tests { 131 132 /** 133 * Construct the console object 134 * 135 * @param array basic configuration 136 * @param array command line 137 */ 138 public function __construct(TestsConfiguration $config) { 139 $this->config = $config; 140 141 if ($this->config->hasFlag('help') || 142 $this->config->hasFlag('h')) { 143 $this->showUsage(); 144 exit; 145 } 146 } 147 148 /** 149 * Find valid paths as specified by configuration 150 * 151 */ 152 public function findPaths($in = null) { 153 $paths = array(); 154 $where = ($in != null) ? array($in) : $this->config['path']; 155 156 foreach ($where as $path) { 157 if ($path) { 158 if (is_dir($path)) { 159 $paths[] = $path; 160 foreach (scandir($path) as $child) { 161 if ($child != '.' && $child != '..') { 162 $paths = array_merge( 163 $paths, $this->findPaths("$path/$child")); 164 } 165 } 166 } 167 } 168 } 169 170 return $paths; 171 } 172 173 /** 174 * 175 * @param string the path to log 176 */ 177 public function logPath($path) { 178 printf( 179 '%s [%s]%s', 180 str_repeat( 181 '-', $this->config['width'] - strlen($path)), 182 $path, PHP_EOL); 183 } 184 185 /** 186 * 187 * @param string the path to log 188 */ 189 public function logPathStats($path) { 190 if (!isset($this->stats[$path])) { 191 return; 192 } 193 194 $total = array_sum($this->stats[$path]); 195 196 if ($total) { 197 @$this->totals[true] += $this->stats[$path][true]; 198 @$this->totals[false] += $this->stats[$path][false]; 199 200 $stats = @sprintf( 201 "%d/%d %%%d", 202 $this->stats[$path][true], 203 $this->stats[$path][false], 204 (100 / $total) * $this->stats[$path][true]); 205 206 printf( 207 '%s [%s]%s', 208 str_repeat( 209 ' ', $this->config['width'] - strlen($stats)), 210 $stats, PHP_EOL); 211 212 printf("%s%s", str_repeat('-', $this->config['width']+3), PHP_EOL); 213 printf("%s", PHP_EOL); 214 } 215 } 216 217 /** 218 * 219 */ 220 public function logStats() { 221 $total = array_sum($this->totals); 222 $stats = @sprintf( 223 "%d/%d %%%d", 224 $this->totals[true], 225 $this->totals[false], 226 (100 / $total) * $this->totals[true]); 227 printf( 228 '%s [%s]%s', 229 str_repeat( 230 ' ', $this->config['width'] - strlen($stats)), 231 $stats, PHP_EOL); 232 233 } 234 235 /** 236 * 237 */ 238 protected function showUsage() { 239 printf('usage: php %s [flags] [options]%s', $this->config['exec'], PHP_EOL); 240 printf('[options]:%s', PHP_EOL); 241 printf("\t--path\t\tadd a path to scan outside of tests directory%s", PHP_EOL); 242 printf("\t--width\t\tset line width%s", PHP_EOL); 243 printf("\t--options\toptions to pass to phpdbg%s", PHP_EOL); 244 printf("\t--phpdbg\tpath to phpdbg binary%s", PHP_EOL); 245 printf('[flags]:%s', PHP_EOL); 246 printf("\t-diff2stdout\t\twrite diff to stdout instead of files%s", PHP_EOL); 247 printf("\t-nodiff\t\tdo not write diffs on failure%s", PHP_EOL); 248 printf("\t-nolog\t\tdo not write logs on failure%s", PHP_EOL); 249 printf('[examples]:%s', PHP_EOL); 250 printf("\tphp %s --phpdbg=/usr/local/bin/phpdbg --path=/usr/src/phpdbg/tests --options -n%s", 251 $this->config['exec'], PHP_EOL); 252 253 } 254 255 /** 256 * Find valid tests at the specified path (assumed valid) 257 * 258 * @param string a valid path 259 */ 260 public function findTests($path) { 261 $tests = array(); 262 263 foreach (scandir($path) as $file) { 264 if ($file == '.' || $file == '..') 265 continue; 266 267 $test = sprintf('%s/%s', $path, $file); 268 269 if (preg_match('~\.test$~', $test)) { 270 $tests[] = new Test($this->config, $test); 271 } 272 } 273 274 return $tests; 275 } 276 277 /** 278 * 279 * @param Test the test to log 280 */ 281 public function logTest($path, Test $test) { 282 @$this->stats[$path][($result=$test->getResult())]++; 283 284 printf( 285 "%-{$this->config['lwidth']}s %-{$this->config['rwidth']}s [%s]%s", 286 $test->name, 287 $test->purpose, 288 $result ? "PASS" : "FAIL", 289 PHP_EOL); 290 291 return $result; 292 } 293 294 protected $config; 295 } 296 297 class Test { 298 /* 299 * Expect exact line for line match 300 */ 301 const EXACT = 0x00000001; 302 303 /* 304 * Expect strpos() !== false 305 */ 306 const STRING = 0x00000010; 307 308 /* 309 * Expect stripos() !== false 310 */ 311 const CISTRING = 0x00000100; 312 313 /* 314 * Formatted output 315 */ 316 const FORMAT = 0x00001000; 317 318 /** 319 * Format specifiers 320 */ 321 private static $format = array( 322 'search' => array( 323 '%e', 324 '%s', 325 '%S', 326 '%a', 327 '%A', 328 '%w', 329 '%i', 330 '%d', 331 '%x', 332 '%f', 333 '%c', 334 '%t', 335 '%T' 336 ), 337 'replace' => array( 338 DIR_SEP, 339 '[^\r\n]+', 340 '[^\r\n]*', 341 '.+', 342 '.*', 343 '\s*', 344 '[+-]?\d+', 345 '\d+', 346 '[0-9a-fA-F]+', 347 '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?', 348 '.', 349 '\t', 350 '\t+' 351 ) 352 ); 353 354 /** 355 * Constructs a new Test object given a specilized phpdbginit file 356 * 357 * @param array configuration 358 * @param string file 359 */ 360 public function __construct(TestsConfiguration $config, $file) { 361 if (($handle = fopen($file, 'r'))) { 362 while (($line = fgets($handle))) { 363 $trim = trim($line); 364 365 switch (substr($trim, 0, 1)) { 366 case '#': if (($chunks = array_map('trim', preg_split('~:~', substr($trim, 1), 2)))) { 367 if (property_exists($this, $chunks[0])) { 368 switch ($chunks[0]) { 369 case 'expect': { 370 if ($chunks[1]) { 371 switch (strtoupper($chunks[1])) { 372 case 'TEST::EXACT': 373 case 'EXACT': { $this->expect = TEST::EXACT; } break; 374 375 case 'TEST::STRING': 376 case 'STRING': { $this->expect = TEST::STRING; } break; 377 378 case 'TEST::CISTRING': 379 case 'CISTRING': { $this->expect = TEST::CISTRING; } break; 380 381 case 'TEST::FORMAT': 382 case 'FORMAT': { $this->expect = TEST::FORMAT; } break; 383 384 default: 385 throw new TestConfigurationException( 386 $this->config, "unknown type of expectation (%s)", $chunks[1]); 387 } 388 } 389 } break; 390 391 default: { 392 $this->$chunks[0] = $chunks[1]; 393 } 394 } 395 } else switch(substr($trim, 1, 1)) { 396 case '#': { /* do nothing */ } break; 397 398 default: { 399 $line = preg_replace( 400 "~(\r\n)~", "\n", substr($trim, 1)); 401 402 $line = trim($line); 403 404 switch ($this->expect) { 405 case TEST::FORMAT: 406 $this->match[] = str_replace( 407 self::$format['search'], 408 self::$format['replace'], preg_quote($line)); 409 break; 410 411 default: $this->match[] = $line; 412 } 413 } 414 } 415 } break; 416 417 default: 418 break 2; 419 } 420 } 421 fclose($handle); 422 423 $this->config = $config; 424 $this->file = $file; 425 } 426 } 427 428 /** 429 * Obvious!! 430 * 431 */ 432 public function getResult() { 433 $options = sprintf('-i%s -nqb', $this->file); 434 435 if ($this->options) { 436 $options = sprintf( 437 '%s %s %s', 438 $options, 439 $this->config['options'], 440 $this->options 441 ); 442 } else { 443 $options = sprintf( 444 '%s %s', $options, $this->config['options'] 445 ); 446 } 447 448 $result = `{$this->config['phpdbg']} {$options}`; 449 450 if ($result) { 451 foreach (preg_split('~(\r|\n)~', $result) as $num => $line) { 452 if (!$line && !isset($this->match[$num])) 453 continue; 454 455 switch ($this->expect) { 456 case TEST::EXACT: { 457 if (strcmp($line, $this->match[$num]) !== 0) { 458 $this->diff['wants'][$num] = &$this->match[$num]; 459 $this->diff['gets'][$num] = $line; 460 } 461 } continue 2; 462 463 case TEST::STRING: { 464 if (strpos($line, $this->match[$num]) === false) { 465 $this->diff['wants'][$num] = &$this->match[$num]; 466 $this->diff['gets'][$num] = $line; 467 } 468 } continue 2; 469 470 case TEST::CISTRING: { 471 if (stripos($line, $this->match[$num]) === false) { 472 $this->diff['wants'][$num] = &$this->match[$num]; 473 $this->diff['gets'][$num] = $line; 474 } 475 } continue 2; 476 477 case TEST::FORMAT: { 478 $line = trim($line); 479 if (!preg_match("/^{$this->match[$num]}\$/s", $line)) { 480 $this->diff['wants'][$num] = &$this->match[$num]; 481 $this->diff['gets'][$num] = $line; 482 } 483 } continue 2; 484 } 485 } 486 } 487 488 $this->writeLog($result); 489 $this->writeDiff(); 490 491 return (count($this->diff) == 0); 492 } 493 494 /** 495 * Write diff to disk if configuration allows it 496 * 497 */ 498 protected function writeDiff() { 499 if (count($this->diff['wants'])) { 500 if (!$this->config->hasFlag('nodiff')) { 501 if ($this->config->hasFlag('diff2stdout')) { 502 $difffile = "php://stdout"; 503 file_put_contents($difffile, "====DIFF====\n"); 504 } else { 505 $difffile = sprintf( 506 '%s/%s.diff', 507 dirname($this->file), basename($this->file)); 508 } 509 510 if (($diff = fopen($difffile, 'w+'))) { 511 512 foreach ($this->diff['wants'] as $line => $want) { 513 $got = $this->diff['gets'][$line]; 514 515 fprintf( 516 $diff, '(%d) -%s%s', $line+1, $want, PHP_EOL); 517 fprintf( 518 $diff, '(%d) +%s%s', $line+1, $got, PHP_EOL); 519 } 520 521 fclose($diff); 522 } 523 } 524 } else unlink($diff); 525 } 526 527 /** 528 * Write log to disk if configuration allows it 529 * 530 */ 531 protected function writeLog($result = null) { 532 $log = sprintf( 533 '%s/%s.log', 534 dirname($this->file), basename($this->file)); 535 536 if (count($this->diff) && $result) { 537 if (!in_array('nolog', $this->config['flags'])) { 538 @file_put_contents( 539 $log, $result); 540 } 541 } else unlink($log); 542 } 543 544 public $name; 545 public $purpose; 546 public $file; 547 public $options; 548 public $expect; 549 550 protected $match; 551 protected $diff; 552 protected $stats; 553 protected $totals; 554 } 555} 556 557namespace { 558 use \phpdbg\Testing\Test; 559 use \phpdbg\Testing\Tests; 560 use \phpdbg\Testing\TestsConfiguration; 561 562 $cwd = dirname(__FILE__); 563 $cmd = $_SERVER['argv']; 564 565 $retval = 0; 566 567 { 568 $config = new TestsConfiguration(array( 569 'exec' => realpath(array_shift($cmd)), 570 'phpdbg' => realpath(sprintf( 571 '%s/../phpdbg', $cwd 572 )), 573 'path' => array( 574 realpath(dirname(__FILE__)) 575 ), 576 'flags' => array(), 577 'width' => 75 578 ), $cmd); 579 580 $tests = new Tests($config); 581 582 foreach ($tests->findPaths() as $path) { 583 $tests->logPath($path); 584 585 foreach ($tests->findTests($path) as $test) { 586 $retval |= !$tests->logTest($path, $test); 587 } 588 589 $tests->logPathStats($path); 590 } 591 592 $tests->logStats(); 593 } 594 595 die($retval); 596} 597?> 598