config = array_shift($argv); $this->message = vsprintf( array_shift($argv), $argv); } } } /** * * @package phpdbg * @subpackage testing */ class TestsConfiguration implements \ArrayAccess { /** * * @param array basic configuration * @param array argv */ public function __construct($config, $cmd) { $this->options = $config; while (($key = array_shift($cmd))) { switch (substr($key, 0, 1)) { case '-': switch(substr($key, 1, 1)) { case '-': { $arg = substr($key, 2); if (($e=strpos($arg, '=')) !== false) { $key = substr($arg, 0, $e); $value = substr($arg, $e+1); } else { $key = $arg; $value = array_shift($cmd); } if (isset($key) && isset($value)) { switch ($key) { case 'phpdbg': case 'width': $this->options[$key] = $value; break; default: { if (isset($config[$key])) { if (is_array($config[$key])) { $this->options[$key][] = $value; } else { $this->options[$key] = array($config[$key], $value); } } else { $this->options[$key] = $value; } } } } } break; default: $this->flags[] = substr($key, 1); } break; } } if (!is_executable($this->options['phpdbg'])) { throw new TestConfigurationException( $this->options, 'phpdbg could not be found at the specified path (%s)', $this->options['phpdbg']); } else $this->options['phpdbg'] = realpath($this->options['phpdbg']); $this->options['width'] = (integer) $this->options['width']; /* display properly, all the time */ if ($this->options['width'] < 50) { $this->options['width'] = 50; } /* calculate column widths */ $this->options['lwidth'] = ceil($this->options['width'] / 3); $this->options['rwidth'] = ceil($this->options['width'] - $this->options['lwidth']) - 5; } public function hasFlag($flag) { return in_array( $flag, $this->flags); } public function offsetExists($offset) { return isset($this->options[$offset]); } public function offsetGet($offset) { return $this->options[$offset]; } public function offsetUnset($offset) { unset($this->options[$offset]); } public function offsetSet($offset, $data) { $this->options[$offset] = $data; } protected $options = array(); protected $flags = array(); } /** * Tests is the console programming API for the test suite * * @package phpdbg * @subpackage testing */ class Tests { /** * Construct the console object * * @param array basic configuration * @param array command line */ public function __construct(TestsConfiguration $config) { $this->config = $config; if ($this->config->hasFlag('help') || $this->config->hasFlag('h')) { $this->showUsage(); exit; } } /** * Find valid paths as specified by configuration * */ public function findPaths($in = null) { $paths = array(); $where = ($in != null) ? array($in) : $this->config['path']; foreach ($where as $path) { if ($path) { if (is_dir($path)) { $paths[] = $path; foreach (scandir($path) as $child) { if ($child != '.' && $child != '..') { $paths = array_merge( $paths, $this->findPaths("$path/$child")); } } } } } return $paths; } /** * * @param string the path to log */ public function logPath($path) { printf( '%s [%s]%s', str_repeat( '-', $this->config['width'] - strlen($path)), $path, PHP_EOL); } /** * * @param string the path to log */ public function logPathStats($path) { if (!isset($this->stats[$path])) { return; } $total = array_sum($this->stats[$path]); if ($total) { @$this->totals[true] += $this->stats[$path][true]; @$this->totals[false] += $this->stats[$path][false]; $stats = @sprintf( "%d/%d %%%d", $this->stats[$path][true], $this->stats[$path][false], (100 / $total) * $this->stats[$path][true]); printf( '%s [%s]%s', str_repeat( ' ', $this->config['width'] - strlen($stats)), $stats, PHP_EOL); printf("%s%s", str_repeat('-', $this->config['width']+3), PHP_EOL); printf("%s", PHP_EOL); } } /** * */ public function logStats() { $total = array_sum($this->totals); $stats = @sprintf( "%d/%d %%%d", $this->totals[true], $this->totals[false], (100 / $total) * $this->totals[true]); printf( '%s [%s]%s', str_repeat( ' ', $this->config['width'] - strlen($stats)), $stats, PHP_EOL); } /** * */ protected function showUsage() { printf('usage: php %s [flags] [options]%s', $this->config['exec'], PHP_EOL); printf('[options]:%s', PHP_EOL); printf("\t--path\t\tadd a path to scan outside of tests directory%s", PHP_EOL); printf("\t--width\t\tset line width%s", PHP_EOL); printf("\t--options\toptions to pass to phpdbg%s", PHP_EOL); printf("\t--phpdbg\tpath to phpdbg binary%s", PHP_EOL); printf('[flags]:%s', PHP_EOL); printf("\t-diff2stdout\t\twrite diff to stdout instead of files%s", PHP_EOL); printf("\t-nodiff\t\tdo not write diffs on failure%s", PHP_EOL); printf("\t-nolog\t\tdo not write logs on failure%s", PHP_EOL); printf('[examples]:%s', PHP_EOL); printf("\tphp %s --phpdbg=/usr/local/bin/phpdbg --path=/usr/src/phpdbg/tests --options -n%s", $this->config['exec'], PHP_EOL); } /** * Find valid tests at the specified path (assumed valid) * * @param string a valid path */ public function findTests($path) { $tests = array(); foreach (scandir($path) as $file) { if ($file == '.' || $file == '..') continue; $test = sprintf('%s/%s', $path, $file); if (preg_match('~\.test$~', $test)) { $tests[] = new Test($this->config, $test); } } return $tests; } /** * * @param Test the test to log */ public function logTest($path, Test $test) { @$this->stats[$path][($result=$test->getResult())]++; printf( "%-{$this->config['lwidth']}s %-{$this->config['rwidth']}s [%s]%s", $test->name, $test->purpose, $result ? "PASS" : "FAIL", PHP_EOL); return $result; } protected $config; } class Test { /* * Expect exact line for line match */ const EXACT = 0x00000001; /* * Expect strpos() !== false */ const STRING = 0x00000010; /* * Expect stripos() !== false */ const CISTRING = 0x00000100; /* * Formatted output */ const FORMAT = 0x00001000; /** * Format specifiers */ private static $format = array( 'search' => array( '%e', '%s', '%S', '%a', '%A', '%w', '%i', '%d', '%x', '%f', '%c', '%t', '%T' ), 'replace' => array( DIR_SEP, '[^\r\n]+', '[^\r\n]*', '.+', '.*', '\s*', '[+-]?\d+', '\d+', '[0-9a-fA-F]+', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?', '.', '\t', '\t+' ) ); /** * Constructs a new Test object given a specilized phpdbginit file * * @param array configuration * @param string file */ public function __construct(TestsConfiguration $config, $file) { if (($handle = fopen($file, 'r'))) { while (($line = fgets($handle))) { $trim = trim($line); switch (substr($trim, 0, 1)) { case '#': if (($chunks = array_map('trim', preg_split('~:~', substr($trim, 1), 2)))) { if (property_exists($this, $chunks[0])) { switch ($chunks[0]) { case 'expect': { if ($chunks[1]) { switch (strtoupper($chunks[1])) { case 'TEST::EXACT': case 'EXACT': { $this->expect = TEST::EXACT; } break; case 'TEST::STRING': case 'STRING': { $this->expect = TEST::STRING; } break; case 'TEST::CISTRING': case 'CISTRING': { $this->expect = TEST::CISTRING; } break; case 'TEST::FORMAT': case 'FORMAT': { $this->expect = TEST::FORMAT; } break; default: throw new TestConfigurationException( $this->config, "unknown type of expectation (%s)", $chunks[1]); } } } break; default: { $this->$chunks[0] = $chunks[1]; } } } else switch(substr($trim, 1, 1)) { case '#': { /* do nothing */ } break; default: { $line = preg_replace( "~(\r\n)~", "\n", substr($trim, 1)); $line = trim($line); switch ($this->expect) { case TEST::FORMAT: $this->match[] = str_replace( self::$format['search'], self::$format['replace'], preg_quote($line)); break; default: $this->match[] = $line; } } } } break; default: break 2; } } fclose($handle); $this->config = $config; $this->file = $file; } } /** * Obvious!! * */ public function getResult() { $options = sprintf('-i%s -nqb', $this->file); if ($this->options) { $options = sprintf( '%s %s %s', $options, $this->config['options'], $this->options ); } else { $options = sprintf( '%s %s', $options, $this->config['options'] ); } $result = `{$this->config['phpdbg']} {$options}`; if ($result) { foreach (preg_split('~(\r|\n)~', $result) as $num => $line) { if (!$line && !isset($this->match[$num])) continue; switch ($this->expect) { case TEST::EXACT: { if (strcmp($line, $this->match[$num]) !== 0) { $this->diff['wants'][$num] = &$this->match[$num]; $this->diff['gets'][$num] = $line; } } continue 2; case TEST::STRING: { if (strpos($line, $this->match[$num]) === false) { $this->diff['wants'][$num] = &$this->match[$num]; $this->diff['gets'][$num] = $line; } } continue 2; case TEST::CISTRING: { if (stripos($line, $this->match[$num]) === false) { $this->diff['wants'][$num] = &$this->match[$num]; $this->diff['gets'][$num] = $line; } } continue 2; case TEST::FORMAT: { $line = trim($line); if (!preg_match("/^{$this->match[$num]}\$/s", $line)) { $this->diff['wants'][$num] = &$this->match[$num]; $this->diff['gets'][$num] = $line; } } continue 2; } } } $this->writeLog($result); $this->writeDiff(); return (count($this->diff) == 0); } /** * Write diff to disk if configuration allows it * */ protected function writeDiff() { if (count($this->diff['wants'])) { if (!$this->config->hasFlag('nodiff')) { if ($this->config->hasFlag('diff2stdout')) { $difffile = "php://stdout"; file_put_contents($difffile, "====DIFF====\n"); } else { $difffile = sprintf( '%s/%s.diff', dirname($this->file), basename($this->file)); } if (($diff = fopen($difffile, 'w+'))) { foreach ($this->diff['wants'] as $line => $want) { $got = $this->diff['gets'][$line]; fprintf( $diff, '(%d) -%s%s', $line+1, $want, PHP_EOL); fprintf( $diff, '(%d) +%s%s', $line+1, $got, PHP_EOL); } fclose($diff); } } } else unlink($diff); } /** * Write log to disk if configuration allows it * */ protected function writeLog($result = null) { $log = sprintf( '%s/%s.log', dirname($this->file), basename($this->file)); if (count($this->diff) && $result) { if (!in_array('nolog', $this->config['flags'])) { @file_put_contents( $log, $result); } } else unlink($log); } public $name; public $purpose; public $file; public $options; public $expect; protected $match; protected $diff; protected $stats; protected $totals; } } namespace { use \phpdbg\Testing\Test; use \phpdbg\Testing\Tests; use \phpdbg\Testing\TestsConfiguration; $cwd = dirname(__FILE__); $cmd = $_SERVER['argv']; $retval = 0; { $config = new TestsConfiguration(array( 'exec' => realpath(array_shift($cmd)), 'phpdbg' => realpath(sprintf( '%s/../phpdbg', $cwd )), 'path' => array( realpath(dirname(__FILE__)) ), 'flags' => array(), 'width' => 75 ), $cmd); $tests = new Tests($config); foreach ($tests->findPaths() as $path) { $tests->logPath($path); foreach ($tests->findTests($path) as $test) { $retval |= !$tests->logTest($path, $test); } $tests->logPathStats($path); } $tests->logStats(); } die($retval); } ?>