xref: /PHP-5.6/sapi/phpdbg/tests/run-tests.php (revision f677889a)
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