code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester;

/**
 * Data provider helpers.
 */
class DataProvider
{

	/**
	 * @param  string  path to data provider file
	 * @param  string  filtering condition
	 * @return array
	 * @throws \Exception
	 */
	public static function load($file, $query = NULL)
	{
		if (!is_file($file)) {
			throw new \Exception("Missing data-provider file '$file'.");
		}

		if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
			$data = call_user_func(function () {
				return require func_get_arg(0);
			}, realpath($file));

			if ($data instanceof \Traversable) {
				$data = iterator_to_array($data);
			} elseif (!is_array($data)) {
				throw new \Exception("Data provider file '$file' did not return array or Traversable.");
			}

		} else {
			$data = @parse_ini_file($file, TRUE); // @ is escalated to exception
			if ($data === FALSE) {
				throw new \Exception("Cannot parse data-provider file '$file'.");
			}
		}

		foreach ($data as $key => $value) {
			if (!self::testQuery($key, $query)) {
				unset($data[$key]);
			}
		}

		if (!$data) {
			throw new \Exception("No records in data-provider file '$file'" . ($query ? " for query '$query'" : '') . '.');
		}
		return $data;
	}

	/**
	 * @param  string  tested subject
	 * @param  string  condition
	 * @return bool
	 */
	public static function testQuery($input, $query)
	{
		static $replaces = array('' => '=', '=>' => '>=', '=<' => '<=');
		$tokens = preg_split('#\s+#', $input);
		preg_match_all('#\s*,?\s*(<=|=<|<|==|=|!=|<>|>=|=>|>)?\s*([^\s,]+)#A', $query, $queryParts, PREG_SET_ORDER);
		foreach ($queryParts as $queryPart) {
			list(, $operator, $operand) = $queryPart;
			$operator = isset($replaces[$operator]) ? $replaces[$operator] : $operator;
			$token = array_shift($tokens);
			$res = preg_match('#^[0-9.]+\z#', $token)
				? version_compare($token, $operand, $operator)
				: self::compare($token, $operator, $operand);
			if (!$res) {
				return FALSE;
			}
		}
		return TRUE;
	}

	private static function compare($l, $operator, $r)
	{
		switch ($operator) {
		case '>':
			return $l > $r;
		case '=>':
		case '>=':
			return $l >= $r;
		case '<':
			return $l < $r;
		case '=<':
		case '<=':
			return $l <= $r;
		case '=':
		case '==':
			return $l == $r;
		case '!':
		case '!=':
		case '<>':
			return $l != $r;
		}
		throw new \InvalidArgumentException("Unknown operator $operator.");
	}

	/**
	 * @internal
	 * @param  string
	 * @param  string
	 * @return array
	 * @throws \Exception
	 */
	public static function parseAnnotation($annotation, $file)
	{
		if (!preg_match('#^(\??)\s*([^,\s]+)\s*,?\s*(\S.*)?()#', $annotation, $m)) {
			throw new \Exception("Invalid @dataProvider value '$annotation'.");
		}
		return array(dirname($file) . DIRECTORY_SEPARATOR . $m[2], $m[3], (bool) $m[1]);
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester;

/**
 * DomQuery simplifies querying (X)HTML documents.
 */
class DomQuery extends \SimpleXMLElement
{

	/**
	 * @return DomQuery
	 */
	public static function fromHtml($html)
	{
		if (strpos($html, '<') === FALSE) {
			$html = '<body>' . $html;
		}

		$html = preg_replace('#<(keygen|source|track|wbr)(?=\s|>)("[^"]*"|\'[^\']*\'|[^"\'>]+)*+(?<!/)>#', '<$1$2 />', $html);

		$dom = new \DOMDocument();
		$old = libxml_use_internal_errors(TRUE);
		libxml_clear_errors();
		$dom->loadHTML($html);
		$errors = libxml_get_errors();
		libxml_use_internal_errors($old);

		$re = '#Tag (article|aside|audio|bdi|canvas|data|datalist|figcaption|figure|footer|header|keygen|main|mark'
			. '|meter|nav|output|progress|rb|rp|rt|rtc|ruby|section|source|template|time|track|video|wbr) invalid#';
		foreach ($errors as $error) {
			if (!preg_match($re, $error->message)) {
				trigger_error(__METHOD__ . ": $error->message on line $error->line.", E_USER_WARNING);
			}
		}
		return simplexml_import_dom($dom, __CLASS__);
	}

	/**
	 * @return DomQuery
	 */
	public static function fromXml($xml)
	{
		return simplexml_load_string($xml, __CLASS__);
	}

	/**
	 * Returns array of descendants filtered by a selector.
	 * @return DomQuery[]
	 */
	public function find($selector)
	{
		return $this->xpath(self::css2xpath($selector));
	}

	/**
	 * Check the current document against a selector.
	 * @return bool
	 */
	public function has($selector)
	{
		return (bool) $this->find($selector);
	}

	/**
	 * Transforms CSS expression to XPath.
	 * @return string
	 */
	public static function css2xpath($css)
	{
		$xpath = '//*';
		preg_match_all('/
			([#.:]?)([a-z][a-z0-9_-]*)|               # id, class, pseudoclass (1,2)
			\[
				([a-z0-9_-]+)
				(?:
					([~*^$]?)=(
						"[^"]*"|
						\'[^\']*\'|
						[^\]]+
					)
				)?
			\]|                                       # [attr=val] (3,4,5)
			\s*([>,+~])\s*|                           # > , + ~ (6)
			(\s+)|                                    # whitespace (7)
			(\*)                                      # * (8)
		/ix', trim($css), $matches, PREG_SET_ORDER);
		foreach ($matches as $m) {
			if ($m[1] === '#') { // #ID
				$xpath .= "[@id='$m[2]']";
			} elseif ($m[1] === '.') { // .class
				$xpath .= "[contains(concat(' ', normalize-space(@class), ' '), ' $m[2] ')]";
			} elseif ($m[1] === ':') { // :pseudo-class
				throw new \InvalidArgumentException('Not implemented.');
			} elseif ($m[2]) { // tag
				$xpath = rtrim($xpath, '*') . $m[2];
			} elseif ($m[3]) { // [attribute]
				$attr = '@' . strtolower($m[3]);
				if (!isset($m[5])) {
					$xpath .= "[$attr]";
					continue;
				}
				$val = trim($m[5], '"\'');
				if ($m[4] === '') {
					$xpath .= "[$attr='$val']";
				} elseif ($m[4] === '~') {
					$xpath .= "[contains(concat(' ', normalize-space($attr), ' '), ' $val ')]";
				} elseif ($m[4] === '*') {
					$xpath .= "[contains($attr, '$val')]";
				} elseif ($m[4] === '^') {
					$xpath .= "[starts-with($attr, '$val')]";
				} elseif ($m[4] === '$') {
					$xpath .= "[substring($attr, string-length($attr)-0)='$val']";
				}
			} elseif ($m[6] === '>') {
				$xpath .= '/*';
			} elseif ($m[6] === ',') {
				$xpath .= '|//*';
			} elseif ($m[6] === '~') {
				$xpath .= '/following-sibling::*';
			} elseif ($m[6] === '+') {
				throw new \InvalidArgumentException('Not implemented.');
			} elseif ($m[7]) {
				$xpath .= '//*';
			}
		}
		return $xpath;
	}

}

repo_name: Cerx/ToDo

license: mit


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester;

/**
 * Dumps PHP variables.
 */
class Dumper
{
	public static $maxLength = 70;
	public static $maxDepth = 10;
	public static $dumpDir = 'output';
	public static $maxPathSegments = 3;

	/**
	 * Dumps information about a variable in readable format.
	 * @param  mixed  variable to dump
	 * @return string
	 */
	public static function toLine($var)
	{
		static $table;
		if ($table === NULL) {
			foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) {
				$table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
			}
			$table['\\'] = '\\\\';
			$table["\r"] = '\r';
			$table["\n"] = '\n';
			$table["\t"] = '\t';
		}

		if (is_bool($var)) {
			return $var ? 'TRUE' : 'FALSE';

		} elseif ($var === NULL) {
			return 'NULL';

		} elseif (is_int($var)) {
			return "$var";

		} elseif (is_float($var)) {
			if (!is_finite($var)) {
				return str_replace('.0', '', var_export($var, TRUE)); // workaround for PHP 7.0.2
			}
			$var = str_replace(',', '.', "$var");
			return strpos($var, '.') === FALSE ? $var . '.0' : $var; // workaround for PHP < 7.0.2

		} elseif (is_string($var)) {
			if (preg_match('#^(.{' . self::$maxLength . '}).#su', $var, $m)) {
				$var = "$m[1]...";
			} elseif (strlen($var) > self::$maxLength) {
				$var = substr($var, 0, self::$maxLength) . '...';
			}
			return (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $var) || preg_last_error() ? '"' . strtr($var, $table) . '"' : "'$var'");

		} elseif (is_array($var)) {
			$out = '';
			$counter = 0;
			foreach ($var as $k => & $v) {
				$out .= ($out === '' ? '' : ', ');
				if (strlen($out) > self::$maxLength) {
					$out .= '...';
					break;
				}
				$out .= ($k === $counter ? '' : self::toLine($k) . ' => ')
					. (is_array($v) && $v ? 'array(...)' : self::toLine($v));
				$counter = is_int($k) ? max($k + 1, $counter) : $counter;
			}
			return "array($out)";

		} elseif ($var instanceof \Exception || $var instanceof \Throwable) {
			return 'Exception ' . get_class($var) . ': ' . ($var->getCode() ? '#' . $var->getCode() . ' ' : '') . $var->getMessage();

		} elseif (is_object($var)) {
			return self::objectToLine($var);

		} elseif (is_resource($var)) {
			return 'resource(' . get_resource_type($var) . ')';

		} else {
			return 'unknown type';
		}
	}

	/**
	 * Formats object to line.
	 * @param  object
	 * @return string
	 */
	private static function objectToLine($object)
	{
		$line = get_class($object);
		if ($object instanceof \DateTime || $object instanceof \DateTimeInterface) {
			$line .= '(' . $object->format('Y-m-d H:i:s O') . ')';
		}

		return $line . '(' . self::hash($object) . ')';
	}

	/**
	 * Dumps variable in PHP format.
	 * @param  mixed  variable to dump
	 * @return string
	 */
	public static function toPhp($var)
	{
		return self::_toPhp($var);
	}

	/**
	 * Returns object's stripped hash.
	 * @param  object
	 * @return string
	 */
	private static function hash($object)
	{
		return '#' . substr(md5(spl_object_hash($object)), 0, 4);
	}

	/**
	 * @return string
	 */
	private static function _toPhp(&$var, & $list = array(), $level = 0, & $line = 1)
	{
		if (is_float($var)) {
			$var = str_replace(',', '.', "$var");
			return strpos($var, '.') === FALSE ? $var . '.0' : $var;

		} elseif (is_bool($var)) {
			return $var ? 'TRUE' : 'FALSE';

		} elseif (is_string($var) && (preg_match('#[^\x09\x20-\x7E\xA0-\x{10FFFF}]#u', $var) || preg_last_error())) {
			static $table;
			if ($table === NULL) {
				foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) {
					$table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
				}
				$table['\\'] = '\\\\';
				$table["\r"] = '\r';
				$table["\n"] = '\n';
				$table["\t"] = '\t';
				$table['$'] = '\$';
				$table['"'] = '\"';
			}
			return '"' . strtr($var, $table) . '"';

		} elseif (is_array($var)) {
			$space = str_repeat("\t", $level);

			static $marker;
			if ($marker === NULL) {
				$marker = uniqid("\x00", TRUE);
			}
			if (empty($var)) {
				$out = '';

			} elseif ($level > self::$maxDepth || isset($var[$marker])) {
				return '/* Nesting level too deep or recursive dependency */';

			} else {
				$out = "\n$space";
				$outShort = '';
				$var[$marker] = TRUE;
				$oldLine = $line;
				$line++;
				$counter = 0;
				foreach ($var as $k => &$v) {
					if ($k !== $marker) {
						$item = ($k === $counter ? '' : self::_toPhp($k, $list, $level + 1, $line) . ' => ') . self::_toPhp($v, $list, $level + 1, $line);
						$counter = is_int($k) ? max($k + 1, $counter) : $counter;
						$outShort .= ($outShort === '' ? '' : ', ') . $item;
						$out .= "\t$item,\n$space";
						$line++;
					}
				}
				unset($var[$marker]);
				if (strpos($outShort, "\n") === FALSE && strlen($outShort) < self::$maxLength) {
					$line = $oldLine;
					$out = $outShort;
				}
			}
			return 'array(' . $out . ')';

		} elseif ($var instanceof \Closure) {
			$rc = new \ReflectionFunction($var);
			return "/* Closure defined in file {$rc->getFileName()} on line {$rc->getStartLine()} */";

		} elseif (is_object($var)) {
			if (PHP_VERSION_ID >= 70000 && ($rc = new \ReflectionObject($var)) && $rc->isAnonymous()) {
				return "/* Anonymous class defined in file {$rc->getFileName()} on line {$rc->getStartLine()} */";
			}
			$arr = (array) $var;
			$space = str_repeat("\t", $level);
			$class = get_class($var);
			$used = & $list[spl_object_hash($var)];

			if (empty($arr)) {
				$out = '';

			} elseif ($used) {
				return "/* $class dumped on line $used */";

			} elseif ($level > self::$maxDepth) {
				return '/* Nesting level too deep */';

			} else {
				$out = "\n";
				$used = $line;
				$line++;
				foreach ($arr as $k => &$v) {
					if ($k[0] === "\x00") {
						$k = substr($k, strrpos($k, "\x00") + 1);
					}
					$out .= "$space\t" . self::_toPhp($k, $list, $level + 1, $line) . ' => ' . self::_toPhp($v, $list, $level + 1, $line) . ",\n";
					$line++;
				}
				$out .= $space;
			}
			$hash = self::hash($var);
			return $class === 'stdClass'
				? "(object) /* $hash */ array($out)"
				: "$class::__set_state(/* $hash */ array($out))";

		} elseif (is_resource($var)) {
			return '/* resource ' . get_resource_type($var) . ' */';

		} else {
			$res = var_export($var, TRUE);
			$line += substr_count($res, "\n");
			return $res;
		}
	}

	/**
	 * @param  \Exception|\Throwable
	 * @internal
	 */
	public static function dumpException($e)
	{
		$trace = $e->getTrace();
		array_splice($trace, 0, $e instanceof \ErrorException ? 1 : 0, array(array('file' => $e->getFile(), 'line' => $e->getLine())));

		$testFile = NULL;
		foreach (array_reverse($trace) as $item) {
			if (isset($item['file'])) { // in case of shutdown handler, we want to skip inner-code blocks and debugging calls
				$testFile = $item['file'];
				break;
			}
		}

		if ($e instanceof AssertException) {
			$expected = $e->expected;
			$actual = $e->actual;

			if (is_object($expected) || is_array($expected) || (is_string($expected) && strlen($expected) > self::$maxLength)
				|| is_object($actual) || is_array($actual) || (is_string($actual) && strlen($actual) > self::$maxLength)
			) {
				$args = isset($_SERVER['argv'][1])
					? '.[' . implode(' ', preg_replace(array('#^-*(.{1,20}).*#i', '#[^=a-z0-9. -]+#i'), array('$1', '-'), array_slice($_SERVER['argv'], 1))) . ']'
					: '';
				$stored[] = self::saveOutput($testFile, $expected, $args . '.expected');
				$stored[] = self::saveOutput($testFile, $actual, $args . '.actual');
			}

			if ((is_string($actual) && is_string($expected))) {
				for ($i = 0; $i < strlen($actual) && isset($expected[$i]) && $actual[$i] === $expected[$i]; $i++);
				$i = max(0, min($i, max(strlen($actual), strlen($expected)) - self::$maxLength));
				for (; $i && $i < count($actual) && $actual[$i - 1] >= "\x80" && $actual[$i] >= "\x80" && $actual[$i] < "\xC0"; $i--);
				if ($i) {
					$expected = substr_replace($expected, '...', 0, $i);
					$actual = substr_replace($actual, '...', 0, $i);
				}
			}

			$message = 'Failed: ' . $e->origMessage;
			if (((is_string($actual) && is_string($expected)) || (is_array($actual) && is_array($expected)))
				&& preg_match('#^(.*)(%\d)(.*)(%\d.*)\z#s', $message, $m)
			) {
				if (($delta = strlen($m[1]) - strlen($m[3])) >= 3) {
					$message = "$m[1]$m[2]\n" . str_repeat(' ', $delta - 3) . "...$m[3]$m[4]";
				} else {
					$message = "$m[1]$m[2]$m[3]\n" . str_repeat(' ', strlen($m[1]) - 4) . "... $m[4]";
				}
			}
			$message = strtr($message, array(
				'%1' => self::color('yellow') . self::toLine($actual) . self::color('white'),
				'%2' => self::color('yellow') . self::toLine($expected) . self::color('white'),
			));
		} else {
			$message = ($e instanceof \ErrorException ? Helpers::errorTypeToString($e->getSeverity()) : get_class($e))
				. ': ' . preg_replace('#[\x00-\x09\x0B-\x1F]+#', ' ', $e->getMessage());
		}

		$s = self::color('white', $message) . "\n\n"
			. (isset($stored) ? 'diff ' . Helpers::escapeArg($stored[0]) . ' ' . Helpers::escapeArg($stored[1]) . "\n\n" : '');

		foreach ($trace as $item) {
			$item += array('file' => NULL, 'class' => NULL, 'type' => NULL, 'function' => NULL);
			if ($e instanceof AssertException && $item['file'] === __DIR__ . DIRECTORY_SEPARATOR . 'Assert.php') {
				continue;
			}
			$line = $item['class'] === 'Tester\Assert' && method_exists($item['class'], $item['function'])
				&& ($tmp = file($item['file'])) && strpos($tmp = $tmp[$item['line'] - 1], "::$item[function](") ? $tmp : NULL;

			$s .= 'in '
				. ($item['file']
					? (
						($item['file'] === $testFile ? self::color('white') : '')
						. implode(DIRECTORY_SEPARATOR, array_slice(explode(DIRECTORY_SEPARATOR, $item['file']), -self::$maxPathSegments))
						. "($item[line])" . self::color('gray') . ' '
					)
					: '[internal function]'
				)
				. ($line
					? trim($line)
					: $item['class'] . $item['type'] . $item['function'] . ($item['function'] ? '()' : '')
				)
				. self::color() . "\n";
		}

		if ($e->getPrevious()) {
			$s .= "\n(previous) " . static::dumpException($e->getPrevious());
		}
		return $s;
	}

	/**
	 * Dumps data to folder 'output'.
	 * @return string
	 * @internal
	 */
	public static function saveOutput($testFile, $content, $suffix = '')
	{
		$path = self::$dumpDir . DIRECTORY_SEPARATOR . basename($testFile, '.phpt') . $suffix;
		if (!preg_match('#/|\w:#A', self::$dumpDir)) {
			$path = dirname($testFile) . DIRECTORY_SEPARATOR . $path;
		}
		@mkdir(dirname($path)); // @ - directory may already exist
		file_put_contents($path, is_string($content) ? $content : (self::toPhp($content) . "\n"));
		return $path;
	}

	/**
	 * Applies color to string.
	 * @return string
	 */
	public static function color($color = NULL, $s = NULL)
	{
		static $colors = array(
			'black' => '0;30', 'gray' => '1;30', 'silver' => '0;37', 'white' => '1;37',
			'navy' => '0;34', 'blue' => '1;34', 'green' => '0;32', 'lime' => '1;32',
			'teal' => '0;36', 'aqua' => '1;36', 'maroon' => '0;31', 'red' => '1;31',
			'purple' => '0;35', 'fuchsia' => '1;35', 'olive' => '0;33', 'yellow' => '1;33',
			NULL => '0',
		);
		$c = explode('/', $color);
		return "\033["
			. str_replace(';', "m\033[", $colors[$c[0]] . (empty($c[1]) ? '' : ';4' . substr($colors[$c[1]], -1)))
			. 'm' . $s . ($s === NULL ? '' : "\033[0m");
	}

	public static function removeColors($s)
	{
		return preg_replace('#\033\[[\d;]+m#', '', $s);
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester;

/**
 * Testing environment.
 */
class Environment
{
	/** Should Tester use console colors? */
	const COLORS = 'NETTE_TESTER_COLORS';

	/** Test is runned by Runner */
	const RUNNER = 'NETTE_TESTER_RUNNER';

	/** Code coverage file */
	const COVERAGE = 'NETTE_TESTER_COVERAGE';

	/** @var bool  used for debugging Tester itself */
	public static $debugMode = TRUE;

	/** @var bool */
	public static $checkAssertions = FALSE;

	/** @var bool */
	public static $useColors;

	/** @var int initial output buffer level */
	private static $obLevel;

	/**
	 * Configures testing environment.
	 * @return void
	 */
	public static function setup()
	{
		self::setupErrors();
		self::setupColors();
		self::$obLevel = ob_get_level();

		class_exists('Tester\Runner\Job');
		class_exists('Tester\Dumper');
		class_exists('Tester\Assert');

		$annotations = self::getTestAnnotations();
		self::$checkAssertions = !isset($annotations['outputmatch']) && !isset($annotations['outputmatchfile']);

		if (getenv(self::COVERAGE)) {
			CodeCoverage\Collector::start(getenv(self::COVERAGE));
		}
	}

	/**
	 * Configures colored output.
	 * @return void
	 */
	public static function setupColors()
	{
		self::$useColors = getenv(self::COLORS) !== FALSE
			? (bool) getenv(self::COLORS)
			: ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg')
				&& ((function_exists('posix_isatty') && posix_isatty(STDOUT))
					|| getenv('ConEmuANSI') === 'ON' || getenv('ANSICON') !== FALSE) || getenv('term') === 'xterm-256color');

		$colors = & self::$useColors;
		ob_start(function ($s) use (& $colors) {
			return $colors ? $s : Dumper::removeColors($s);
		}, PHP_VERSION_ID < 50400 ? 2 : 1, FALSE);
	}

	/**
	 * Configures PHP error handling.
	 * @return void
	 */
	public static function setupErrors()
	{
		error_reporting(E_ALL | E_STRICT);
		ini_set('display_errors', TRUE);
		ini_set('html_errors', FALSE);
		ini_set('log_errors', FALSE);

		set_exception_handler(array(__CLASS__, 'handleException'));

		set_error_handler(function ($severity, $message, $file, $line) {
			if (in_array($severity, array(E_RECOVERABLE_ERROR, E_USER_ERROR), TRUE) || ($severity & error_reporting()) === $severity) {
				Environment::handleException(new \ErrorException($message, 0, $severity, $file, $line));
			}
			return FALSE;
		});

		register_shutdown_function(function () {
			Assert::$onFailure = array(__CLASS__, 'handleException'); // note that Runner is unable to catch this errors in CLI & PHP 5.4.0 - 5.4.6 due PHP bug #62725

			$error = error_get_last();
			register_shutdown_function(function () use ($error) {
				if (in_array($error['type'], array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE), TRUE)) {
					if (($error['type'] & error_reporting()) !== $error['type']) { // show fatal errors hidden by @shutup
						Environment::removeOutputBuffers();
						echo "\nFatal error: $error[message] in $error[file] on line $error[line]\n";
					}
				} elseif (Environment::$checkAssertions && !Assert::$counter) {
					Environment::removeOutputBuffers();
					echo "\nError: This test forgets to execute an assertion.\n";
					exit(Runner\Job::CODE_FAIL);
				}
			});
		});
	}

	/**
	 * @param  \Exception|\Throwable
	 * @internal
	 */
	public static function handleException($e)
	{
		self::removeOutputBuffers();
		self::$checkAssertions = FALSE;
		echo self::$debugMode ? Dumper::dumpException($e) : "\nError: {$e->getMessage()}\n";
		exit($e instanceof AssertException ? Runner\Job::CODE_FAIL : Runner\Job::CODE_ERROR);
	}

	/**
	 * Skips this test.
	 * @return void
	 */
	public static function skip($message = '')
	{
		self::$checkAssertions = FALSE;
		echo "\nSkipped:\n$message\n";
		die(Runner\Job::CODE_SKIP);
	}

	/**
	 * Locks the parallel tests.
	 * @param  string
	 * @param  string  lock store directory
	 * @return void
	 */
	public static function lock($name = '', $path = '')
	{
		static $locks;
		$file = "$path/lock-" . md5($name);
		if (!isset($locks[$file])) {
			flock($locks[$file] = fopen($file, 'w'), LOCK_EX);
		}
	}

	/**
	 * Returns current test annotations.
	 * @return array
	 */
	public static function getTestAnnotations()
	{
		$trace = debug_backtrace();
		$file = $trace[count($trace) - 1]['file'];
		return Helpers::parseDocComment(file_get_contents($file)) + array('file' => $file);
	}

	/**
	 * Loads data according to the file annotation or specified by Tester\Runner\TestHandler::initiateDataProvider()
	 * @return array
	 */
	public static function loadData()
	{
		if (isset($_SERVER['argv']) && ($tmp = preg_filter('#--dataprovider=(.*)#Ai', '$1', $_SERVER['argv']))) {
			list($query, $file) = explode('|', reset($tmp), 2);

		} else {
			$annotations = self::getTestAnnotations();
			if (!isset($annotations['dataprovider'])) {
				throw new \Exception('Missing annotation @dataProvider.');
			}
			$provider = (array) $annotations['dataprovider'];
			list($file, $query) = DataProvider::parseAnnotation($provider[0], $annotations['file']);
		}
		$data = DataProvider::load($file, $query);
		return reset($data);
	}

	/**
	 * @internal
	 */
	public static function removeOutputBuffers()
	{
		while (ob_get_level() > self::$obLevel && @ob_end_flush()); // @ may be not removable
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester;

/**
 * Mock files.
 */
class FileMock
{
	const PROTOCOL = 'mock';

	/** @var string[] */
	public static $files = array();

	/** @var string */
	private $content;

	/** @var int */
	private $pos;

	/** @var bool */
	private $isReadable;

	/** @var bool */
	private $isWritable;

	/**
	 * @return string  file name
	 */
	public static function create($content, $extension = NULL)
	{
		self::register();

		static $id;
		$name = self::PROTOCOL . '://' . (++$id) . '.' . $extension;
		self::$files[$name] = $content;
		return $name;
	}

	public static function register()
	{
		if (!in_array(self::PROTOCOL, stream_get_wrappers(), TRUE)) {
			stream_wrapper_register(self::PROTOCOL, __CLASS__);
		}
	}

	public function stream_open($path, $mode)
	{
		if (!preg_match('#^([rwaxc]).*?(\+)?#', $mode, $m)) {
			// Windows: failed to open stream: Bad file descriptor
			// Linux: failed to open stream: Illegal seek
			$this->warning("failed to open stream: Invalid mode '$mode'");
			return FALSE;

		} elseif ($m[1] === 'x' && isset(self::$files[$path])) {
			$this->warning('failed to open stream: File exists');
			return FALSE;

		} elseif ($m[1] === 'r' && !isset(self::$files[$path])) {
			$this->warning('failed to open stream: No such file or directory');
			return FALSE;

		} elseif ($m[1] === 'w' || $m[1] === 'x') {
			self::$files[$path] = '';
		}

		$this->content = & self::$files[$path];
		$this->pos = $m[1] === 'a' ? strlen($this->content) : 0;

		$this->isReadable = isset($m[2]) || $m[1] === 'r';
		$this->isWritable = isset($m[2]) || $m[1] !== 'r';

		return TRUE;
	}

	public function stream_read($len)
	{
		if (!$this->isReadable) {
			return '';
		}

		$res = substr($this->content, $this->pos, $len);
		$this->pos += strlen($res);
		return $res;
	}

	public function stream_write($data)
	{
		if (!$this->isWritable) {
			return 0;
		}

		$this->content = substr($this->content, 0, $this->pos)
			. str_repeat("\x00", max(0, $this->pos - strlen($this->content)))
			. $data
			. substr($this->content, $this->pos + strlen($data));
		$this->pos += strlen($data);
		return strlen($data);
	}

	public function stream_tell()
	{
		return $this->pos;
	}

	public function stream_eof()
	{
		return $this->pos >= strlen($this->content);
	}

	public function stream_seek($offset, $whence)
	{
		if ($whence === SEEK_CUR) {
			$offset += $this->pos;
		} elseif ($whence === SEEK_END) {
			$offset += strlen($this->content);
		}
		if ($offset >= 0) {
			$this->pos = $offset;
			return TRUE;
		} else {
			return FALSE;
		}
	}

	public function stream_truncate($size)
	{
		$this->content = (string) substr($this->content, 0, $size)
			. str_repeat("\x00", max(0, $size - strlen($this->content)));
		return TRUE;
	}

	public function stream_stat()
	{
		return array('mode' => 0100666, 'size' => strlen($this->content));
	}

	public function url_stat($path, $flags)
	{
		return isset(self::$files[$path])
			? array('mode' => 0100666, 'size' => strlen(self::$files[$path]))
			: FALSE;
	}

	public function stream_lock($operation)
	{
		return FALSE;
	}

	public function unlink($path)
	{
		if (isset(self::$files[$path])) {
			unset(self::$files[$path]);
			return TRUE;
		}

		$this->warning('No such file');
		return FALSE;
	}

	private function warning($message)
	{
		$bt = PHP_VERSION_ID < 50400 ? debug_backtrace(FALSE) : debug_backtrace(0, 3);
		if (isset($bt[2]['function'])) {
			$message = $bt[2]['function'] . '(' . @$bt[2]['args'][0] . '): ' . $message;
		}

		trigger_error($message, E_USER_WARNING);
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester;

/**
 * Test helpers.
 */
class Helpers
{

	/**
	 * Purges directory.
	 * @param  string
	 * @return void
	 */
	public static function purge($dir)
	{
		if (!is_dir($dir)) {
			mkdir($dir);
		}
		foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST) as $entry) {
			if ($entry->isDir()) {
				rmdir($entry);
			} else {
				unlink($entry);
			}
		}
	}

	/**
	 * Parse phpDoc comment.
	 * @return array
	 * @internal
	 */
	public static function parseDocComment($s)
	{
		$options = array();
		if (!preg_match('#^/\*\*(.*?)\*/#ms', $s, $content)) {
			return array();
		}
		if (preg_match('#^[ \t\*]*+([^\s@].*)#mi', $content[1], $matches)) {
			$options[0] = trim($matches[1]);
		}
		preg_match_all('#^[ \t\*]*@(\w+)([^\w\r\n].*)?#mi', $content[1], $matches, PREG_SET_ORDER);
		foreach ($matches as $match) {
			$ref = & $options[strtolower($match[1])];
			if (isset($ref)) {
				$ref = (array) $ref;
				$ref = & $ref[];
			}
			$ref = isset($match[2]) ? trim($match[2]) : '';
		}
		return $options;
	}

	/**
	 * @internal
	 */
	public static function errorTypeToString($type)
	{
		$consts = get_defined_constants(TRUE);
		foreach ($consts['Core'] as $name => $val) {
			if ($type === $val && substr($name, 0, 2) === 'E_') {
				return $name;
			}
		}
	}

	/**
	 * Escape a string to be used as a shell argument.
	 * @return string
	 */
	public static function escapeArg($s)
	{
		if (preg_match('#^[a-z0-9._-]+\z#i', $s)) {
			return $s;
		}

		return defined('PHP_WINDOWS_VERSION_BUILD')
			? '"' . str_replace('"', '""', $s) . '"'
			: escapeshellarg($s);
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester;

/**
 * Single test case.
 */
class TestCase
{
	/** @internal */
	const LIST_METHODS = 'nette-tester-list-methods',
		METHOD_PATTERN = '#^test[A-Z0-9_]#';

	/** @var bool */
	private $handleErrors = FALSE;

	/** @var callable|NULL|FALSE */
	private $prevErrorHandler = FALSE;

	/**
	 * Runs the test case.
	 * @return void
	 */
	public function run($method = NULL)
	{
		$r = new \ReflectionObject($this);
		$methods = array_values(preg_grep(self::METHOD_PATTERN, array_map(function (\ReflectionMethod $rm) {
			return $rm->getName();
		}, $r->getMethods())));

		if (substr($method, 0, 2) === '--') { // back compatibility
			$method = NULL;
		}

		if ($method === NULL && isset($_SERVER['argv']) && ($tmp = preg_filter('#(--method=)?([\w-]+)$#Ai', '$2', $_SERVER['argv']))) {
			$method = reset($tmp);
			if ($method === self::LIST_METHODS) {
				Environment::$checkAssertions = FALSE;
				header('Content-Type: text/plain');
				echo '[' . implode(',', $methods) . ']';
				return;
			}
		}

		if ($method === NULL) {
			foreach ($methods as $method) {
				$this->runTest($method);
			}
		} elseif (in_array($method, $methods, TRUE)) {
			$this->runTest($method);
		} else {
			throw new TestCaseException("Method '$method' does not exist or it is not a testing method.");
		}
	}

	/**
	 * Runs the test method.
	 * @param  string  test method name
	 * @param  array  test method parameters (dataprovider bypass)
	 * @return void
	 */
	public function runTest($method, array $args = NULL)
	{
		$method = new \ReflectionMethod($this, $method);
		if (!$method->isPublic()) {
			throw new TestCaseException("Method {$method->getName()} is not public. Make it public or rename it.");
		}

		$info = Helpers::parseDocComment($method->getDocComment()) + array('dataprovider' => NULL, 'throws' => NULL);

		if ($info['throws'] === '') {
			throw new TestCaseException("Missing class name in @throws annotation for {$method->getName()}().");
		} elseif (is_array($info['throws'])) {
			throw new TestCaseException("Annotation @throws for {$method->getName()}() can be specified only once.");
		} else {
			$throws = preg_split('#\s+#', $info['throws'], 2) + array(NULL, NULL);
		}

		$data = array();
		if ($args === NULL) {
			$defaultParams = array();
			foreach ($method->getParameters() as $param) {
				$defaultParams[$param->getName()] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : NULL;
			}

			foreach ((array) $info['dataprovider'] as $provider) {
				$res = $this->getData($provider);
				if (!is_array($res) && !$res instanceof \Traversable) {
					throw new TestCaseException("Data provider $provider() doesn't return array or Traversable.");
				}
				foreach ($res as $set) {
					$data[] = is_string(key($set)) ? array_merge($defaultParams, $set) : $set;
				}
			}

			if (!$info['dataprovider']) {
				if ($method->getNumberOfRequiredParameters()) {
					throw new TestCaseException("Method {$method->getName()}() has arguments, but @dataProvider is missing.");
				}
				$data[] = array();
			}
		} else {
			$data[] = $args;
		}

		if ($this->prevErrorHandler === FALSE) {
			$me = $this;
			$handleErrors = & $this->handleErrors;
			$prev = & $this->prevErrorHandler;

			$prev = set_error_handler(function ($severity) use ($me, & $prev, & $handleErrors) {
				if ($handleErrors && ($severity & error_reporting()) === $severity) {
					$handleErrors = FALSE;
					$rm = new \ReflectionMethod($me, 'tearDown');
					$rm->setAccessible(TRUE);

					set_error_handler(function() {});  // mute all errors
					$rm->invoke($me);
					restore_error_handler();
				}

				return $prev ? call_user_func_array($prev, func_get_args()) : FALSE;
			});
		}

		foreach ($data as $params) {
			try {
				$this->setUp();

				$this->handleErrors = TRUE;
				try {
					if ($info['throws']) {
						$tmp = $this;
						$e = Assert::error(function () use ($tmp, $method, $params) {
							call_user_func_array(array($tmp, $method->getName()), $params);
						}, $throws[0], $throws[1]);
						if ($e instanceof AssertException) {
							throw $e;
						}
					} else {
						call_user_func_array(array($this, $method->getName()), $params);
					}
				} catch (\Exception $testException) {
				}
				$this->handleErrors = FALSE;

				try {
					$this->tearDown();
				} catch (\Exception $tearDownException) {
				}

				if (isset($testException)) {
					throw $testException;
				} elseif (isset($tearDownException)) {
					throw $tearDownException;
				}

			} catch (AssertException $e) {
				throw $e->setMessage("$e->origMessage in {$method->getName()}" . (substr(Dumper::toLine($params), 5)));
			}
		}
	}

	/**
	 * @return array
	 */
	protected function getData($provider)
	{
		if (strpos($provider, '.') === FALSE) {
			return $this->$provider();
		} else {
			$rc = new \ReflectionClass($this);
			list($file, $query) = DataProvider::parseAnnotation($provider, $rc->getFileName());
			return DataProvider::load($file, $query);
		}
	}

	/**
	 * This method is called before a test is executed.
	 * @return void
	 */
	protected function setUp()
	{
	}

	/**
	 * This method is called after a test is executed.
	 * @return void
	 */
	protected function tearDown()
	{
	}

}

class TestCaseException extends \Exception
{
}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester\Runner;

use Tester\CodeCoverage;
use Tester\Environment;
use Tester\Helpers;
use Tester\Dumper;

/**
 * CLI Tester.
 */
class CliTester
{
	/** @var array */
	private $options;

	/** @var PhpInterpreter */
	private $interpreter;

	/** @return int|NULL */
	public function run()
	{
		Environment::setupColors();
		Environment::setupErrors();

		ob_start();
		$cmd = $this->loadOptions();

		Environment::$debugMode = (bool) $this->options['--debug'];
		if (isset($this->options['--colors'])) {
			Environment::$useColors = (bool) $this->options['--colors'];
		} elseif (in_array($this->options['-o'], array('tap', 'junit'))) {
			Environment::$useColors = FALSE;
		}

		if ($cmd->isEmpty() || $this->options['--help']) {
			$cmd->help();
			return;
		}

		$this->createPhpInterpreter();

		if ($this->options['--info']) {
			$job = new Job(__DIR__ . '/info.php', $this->interpreter);
			$job->run();
			echo $job->getOutput();
			return;
		}

		if ($this->options['--coverage']) {
			$coverageFile = $this->prepareCodeCoverage();
		}

		$runner = $this->createRunner();

		if ($this->options['-o'] !== NULL) {
			ob_clean();
		}
		ob_end_flush();

		if ($this->options['--watch']) {
			$this->watch($runner);
			return;
		}

		$result = $runner->run();

		if (isset($coverageFile)) {
			$this->finishCodeCoverage($coverageFile);
		}

		return $result ? 0 : 1;
	}

	/** @return CommandLine */
	private function loadOptions()
	{
		echo <<<'XX'
 _____ ___  ___ _____ ___  ___
|_   _/ __)( __/_   _/ __)| _ )
  |_| \___ /___) |_| \___ |_|_\  v1.7.1

XX;

		$cmd = new CommandLine(<<<XX
Usage:
    tester.php [options] [<test file> | <directory>]...

Options:
    -p <path>                    Specify PHP interpreter to run (default: php-cgi).
    -c <path>                    Look for php.ini file (or look in directory) <path>.
    -l | --log <path>            Write log to file <path>.
    -d <key=value>...            Define INI entry 'key' with value 'val'.
    -s                           Show information about skipped tests.
    --stop-on-fail               Stop execution upon the first failure.
    -j <num>                     Run <num> jobs in parallel (default: 8).
    -o <console|tap|junit|none>  Specify output format.
    -w | --watch <path>          Watch directory.
    -i | --info                  Show tests environment info and exit.
    --setup <path>               Script for runner setup.
    --colors [1|0]               Enable or disable colors.
    --coverage <path>            Generate code coverage report to file.
    --coverage-src <path>        Path to source code.
    -h | --help                  This help.

XX
		, array(
			'-c' => array(CommandLine::REALPATH => TRUE),
			'--watch' => array(CommandLine::REPEATABLE => TRUE, CommandLine::REALPATH => TRUE),
			'--setup' => array(CommandLine::REALPATH => TRUE),
			'paths' => array(CommandLine::REPEATABLE => TRUE, CommandLine::VALUE => getcwd()),
			'--debug' => array(),
			'--coverage-src' => array(CommandLine::REALPATH => TRUE),
		));

		if (isset($_SERVER['argv'])) {
			if ($tmp = array_search('-log', $_SERVER['argv'])) {
				$_SERVER['argv'][$tmp] = '--log';
			}

			if ($tmp = array_search('--tap', $_SERVER['argv'])) {
				unset($_SERVER['argv'][$tmp]);
				$_SERVER['argv'] = array_merge($_SERVER['argv'], array('-o', 'tap'));
			}
		}

		$this->options = $cmd->parse();
		return $cmd;
	}

	/** @return void */
	private function createPhpInterpreter()
	{
		$args = '';
		if ($this->options['-c']) {
			$args .= ' -c ' . Helpers::escapeArg($this->options['-c']);
		} elseif (!$this->options['--info']) {
			echo "Note: No php.ini is used.\n";
		}

		if (in_array($this->options['-o'], array('tap', 'junit'))) {
			$args .= ' -d html_errors=off';
		}

		foreach ($this->options['-d'] as $item) {
			$args .= ' -d ' . Helpers::escapeArg($item);
		}

		// Is the executable Zend PHP or HHVM?
		$proc = @proc_open( // @ is escalated to exception
			$this->options['-p'] . ' --version',
			array(array('pipe', 'r'), array('pipe', 'w'), array('pipe', 'w')),
			$pipes,
			NULL,
			NULL,
			array('bypass_shell' => TRUE)
		);
		if ($proc === FALSE) {
			throw new \Exception('Cannot run PHP interpreter ' . $this->options['-p'] . '. Use -p option.');
		}
		$output = stream_get_contents($pipes[1]);
		$error = stream_get_contents($pipes[2]);
		if (proc_close($proc)) {
			throw new \Exception("Unable to run '{$this->options['-p']}': " . preg_replace('#[\r\n ]+#', ' ', $error));
		}

		if (preg_match('#HipHop VM#', $output)) {
			$this->interpreter = new HhvmPhpInterpreter($this->options['-p'], $args);
		} elseif (strpos($output, 'phpdbg') !== FALSE) {
			$this->interpreter = new ZendPhpDbgInterpreter($this->options['-p'], $args);
		} else {
			$this->interpreter = new ZendPhpInterpreter($this->options['-p'], $args);
		}

		if ($this->interpreter->getErrorOutput()) {
			echo Dumper::color('red', 'PHP startup error: ' . $this->interpreter->getErrorOutput()) . "\n";
			if ($this->interpreter->isCgi()) {
				echo "(note that PHP CLI generates better error messages)\n";
			}
		}
	}

	/** @return Runner */
	private function createRunner()
	{
		$runner = new Runner($this->interpreter);
		$runner->paths = $this->options['paths'];
		$runner->threadCount = max(1, (int) $this->options['-j']);
		$runner->stopOnFail = $this->options['--stop-on-fail'];

		if ($this->options['-o'] !== 'none') {
			switch ($this->options['-o']) {
				case 'tap':
					$runner->outputHandlers[] = new Output\TapPrinter($runner);
					break;
				case 'junit':
					$runner->outputHandlers[] = new Output\JUnitPrinter($runner);
					break;
				default:
					$runner->outputHandlers[] = new Output\ConsolePrinter($runner, $this->options['-s']);
			}
		}

		if ($this->options['--log']) {
			echo "Log: {$this->options['--log']}\n";
			$runner->outputHandlers[] = new Output\Logger($runner, $this->options['--log']);
		}

		if ($this->options['--setup']) {
			call_user_func(function () use ($runner) {
				require func_get_arg(0);
			}, $this->options['--setup']);
		}
		return $runner;
	}

	/** @return string */
	private function prepareCodeCoverage()
	{
		if (!$this->interpreter->hasXdebug()) {
			$alternative = PHP_VERSION_ID >= 70000 ? ' or phpdbg SAPI' : '';
			throw new \Exception("Code coverage functionality requires Xdebug extension$alternative (used {$this->interpreter->getCommandLine()})");
		}
		file_put_contents($this->options['--coverage'], '');
		$file = realpath($this->options['--coverage']);
		putenv(Environment::COVERAGE . '=' . $file);
		echo "Code coverage: {$file}\n";
		if (preg_match('#\.(?:html?|xml)\z#', $file)) {
			return $file;
		}
	}

	/** @return void */
	private function finishCodeCoverage($file)
	{
		if (!in_array($this->options['-o'], array('none', 'tap', 'junit'), TRUE)) {
			echo "Generating code coverage report\n";
		}
		if (pathinfo($file, PATHINFO_EXTENSION) === 'xml') {
			$generator = new CodeCoverage\Generators\CloverXMLGenerator($file, $this->options['--coverage-src']);
		} else {
			$generator = new CodeCoverage\Generators\HtmlGenerator($file, $this->options['--coverage-src']);
		}
		$generator->render($file);
	}

	/** @return void */
	private function watch(Runner $runner)
	{
		$prev = array();
		$counter = 0;
		while (TRUE) {
			$state = array();
			foreach ($this->options['--watch'] as $directory) {
				foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory)) as $file) {
					if (substr($file->getExtension(), 0, 3) === 'php' && substr($file->getBasename(), 0, 1) !== '.') {
						$state[(string) $file] = md5_file((string) $file);
					}
				}
			}
			if ($state !== $prev) {
				$prev = $state;
				$runner->run();
			}
			echo 'Watching ' . implode(', ', $this->options['--watch']) . ' ' . str_repeat('.', ++$counter % 5) . "    \r";
			sleep(2);
		}
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester\Runner;

/**
 * Stupid command line arguments parser.
 */
class CommandLine
{
	const
		ARGUMENT = 'argument',
		OPTIONAL = 'optional',
		REPEATABLE = 'repeatable',
		ENUM = 'enum',
		REALPATH = 'realpath',
		VALUE = 'default';

	/** @var array[] */
	private $options = array();

	/** @var string[] */
	private $aliases = array();

	/** @var bool[] */
	private $positional = array();

	/** @var string */
	private $help;

	public function __construct($help, array $defaults = array())
	{
		$this->help = $help;
		$this->options = $defaults;

		preg_match_all('#^[ \t]+(--?\w.*?)(?:  .*\(default: (.*)\)|  |\r|$)#m', $help, $lines, PREG_SET_ORDER);
		foreach ($lines as $line) {
			preg_match_all('#(--?\w[\w-]*)(?:[= ](<.*?>|\[.*?]|\w+)(\.{0,3}))?[ ,|]*#A', $line[1], $m);
			if (!count($m[0]) || count($m[0]) > 2 || implode('', $m[0]) !== $line[1]) {
				throw new \InvalidArgumentException("Unable to parse '$line[1]'.");
			}

			$name = end($m[1]);
			$opts = isset($this->options[$name]) ? $this->options[$name] : array();
			$this->options[$name] = $opts + array(
				self::ARGUMENT => (bool) end($m[2]),
				self::OPTIONAL => isset($line[2]) || (substr(end($m[2]), 0, 1) === '[') || isset($opts[self::VALUE]),
				self::REPEATABLE => (bool) end($m[3]),
				self::ENUM => count($enums = explode('|', trim(end($m[2]), '<[]>'))) > 1 ? $enums : NULL,
				self::VALUE => isset($line[2]) ? $line[2] : NULL,
			);
			if ($name !== $m[1][0]) {
				$this->aliases[$m[1][0]] = $name;
			}
		}

		foreach ($this->options as $name => $foo) {
			if ($name[0] !== '-') {
				$this->positional[] = $name;
			}
		}
	}

	public function parse(array $args = NULL)
	{
		if ($args === NULL) {
			$args = isset($_SERVER['argv']) ? array_slice($_SERVER['argv'], 1) : array();
		}
		$params = array();
		reset($this->positional);
		$i = 0;
		while ($i < count($args)) {
			$arg = $args[$i++];
			if ($arg[0] !== '-') {
				if (!current($this->positional)) {
					throw new \Exception("Unexpected parameter $arg.");
				}
				$name = current($this->positional);
				$this->checkArg($this->options[$name], $arg);
				if (empty($this->options[$name][self::REPEATABLE])) {
					$params[$name] = $arg;
					next($this->positional);
				} else {
					$params[$name][] = $arg;
				}
				continue;
			}

			list($name, $arg) = strpos($arg, '=') ? explode('=', $arg, 2) : array($arg, TRUE);

			if (isset($this->aliases[$name])) {
				$name = $this->aliases[$name];

			} elseif (!isset($this->options[$name])) {
				throw new \Exception("Unknown option $name.");
			}

			$opt = $this->options[$name];

			if ($arg !== TRUE && empty($opt[self::ARGUMENT])) {
				throw new \Exception("Option $name has not argument.");

			} elseif ($arg === TRUE && !empty($opt[self::ARGUMENT])) {
				if (isset($args[$i]) && $args[$i][0] !== '-') {
					$arg = $args[$i++];
				} elseif (empty($opt[self::OPTIONAL])) {
					throw new \Exception("Option $name requires argument.");
				}
			}

			if (!empty($opt[self::ENUM]) && !in_array($arg, $opt[self::ENUM], TRUE) && !($opt[self::OPTIONAL] && $arg === TRUE)) {
				throw new \Exception("Value of option $name must be " . implode(', or ', $opt[self::ENUM]) . ".");
			}
			$this->checkArg($opt, $arg);

			if (empty($opt[self::REPEATABLE])) {
				$params[$name] = $arg;
			} else {
				$params[$name][] = $arg;
			}
		}

		foreach ($this->options as $name => $opt) {
			if (isset($params[$name])) {
				continue;
			} elseif (isset($opt[self::VALUE])) {
				$params[$name] = $opt[self::VALUE];
			} elseif ($name[0] !== '-' && empty($opt[self::OPTIONAL])) {
				throw new \Exception("Missing required argument <$name>.");
			} else {
				$params[$name] = NULL;
			}
			if (!empty($opt[self::REPEATABLE])) {
				$params[$name] = (array) $params[$name];
			}
		}
		return $params;
	}

	public function help()
	{
		echo $this->help;
	}

	public function checkArg(array $opt, & $arg)
	{
		if (!empty($opt[self::REALPATH])) {
			$path = realpath($arg);
			if ($path === FALSE) {
				throw new \Exception("File path '$arg' not found.");
			}
			$arg = $path;
		}
	}

	public function isEmpty()
	{
		return !isset($_SERVER['argv']) || count($_SERVER['argv']) < 2;
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester\Runner;

use Tester\Helpers;

/**
 * HHVM command-line executable.
 */
class HhvmPhpInterpreter implements PhpInterpreter
{
	/** @var string  HHVM arguments */
	public $arguments;

	/** @var string  HHVM executable */
	private $path;

	/** @var string  HHVM version */
	private $version;

	/** @var string  PHP version */
	private $phpVersion;

	/** @var string */
	private $error;

	public function __construct($path, $args = NULL)
	{
		$this->path = Helpers::escapeArg($path);
		$proc = proc_open(
			"$this->path --php $args -r " . Helpers::escapeArg('echo HHVM_VERSION . "|" . PHP_VERSION;'),
			array(array('pipe', 'r'), array('pipe', 'w'), array('pipe', 'w')),
			$pipes,
			NULL,
			NULL,
			array('bypass_shell' => TRUE)
		);
		$output = stream_get_contents($pipes[1]);
		$this->error = trim(stream_get_contents($pipes[2]));

		if (proc_close($proc)) {
			throw new \Exception("Unable to run '$path': " . preg_replace('#[\r\n ]+#', ' ', $this->error));
		} elseif (count($tmp = explode('|', $output)) !== 2) {
			throw new \Exception("Unable to detect HHVM version (output: $output).");
		}

		list($this->version, $this->phpVersion) = $tmp;
		if (version_compare($this->version, '3.3.0', '<')) {
			throw new \Exception('HHVM below version 3.3.0 is not supported.');
		}
		$this->arguments = ' --php -d hhvm.log.always_log_unhandled_exceptions=false' . ($args ? " $args" : ''); // HHVM issue #3019
	}

	/**
	 * @return string
	 */
	public function getCommandLine()
	{
		return $this->path . $this->arguments;
	}

	/**
	 * @return string
	 */
	public function getVersion()
	{
		return $this->phpVersion;
	}

	/**
	 * @return bool
	 */
	public function hasXdebug()
	{
		return FALSE;
	}

	/**
	 * @return bool
	 */
	public function isCgi()
	{
		return FALSE;
	}

	/**
	 * @return string
	 */
	public function getErrorOutput()
	{
		return $this->error;
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester\Runner;

use Tester\Environment;

/**
 * Single test job.
 */
class Job
{
	const
		CODE_NONE = -1,
		CODE_OK = 0,
		CODE_SKIP = 177,
		CODE_FAIL = 178,
		CODE_ERROR = 255;

	/** waiting time between process activity check in microseconds */
	const RUN_USLEEP = 10000;

	const
		RUN_ASYNC = 1,
		RUN_COLLECT_ERRORS = 2;

	/** @var string  test file */
	private $file;

	/** @var string[]  test arguments */
	private $args;

	/** @var string  test output */
	private $output;

	/** @var string|NULL  test error output */
	private $errorOutput;

	/** @var string[]  output headers */
	private $headers;

	/** @var PhpInterpreter */
	private $interpreter;

	/** @var resource */
	private $proc;

	/** @var resource */
	private $stdout;

	/** @var resource */
	private $stderr;

	/** @var int */
	private $exitCode = self::CODE_NONE;

	/**
	 * @param  string  test file name
	 * @return void
	 */
	public function __construct($testFile, PhpInterpreter $interpreter, array $args = NULL)
	{
		$this->file = (string) $testFile;
		$this->interpreter = $interpreter;
		$this->args = (array) $args;
	}

	/**
	 * Runs single test.
	 * @param  int RUN_ASYNC | RUN_COLLECT_ERRORS
	 * @return void
	 */
	public function run($flags = NULL)
	{
		putenv(Environment::RUNNER . '=1');
		putenv(Environment::COLORS . '=' . (int) Environment::$useColors);
		$this->proc = proc_open(
			$this->interpreter->getCommandLine()
			. ' -n -d register_argc_argv=on ' . \Tester\Helpers::escapeArg($this->file) . ' ' . implode(' ', $this->args),
			array(
				array('pipe', 'r'),
				array('pipe', 'w'),
				array('pipe', 'w'),
			),
			$pipes,
			dirname($this->file),
			NULL,
			array('bypass_shell' => TRUE)
		);

		list($stdin, $this->stdout, $stderr) = $pipes;
		fclose($stdin);
		if ($flags & self::RUN_COLLECT_ERRORS) {
			$this->stderr = $stderr;
		} else {
			fclose($stderr);
		}

		if ($flags & self::RUN_ASYNC) {
			stream_set_blocking($this->stdout, 0); // on Windows does not work with proc_open()
			if ($this->stderr) {
				stream_set_blocking($this->stderr, 0);
			}
		} else {
			while ($this->isRunning()) {
				usleep(self::RUN_USLEEP); // stream_select() doesn't work with proc_open()
			}
		}
	}

	/**
	 * Checks if the test is still running.
	 * @return bool
	 */
	public function isRunning()
	{
		if (!is_resource($this->stdout)) {
			return FALSE;
		}
		$this->output .= stream_get_contents($this->stdout);
		if ($this->stderr) {
			$this->errorOutput .= stream_get_contents($this->stderr);
		}

		$status = proc_get_status($this->proc);
		if ($status['running']) {
			return TRUE;
		}

		fclose($this->stdout);
		if ($this->stderr) {
			fclose($this->stderr);
		}
		$code = proc_close($this->proc);
		$this->exitCode = $code === self::CODE_NONE ? $status['exitcode'] : $code;

		if ($this->interpreter->isCgi() && count($tmp = explode("\r\n\r\n", $this->output, 2)) >= 2) {
			list($headers, $this->output) = $tmp;
			foreach (explode("\r\n", $headers) as $header) {
				$a = strpos($header, ':');
				if ($a !== FALSE) {
					$this->headers[trim(substr($header, 0, $a))] = (string) trim(substr($header, $a + 1));
				}
			}
		}
		return FALSE;
	}

	/**
	 * Returns test file path.
	 * @return string
	 */
	public function getFile()
	{
		return $this->file;
	}

	/**
	 * Returns script arguments.
	 * @return string[]
	 */
	public function getArguments()
	{
		return $this->args;
	}

	/**
	 * Returns exit code.
	 * @return int
	 */
	public function getExitCode()
	{
		return $this->exitCode;
	}

	/**
	 * Returns test output.
	 * @return string
	 */
	public function getOutput()
	{
		return $this->output;
	}

	/**
	 * Returns test error output.
	 * @return string|NULL
	 */
	public function getErrorOutput()
	{
		return $this->errorOutput;
	}

	/**
	 * Returns output headers.
	 * @return string[]
	 */
	public function getHeaders()
	{
		return $this->headers;
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester\Runner\Output;

use Tester;
use Tester\Dumper;
use Tester\Runner\Runner;

/**
 * Console printer.
 */
class ConsolePrinter implements Tester\Runner\OutputHandler
{
	/** @var Runner */
	private $runner;

	/** @var bool  display skipped tests information? */
	private $displaySkipped = FALSE;

	/** @var string */
	private $buffer;

	/** @var float */
	private $time;

	public function __construct(Runner $runner, $displaySkipped = FALSE)
	{
		$this->runner = $runner;
		$this->displaySkipped = $displaySkipped;
	}

	public function begin()
	{
		$this->time = -microtime(TRUE);
		echo 'PHP ' . $this->runner->getInterpreter()->getVersion()
			. ' | ' . $this->runner->getInterpreter()->getCommandLine()
			. " | {$this->runner->threadCount} thread" . ($this->runner->threadCount > 1 ? 's' : '') . "\n\n";
	}

	public function result($testName, $result, $message)
	{
		$outputs = array(
			Runner::PASSED => '.',
			Runner::SKIPPED => 's',
			Runner::FAILED => Dumper::color('white/red', 'F'),
		);
		echo $outputs[$result];

		$message = '   ' . str_replace("\n", "\n   ", trim($message)) . "\n\n";
		if ($result === Runner::FAILED) {
			$this->buffer .= Dumper::color('red', "-- FAILED: $testName") . "\n$message";
		} elseif ($result === Runner::SKIPPED && $this->displaySkipped) {
			$this->buffer .= "-- Skipped: $testName\n$message";
		}
	}

	public function end()
	{
		$jobCount = $this->runner->getJobCount();
		$results = $this->runner->getResults();
		$count = array_sum($results);
		echo !$jobCount ? "No tests found\n" :
			"\n\n" . $this->buffer . "\n"
			. ($results[Runner::FAILED] ? Dumper::color('white/red') . 'FAILURES!' : Dumper::color('white/green') . 'OK')
			. " ($jobCount test" . ($jobCount > 1 ? 's' : '') . ", "
			. ($results[Runner::FAILED] ? $results[Runner::FAILED] . ' failure' . ($results[Runner::FAILED] > 1 ? 's' : '') . ', ' : '')
			. ($results[Runner::SKIPPED] ? $results[Runner::SKIPPED] . ' skipped, ' : '')
			. ($jobCount !== $count ? ($jobCount - $count) . ' not run, ' : '')
			. sprintf('%0.1f', $this->time + microtime(TRUE)) . ' seconds)' . Dumper::color() . "\n";

		$this->buffer = NULL;
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester\Runner\Output;

use Tester;
use Tester\Runner\Runner;

/**
 * JUnit xml format printer.
 */
class JUnitPrinter implements Tester\Runner\OutputHandler
{
	/** @var Runner */
	private $runner;

	/** @var resource */
	private $file;

	/** @var string */
	private $buffer;

	/** @var float */
	private $startTime;

	public function __construct(Runner $runner, $file = 'php://output')
	{
		$this->runner = $runner;
		$this->file = fopen($file, 'w');
	}

	public function begin()
	{
		$this->startTime = microtime(TRUE);
		fwrite($this->file, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<testsuites>\n");
	}

	public function result($testName, $result, $message)
	{
		$this->buffer .= "\t\t<testcase classname=\"" . htmlspecialchars($testName) . '" name="' . htmlspecialchars($testName) . '"';

		switch ($result) {
			case Runner::FAILED:
				$this->buffer .= ">\n\t\t\t<failure message=\"" . htmlspecialchars($message) . "\"/>\n\t\t</testcase>\n";
				break;
			case Runner::SKIPPED:
				$this->buffer .= ">\n\t\t\t<skipped/>\n\t\t</testcase>\n";
				break;
			case Runner::PASSED:
				$this->buffer .= "/>\n";
				break;
		}
	}

	public function end()
	{
		$time = sprintf('%0.1f', microtime(TRUE) - $this->startTime);
		$output = $this->buffer;
		$results = $this->runner->getResults();
		$this->buffer = "\t<testsuite errors=\"{$results[3]}\" skipped=\"{$results[2]}\" tests=\"" . array_sum($results) . "\" time=\"$time\" timestamp=\"" . date('Y-m-d\TH:i:s') . "\">\n";
		$this->buffer .= $output;
		$this->buffer .= "\t</testsuite>";

		fwrite($this->file, $this->buffer . "\n</testsuites>\n");
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester\Runner\Output;

use Tester;
use Tester\Runner\Runner;

/**
 * Verbose logger.
 */
class Logger implements Tester\Runner\OutputHandler
{
	/** @var Runner */
	private $runner;

	/** @var resource */
	private $file;

	public function __construct(Runner $runner, $file = 'php://output')
	{
		$this->runner = $runner;
		$this->file = fopen($file, 'w');
	}

	public function begin()
	{
		fwrite($this->file, 'PHP ' . $this->runner->getInterpreter()->getVersion()
			. ' | ' . $this->runner->getInterpreter()->getCommandLine()
			. " | {$this->runner->threadCount} threads\n\n");
	}

	public function result($testName, $result, $message)
	{
		$message = '   ' . str_replace("\n", "\n   ", Tester\Dumper::removeColors(trim($message)));
		$outputs = array(
			Runner::PASSED => "-- OK: $testName",
			Runner::SKIPPED => "-- Skipped: $testName\n$message",
			Runner::FAILED => "-- FAILED: $testName\n$message",
		);
		fwrite($this->file, $outputs[$result] . "\n\n");
	}

	public function end()
	{
		$jobCount = $this->runner->getJobCount();
		$results = $this->runner->getResults();
		$count = array_sum($results);
		fwrite($this->file,
			($results[Runner::FAILED] ? 'FAILURES!' : 'OK')
			. " ($jobCount tests"
			. ($results[Runner::FAILED] ? ", {$results[Runner::FAILED]} failures" : '')
			. ($results[Runner::SKIPPED] ? ", {$results[Runner::SKIPPED]} skipped" : '')
			. ($jobCount !== $count ? ', ' . ($jobCount - $count) . ' not run' : '')
			. ')'
		);
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester\Runner\Output;

use Tester;
use Tester\Runner\Runner;

/**
 * Test Anything Protocol, http://testanything.org
 */
class TapPrinter implements Tester\Runner\OutputHandler
{
	/** @var Runner */
	private $runner;

	/** @var resource */
	private $file;

	public function __construct(Runner $runner, $file = 'php://output')
	{
		$this->runner = $runner;
		$this->file = fopen($file, 'w');
	}

	public function begin()
	{
		fwrite($this->file, "TAP version 13\n");
	}

	public function result($testName, $result, $message)
	{
		$message = str_replace("\n", "\n# ", trim($message));
		$outputs = array(
			Runner::PASSED => "ok $testName",
			Runner::SKIPPED => "ok $testName #skip $message",
			Runner::FAILED => "not ok $testName\n# $message",
		);
		fwrite($this->file, $outputs[$result] . "\n");
	}

	public function end()
	{
		fwrite($this->file, '1..' . array_sum($this->runner->getResults()));
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester\Runner;

use Tester;

/**
 * Runner output.
 */
interface OutputHandler
{

	function begin();

	function result($testName, $result, $message);

	function end();

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester\Runner;

interface PhpInterpreter
{

	/**
	 * @return string
	 */
	function getCommandLine();

	/**
	 * @return string
	 */
	function getVersion();

	/**
	 * @return bool
	 */
	function hasXdebug();

	/**
	 * @return bool
	 */
	function isCgi();

	/**
	 * @return string
	 */
	function getErrorOutput();

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester\Runner;

use Tester;

/**
 * Test runner.
 */
class Runner
{
	const
		PASSED = 1,
		SKIPPED = 2,
		FAILED = 3;

	const TEST_FILE_EXTENSION = 'phpt';

	/** @var string[]  paths to test files/directories */
	public $paths = array();

	/** @var int  run in parallel threads */
	public $threadCount = 1;

	/** @var TestHandler */
	public $testHandler;

	/** @var OutputHandler[] */
	public $outputHandlers = array();

	/** @var bool */
	public $stopOnFail = FALSE;

	/** @var PhpInterpreter */
	private $interpreter;

	/** @var Job[] */
	private $jobs;

	/** @var int */
	private $jobCount;

	/** @var array */
	private $results;

	/** @var bool */
	private $interrupted;

	public function __construct(PhpInterpreter $interpreter)
	{
		$this->interpreter = $interpreter;
		$this->testHandler = new TestHandler($this);
	}

	/**
	 * Runs all tests.
	 * @return bool
	 */
	public function run()
	{
		$this->interrupted = FALSE;

		foreach ($this->outputHandlers as $handler) {
			$handler->begin();
		}

		$this->results = array(self::PASSED => 0, self::SKIPPED => 0, self::FAILED => 0);
		$this->jobs = $running = array();
		foreach ($this->paths as $path) {
			$this->findTests($path);
		}
		$this->jobCount = count($this->jobs) + array_sum($this->results);

		$this->installInterruptHandler();
		while (($this->jobs || $running) && !$this->isInterrupted()) {
			for ($i = count($running); $this->jobs && $i < $this->threadCount; $i++) {
				$running[] = $job = array_shift($this->jobs);
				$async = $this->threadCount > 1 && (count($running) + count($this->jobs) > 1);
				$job->run($async ? $job::RUN_ASYNC : NULL);
			}

			if (count($running) > 1) {
				usleep(Job::RUN_USLEEP); // stream_select() doesn't work with proc_open()
			}

			foreach ($running as $key => $job) {
				if ($this->isInterrupted()) {
					break 2;
				}

				if (!$job->isRunning()) {
					$this->testHandler->assess($job);
					unset($running[$key]);
				}
			}
		}
		$this->removeInterruptHandler();

		foreach ($this->outputHandlers as $handler) {
			$handler->end();
		}
		return !$this->results[self::FAILED];
	}

	/**
	 * @return void
	 */
	private function findTests($path)
	{
		if (strpbrk($path, '*?') === FALSE && !file_exists($path)) {
			throw new \InvalidArgumentException("File or directory '$path' not found.");
		}

		if (is_dir($path)) {
			foreach (glob(str_replace('[', '[[]', $path) . '/*', GLOB_ONLYDIR) ?: array() as $dir) {
				$this->findTests($dir);
			}
			$path .= '/*.' . self::TEST_FILE_EXTENSION;
		}
		foreach (glob(str_replace('[', '[[]', $path)) ?: array() as $file) {
			if (is_file($file)) {
				$this->testHandler->initiate(realpath($file));
			}
		}
	}

	/**
	 * Appends new job to queue.
	 * @return void
	 */
	public function addJob(Job $job)
	{
		$this->jobs[] = $job;
	}

	/**
	 * Get count of jobs.
	 * @return int
	 */
	public function getJobCount()
	{
		return $this->jobCount;
	}

	/**
	 * Writes to output handlers.
	 * @return void
	 */
	public function writeResult($testName, $result, $message = NULL)
	{
		$this->results[$result]++;
		foreach ($this->outputHandlers as $handler) {
			$handler->result($testName, $result, $message);
		}

		if ($this->stopOnFail && $result === self::FAILED) {
			$this->interrupted = TRUE;
		}
	}

	/**
	 * @return PhpInterpreter
	 */
	public function getInterpreter()
	{
		return $this->interpreter;
	}

	/**
	 * @return array
	 */
	public function getResults()
	{
		return $this->results;
	}

	/**
	 * @return void
	 */
	private function installInterruptHandler()
	{
		if (extension_loaded('pcntl')) {
			$interrupted = & $this->interrupted;
			pcntl_signal(SIGINT, function () use (& $interrupted) {
				pcntl_signal(SIGINT, SIG_DFL);
				$interrupted = TRUE;
			});
		}
	}

	/**
	 * @return void
	 */
	private function removeInterruptHandler()
	{
		if (extension_loaded('pcntl')) {
			pcntl_signal(SIGINT, SIG_DFL);
		}
	}

	/**
	 * @return bool
	 */
	private function isInterrupted()
	{
		if (extension_loaded('pcntl')) {
			pcntl_signal_dispatch();
		}

		return $this->interrupted;
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester\Runner;

use Tester;
use Tester\Dumper;
use Tester\Helpers;

/**
 * Default test behavior.
 */
class TestHandler
{
	const HTTP_OK = 200;

	/** @var Runner */
	private $runner;

	public function __construct(Runner $runner)
	{
		$this->runner = $runner;
	}

	/**
	 * @return void
	 */
	public function initiate($file)
	{
		list($annotations, $testName) = $this->getAnnotations($file);
		$php = clone $this->runner->getInterpreter();
		$jobsArgs = array(array());

		foreach (get_class_methods($this) as $method) {
			if (!preg_match('#^initiate(.+)#', strtolower($method), $m) || !isset($annotations[$m[1]])) {
				continue;
			}
			foreach ((array) $annotations[$m[1]] as $value) {
				$res = $this->$method($value, $php, $file);
				if ($res && is_int($res[0])) { // [Runner::*, message]
					$this->runner->writeResult($testName, $res[0], $res[1]);
					return;
				} elseif ($res && $res[1]) { // [param name, values]
					$tmp = array();
					foreach ($res[1] as $val) {
						foreach ($jobsArgs as $args) {
							$args[] = Helpers::escapeArg("--$res[0]=$val");
							$tmp[] = $args;
						}
					}
					$jobsArgs = $tmp;
				}
			}
		}

		foreach ($jobsArgs as $args) {
			$this->runner->addJob(new Job($file, $php, $args));
		}
	}

	/**
	 * @return void
	 */
	public function assess(Job $job)
	{
		list($annotations, $testName) = $this->getAnnotations($job->getFile());
		$testName .= $job->getArguments()
			? ' [' . implode(' ', preg_replace(array('#["\'-]*(.+?)["\']?$#A', '#(.{30}).+#A'), array('$1', '$1...'), $job->getArguments())) . ']'
			: '';
		$annotations += array(
			'exitcode' => Job::CODE_OK,
			'httpcode' => self::HTTP_OK,
		);

		foreach (get_class_methods($this) as $method) {
			if (!preg_match('#^assess(.+)#', strtolower($method), $m) || !isset($annotations[$m[1]])) {
				continue;
			}
			foreach ((array) $annotations[$m[1]] as $arg) {
				if ($res = $this->$method($job, $arg)) {
					$this->runner->writeResult($testName, $res[0], $res[1]);
					return;
				}
			}
		}
		$this->runner->writeResult($testName, Runner::PASSED);
	}

	private function initiateSkip($message)
	{
		return array(Runner::SKIPPED, $message);
	}

	private function initiatePhpVersion($version, PhpInterpreter $interpreter)
	{
		if (preg_match('#^(<=|<|==|=|!=|<>|>=|>)?\s*(.+)#', $version, $matches)
			&& version_compare($matches[2], $interpreter->getVersion(), $matches[1] ?: '>='))
		{
			return array(Runner::SKIPPED, "Requires PHP $version.");
		}
	}

	private function initiatePhpIni($value, PhpInterpreter $interpreter)
	{
		$interpreter->arguments .= ' -d ' . Helpers::escapeArg($value);
	}

	private function initiateDataProvider($provider, PhpInterpreter $interpreter, $file)
	{
		try {
			list($dataFile, $query, $optional) = Tester\DataProvider::parseAnnotation($provider, $file);
			$data = Tester\DataProvider::load($dataFile, $query);
		} catch (\Exception $e) {
			return array(empty($optional) ? Runner::FAILED : Runner::SKIPPED, $e->getMessage());
		}

		$res = array();
		foreach (array_keys($data) as $item) {
			$res[] = "$item|$dataFile";
		}
		return array('dataprovider', $res);
	}

	private function initiateMultiple($count, PhpInterpreter $interpreter, $file)
	{
		return array('multiple', range(0, (int) $count - 1));
	}

	private function initiateTestCase($foo, PhpInterpreter $interpreter, $file)
	{
		$job = new Job($file, $interpreter, array(Helpers::escapeArg('--method=' . Tester\TestCase::LIST_METHODS)));
		$job->run();

		if (in_array($job->getExitCode(), array(Job::CODE_ERROR, Job::CODE_FAIL, Job::CODE_SKIP), TRUE)) {
			return array($job->getExitCode() === Job::CODE_SKIP ? Runner::SKIPPED : Runner::FAILED, $job->getOutput());
		}

		if (!preg_match('#\[([^[]*)]#', strrchr($job->getOutput(), '['), $m)) {
			return array(Runner::FAILED, "Cannot list TestCase methods in file '$file'. Do you call TestCase::run() in it?");
		} elseif (!strlen($m[1])) {
			return array(Runner::SKIPPED, "TestCase in file '$file' does not contain test methods.");
		}

		return array('method', explode(',', $m[1]));
	}

	private function assessExitCode(Job $job, $code)
	{
		$code = (int) $code;
		if ($job->getExitCode() === Job::CODE_SKIP) {
			$message = preg_match('#.*Skipped:\n(.*?)\z#s', $output = $job->getOutput(), $m)
				? $m[1]
				: $output;
			return array(Runner::SKIPPED, trim($message));

		} elseif ($job->getExitCode() !== $code) {
			$message = $job->getExitCode() !== Job::CODE_FAIL ? "Exited with error code {$job->getExitCode()} (expected $code)" : '';
			return array(Runner::FAILED, trim($message . "\n" . $job->getOutput()));
		}
	}

	private function assessHttpCode(Job $job, $code)
	{
		if (!$this->runner->getInterpreter()->isCgi()) {
			return;
		}
		$headers = $job->getHeaders();
		$actual = isset($headers['Status']) ? (int) $headers['Status'] : self::HTTP_OK;
		$code = (int) $code;
		if ($code && $code !== $actual) {
			return array(Runner::FAILED, "Exited with HTTP code $actual (expected $code)");
		}
	}

	private function assessOutputMatchFile(Job $job, $file)
	{
		$file = dirname($job->getFile()) . DIRECTORY_SEPARATOR . $file;
		if (!is_file($file)) {
			return array(Runner::FAILED, "Missing matching file '$file'.");
		}
		return $this->assessOutputMatch($job, file_get_contents($file));
	}

	private function assessOutputMatch(Job $job, $content)
	{
		if (!Tester\Assert::isMatching($content, $job->getOutput())) {
			Dumper::saveOutput($job->getFile(), $job->getOutput(), '.actual');
			Dumper::saveOutput($job->getFile(), $content, '.expected');
			return array(Runner::FAILED, 'Failed: output should match ' . Dumper::toLine(rtrim($content)));
		}
	}

	private function getAnnotations($file)
	{
		$annotations = Helpers::parseDocComment(file_get_contents($file));
		$testName = (isset($annotations[0]) ? preg_replace('#^TEST:\s*#i', '', $annotations[0]) . ' | ' : '')
			. implode(DIRECTORY_SEPARATOR, array_slice(explode(DIRECTORY_SEPARATOR, $file), -3));
		return array($annotations, $testName);
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (http://davidgrudl.com)
 */

namespace Tester\Runner;

use Tester\Helpers;

/**
 * Zend phpdbg command-line executable.
 */
class ZendPhpDbgInterpreter implements PhpInterpreter
{
	/** @var string  PHP arguments */
	public $arguments;

	/** @var string  PHP executable */
	private $path;

	/** @var string  PHP version */
	private $version;

	/** @var string */
	private $error;

	public function __construct($path, $args = NULL)
	{
		$this->path = Helpers::escapeArg($path);
		$proc = proc_open(
			"$this->path -n $args -V",
			array(array('pipe', 'r'), array('pipe', 'w'), array('pipe', 'w')),
			$pipes,
			NULL,
			NULL,
			array('bypass_shell' => TRUE)
		);
		$output = stream_get_contents($pipes[1]);

		$this->error = trim(stream_get_contents($pipes[2]));
		if (proc_close($proc)) {
			throw new \Exception("Unable to run '$path': " . preg_replace('#[\r\n ]+#', ' ', $this->error));
		} elseif (!preg_match('#^PHP ([\w.-]+)#im', $output, $matches)) {
			throw new \Exception("Unable to detect PHP version (output: $output).");
		} elseif (version_compare($matches[1], '7.0.0', '<')) {
			throw new \Exception('Unable to use phpdbg on PHP < 7.0.0.');
		}

		$this->version = $matches[1];
		$this->arguments = $args;
	}

	/**
	 * @return string
	 */
	public function getCommandLine()
	{
		return $this->path . ' -qrrb -S cli' . $this->arguments;
	}

	/**
	 * @return string
	 */
	public function getVersion()
	{
		return $this->version;
	}

	/**
	 * @return bool
	 */
	public function hasXdebug()
	{
		return TRUE;
	}

	/**
	 * @return bool
	 */
	public function isCgi()
	{
		return FALSE;
	}

	/**
	 * @return string
	 */
	public function getErrorOutput()
	{
		return $this->error;
	}

}


code:
<?php

/**
 * This file is part of the Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

namespace Tester\Runner;

use Tester\Helpers;

/**
 * Zend PHP command-line executable.
 */
class ZendPhpInterpreter implements PhpInterpreter
{
	/** @var string  PHP arguments */
	public $arguments;

	/** @var string  PHP executable */
	private $path;

	/** @var string  PHP version */
	private $version;

	/** @var bool is CGI? */
	private $cgi;

	/** @var bool */
	private $xdebug;

	/** @var string */
	private $error;

	public function __construct($path, $args = NULL)
	{
		$this->path = Helpers::escapeArg($path);
		$proc = proc_open(
			"$this->path -n $args -v", // -v must be the last
			array(array('pipe', 'r'), array('pipe', 'w'), array('pipe', 'w')),
			$pipes,
			NULL,
			NULL,
			array('bypass_shell' => TRUE)
		);
		$output = stream_get_contents($pipes[1]);
		$this->error = trim(stream_get_contents($pipes[2]));
		if (proc_close($proc)) {
			throw new \Exception("Unable to run '$path': " . preg_replace('#[\r\n ]+#', ' ', $this->error));
		} elseif (!preg_match('#^PHP (\S+).*c(g|l)i#i', $output, $matches)) {
			throw new \Exception("Unable to detect PHP version (output: $output).");
		}

		$this->version = $matches[1];
		$this->cgi = strcasecmp($matches[2], 'g') === 0;
		$this->arguments = $args;

		$job = new Job(__DIR__ . '/info.php', $this, array('xdebug'));
		$job->run();
		$this->xdebug = !$job->getExitCode();
	}

	/**
	 * @return string
	 */
	public function getCommandLine()
	{
		return $this->path . $this->arguments;
	}

	/**
	 * @return string
	 */
	public function getVersion()
	{
		return $this->version;
	}

	/**
	 * @return bool
	 */
	public function hasXdebug()
	{
		return $this->xdebug;
	}

	/**
	 * @return bool
	 */
	public function isCgi()
	{
		return $this->cgi;
	}

	/**
	 * @return string
	 */
	public function getErrorOutput()
	{
		return $this->error;
	}

}


code:
<?php

/**
 * @internal
 */

if (isset($_SERVER['argv'][1])) {
	die(extension_loaded($_SERVER['argv'][1]) ? 0 : 1);
}

$iniFiles = array_merge(
	($tmp = php_ini_loaded_file()) === FALSE ? array() : array($tmp),
	(function_exists('php_ini_scanned_files') && strlen($tmp = php_ini_scanned_files())) ? explode(",\n", trim($tmp)) : array()
);

$extensions = get_loaded_extensions();
natcasesort($extensions);

$isHhvm = defined('HHVM_VERSION');

$values = array(
	'PHP binary' => defined('PHP_BINARY') ? PHP_BINARY : '(not available)',

	'PHP version' . ($isHhvm ? '; HHVM version' : '') => PHP_VERSION . ' (' . PHP_SAPI . ')' . ($isHhvm ? '; ' . HHVM_VERSION : ''),

	'Loaded php.ini files' => count($iniFiles) ? implode(', ', $iniFiles) : ($isHhvm ? '(unable to detect under HHVM)' : '(none)'),

	'Loaded extensions' => count($extensions) ? implode(', ', $extensions) : '(none)',
);

foreach ($values as $title => $value) {
	echo "\033[1;32m$title\033[0m:\n$value\n\n";
}

echo "\n\n";


code:
<?php

/**
 * Test environment initialization.
 */

require __DIR__ . '/Framework/Helpers.php';
require __DIR__ . '/Framework/Environment.php';
require __DIR__ . '/Framework/DataProvider.php';
require __DIR__ . '/Framework/Assert.php';
require __DIR__ . '/Framework/AssertException.php';
require __DIR__ . '/Framework/Dumper.php';
require __DIR__ . '/Framework/FileMock.php';
require __DIR__ . '/Framework/TestCase.php';
require __DIR__ . '/Framework/DomQuery.php';
require __DIR__ . '/CodeCoverage/Collector.php';
require __DIR__ . '/Runner/Job.php';

Tester\Environment::setup();


code:
#!/usr/bin/env php
<?php

require __DIR__ . '/tester.php';

path: vendor/nette/tester/src/tester


code:
<?php

/**
 * Nette Tester.
 * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
 */

require __DIR__ . '/Runner/PhpInterpreter.php';
require __DIR__ . '/Runner/ZendPhpInterpreter.php';
require __DIR__ . '/Runner/ZendPhpDbgInterpreter.php';
require __DIR__ . '/Runner/HhvmPhpInterpreter.php';
require __DIR__ . '/Runner/Runner.php';
require __DIR__ . '/Runner/CliTester.php';
require __DIR__ . '/Runner/Job.php';
require __DIR__ . '/Runner/CommandLine.php';
require __DIR__ . '/Runner/TestHandler.php';
require __DIR__ . '/Runner/OutputHandler.php';
require __DIR__ . '/Runner/Output/Logger.php';
require __DIR__ . '/Runner/Output/TapPrinter.php';
require __DIR__ . '/Runner/Output/ConsolePrinter.php';
require __DIR__ . '/Runner/Output/JUnitPrinter.php';
require __DIR__ . '/Framework/Helpers.php';
require __DIR__ . '/Framework/Environment.php';
require __DIR__ . '/Framework/Assert.php';
require __DIR__ . '/Framework/AssertException.php';
require __DIR__ . '/Framework/Dumper.php';
require __DIR__ . '/Framework/DataProvider.php';
require __DIR__ . '/Framework/TestCase.php';
require __DIR__ . '/CodeCoverage/PhpParser.php';
require __DIR__ . '/CodeCoverage/Generators/AbstractGenerator.php';
require __DIR__ . '/CodeCoverage/Generators/HtmlGenerator.php';
require __DIR__ . '/CodeCoverage/Generators/CloverXMLGenerator.php';

$tester = new Tester\Runner\CliTester;
die($tester->run());


nette/tokenizer

description: Nette Tokenizer

minimum-stability: dev


code:
Nette Tokenizer
===============

[![Downloads this Month](https://img.shields.io/packagist/dm/nette/tokenizer.svg)](https://packagist.org/packages/nette/tokenizer)
[![Build Status](https://travis-ci.org/nette/tokenizer.svg?branch=v2.2)](https://travis-ci.org/nette/tokenizer)

Tokenizer is a tool that uses regular expressions to split given string into tokens. What the hell is that good for, you might ask? Well, you can create your own languages! Tokenizer is used in [Latte](https://github.com/nette/latte) for example.

## String tokenization

Let's create a simple tokenizer that separates strings to numbers, whitespaces and letters.

```php
$tokenizer = new Tokenizer(array(
	T_DNUMBER => '\d+',
	T_WHITESPACE => '\s+',
	T_STRING => '\w+',
));
```

*Hint: In case you are wondering where the T_ constants come from, they are [internal type](http://php.net/manual/tokens.php) used for parsing code. They cover most of the common token names we usually need. Keep in mind their value is not guaranteed so don't use numbers for comparison.*

Now when we give it a string, it will return array of tokens.

```php
$tokens = $tokenizer->tokenize("say \n123");
```

The resulting array of tokens would look like this.

```php
array(
	array('say', 0, T_STRING),
	array(" \n", 3, T_WHITESPACE),
	array('123', 5, T_DNUMBER),
)
```

Also, you should use constants from `Tokenizer` to access the individual values or expand them using `list()`.

```php
$firstToken = $tokens[0];
echo $firstToken[Tokenizer::VALUE]; // say
echo $firstToken[Tokenizer::OFFSET]; // 0
echo $firstToken[Tokenizer::TYPE]; // 308, which is the value of T_STRING

// or shorter
list($value, $offset, $type) = $tokens[0];
```

Simple, isn't it?

## Processing the tokens

Now we know how to create tokens from string. Let's effectively process them using `TokenIterator`. The `TokenIterator` is not a standard iterator. You cannot `foreach` over it, it doesn't implement `Traversable` interface. But it has a lot of really awesome methods if you need to traverse tokens!

Let's try to parse a simple annotation from PHPDoc and create an object from it. What regular expressions do we need for tokens? All the annotations start with `@`, then there is a name, whitespace and it's value.

- `@` for the annotation start
- `\\s+` for whitespaces
- `\\w+` for strings

This should work on simple annotations, right? Now let's define few classes to demonstrate.

```php
class Author
{
	public $name;

	public function __construct($name)
	{
		$this->name = $name;
	}
}

class Package
{
	public $name;

	public function __construct($name)
	{
		$this->name = $name;
	}
}
```

and input string that we will try to parse.

```php
$input = "
	@author David Grudl
	@package Nette
";
```

Let's create a `Parser` class that will accept the string and return an array of objects. It will be very naive and simple.

```php
class Parser
{
	const T_AT = 1;
	const T_WHITESPACE = 2;
	const T_STRING = 3;

	/** @var \Nette\Utils\Tokenizer */
	private $tokenizer;

	/** @var \Nette\Utils\TokenIterator */
	private $iterator;

	public function __construct()
	{
		$this->tokenizer = new Tokenizer(array(
			self::T_AT => '@',
			self::T_WHITESPACE => '\\s+',
			self::T_STRING => '\\w+',
		));
	}

	public function parse($input)
	{
		$this->iterator = new TokenIterator($this->tokenizer->tokenize($input));

		$result = array();
		while ($this->iterator->nextToken()) {
			if ($this->iterator->isCurrent(self::T_AT)) {
				$result[] = $this->parseAnnotation();
			}
		}

		return $result;
	}

	protected function parseAnnotation()
	{
		$name = $this->iterator->joinUntil(self::T_WHITESPACE);
		$this->iterator->nextUntil(self::T_STRING);
		$content = $this->iterator->joinUntil(self::T_AT);

		return new $name(trim($content));
	}
}
```

```php
$parser = new Parser();
$annotations = $parser->parse($input);
```

So what the `parse()` method does? It iterates over the tokens and searches for `@` which is the symbol annotations start with. Calling `nextToken()` moves the cursor to the next token. Method `isCurrent()` checks if the current token at the cursor is the given type. Then, if the `@` is found, the `parse()` method calls `parseAnnotation()` which expects the annotations to be in a very speficic format.

First, using the method `joinUntil()`, the iterator keeps moving the cursor and appending the string to the buffer until it finds token of required type, then stops and returns the buffer output. Because there is only one token of type `T_STRING` at that given position and it's `'name'`, there will be value `'name'` in variable `$name`.

Method `nextUntil()` is similar like `joinUntil()` but it has no buffer. It only moves the cursor until it finds the token. So this call simply skips all the whitespaces after annotation name.

And then, there is another `joinUntil()`, that searches for next `@`. This specific call will return `"David Grudl\n    "`.

And there we go, we've parsed one whole annotation! Now we can create an instance of class for that specific annotation and pass it the parsed value. The `$content` probably ends with whitespaces, so we have to trim it.

Try copypasting the code and running it. If you dump the `$annotations` variable it should return some similar output.

```
array (2)
   0 => Author
   |  name => "David Grudl"
   |
   1 => Package
      name => "Nette"
```

## TokenIterator methods

The iterator can return current token using method `currentToken()` or only it's value using `currentValue()`.

`nextToken()` moves the cursor and returns the token. If you give it no arguments, it simply returns next token.

`nextValue()` is just like `nextToken()` but it only returns the token value.

Most of the methods also accept multiple arguments so you can search for multiple types at once.

```php
// iterate until a string or a whitespace is found, then stop and return the following token
$token = $iterator->nextToken(T_STRING, T_WHITESPACE);

// give me next token
$token = $iterator->nextToken();
```

You can also search by the token value.

```php
// move the cursor until you find token containing only '@', then stop and return it
$token = $iterator->nextToken('@');
```

`nextUntil()` moves the cursor and returns the array of all the tokens it sees until it finds the desired token, but it stops before the token. It can accept multiple arguments.

`joinUntil()` is similar to `nextUntil()`, but joins the token values in a buffer and when it finds the desired token, it stops before it and returns the buffer contents.

`joinAll()` simply concatenates all the remaining token values and returns it. It moves the cursor to the end of the token stream

`nextAll()` is just like `joinAll()`, but it returns array of the tokens.

`isCurrent()` checks if the current token or the current token's value is equal to one of given arguments.

```php
// is the current token '@' or type of T_AT?
$iterator->isCurrent(T_AT, '@');
```

`isNext()` is just like `isCurrent()` but it checks the next token.

`isPrev()` is just like `isCurrent()` but it checks the previous token.

And the last method `reset()` resets the cursor, so you can iterate the token stream again.


code:
<?php

/**
 * This file is part of the Nette Framework (http://nette.org)
 * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
 */

namespace Nette\Utils;

/**
 * Traversing helper.
 */
class TokenIterator
{
	/** @var array */
	public $tokens;

	/** @var int */
	public $position = -1;

	/** @var array */
	public $ignored = array();

	/**
	 * @param array[]
	 */
	public function __construct(array $tokens)
	{
		$this->tokens = $tokens;
	}

	/**
	 * Returns current token.
	 * @return array|NULL
	 */
	public function currentToken()
	{
		return isset($this->tokens[$this->position])
			? $this->tokens[$this->position]
			: NULL;
	}

	/**
	 * Returns current token value.
	 * @return string|NULL
	 */
	public function currentValue()
	{
		return isset($this->tokens[$this->position])
			? $this->tokens[$this->position][Tokenizer::VALUE]
			: NULL;
	}

	/**
	 * Returns next token.
	 * @param  int|string  (optional) desired token type or value
	 * @return array|NULL
	 */
	public function nextToken()
	{
		return $this->scan(func_get_args(), TRUE, TRUE); // onlyFirst, advance
	}

	/**
	 * Returns next token value.
	 * @param  int|string  (optional) desired token type or value
	 * @return string|NULL
	 */
	public function nextValue()
	{
		return $this->scan(func_get_args(), TRUE, TRUE, TRUE); // onlyFirst, advance, strings
	}

	/**
	 * Returns all next tokens.
	 * @param  int|string  (optional) desired token type or value
	 * @return array[]
	 */
	public function nextAll()
	{
		return $this->scan(func_get_args(), FALSE, TRUE); // advance
	}

	/**
	 * Returns all next tokens until it sees a given token type or value.
	 * @param  int|string  token type or value to stop before
	 * @return array[]
	 */
	public function nextUntil($arg)
	{
		return $this->scan(func_get_args(), FALSE, TRUE, FALSE, TRUE); // advance, until
	}

	/**
	 * Returns concatenation of all next token values.
	 * @param  int|string  (optional) token type or value to be joined
	 * @return string
	 */
	public function joinAll()
	{
		return $this->scan(func_get_args(), FALSE, TRUE, TRUE); // advance, strings
	}

	/**
	 * Returns concatenation of all next tokens until it sees a given token type or value.
	 * @param  int|string  token type or value to stop before
	 * @return string
	 */
	public function joinUntil($arg)
	{
		return $this->scan(func_get_args(), FALSE, TRUE, TRUE, TRUE); // advance, strings, until
	}

	/**
	 * Checks the current token.
	 * @param  int|string  token type or value
	 * @return bool
	 */
	public function isCurrent($arg)
	{
		if (!isset($this->tokens[$this->position])) {
			return FALSE;
		}
		$args = func_get_args();
		$token = $this->tokens[$this->position];
		return in_array($token[Tokenizer::VALUE], $args, TRUE)
			|| (isset($token[Tokenizer::TYPE]) && in_array($token[Tokenizer::TYPE], $args, TRUE));
	}

	/**
	 * Checks the next token existence.
	 * @param  int|string  (optional) token type or value
	 * @return bool
	 */
	public function isNext()
	{
		return (bool) $this->scan(func_get_args(), TRUE, FALSE); // onlyFirst
	}

	/**
	 * Checks the previous token existence.
	 * @param  int|string  (optional) token type or value
	 * @return bool
	 */
	public function isPrev()
	{
		return (bool) $this->scan(func_get_args(), TRUE, FALSE, FALSE, FALSE, TRUE); // onlyFirst, prev
	}

	/**
	 * @return self
	 */
	public function reset()
	{
		$this->position = -1;
		return $this;
	}

	/**
	 * Moves cursor to next token.
	 */
	protected function next()
	{
		$this->position++;
	}

	/**
	 * Looks for (first) (not) wanted tokens.
	 * @param  array of desired token types or values
	 * @param  bool
	 * @param  bool
	 * @param  bool
	 * @param  bool
	 * @param  bool
	 * @return mixed
	 */
	protected function scan($wanted, $onlyFirst, $advance, $strings = FALSE, $until = FALSE, $prev = FALSE)
	{
		$res = $onlyFirst ? NULL : ($strings ? '' : array());
		$pos = $this->position + ($prev ? -1 : 1);
		do {
			if (!isset($this->tokens[$pos])) {
				if (!$wanted && $advance && !$prev && $pos <= count($this->tokens)) {
					$this->next();
				}
				return $res;
			}

			$token = $this->tokens[$pos];
			$type = isset($token[Tokenizer::TYPE]) ? $token[Tokenizer::TYPE] : NULL;
			if (!$wanted || (in_array($token[Tokenizer::VALUE], $wanted, TRUE) || in_array($type, $wanted, TRUE)) ^ $until) {
				while ($advance && !$prev && $pos > $this->position) {
					$this->next();
				}

				if ($onlyFirst) {
					return $strings ? $token[Tokenizer::VALUE] : $token;
				} elseif ($strings) {
					$res .= $token[Tokenizer::VALUE];
				} else {
					$res[] = $token;
				}

			} elseif ($until || !in_array($type, $this->ignored, TRUE)) {
				return $res;
			}
			$pos += $prev ? -1 : 1;
		} while (TRUE);
	}

}


code:
<?php

/**
 * This file is part of the Nette Framework (http://nette.org)
 * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
 */

namespace Nette\Utils;

/**
 * Simple lexical analyser.
 */
class Tokenizer
{
	const VALUE = 0,
		OFFSET = 1,
		TYPE = 2;

	/** @var string */
	private $re;

	/** @var array|FALSE */
	private $types;

	/**
	 * @param  array of [(int|string) token type => (string) pattern]
	 * @param  string  regular expression flags
	 */
	public function __construct(array $patterns, $flags = '')
	{
		$this->re = '~(' . implode(')|(', $patterns) . ')~A' . $flags;
		$keys = array_keys($patterns);
		$this->types = $keys === range(0, count($patterns) - 1) ? FALSE : $keys;
	}

	/**
	 * Tokenizes string.
	 * @param  string
	 * @return array
	 * @throws TokenizerException
	 */
	public function tokenize($input)
	{
		if ($this->types) {
			preg_match_all($this->re, $input, $tokens, PREG_SET_ORDER);
			$len = 0;
			$count = count($this->types);
			foreach ($tokens as & $match) {
				$type = NULL;
				for ($i = 1; $i <= $count; $i++) {
					if (!isset($match[$i])) {
						break;
					} elseif ($match[$i] != NULL) {
						$type = $this->types[$i - 1]; break;
					}
				}
				$match = array(self::VALUE => $match[0], self::OFFSET => $len, self::TYPE => $type);
				$len += strlen($match[self::VALUE]);
			}
			if ($len !== strlen($input)) {
				$errorOffset = $len;
			}

		} else {
			$tokens = preg_split($this->re, $input, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_DELIM_CAPTURE);
			$last = end($tokens);
			if ($tokens && !preg_match($this->re, $last[0])) {
				$errorOffset = $last[1];
			}
		}

		if (isset($errorOffset)) {
			list($line, $col) = $this->getCoordinates($input, $errorOffset);
			$token = str_replace("\n", '\n', substr($input, $errorOffset, 10));
			throw new TokenizerException("Unexpected '$token' on line $line, column $col.");
		}
		return $tokens;
	}

	/**
	 * Returns position of token in input string.
	 * @param  string
	 * @param  int
	 * @return array of [line, column]
	 */
	public static function getCoordinates($text, $offset)
	{
		$text = substr($text, 0, $offset);
		return array(substr_count($text, "\n") + 1, $offset - strrpos("\n" . $text, "\n") + 1);
	}

}


code:
<?php

/**
 * This file is part of the Nette Framework (http://nette.org)
 *
 * Copyright (c) 2004, 2011 David Grudl (http://davidgrudl.com)
 *
 * For the full copyright and license information, please view
 * the file license.txt that was distributed with this source code.
 */

namespace Nette\Utils;

/**
 * The exception that indicates tokenizer error.
 */
class TokenizerException extends \Exception
{
}


nette/utils

description: Nette Utility Classes

ext-iconv: to use Strings::webalize() and toAscii()

ext-intl: for script transliteration in Strings::webalize() and toAscii()

ext-mbstring: to use Strings::lower() etc...

ext-gd: to use Image


code:
<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

namespace Nette\Iterators;

use Nette;
use Nette\Utils\ObjectMixin;

/**
 * Smarter caching iterator.
 *
 * @property-read bool $first
 * @property-read bool $last
 * @property-read bool $empty
 * @property-read bool $odd
 * @property-read bool $even
 * @property-read int $counter
 * @property-read mixed $nextKey
 * @property-read mixed $nextValue
 */
class CachingIterator extends \CachingIterator implements \Countable
{
	/** @var int */
	private $counter = 0;

	public function __construct($iterator)
	{
		if (is_array($iterator) || $iterator instanceof \stdClass) {
			$iterator = new \ArrayIterator($iterator);

		} elseif ($iterator instanceof \IteratorAggregate) {
			do {
				$iterator = $iterator->getIterator();
			} while ($iterator instanceof \IteratorAggregate);

		} elseif ($iterator instanceof \Traversable) {
			if (!$iterator instanceof \Iterator) {
				$iterator = new \IteratorIterator($iterator);
			}
		} else {
			throw new Nette\InvalidArgumentException(sprintf('Invalid argument passed to %s; array or Traversable expected, %s given.', __CLASS__, is_object($iterator) ? get_class($iterator) : gettype($iterator)));
		}

		parent::__construct($iterator, 0);
	}

	/**
	 * Is the current element the first one?
	 * @param  int  grid width
	 * @return bool
	 */
	public function isFirst($width = NULL)
	{
		return $this->counter === 1 || ($width && $this->counter !== 0 && (($this->counter - 1) % $width) === 0);
	}

	/**
	 * Is the current element the last one?
	 * @param  int  grid width
	 * @return bool
	 */
	public function isLast($width = NULL)
	{
		return !$this->hasNext() || ($width && ($this->counter % $width) === 0);
	}

	/**
	 * Is the iterator empty?
	 * @return bool
	 */
	public function isEmpty()
	{
		return $this->counter === 0;
	}

	/**
	 * Is the counter odd?
	 * @return bool
	 */
	public function isOdd()
	{
		return $this->counter % 2 === 1;
	}

	/**
	 * Is the counter even?
	 * @return bool
	 */
	public function isEven()
	{
		return $this->counter % 2 === 0;
	}

	/**
	 * Returns the counter.
	 * @return int
	 */
	public function getCounter()
	{
		return $this->counter;
	}

	/**
	 * Returns the count of elements.
	 * @return int
	 */
	public function count()
	{
		$inner = $this->getInnerIterator();
		if ($inner instanceof \Countable) {
			return $inner->count();

		} else {
			throw new Nette\NotSupportedException('Iterator is not countable.');
		}
	}

	/**
	 * Forwards to the next element.
	 * @return void
	 */
	public function next()
	{
		parent::next();
		if (parent::valid()) {
			$this->counter++;
		}
	}

	/**
	 * Rewinds the Iterator.
	 * @return void
	 */
	public function rewind()
	{
		parent::rewind();
		$this->counter = parent::valid() ? 1 : 0;
	}

	/**
	 * Returns the next key.
	 * @return mixed
	 */
	public function getNextKey()
	{
		return $this->getInnerIterator()->key();
	}

	/**
	 * Returns the next element.
	 * @return mixed
	 */
	public function getNextValue()
	{
		return $this->getInnerIterator()->current();
	}

	/********************* Nette\Object behaviour ****************d*g**/

	/**
	 * Call to undefined method.
	 * @param  string  method name
	 * @param  array   arguments
	 * @return mixed
	 * @throws Nette\MemberAccessException
	 */
	public function __call($name, $args)
	{
		return ObjectMixin::call($this, $name, $args);
	}

	/**
	 * Returns property value. Do not call directly.
	 * @param  string  property name
	 * @return mixed   property value
	 * @throws Nette\MemberAccessException if the property is not defined.
	 */
	public function &__get($name)
	{
		return ObjectMixin::get($this, $name);
	}

	/**
	 * Sets value of a property. Do not call directly.
	 * @param  string  property name
	 * @param  mixed   property value
	 * @return void
	 * @throws Nette\MemberAccessException if the property is not defined or is read-only
	 */
	public function __set($name, $value)
	{
		ObjectMixin::set($this, $name, $value);
	}

	/**
	 * Is property defined?
	 * @param  string  property name
	 * @return bool
	 */
	public function __isset($name)
	{
		return ObjectMixin::has($this, $name);
	}

	/**
	 * Access to undeclared property.
	 * @param  string  property name
	 * @return void
	 * @throws Nette\MemberAccessException
	 */
	public function __unset($name)
	{
		ObjectMixin::remove($this, $name);
	}

}


code:
<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

namespace Nette\Iterators;

use Nette;

/**
 * CallbackFilterIterator for PHP < 5.4.
 */
class Filter extends \FilterIterator
{
	/** @var callable */
	protected $callback;

	public function __construct(\Iterator $iterator, $callback)
	{
		parent::__construct($iterator);
		$this->callback = Nette\Utils\Callback::check($callback);
	}

	public function accept()
	{
		return call_user_func($this->callback, $this->current(), $this->key(), $this);
	}

}


code:
<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

namespace Nette\Iterators;

use Nette;

/**
 * Applies the callback to the elements of the inner iterator.
 */
class Mapper extends \IteratorIterator
{
	/** @var callable */
	private $callback;

	public function __construct(\Traversable $iterator, $callback)
	{
		parent::__construct($iterator);
		$this->callback = Nette\Utils\Callback::check($callback);
	}

	public function current()
	{
		return call_user_func($this->callback, parent::current(), parent::key());
	}

}


code:
<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

namespace Nette\Iterators;

/**
 * RecursiveCallbackFilterIterator for PHP < 5.4.
 */
class RecursiveFilter extends Filter implements \RecursiveIterator
{

	public function __construct(\RecursiveIterator $iterator, $callback)
	{
		parent::__construct($iterator, $callback);
	}

	public function hasChildren()
	{
		return $this->getInnerIterator()->hasChildren();
	}

	public function getChildren()
	{
		return new static($this->getInnerIterator()->getChildren(), $this->callback);
	}

}


code:
<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

namespace Nette\Utils;

use Nette;

/**
 * Provides objects to work as array.
 */
class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \IteratorAggregate
{

	/**
	 * @param  array to wrap
	 * @param  bool
	 * @return self
	 */
	public static function from($arr, $recursive = TRUE)
	{
		$obj = new static;
		foreach ($arr as $key => $value) {
			if ($recursive && is_array($value)) {
				$obj->$key = static::from($value, TRUE);
			} else {
				$obj->$key = $value;
			}
		}
		return $obj;
	}

	/**
	 * Returns an iterator over all items.
	 * @return \RecursiveArrayIterator
	 */
	public function getIterator()
	{
		return new \RecursiveArrayIterator((array) $this);
	}

	/**
	 * Returns items count.
	 * @return int
	 */
	public function count()
	{
		return count((array) $this);
	}

	/**
	 * Replaces or appends a item.
	 * @return void
	 */
	public function offsetSet($key, $value)
	{
		if (!is_scalar($key)) { // prevents NULL
			throw new Nette\InvalidArgumentException(sprintf('Key must be either a string or an integer, %s given.', gettype($key)));
		}
		$this->$key = $value;
	}

	/**
	 * Returns a item.
	 * @return mixed
	 */
	public function offsetGet($key)
	{
		return $this->$key;
	}

	/**
	 * Determines whether a item exists.
	 * @return bool
	 */
	public function offsetExists($key)
	{
		return isset($this->$key);
	}

	/**
	 * Removes the element from this list.
	 * @return void
	 */
	public function offsetUnset($key)
	{
		unset($this->$key);
	}

}