Code coverage 62 %

0 %
tester.php
<?php

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

require __DIR__ '/Runner/Test.php';
require 
__DIR__ '/Runner/PhpInterpreter.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';


die((new 
Tester\Runner\CliTester)->run());
97 %
Framework/Dumper.php
<?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($vartrue)); // 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($var0self::$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 '[...]' self::toLine($v));
                
$counter is_int($k) ? max($k 1$counter) : $counter;
            }
            return 
"[$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)), 04);
    }


    
/**
     * @return string
     */
    
private static function _toPhp(&$var, &$list = [], $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 (
$var === null) {
            return 
'null';

        } 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 
'[' $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($kstrrpos($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 */ [$out]"
                
"$class::__set_state(/* $hash */ [$out])";

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

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


    
/**
     * @param  \Exception|\Throwable
     * @internal
     */
    
public static function dumpException($e)
    {
        
$trace $e->getTrace();
        
array_splice($trace0$e instanceof \ErrorException 0, [['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(['#^-*(.{1,20}).*#i''#[^=a-z0-9. -]+#i'], ['$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++);
                for (; 
$i && $i strlen($actual) && $actual[$i 1] >= "\x80" && $actual[$i] >= "\x80" && $actual[$i] < "\xC0"$i--);
                
$i max(0min(
                    
$i - (int) (self::$maxLength 3), // try to display 1/3 of shorter string
                    
max(strlen($actual), strlen($expected)) - self::$maxLength // 3 = length of ...
                
));
                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, [
                
'%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 += ['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'])
                && 
strpos($tmp file($item['file'])[$item['line'] - 1], "::$item[function](") ? $tmp null;

            
$s .= 'in '
                
. ($item['file']
                    ? (
                        (
$item['file'] === $testFile self::color('white') : '')
                        . 
implode(DIRECTORY_SEPARATORarray_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 pathinfo($testFilePATHINFO_FILENAME) . $suffix;
        if (!
preg_match('#/|\w:#A'self::$dumpDir)) {
            
$path dirname($testFile) . DIRECTORY_SEPARATOR $path;
        }
        @
mkdir(dirname($path)); // @ - directory may already exist
        
file_put_contents($pathis_string($content) ? $content : (self::toPhp($content) . "\n"));
        return 
$path;
    }


    
/**
     * Applies color to string.
     * @return string
     */
    
public static function color($color ''$s null)
    {
        static 
$colors = [
            
'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 
"\e["
            
str_replace(';'"m\e["$colors[$c[0]] . (empty($c[1]) ? '' ';4' substr($colors[$c[1]], -1)))
            . 
'm' $s . ($s === null '' "\e[0m");
    }


    public static function 
removeColors($s)
    {
        return 
preg_replace('#\e\[[\d;]+m#'''$s);
    }
}
100 %
Framework/AssertException.php
<?php

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

namespace Tester;


/**
 * Assertion exception.
 */
class AssertException extends \Exception
{
    public 
$origMessage;

    public 
$actual;

    public 
$expected;


    public function 
__construct($message$expected$actual$previous null)
    {
        
parent::__construct(''0$previous);
        
$this->expected $expected;
        
$this->actual $actual;
        
$this->setMessage($message);
    }


    public function 
setMessage($message)
    {
        
$this->origMessage $message;
        
$this->message strtr($message, [
            
'%1' => Dumper::toLine($this->actual),
            
'%2' => Dumper::toLine($this->expected),
        ]);
        return 
$this;
    }
}
98 %
Framework/TestCase.php
<?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()
    {
        if (
func_num_args()) {
            throw new \
LogicException('Calling TestCase::run($method) is deprecated. Use TestCase::runTest($method) instead.');
        }

        
$methods array_values(preg_grep(self::METHOD_PATTERNarray_map(function (\ReflectionMethod $rm) {
            return 
$rm->getName();
        }, (new \
ReflectionObject($this))->getMethods())));

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

        } else {
            foreach (
$methods as $method) {
                
$this->runTest($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)
    {
        if (!
method_exists($this$method)) {
            throw new 
TestCaseException("Method '$method' does not exist.");
        } elseif (!
preg_match(self::METHOD_PATTERN$method)) {
            throw new 
TestCaseException("Method '$method' is not a testing method.");
        }

        
$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((string) $method->getDocComment()) + ['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);
        }

        
$data = [];
        if (
$args === null) {
            
$defaultParams = [];
            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[] = [];
            }
        } else {
            
$data[] = $args;
        }


        if (
$this->prevErrorHandler === false) {
            
$this->prevErrorHandler set_error_handler(function ($severity) {
                if (
$this->handleErrors && ($severity error_reporting()) === $severity) {
                    
$this->handleErrors false;
                    
$this->silentTearDown();
                }

                return 
$this->prevErrorHandler call_user_func_array($this->prevErrorHandlerfunc_get_args()) : false;
            });
        }


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

                
$this->handleErrors true;
                try {
                    if (
$info['throws']) {
                        
$e Assert::error(function () use ($method$params) {
                            
call_user_func_array([$this$method->getName()], $params);
                        }, ...
$throws);
                        if (
$e instanceof AssertException) {
                            throw 
$e;
                        }
                    } else {
                        
call_user_func_array([$this$method->getName()], $params);
                    }
                } catch (\
Exception $e) {
                    
$this->handleErrors false;
                    
$this->silentTearDown();
                    throw 
$e;
                }
                
$this->handleErrors false;

                
$this->tearDown();

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


    
/**
     * @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()
    {
    }


    private function 
silentTearDown()
    {
        
set_error_handler(function () {});
        try {
            
$this->tearDown();
        } catch (\
Exception $e) {
        }
        
restore_error_handler();
    }
}


class 
TestCaseException extends \Exception
{
}
65 %
Framework/DataProvider.php
<?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 '')
    {
        if (!
is_file($file)) {
            throw new \
Exception("Missing data-provider file '$file'.");
        }

        if (
pathinfo($filePATHINFO_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($filetrue); // @ 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 = ['' => '=''=>' => '>=''=<' => '<='];
        
$tokens preg_split('#\s+#'$input);
        
preg_match_all('#\s*,?\s*(<=|=<|<|==|=|!=|<>|>=|=>|>)?\s*([^\s,]+)#A'$query$queryPartsPREG_SET_ORDER);
        foreach (
$queryParts as list(, $operator$operand)) {
            
$operator = isset($replaces[$operator]) ? $replaces[$operator] : $operator;
            
$token = (string) 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 [
dirname($file) . DIRECTORY_SEPARATOR $m[2], $m[3], (bool) $m[1]];
    }
}
100 %
Framework/DomQuery.php
<?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;
        }

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

        
// fix parsing of </ inside scripts
        
$html preg_replace_callback('#(<script(?=\s|>)(?:"[^"]*"|\'[^\']*\'|[^"\'>])*+>)(.*?)(</script>)#s', function ($m) {
            return 
$m[1] . str_replace('</''<\/'$m[2]) . $m[3];
        }, 
$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), $matchesPREG_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;
    }
}
41 %
Framework/Environment.php
<?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 run by Runner */
    
const RUNNER 'NETTE_TESTER_RUNNER';

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

    
/** Thread number when run tests in multi threads */
    
const THREAD 'NETTE_TESTER_THREAD';

    
/** @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');

        
ob_start(function ($s) {
            return 
self::$useColors $s Dumper::removeColors($s);
        }, 
1PHP_OUTPUT_HANDLER_FLUSHABLE);
    }


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

        
set_exception_handler([__CLASS__'handleException']);

        
set_error_handler(function ($severity$message$file$line) {
            if (
in_array($severity, [E_RECOVERABLE_ERRORE_USER_ERROR], true) || ($severity error_reporting()) === $severity) {
                
self::handleException(new \ErrorException($message0$severity$file$line));
            }
            return 
false;
        });

        
register_shutdown_function(function () {
            
Assert::$onFailure = [__CLASS__'handleException'];

            
$error error_get_last();
            
register_shutdown_function(function () use ($error) {
                if (
in_array($error['type'], [E_ERRORE_CORE_ERRORE_COMPILE_ERRORE_PARSE], true)) {
                    if ((
$error['type'] & error_reporting()) !== $error['type']) { // show fatal errors hidden by @shutup
                        
self::removeOutputBuffers();
                        echo 
"\nFatal error: $error[message] in $error[file] on line $error[line]\n";
                    }
                } elseif (
self::$checkAssertions && !Assert::$counter) {
                    
self::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)) + ['file' => $file];
    }


    
/**
     * Removes keyword final from source codes.
     * @return void
     */
    
public static function bypassFinals()
    {
        
FileMutator::addMutator(function ($code) {
            if (
strpos($code'final') !== false) {
                
$tokens token_get_all($code);
                
$code '';
                foreach (
$tokens as $token) {
                    
$code .= is_array($token)
                        ? (
$token[0] === T_FINAL '' $token[1])
                        : 
$token;
                }
            }
            return 
$code;
        });
    }


    
/**
     * 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);
    }


    private static function 
removeOutputBuffers()
    {
        while (
ob_get_level() > self::$obLevel && @ob_end_flush()); // @ may be not removable
    
}
}
75 %
Framework/Helpers.php
<?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 = [];
        if (!
preg_match('#^/\*\*(.*?)\*/#ms'$s$content)) {
            return [];
        }
        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], $matchesPREG_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($name02) === '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);
    }
}
96 %
Framework/FileMock.php
<?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 = [];

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

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

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

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

    
/** @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::PROTOCOLstream_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->content = (string) $this->content;
        
$this->appendMode $m[1] === 'a';
        
$this->readingPos 0;
        
$this->writingPos $this->appendMode 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($length)
    {
        if (!
$this->isReadable) {
            return 
'';
        }

        
$result substr($this->content$this->readingPos$length);
        
$this->readingPos += strlen($result);
        
$this->writingPos += $this->appendMode strlen($result);
        return 
$result;
    }


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

        
$length strlen($data);
        
$this->content str_pad($this->content$this->writingPos"\x00");
        
$this->content substr_replace($this->content$data$this->writingPos$length);
        
$this->readingPos += $length;
        
$this->writingPos += $length;
        return 
$length;
    }


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


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


    public function 
stream_seek($offset$whence)
    {
        if (
$whence === SEEK_CUR) {
            
$offset += $this->readingPos;
        } elseif (
$whence === SEEK_END) {
            
$offset += strlen($this->content);
        }
        if (
$offset >= 0) {
            
$this->readingPos $offset;
            
$this->writingPos $this->appendMode $this->writingPos $offset;
            return 
true;
        } else {
            return 
false;
        }
    }


    public function 
stream_truncate($size)
    {
        if (!
$this->isWritable) {
            return 
false;
        }

        
$this->content substr(str_pad($this->content$size"\x00"), 0$size);
        
$this->writingPos $this->appendMode $size $this->writingPos;
        return 
true;
    }


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


    public function 
url_stat($path$flags)
    {
        return isset(
self::$files[$path])
            ? [
'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 debug_backtrace(03);
        if (isset(
$bt[2]['function'])) {
            
$message $bt[2]['function'] . '(' . @$bt[2]['args'][0] . '): ' $message;
        }

        
trigger_error($messageE_USER_WARNING);
    }
}
97 %
Framework/Assert.php
<?php

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

namespace Tester;


/**
 * Assertion test helpers.
 */
class Assert
{
    
/** used by equal() for comparing floats */
    
const EPSILON 1e-10;

    
/** used by match(); in values, each $ followed by number is backreference */
    
public static $patterns = [
        
'%%' => '%',            // one % character
        
'%a%' => '[^\r\n]+',    // one or more of anything except the end of line characters
        
'%a\?%' => '[^\r\n]*',  // zero or more of anything except the end of line characters
        
'%A%' => '.+',          // one or more of anything including the end of line characters
        
'%A\?%' => '.*',        // zero or more of anything including the end of line characters
        
'%s%' => '[\t ]+',      // one or more white space characters except the end of line characters
        
'%s\?%' => '[\t ]*',    // zero or more white space characters except the end of line characters
        
'%S%' => '\S+',         // one or more of characters except the white space
        
'%S\?%' => '\S*',       // zero or more of characters except the white space
        
'%c%' => '[^\r\n]',     // a single character of any sort (except the end of line)
        
'%d%' => '[0-9]+',      // one or more digits
        
'%d\?%' => '[0-9]*',    // zero or more digits
        
'%i%' => '[+-]?[0-9]+'// signed integer value
        
'%f%' => '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?'// floating point number
        
'%h%' => '[0-9a-fA-F]+'// one or more HEX digits
        
'%w%' => '[0-9a-zA-Z_]+'//one or more alphanumeric characters
        
'%ds%' => '[\\\\/]',    // directory separator
        
'%(\[.+\][+*?{},\d]*)%' => '$1'// range
    
];

    
/** @var callable  function (AssertException $exception) */
    
public static $onFailure;

    
/** @var int  the count of assertions */
    
public static $counter 0;


    
/**
     * Checks assertion. Values must be exactly the same.
     * @return void
     */
    
public static function same($expected$actual$description null)
    {
        
self::$counter++;
        if (
$actual !== $expected) {
            
self::fail(self::describe('%1 should be %2'$description), $actual$expected);
        }
    }


    
/**
     * Checks assertion. Values must not be exactly the same.
     * @return void
     */
    
public static function notSame($expected$actual$description null)
    {
        
self::$counter++;
        if (
$actual === $expected) {
            
self::fail(self::describe('%1 should not be %2'$description), $actual$expected);
        }
    }


    
/**
     * Checks assertion. The identity of objects and the order of keys in the arrays are ignored.
     * @return void
     */
    
public static function equal($expected$actual$description null)
    {
        
self::$counter++;
        if (!
self::isEqual($expected$actual)) {
            
self::fail(self::describe('%1 should be equal to %2'$description), $actual$expected);
        }
    }


    
/**
     * Checks assertion. The identity of objects and the order of keys in the arrays are ignored.
     * @return void
     */
    
public static function notEqual($expected$actual$description null)
    {
        
self::$counter++;
        if (
self::isEqual($expected$actual)) {
            
self::fail(self::describe('%1 should not be equal to %2'$description), $actual$expected);
        }
    }


    
/**
     * Checks assertion. Values must contains expected needle.
     * @return void
     */
    
public static function contains($needle$actual$description null)
    {
        
self::$counter++;
        if (
is_array($actual)) {
            if (!
in_array($needle$actualtrue)) {
                
self::fail(self::describe('%1 should contain %2'$description), $actual$needle);
            }
        } elseif (
is_string($actual)) {
            if (
$needle !== '' && strpos($actual$needle) === false) {
                
self::fail(self::describe('%1 should contain %2'$description), $actual$needle);
            }
        } else {
            
self::fail(self::describe('%1 should be string or array'$description), $actual);
        }
    }


    
/**
     * Checks assertion. Values must not contains expected needle.
     * @return void
     */
    
public static function notContains($needle$actual$description null)
    {
        
self::$counter++;
        if (
is_array($actual)) {
            if (
in_array($needle$actualtrue)) {
                
self::fail(self::describe('%1 should not contain %2'$description), $actual$needle);
            }
        } elseif (
is_string($actual)) {
            if (
$needle === '' || strpos($actual$needle) !== false) {
                
self::fail(self::describe('%1 should not contain %2'$description), $actual$needle);
            }
        } else {
            
self::fail(self::describe('%1 should be string or array'$description), $actual);
        }
    }


    
/**
     * Checks TRUE assertion.
     * @param  mixed  actual
     * @param  string  fail message
     * @return void
     */
    
public static function true($actual$description null)
    {
        
self::$counter++;
        if (
$actual !== true) {
            
self::fail(self::describe('%1 should be TRUE'$description), $actual);
        }
    }


    
/**
     * Checks FALSE assertion.
     * @param  mixed  actual
     * @param  string  fail message
     * @return void
     */
    
public static function false($actual$description null)
    {
        
self::$counter++;
        if (
$actual !== false) {
            
self::fail(self::describe('%1 should be FALSE'$description), $actual);
        }
    }


    
/**
     * Checks NULL assertion.
     * @param  mixed  actual
     * @param  string  fail message
     * @return void
     */
    
public static function null($actual$description null)
    {
        
self::$counter++;
        if (
$actual !== null) {
            
self::fail(self::describe('%1 should be NULL'$description), $actual);
        }
    }


    
/**
     * Checks Not a Number assertion.
     * @param  mixed  actual
     * @param  string  fail message
     * @return void
     */
    
public static function nan($actual$description null)
    {
        
self::$counter++;
        if (!
is_float($actual) || !is_nan($actual)) {
            
self::fail(self::describe('%1 should be NAN'$description), $actual);
        }
    }


    
/**
     * Checks truthy assertion.
     * @param  mixed  actual
     * @param  string  fail message
     * @return void
     */
    
public static function truthy($actual$description null)
    {
        
self::$counter++;
        if (!
$actual) {
            
self::fail(self::describe('%1 should be truthy'$description), $actual);
        }
    }


    
/**
     * Checks falsey (empty) assertion.
     * @param  mixed  actual
     * @param  string  fail message
     * @return void
     */
    
public static function falsey($actual$description null)
    {
        
self::$counter++;
        if (
$actual) {
            
self::fail(self::describe('%1 should be falsey'$description), $actual);
        }
    }


    
/**
     * Checks if subject has expected count.
     * @param  int    expected count
     * @param  mixed  subject
     * @param  string  fail message
     * @return void
     */
    
public static function count($count$value$description null)
    {
        
self::$counter++;
        if (!
$value instanceof \Countable && !is_array($value)) {
            
self::fail(self::describe('%1 should be array or countable object'$description), $value);

        } elseif (
count($value) !== $count) {
            
self::fail(self::describe('Count %1 should be %2'$description), count($value), $count);
        }
    }


    
/**
     * Checks assertion.
     * @return void
     */
    
public static function type($type$value$description null)
    {
        
self::$counter++;
        if (!
is_object($type) && !is_string($type)) {
            throw new \
Exception('Type must be a object or a string.');

        } elseif (
$type === 'list') {
            if (!
is_array($value) || ($value && array_keys($value) !== range(0count($value) - 1))) {
                
self::fail(self::describe("%1 should be $type"$description), $value);
            }

        } elseif (
in_array($type, ['array''bool''callable''float',
            
'int''integer''null''object''resource''scalar''string', ], true)
        ) {
            if (!
call_user_func("is_$type"$value)) {
                
self::fail(self::describe(gettype($value) . " should be $type"$description));
            }

        } elseif (!
$value instanceof $type) {
            
$actual is_object($value) ? get_class($value) : gettype($value);
            
self::fail(self::describe("$actual should be instance of $type"$description));
        }
    }


    
/**
     * Checks if the function throws exception.
     * @param  callable
     * @param  string class
     * @param  string message
     * @param  int code
     * @return \Exception
     */
    
public static function exception(callable $function$class$message null$code null)
    {
        
self::$counter++;
        
$e null;
        try {
            
call_user_func($function);
        } catch (\
Exception $e) {
        } catch (\
Throwable $e) {
        }
        if (
$e === null) {
            
self::fail("$class was expected, but none was thrown");

        } elseif (!
$e instanceof $class) {
            
self::fail("$class was expected but got " get_class($e) . ($e->getMessage() ? " ({$e->getMessage()})" ''), nullnull$e);

        } elseif (
$message && !self::isMatching($message$e->getMessage())) {
            
self::fail("$class with a message matching %2 was expected but got %1"$e->getMessage(), $message);

        } elseif (
$code !== null && $e->getCode() !== $code) {
            
self::fail("$class with a code %2 was expected but got %1"$e->getCode(), $code);
        }
        return 
$e;
    }


    
/**
     * Checks if the function throws exception, alias for exception().
     * @return \Exception
     */
    
public static function throws(callable $function$class$message null$code null)
    {
        return 
self::exception($function$class$message$code);
    }


    
/**
     * Checks if the function generates PHP error or throws exception.
     * @param  callable
     * @param  int|string|array
     * @param  string message
     * @return null|\Exception
     */
    
public static function error(callable $function$expectedType$expectedMessage null)
    {
        if (
is_string($expectedType) && !preg_match('#^E_[A-Z_]+\z#'$expectedType)) {
            return static::
exception($function$expectedType$expectedMessage);
        }

        
self::$counter++;
        
$expected is_array($expectedType) ? $expectedType : [[$expectedType$expectedMessage]];
        foreach (
$expected as &$item) {
            list(
$expectedType$expectedMessage) = $item;
            if (
is_int($expectedType)) {
                
$item[2] = Helpers::errorTypeToString($expectedType);
            } elseif (
is_string($expectedType)) {
                
$item[0] = constant($item[2] = $expectedType);
            } else {
                throw new \
Exception('Error type must be E_* constant.');
            }
        }

        
set_error_handler(function ($severity$message$file$line) use (&$expected) {
            if ((
$severity error_reporting()) !== $severity) {
                return;
            }

            
$errorStr Helpers::errorTypeToString($severity) . ($message " ($message)" '');
            list(
$expectedType$expectedMessage$expectedTypeStr) = array_shift($expected);
            if (
$expectedType === null) {
                
self::fail("Generated more errors than expected: $errorStr was generated in file $file on line $line");

            } elseif (
$severity !== $expectedType) {
                
self::fail("$expectedTypeStr was expected, but $errorStr was generated in file $file on line $line");

            } elseif (
$expectedMessage && !self::isMatching($expectedMessage$message)) {
                
self::fail("$expectedTypeStr with a message matching %2 was expected but got %1"$message$expectedMessage);
            }
        });

        
reset($expected);
        try {
            
call_user_func($function);
            
restore_error_handler();
        } catch (\
Exception $e) {
            
restore_error_handler();
            throw 
$e;
        }

        if (
$expected) {
            
self::fail('Error was expected, but was not generated');
        }
    }


    
/**
     * Checks that the function does not generate PHP error and does not throw exception.
     * @param  callable
     * @return void
     */
    
public static function noError($function)
    {
        
self::error($function, []);
    }


    
/**
     * Compares result using regular expression or mask:
     *   %a%    one or more of anything except the end of line characters
     *   %a?%   zero or more of anything except the end of line characters
     *   %A%    one or more of anything including the end of line characters
     *   %A?%   zero or more of anything including the end of line characters
     *   %s%    one or more white space characters except the end of line characters
     *   %s?%   zero or more white space characters except the end of line characters
     *   %S%    one or more of characters except the white space
     *   %S?%   zero or more of characters except the white space
     *   %c%    a single character of any sort (except the end of line)
     *   %d%    one or more digits
     *   %d?%   zero or more digits
     *   %i%    signed integer value
     *   %f%    floating point number
     *   %h%    one or more HEX digits
     * @param  string  mask|regexp; only delimiters ~ and # are supported for regexp
     * @param  string actual
     * @param  string  fail message
     * @return void
     */
    
public static function match($pattern$actual$description null)
    {
        
self::$counter++;
        if (!
is_string($pattern)) {
            throw new \
Exception('Pattern must be a string.');

        } elseif (!
is_scalar($actual)) {
            
self::fail(self::describe('%1 should match %2'$description), $actual$pattern);

        } elseif (!
self::isMatching($pattern$actual)) {
            list(
$pattern$actual) = self::expandMatchingPatterns($pattern$actual);
            
self::fail(self::describe('%1 should match %2'$description), $actual$pattern);
        }
    }


    
/**
     * Compares results using mask sorted in file.
     * @return void
     */
    
public static function matchFile($file$actual$description null)
    {
        
self::$counter++;
        
$pattern = @file_get_contents($file); // @ is escalated to exception
        
if ($pattern === false) {
            throw new \
Exception("Unable to read file '$file'.");

        } elseif (!
is_scalar($actual)) {
            
self::fail(self::describe('%1 should match %2'$description), $actual$pattern);

        } elseif (!
self::isMatching($pattern$actual)) {
            list(
$pattern$actual) = self::expandMatchingPatterns($pattern$actual);
            
self::fail(self::describe('%1 should match %2'$description), $actual$pattern);
        }
    }


    
/**
     * Failed assertion
     * @return void
     */
    
public static function fail($message$actual null$expected null$previous null)
    {
        
$e = new AssertException($message$expected$actual$previous);
        if (
self::$onFailure) {
            
call_user_func(self::$onFailure$e);
        } else {
            throw 
$e;
        }
    }


    private static function 
describe($reason$description)
    {
        return (
$description $description ': ' '') . $reason;
    }


    public static function 
with($obj, \Closure $closure)
    {
        return 
$closure->bindTo($obj$obj)->__invoke();
    }


    
/********************* helpers ****************d*g**/


    /**
     * Compares using mask.
     * @return bool
     * @internal
     */
    
public static function isMatching($pattern$actual$strict false)
    {
        if (!
is_string($pattern) || !is_scalar($actual)) {
            throw new \
Exception('Value and pattern must be strings.');
        }

        
$old ini_set('pcre.backtrack_limit''10000000');

        if (!
self::isPcre($pattern)) {
            
$utf8 preg_match('#\x80-\x{10FFFF}]#u'$pattern) ? 'u' '';
            
$suffix = ($strict '\z#sU' '\s*$#sU') . $utf8;
            
$patterns = static::$patterns + [
                
'[.\\\\+*?[^$(){|\#]' => '\$0'// preg quoting
                
'\x00' => '\x00',
                
'[\t ]*\r?\n' => '[\t ]*\r?\n'// right trim
            
];
            
$pattern '#^' preg_replace_callback('#' implode('|'array_keys($patterns)) . '#U' $utf8, function ($m) use ($patterns) {
                foreach (
$patterns as $re => $replacement) {
                    
$s preg_replace("#^$re\\z#"str_replace('\\''\\\\'$replacement), $m[0], 1$count);
                    if (
$count) {
                        return 
$s;
                    }
                }
            }, 
rtrim($pattern" \t\n\r")) . $suffix;
        }

        
$res preg_match($pattern, (string) $actual);
        
ini_set('pcre.backtrack_limit'$old);
        if (
$res === false || preg_last_error()) {
            throw new \
Exception('Error while executing regular expression. (PREG Error Code ' preg_last_error() . ')');
        }
        return (bool) 
$res;
    }


    
/**
     * @return array
     * @internal
     */
    
public static function expandMatchingPatterns($pattern$actual)
    {
        if (
self::isPcre($pattern)) {
            return [
$pattern$actual];
        }

        
$parts preg_split('#(%)#'$pattern, -1PREG_SPLIT_DELIM_CAPTURE);
        for (
$i count($parts); $i >= 0$i--) {
            
$patternX implode(array_slice($parts0$i));
            
$patternY "$patternX%A?%";
            if (
self::isMatching($patternY$actual)) {
                
$patternZ implode(array_slice($parts$i));
                break;
            }
        }

        foreach ([
'%A%''%A?%'] as $greedyPattern) {
            if (
substr($patternX, -strlen($greedyPattern)) === $greedyPattern) {
                
$patternX substr($patternX0, -strlen($greedyPattern));
                
$patternY "$patternX%A?%";
                
$patternZ $greedyPattern $patternZ;
                break;
            }
        }

        
$low 0;
        
$high strlen($actual);
        while (
$low <= $high) {
            
$mid = ($low $high) >> 1;
            if (
self::isMatching($patternYsubstr($actual0$mid))) {
                
$high $mid 1;
            } else {
                
$low $mid 1;
            }
        }

        
$low $high 2;
        
$high strlen($actual);
        while (
$low <= $high) {
            
$mid = ($low $high) >> 1;
            if (!
self::isMatching($patternXsubstr($actual0$mid), true)) {
                
$high $mid 1;
            } else {
                
$low $mid 1;
            }
        }

        
$actualX substr($actual0$high);
        
$actualZ substr($actual$high);

        return [
            
$actualX rtrim(preg_replace('#[\t ]*\r?\n#'"\n"$patternZ)),
            
$actualX rtrim(preg_replace('#[\t ]*\r?\n#'"\n"$actualZ)),
        ];
    }


    
/**
     * Compares two structures. Ignores the identity of objects and the order of keys in the arrays.
     * @return bool
     */
    
private static function isEqual($expected$actual$level 0$objects null)
    {
        if (
$level 10) {
            throw new \
Exception('Nesting level too deep or recursive dependency.');
        }

        if (
is_float($expected) && is_float($actual) && is_finite($expected) && is_finite($actual)) {
            
$diff abs($expected $actual);
            return (
$diff self::EPSILON) || ($diff max(abs($expected), abs($actual)) < self::EPSILON);
        }

        if (
is_object($expected) && is_object($actual) && get_class($expected) === get_class($actual)) {
            
$objects $objects ? clone $objects : new \SplObjectStorage;
            if (isset(
$objects[$expected])) {
                return 
$objects[$expected] === $actual;
            } elseif (
$expected === $actual) {
                return 
true;
            }
            
$objects[$expected] = $actual;
            
$objects[$actual] = $expected;
            
$expected = (array) $expected;
            
$actual = (array) $actual;
        }

        if (
is_array($expected) && is_array($actual)) {
            
ksort($expectedSORT_STRING);
            
ksort($actualSORT_STRING);
            if (
array_keys($expected) !== array_keys($actual)) {
                return 
false;
            }

            foreach (
$expected as $value) {
                if (!
self::isEqual($valuecurrent($actual), $level 1$objects)) {
                    return 
false;
                }
                
next($actual);
            }
            return 
true;
        }

        return 
$expected === $actual;
    }


    
/**
     * @param  string
     * @return bool
     */
    
private static function isPcre($pattern)
    {
        return (bool) 
preg_match('/^([~#]).+(\1)[imsxUu]*\z/s'$pattern);
    }
}
41 %
Framework/FileMutator.php
<?php

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

namespace Tester;


/**
 * PHP file mutator.
 */
class FileMutator
{
    const 
PROTOCOL 'file';

    
/** @var resource|null */
    
public $context;

    
/** @var resource|null */
    
private $handle;

    
/** @var callable[] */
    
private static $mutators = [];


    public static function 
addMutator(callable $mutator)
    {
        
self::$mutators[] = $mutator;
        
stream_wrapper_unregister(self::PROTOCOL);
        
stream_wrapper_register(self::PROTOCOL__CLASS__);
    }


    public function 
dir_closedir()
    {
        
closedir($this->handle);
    }


    public function 
dir_opendir($path$options)
    {
        
$this->handle $this->native('opendir'$path$this->context);
        return (bool) 
$this->handle;
    }


    public function 
dir_readdir()
    {
        return 
readdir($this->handle);
    }


    public function 
dir_rewinddir()
    {
        return 
rewinddir($this->handle);
    }


    public function 
mkdir($path$mode$options)
    {
        return 
$this->native('mkdir'$modefalse$this->context);
    }


    public function 
rename($pathFrom$pathTo)
    {
        return 
$this->native('rename'$pathFrom$pathTo$this->context);
    }


    public function 
rmdir($path$options)
    {
        return 
$this->native('rmdir'$this->context);
    }


    public function 
stream_cast($castAs)
    {
        return 
$this->handle;
    }


    public function 
stream_close()
    {
        
fclose($this->handle);
    }


    public function 
stream_eof()
    {
        return 
feof($this->handle);
    }


    public function 
stream_flush()
    {
        return 
fflush($this->handle);
    }


    public function 
stream_lock($operation)
    {
        return 
flock($this->handle$operation);
    }


    public function 
stream_metadata($path$option$value)
    {
        switch (
$option) {
            case 
STREAM_META_TOUCH:
                return 
$this->native('touch'$path$value[0], $value[1]);
            case 
STREAM_META_OWNER_NAME:
            case 
STREAM_META_OWNER:
                return 
$this->native('chown'$path$value);
            case 
STREAM_META_GROUP_NAME:
            case 
STREAM_META_GROUP:
                return 
$this->native('chgrp'$path$value);
            case 
STREAM_META_ACCESS:
                return 
$this->native('chmod'$path$value);
        }
    }


    public function 
stream_open($path$mode$options, &$openedPath)
    {
        
$usePath = (bool) ($options STREAM_USE_PATH);
        if (
pathinfo($pathPATHINFO_EXTENSION) === 'php') {
            
$content $this->native('file_get_contents'$path$usePath$this->context);
            if (
$content === false) {
                return 
false;
            } else {
                foreach (
self::$mutators as $mutator) {
                    
$content call_user_func($mutator$content);
                }
                
$this->handle tmpfile();
                
$this->native('fwrite'$this->handle$content);
                
$this->native('fseek'$this->handle0);
                return 
true;
            }
        } else {
            
$this->handle $this->context
                
$this->native('fopen'$path$mode$usePath$this->context)
                : 
$this->native('fopen'$path$mode$usePath);
            return (bool) 
$this->handle;
        }
    }


    public function 
stream_read($count)
    {
        return 
fread($this->handle$count);
    }


    public function 
stream_seek($offset$whence SEEK_SET)
    {
        return 
fseek($this->handle$offset$whence);
    }


    public function 
stream_set_option($option$arg1$arg2)
    {
    }


    public function 
stream_stat()
    {
        return 
fstat($this->handle);
    }


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


    public function 
stream_truncate($newSize)
    {
        return 
ftruncate($this->handle$newSize);
    }


    public function 
stream_write($data)
    {
        return 
fwrite($this->handle$data);
    }


    public function 
unlink($path)
    {
        return 
$this->native('unlink'$path);
    }


    public function 
url_stat($path$flags)
    {
        return 
$this->native('fstat'$path$flags);
    }


    private function 
native($func)
    {
        
stream_wrapper_restore(self::PROTOCOL);
        
$res call_user_func_array($funcarray_slice(func_get_args(), 1));
        
stream_wrapper_unregister(self::PROTOCOL);
        
stream_wrapper_register(self::PROTOCOL__CLASS__);
        return 
$res;
    }
}
7 %
bootstrap.php
<?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__ '/Framework/FileMutator.php';
require 
__DIR__ '/CodeCoverage/Collector.php';
require 
__DIR__ '/Runner/Job.php';

Tester\Environment::setup();
100 %
Runner/Output/Logger.php
<?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;
use 
Tester\Runner\Test;


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

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

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

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


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


    public function 
begin()
    {
        
$this->count 0;
        
$this->results = [
            
Test::PASSED => 0,
            
Test::SKIPPED => 0,
            
Test::FAILED => 0,
        ];
        
fwrite($this->file'PHP ' $this->runner->getInterpreter()->getVersion()
            . 
' | ' $this->runner->getInterpreter()->getCommandLine()
            . 
" | {$this->runner->threadCount} threads\n\n");
    }


    public function 
prepare(Test $test)
    {
        
$this->count++;
    }


    public function 
finish(Test $test)
    {
        
$this->results[$test->getResult()]++;
        
$message '   ' str_replace("\n""\n   "Tester\Dumper::removeColors(trim($test->message)));
        
$outputs = [
            
Test::PASSED => "-- OK: {$test->getSignature()}",
            
Test::SKIPPED => "-- Skipped: {$test->getSignature()}\n$message",
            
Test::FAILED => "-- FAILED: {$test->getSignature()}\n$message",
        ];
        
fwrite($this->file$outputs[$test->getResult()] . "\n\n");
    }


    public function 
end()
    {
        
$run array_sum($this->results);
        
fwrite($this->file,
            (
$this->results[Test::FAILED] ? 'FAILURES!' 'OK')
            . 
" ($this->count tests"
            
. ($this->results[Test::FAILED] ? ", {$this->results[Test::FAILED]} failures" '')
            . (
$this->results[Test::SKIPPED] ? ", {$this->results[Test::SKIPPED]} skipped" '')
            . (
$this->count !== $run ', ' . ($this->count $run) . ' not run' '')
            . 
')'
        
);
    }
}
83 %
Runner/Output/ConsolePrinter.php
<?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;
use 
Tester\Runner\Test;


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

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

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

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

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

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

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

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


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


    public function 
begin()
    {
        
$this->count 0;
        
$this->baseDir null;
        
$this->results = [
            
Test::PASSED => 0,
            
Test::SKIPPED => 0,
            
Test::FAILED => 0,
        ];
        
$this->time = -microtime(true);
        
fwrite($this->file$this->runner->getInterpreter()->getShortInfo()
            . 
' | ' $this->runner->getInterpreter()->getCommandLine()
            . 
" | {$this->runner->threadCount} thread" . ($this->runner->threadCount 's' '') . "\n\n");
    }


    public function 
prepare(Test $test)
    {
        if (
$this->baseDir === null) {
            
$this->baseDir dirname($test->getFile()) . DIRECTORY_SEPARATOR;
        } elseif (
strpos($test->getFile(), $this->baseDir) !== 0) {
            
$common array_intersect_assoc(
                
explode(DIRECTORY_SEPARATOR$this->baseDir),
                
explode(DIRECTORY_SEPARATOR$test->getFile())
            );
            
$this->baseDir '';
            
$prev 0;
            foreach (
$common as $i => $part) {
                if (
$i !== $prev++) {
                    break;
                }
                
$this->baseDir .= $part DIRECTORY_SEPARATOR;
            }
        }

        
$this->count++;
    }


    public function 
finish(Test $test)
    {
        
$this->results[$test->getResult()]++;
        
$outputs = [
            
Test::PASSED => '.',
            
Test::SKIPPED => 's',
            
Test::FAILED => Dumper::color('white/red''F'),
        ];
        
fwrite($this->file$outputs[$test->getResult()]);

        
$title = ($test->title "$test->title | " '') . substr($test->getSignature(), strlen($this->baseDir));
        
$message '   ' str_replace("\n""\n   "trim($test->message)) . "\n\n";
        if (
$test->getResult() === Test::FAILED) {
            
$this->buffer .= Dumper::color('red'"-- FAILED: $title") . "\n$message";
        } elseif (
$test->getResult() === Test::SKIPPED && $this->displaySkipped) {
            
$this->buffer .= "-- Skipped: $title\n$message";
        }
    }


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

        
$this->buffer null;
    }
}
100 %
Runner/Output/TapPrinter.php
<?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\Test;


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

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


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


    public function 
begin()
    {
        
$this->results = [
            
Test::PASSED => 0,
            
Test::SKIPPED => 0,
            
Test::FAILED => 0,
        ];
        
fwrite($this->file"TAP version 13\n");
    }


    public function 
prepare(Test $test)
    {
    }


    public function 
finish(Test $test)
    {
        
$this->results[$test->getResult()]++;
        
$message str_replace("\n""\n# "trim($test->message));
        
$outputs = [
            
Test::PASSED => "ok {$test->getSignature()}",
            
Test::SKIPPED => "ok {$test->getSignature()} #skip $message",
            
Test::FAILED => "not ok {$test->getSignature()}\n# $message",
        ];
        
fwrite($this->file$outputs[$test->getResult()] . "\n");
    }


    public function 
end()
    {
        
fwrite($this->file'1..' array_sum($this->results));
    }
}
100 %
Runner/Output/JUnitPrinter.php
<?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\Test;


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

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

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

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


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


    public function 
begin()
    {
        
$this->results = [
            
Test::PASSED => 0,
            
Test::SKIPPED => 0,
            
Test::FAILED => 0,
        ];
        
$this->startTime microtime(true);
        
fwrite($this->file"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<testsuites>\n");
    }


    public function 
prepare(Test $test)
    {
    }


    public function 
finish(Test $test)
    {
        
$this->results[$test->getResult()]++;
        
$this->buffer .= "\t\t<testcase classname=\"" htmlspecialchars($test->getSignature()) . '" name="' htmlspecialchars($test->getSignature()) . '"';

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


    public function 
end()
    {
        
$time sprintf('%0.1f'microtime(true) - $this->startTime);
        
$output $this->buffer;
        
$this->buffer "\t<testsuite errors=\"{$this->results[Test::FAILED]}\" skipped=\"{$this->results[Test::SKIPPED]}\" tests=\"" array_sum($this->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");
    }
}
0 %
Runner/info.php
<?php

/**
 * @internal
 */

$isPhpDbg defined('PHPDBG_VERSION');
$extensions get_loaded_extensions();
natcasesort($extensions);

$info = (object) [
    
'binary' => defined('PHP_BINARY') ? PHP_BINARY null,
    
'version' => PHP_VERSION,
    
'phpDbgVersion' => $isPhpDbg PHPDBG_VERSION null,
    
'sapi' => PHP_SAPI,
    
'iniFiles' => array_merge(
        (
$tmp php_ini_loaded_file()) === false ? [] : [$tmp],
        (
function_exists('php_ini_scanned_files') && strlen($tmp = (string) php_ini_scanned_files())) ? explode(",\n"trim($tmp)) : []
    ),
    
'extensions' => $extensions,
    
'tempDir' => sys_get_temp_dir(),
    
'canMeasureCodeCoverage' => $isPhpDbg || in_array('xdebug'$extensionstrue),
];

if (isset(
$_SERVER['argv'][1])) {
    echo 
serialize($info);
    die();
}

foreach ([
    
'PHP binary' => $info->binary ?: '(not available)',
    
'PHP version' . ($isPhpDbg '; PHPDBG version' '')
        => 
"$info->version ($info->sapi)" . ($isPhpDbg "; $info->phpDbgVersion''),
    
'Loaded php.ini files' => count($info->iniFiles) ? implode(', '$info->iniFiles) : '(none)',
    
'PHP temporary directory' => $info->tempDir == '' '(empty)' $info->tempDir,
    
'Loaded extensions' => count($info->extensions) ? implode(', '$info->extensions) : '(none)',
] as 
$title => $value) {
    echo 
"\e[1;32m$title\e[0m:\n$value\n\n";
}
96 %
Runner/TestHandler.php
<?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;
use 
Tester\TestCase;


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

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


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


    
/**
     * @param  string
     * @return void
     */
    
public function initiate($file)
    {
        list(
$annotations$title) = $this->getAnnotations($file);
        
$php = clone $this->runner->getInterpreter();

        
$tests = [new Test($file$title)];
        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) {
                
/** @var Test[] $prepared */
                
$prepared = [];
                foreach (
$tests as $test) {
                    
$res $this->$method($test$value$php);
                    if (
$res === null) {
                        
$prepared[] = $test;
                    } else {
                        foreach (
is_array($res) ? $res : [$res] as $testVariety) {
                            
/** @var Test $testVariety */
                            
if ($testVariety->hasResult()) {
                                
$this->runner->prepareTest($testVariety);
                                
$this->runner->finishTest($testVariety);
                            } else {
                                
$prepared[] = $testVariety;
                            }
                        }
                    }
                }
                
$tests $prepared;
            }
        }

        foreach (
$tests as $test) {
            
$this->runner->prepareTest($test);
            
$this->runner->addJob(new Job($test$php$this->runner->getEnvironmentVariables()));
        }
    }


    
/**
     * @return void
     */
    
public function assess(Job $job)
    {
        
$test $job->getTest();
        
$annotations $this->getAnnotations($test->getFile())[0] += [
            
'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) {
                
/** @var Test|null $res */
                
if ($res $this->$method($job$arg)) {
                    
$this->runner->finishTest($res);
                    return;
                }
            }
        }
        
$this->runner->finishTest($test->withResult(Test::PASSED$test->message));
    }


    private function 
initiateSkip(Test $test$message)
    {
        return 
$test->withResult(Test::SKIPPED$message);
    }


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


    private function 
initiatePhpExtension(Test $test$valuePhpInterpreter $interpreter)
    {
        foreach (
preg_split('#[\s,]+#'$value) as $extension) {
            if (!
$interpreter->hasExtension($extension)) {
                return 
$test->withResult(Test::SKIPPED"Requires PHP extension $extension.");
            }
        }
    }


    private function 
initiatePhpIni(Test $test$pairPhpInterpreter $interpreter)
    {
        list(
$name$value) = explode('='$pair2) + [=> null];
        
$interpreter->addPhpIniOption($name$value);
    }


    private function 
initiateDataProvider(Test $test$provider)
    {
        try {
            list(
$dataFile$query$optional) = Tester\DataProvider::parseAnnotation($provider$test->getFile());
            
$data Tester\DataProvider::load($dataFile$query);
        } catch (\
Exception $e) {
            return 
$test->withResult(empty($optional) ? Test::FAILED Test::SKIPPED$e->getMessage());
        }

        return 
array_map(function ($item) use ($test$dataFile) {
            return 
$test->withArguments(['dataprovider' => "$item|$dataFile"]);
        }, 
array_keys($data));
    }


    private function 
initiateMultiple(Test $test$count)
    {
        return 
array_map(function ($i) use ($test) {
            return 
$test->withArguments(['multiple' => $i]);
        }, 
range(0, (int) $count 1));
    }


    private function 
initiateTestCase(Test $test$fooPhpInterpreter $interpreter)
    {
        
$job = new Job($test->withArguments(['method' => TestCase::LIST_METHODS]), $interpreter);
        
$job->run();

        if (
in_array($job->getExitCode(), [Job::CODE_ERRORJob::CODE_FAILJob::CODE_SKIP], true)) {
            return 
$test->withResult($job->getExitCode() === Job::CODE_SKIP Test::SKIPPED Test::FAILED$job->getTest()->stdout);
        }

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

        return 
array_map(function ($method) use ($test) {
            return 
$test->withArguments(['method' => $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->getTest()->stdout$m)
                ? 
$m[1]
                : 
$output;
            return 
$job->getTest()->withResult(Test::SKIPPEDtrim($message));

        } elseif (
$job->getExitCode() !== $code) {
            
$message $job->getExitCode() !== Job::CODE_FAIL "Exited with error code {$job->getExitCode()} (expected $code)" '';
            return 
$job->getTest()->withResult(Test::FAILEDtrim($message "\n" $job->getTest()->stdout));
        }
    }


    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 
$job->getTest()->withResult(Test::FAILED"Exited with HTTP code $actual (expected $code)");
        }
    }


    private function 
assessOutputMatchFile(Job $job$file)
    {
        
$file dirname($job->getTest()->getFile()) . DIRECTORY_SEPARATOR $file;
        if (!
is_file($file)) {
            return 
$job->getTest()->withResult(Test::FAILED"Missing matching file '$file'.");
        }
        return 
$this->assessOutputMatch($jobfile_get_contents($file));
    }


    private function 
assessOutputMatch(Job $job$content)
    {
        
$actual $job->getTest()->stdout;
        if (!
Tester\Assert::isMatching($content$actual)) {
            list(
$content$actual) = Tester\Assert::expandMatchingPatterns($content$actual);
            
Dumper::saveOutput($job->getTest()->getFile(), $actual'.actual');
            
Dumper::saveOutput($job->getTest()->getFile(), $content'.expected');
            return 
$job->getTest()->withResult(Test::FAILED'Failed: output should match ' Dumper::toLine($content));
        }
    }


    private function 
getAnnotations($file)
    {
        
$annotations Helpers::parseDocComment(file_get_contents($file));
        
$testTitle = isset($annotations[0]) ? preg_replace('#^TEST:\s*#i'''$annotations[0]) : null;
        return [
$annotations$testTitle];
    }
}
100 %
Runner/Test.php
<?php

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

namespace Tester\Runner;


/**
 * Test represents one result.
 */
class Test
{
    const
        
PREPARED 0,
        
FAILED 1,
        
PASSED 2,
        
SKIPPED 3;


    
/** @var string|null */
    
public $title;

    
/** @var string|null */
    
public $message;

    
/** @var string */
    
public $stdout '';

    
/** @var string */
    
public $stderr '';

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

    
/** @var int */
    
private $result self::PREPARED;

    
/** @var string[]|string[][] */
    
private $args = [];


    
/**
     * @param  string
     * @param  string
     */
    
public function __construct($file$title null)
    {
        
$this->file $file;
        
$this->title $title;
    }


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


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


    
/**
     * @return string
     */
    
public function getSignature()
    {
        
$args implode(' 'array_map(function ($arg) {
            return 
is_array($arg) ? "$arg[0]=$arg[1]$arg;
        }, 
$this->args));

        return 
$this->file . ($args $args'');
    }


    
/**
     * @return int
     */
    
public function getResult()
    {
        return 
$this->result;
    }


    
/**
     * @return bool
     */
    
public function hasResult()
    {
        return 
$this->result !== self::PREPARED;
    }


    
/**
     * @param  array $args
     * @return static
     */
    
public function withArguments(array $args)
    {
        if (
$this->hasResult()) {
            throw new \
LogicException('Cannot change arguments of test which already has a result.');
        }

        
$me = clone $this;
        foreach (
$args as $name => $values) {
            foreach ((array) 
$values as $value) {
                
$me->args[] = is_int($name)
                    ? 
"$value"
                    
: [$name"$value"];
            }
        }
        return 
$me;
    }


    
/**
     * @param  int
     * @param  string
     * @return static
     */
    
public function withResult($result$message)
    {
        if (
$this->hasResult()) {
            throw new \
LogicException("Result of test is already set to $this->result with message '$this->message'.");
        }

        
$me = clone $this;
        
$me->result $result;
        
$me->message $message;
        return 
$me;
    }
}
0 %
Runner/CliTester.php
<?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\Dumper;
use 
Tester\Environment;


/**
 * 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'], ['tap''junit'], true)) {
            
Environment::$useColors false;
        }

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

        
$this->createPhpInterpreter();

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

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

        
$runner $this->createRunner();
        
$runner->setEnvironmentVariable(Environment::RUNNER1);
        
$runner->setEnvironmentVariable(Environment::COLORS, (int) Environment::$useColors);
        if (isset(
$coverageFile)) {
            
$runner->setEnvironmentVariable(Environment::COVERAGE$coverageFile);
        }

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

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

        
$result $runner->run();

        if (isset(
$coverageFile) && preg_match('#\.(?:html?|xml)\z#'$coverageFile)) {
            
$this->finishCodeCoverage($coverageFile);
        }

        return 
$result 1;
    }


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


XX;

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

Options:
    -p <path>                    Specify PHP interpreter to run (default: php).
    -c <path>                    Look for php.ini file (or look in directory) <path>.
    -C                           Use system-wide php.ini.
    -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.
    --temp <path>                Path to temporary directory. Default by sys_get_temp_dir().
    --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
        , [
            
'-c' => [CommandLine::REALPATH => true],
            
'--watch' => [CommandLine::REPEATABLE => trueCommandLine::REALPATH => true],
            
'--setup' => [CommandLine::REALPATH => true],
            
'--temp' => [CommandLine::REALPATH => true],
            
'paths' => [CommandLine::REPEATABLE => trueCommandLine::VALUE => getcwd()],
            
'--debug' => [],
            
'--coverage-src' => [CommandLine::REALPATH => true],
        ]);

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

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

            if (
array_search('-p'$_SERVER['argv'], true) === false) {
                echo 
"Note: Default interpreter is CLI since Tester v2.0. It used to be CGI.\n";
            }
        }

        
$this->options $cmd->parse();
        if (
$this->options['--temp'] === null) {
            if ((
$temp sys_get_temp_dir()) === '') {
                echo 
"Note: System temporary directory is not set.\n";
            } elseif ((
$real realpath($temp)) === false) {
                echo 
"Note: System temporary directory '$temp' does not exist.\n";
            } else {
                
$this->options['--temp'] = rtrim($realDIRECTORY_SEPARATOR);
            }
        }

        return 
$cmd;
    }


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

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

        foreach (
$this->options['-d'] as $item) {
            
array_push($args'-d'$item);
        }

        
$this->interpreter = new PhpInterpreter($this->options['-p'], $args);

        if (
$error $this->interpreter->getStartupError()) {
            echo 
Dumper::color('red'"PHP startup error: $error") . "\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['--temp'] !== null) {
            
$runner->setTempDirectory($this->options['--temp']);
        }

        if (
$this->options['-o'] !== 'none') {
            switch (
$this->options['-o']) {
                case 
'tap':
                    
$runner->outputHandlers[] = new Output\TapPrinter;
                    break;
                case 
'junit':
                    
$runner->outputHandlers[] = new Output\JUnitPrinter;
                    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->canMeasureCodeCoverage()) {
            
$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']);
        echo 
"Code coverage: {$file}\n";
        return 
$file;
    }


    
/** @return void */
    
private function finishCodeCoverage($file)
    {
        if (!
in_array($this->options['-o'], ['none''tap''junit'], true)) {
            echo 
'Generating code coverage report... ';
        }
        if (
pathinfo($filePATHINFO_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);
        echo 
round($generator->getCoveredPercent()) . "% covered\n";
    }


    
/** @return void */
    
private function watch(Runner $runner)
    {
        
$prev = [];
        
$counter 0;
        while (
true) {
            
$state = [];
            foreach (
$this->options['--watch'] as $directory) {
                foreach (new \
RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory)) as $file) {
                    if (
substr($file->getExtension(), 03) === 'php' && substr($file->getBasename(), 01) !== '.') {
                        
$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);
        }
    }
}
95 %
Runner/CommandLine.php
<?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 = [];

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

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

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


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

        
preg_match_all('#^[ \t]+(--?\w.*?)(?:  .*\(default: (.*)\)|  |\r|$)#m'$help$linesPREG_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]) > || 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] : [];
            
$this->options[$name] = $opts + [
                
self::ARGUMENT => (bool) end($m[2]),
                
self::OPTIONAL => isset($line[2]) || (substr(end($m[2]), 01) === '[') || isset($opts[self::VALUE]),
                
self::REPEATABLE => (bool) end($m[3]),
                
self::ENUM => count($enums explode('|'trim(end($m[2]), '<[]>'))) > $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) : [];
        }
        
$params = [];
        
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('='$arg2) : [$argtrue];

            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;
    }
}
85 %
Runner/Job.php
<?php

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

namespace Tester\Runner;

use 
Tester\Helpers;


/**
 * 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 Test */
    
private $test;

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

    
/** @var string[]  environment variables for test */
    
private $envVars;

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

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

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

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

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


    public function 
__construct(Test $testPhpInterpreter $interpreter, array $envVars null)
    {
        if (
$test->getResult() !== Test::PREPARED) {
            throw new \
LogicException("Test '{$test->getSignature()}' already has result '{$test->getResult()}'.");
        }

        
$test->stdout '';
        
$test->stderr '';

        
$this->test $test;
        
$this->interpreter $interpreter;
        
$this->envVars = (array) $envVars;
    }


    
/**
     * @param  string
     * @param  string
     * @return void
     */
    
public function setEnvironmentVariable($name$value)
    {
        
$this->envVars[$name] = $value;
    }


    
/**
     * @param  string
     * @return string
     */
    
public function getEnvironmentVariable($name)
    {
        return 
$this->envVars[$name];
    }


    
/**
     * Runs single test.
     * @param  int self::RUN_ASYNC | self::RUN_COLLECT_ERRORS
     * @return void
     */
    
public function run($flags null)
    {
        foreach (
$this->envVars as $name => $value) {
            
putenv("$name=$value");
        }

        
$args = [];
        foreach (
$this->test->getArguments() as $value) {
            if (
is_array($value)) {
                
$args[] = Helpers::escapeArg("--$value[0]=$value[1]");
            } else {
                
$args[] = Helpers::escapeArg($value);
            }
        }

        
$this->proc proc_open(
            
$this->interpreter->getCommandLine()
            . 
' -d register_argc_argv=on ' Helpers::escapeArg($this->test->getFile()) . ' ' implode(' '$args),
            [
                [
'pipe''r'],
                [
'pipe''w'],
                [
'pipe''w'],
            ],
            
$pipes,
            
dirname($this->test->getFile()),
            
null,
            [
'bypass_shell' => true]
        );

        foreach (
array_keys($this->envVars) as $name) {
            
putenv($name);
        }

        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->stdoutfalse); // on Windows does not work with proc_open()
            
if ($this->stderr) {
                
stream_set_blocking($this->stderrfalse);
            }
        } 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->test->stdout .= stream_get_contents($this->stdout);
        if (
$this->stderr) {
            
$this->test->stderr .= 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->test->stdout2)) >= 2) {
            list(
$headers$this->test->stdout) = $tmp;
            foreach (
explode("\r\n"$headers) as $header) {
                
$pos strpos($header':');
                if (
$pos !== false) {
                    
$this->headers[trim(substr($header0$pos))] = (string) trim(substr($header$pos 1));
                }
            }
        }
        return 
false;
    }


    
/**
     * @return Test
     */
    
public function getTest()
    {
        return 
$this->test;
    }


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


    
/**
     * Returns output headers.
     * @return string[]
     */
    
public function getHeaders()
    {
        return 
$this->headers;
    }
}
100 %
Runner/OutputHandler.php
<?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 
prepare(Test $test);

    function 
finish(Test $test);

    function 
end();
}
69 %
Runner/Runner.php
<?php

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

namespace Tester\Runner;

use 
Tester\Environment;


/**
 * Test runner.
 */
class Runner
{
    
/** @var string[]  paths to test files/directories */
    
public $paths = [];

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

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

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

    
/** @var bool */
    
public $stopOnFail false;

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

    
/** @var array */
    
private $envVars = [];

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

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

    
/** @var string|null */
    
private $tempDir;

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

    
/** @var array */
    
private $lastResults = [];


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


    
/**
     * @param  string
     * @param  string
     * @return void
     */
    
public function setEnvironmentVariable($name$value)
    {
        
$this->envVars[$name] = $value;
    }


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


    
/**
     * @param  string|null
     */
    
public function setTempDirectory($path)
    {
        if (
$path !== null) {
            if (!
is_dir($path) || !is_writable($path)) {
                throw new \
RuntimeException("Path '$path' is not a writable directory.");
            }

            
$path realpath($path) . DIRECTORY_SEPARATOR 'Tester';
            if (!
is_dir($path) && @mkdir($path) === false && !is_dir($path)) {  // @ - directory may exist
                
throw new \RuntimeException("Cannot create '$path' directory.");
            }
        }

        
$this->tempDir $path;
    }


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

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

        
$this->jobs $running = [];
        foreach (
$this->paths as $path) {
            
$this->findTests($path);
        }

        if (
$this->tempDir) {
            
usort($this->jobs, function (Job $aJob $b) {
                return 
$this->getLastResult($a->getTest()) - $this->getLastResult($b->getTest());
            });
        }

        
$threads range(1$this->threadCount);

        
$this->installInterruptHandler();
        while ((
$this->jobs || $running) && !$this->isInterrupted()) {
            while (
$threads && $this->jobs) {
                
$running[] = $job array_shift($this->jobs);
                
$async $this->threadCount && (count($running) + count($this->jobs) > 1);
                
$job->setEnvironmentVariable(Environment::THREADarray_shift($threads));
                
$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()) {
                    
$threads[] = $job->getEnvironmentVariable(Environment::THREAD);
                    
$this->testHandler->assess($job);
                    unset(
$running[$key]);
                }
            }
        }
        
$this->removeInterruptHandler();

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

        return 
$this->result;
    }


    
/**
     * @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) ?: [] as $dir) {
                
$this->findTests($dir);
            }

            
$this->findTests($path '/*.phpt');
            
$this->findTests($path '/*Test.php');

        } else {
            foreach (
glob(str_replace('[''[[]'$path)) ?: [] 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;
    }


    
/**
     * @return void
     */
    
public function prepareTest(Test $test)
    {
        foreach (
$this->outputHandlers as $handler) {
            
$handler->prepare(clone $test);
        }
    }


    
/**
     * Writes to output handlers.
     * @return void
     */
    
public function finishTest(Test $test)
    {
        
$this->result $this->result && ($test->getResult() !== Test::FAILED);

        foreach (
$this->outputHandlers as $handler) {
            
$handler->finish(clone $test);
        }

        if (
$this->tempDir) {
            
$lastResult = &$this->lastResults[$test->getSignature()];
            if (
$lastResult !== $test->getResult()) {
                
file_put_contents($this->getLastResultFilename($test), $lastResult $test->getResult());
            }
        }

        if (
$this->stopOnFail && $test->getResult() === Test::FAILED) {
            
$this->interrupted true;
        }
    }


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


    
/**
     * @return void
     */
    
private function installInterruptHandler()
    {
        if (
extension_loaded('pcntl')) {
            
pcntl_signal(SIGINT, function () {
                
pcntl_signal(SIGINTSIG_DFL);
                
$this->interrupted true;
            });
        }
    }


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


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

        return 
$this->interrupted;
    }


    
/**
     * @return string
     */
    
private function getLastResult(Test $test)
    {
        
$signature $test->getSignature();
        if (isset(
$this->lastResults[$signature])) {
            return 
$this->lastResults[$signature];
        }

        
$file $this->getLastResultFilename($test);
        if (
is_file($file)) {
            return 
$this->lastResults[$signature] = file_get_contents($file);
        }

        return 
$this->lastResults[$signature] = Test::PREPARED;
    }


    
/**
     * @return string
     */
    
private function getLastResultFilename(Test $test)
    {
        return 
$this->tempDir
            
DIRECTORY_SEPARATOR
            
pathinfo($test->getFile(), PATHINFO_FILENAME)
            . 
'.'
            
substr(md5($test->getSignature()), 05)
            . 
'.result';
    }
}
88 %
Runner/PhpInterpreter.php
<?php

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

namespace Tester\Runner;

use 
Tester\Helpers;


/**
 * PHP command-line executable.
 */
class PhpInterpreter
{
    
/** @var string */
    
private $commandLine;

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

    
/** @var \stdClass  created by info.php */
    
private $info;

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


    public function 
__construct($path, array $args = [])
    {
        
$this->commandLine Helpers::escapeArg($path);
        
$proc = @proc_open// @ is escalated to exception
            
$this->commandLine ' --version',
            [[
'pipe''r'], ['pipe''w'], ['pipe''w']],
            
$pipes,
            
null,
            
null,
            [
'bypass_shell' => true]
        );
        if (
$proc === false) {
            throw new \
Exception("Cannot run PHP interpreter $path. Use -p option.");
        }
        
fclose($pipes[0]);
        
$output stream_get_contents($pipes[1]);
        
proc_close($proc);

        
$args ' ' implode(' 'array_map(['Tester\Helpers''escapeArg'], $args));
        if (
strpos($output'phpdbg') !== false) {
            
$args ' -qrrb -S cli' $args;
        }
        
$this->commandLine .= rtrim($args);

        
$proc proc_open(
            
$this->commandLine ' ' Helpers::escapeArg(__DIR__ '/info.php') . ' serialized',
            [[
'pipe''r'], ['pipe''w'], ['pipe''w']],
            
$pipes,
            
null,
            
null,
            [
'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));
        }

        
$parts explode("\r\n\r\n"$output2);
        
$this->cgi count($parts) === 2;
        
$this->info = @unserialize(strstr($parts[$this->cgi], 'O:8:"stdClass"'));
        
$this->error .= strstr($parts[$this->cgi], 'O:8:"stdClass"'true);
        if (!
$this->info) {
            throw new \
Exception("Unable to detect PHP version (output: $output).");

        } elseif (
$this->info->phpDbgVersion && version_compare($this->info->version'7.0.0''<')) {
            throw new \
Exception('Unable to use phpdbg on PHP < 7.0.0.');

        } elseif (
$this->cgi && $this->error) {
            
$this->error .= "\n(note that PHP CLI generates better error messages)";
        }
    }


    
/**
     * @param  string
     * @param  string
     */
    
public function addPhpIniOption($name$value null)
    {
        
$this->commandLine .= ' -d ' Helpers::escapeArg($name . ($value === null '' "=$value"));
    }


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


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


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


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


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


    
/**
     * @return string
     */
    
public function getShortInfo()
    {
        return 
"PHP {$this->info->version} ({$this->info->sapi})"
            
. ($this->info->phpDbgVersion "; PHPDBG {$this->info->phpDbgVersion}'');
    }


    
/**
     * @param  string
     * @return bool
     */
    
public function hasExtension($name)
    {
        return 
in_array(strtolower($name), array_map('strtolower'$this->info->extensions), true);
    }
}
100 %
CodeCoverage/PhpParser.php
<?php

namespace Tester\CodeCoverage;


/**
 * Parses PHP source code and returns:
 * - the start/end line information about functions, classes, interfaces, traits and their methods
 * - the count of code lines
 * - the count of commented code lines
 *
 * @internal
 */
class PhpParser
{
    
/**
     * @param  string  PHP code to analyze
     * @return \stdClass
     *
     * Returned structure is:
     *     stdClass {
     *         linesOfCode: int,
     *         linesOfComments: int,
     *         functions: [functionName => $functionInfo],
     *         classes: [className => $info],
     *         traits: [traitName => $info],
     *         interfaces: [interfaceName => $info],
     *     }
     *
     * where $functionInfo is:
     *     stdClass {
     *         start: int,
     *         end: int
     *     }
     *
     * and $info is:
     *     stdClass {
     *         start: int,
     *         end: int,
     *         methods: [methodName => $methodInfo]
     *     }
     *
     * where $methodInfo is:
     *     stdClass {
     *         start: int,
     *         end: int,
     *         visibility: public|protected|private
     *     }
     */
    
public function parse($code)
    {
        
$tokens = @token_get_all($code); // @ - source code can be written in newer PHP

        
$level $classLevel $functionLevel null;
        
$namespace '';
        
$line 1;

        
$result = (object) [
            
'linesOfCode' => max(1substr_count($code"\n")),
            
'linesOfComments' => 0,
            
'functions' => [],
            
'classes' => [],
            
'traits' => [],
            
'interfaces' => [],
        ];

        while (
$token current($tokens)) {
            
next($tokens);
            if (
is_array($token)) {
                
$line $token[2];
            }

            switch (
is_array($token) ? $token[0] : $token) {
                case 
T_NAMESPACE:
                    
$namespace ltrim(self::fetch($tokens, [T_STRINGT_NS_SEPARATOR]) . '\\''\\');
                    break;

                case 
T_CLASS:
                case 
T_INTERFACE:
                case 
T_TRAIT:
                    if (
$name self::fetch($tokensT_STRING)) {
                        if (
$token[0] === T_CLASS) {
                            
$class = &$result->classes[$namespace $name];
                        } elseif (
$token[0] === T_INTERFACE) {
                            
$class = &$result->interfaces[$namespace $name];
                        } else {
                            
$class = &$result->traits[$namespace $name];
                        }

                        
$classLevel $level 1;
                        
$class = (object) [
                            
'start' => $line,
                            
'end' => null,
                            
'methods' => [],
                        ];
                    }
                    break;

                case 
T_PUBLIC:
                case 
T_PROTECTED:
                case 
T_PRIVATE:
                    
$visibility $token[1];
                    break;

                case 
T_ABSTRACT:
                    
$isAbstract true;
                    break;

                case 
T_FUNCTION:
                    if ((
$name self::fetch($tokensT_STRING)) && !isset($isAbstract)) {
                        if (isset(
$class) && $level === $classLevel) {
                            
$function = &$class->methods[$name];
                            
$function = (object) [
                                
'start' => $line,
                                
'end' => null,
                                
'visibility' => isset($visibility) ? $visibility 'public',
                            ];

                        } else {
                            
$function = &$result->functions[$namespace $name];
                            
$function = (object) [
                                
'start' => $line,
                                
'end' => null,
                            ];
                        }
                        
$functionLevel $level 1;
                    }
                    unset(
$visibility$isAbstract);
                    break;

                case 
T_CURLY_OPEN:
                case 
T_DOLLAR_OPEN_CURLY_BRACES:
                case 
'{':
                    
$level++;
                    break;

                case 
'}':
                    if (isset(
$function) && $level === $functionLevel) {
                        
$function->end $line;
                        unset(
$function);

                    } elseif (isset(
$class) && $level === $classLevel) {
                        
$class->end $line;
                        unset(
$class);
                    }
                    
$level--;
                    break;

                case 
T_COMMENT:
                case 
T_DOC_COMMENT:
                    
$result->linesOfComments += substr_count(trim($token[1]), "\n") + 1;
                    
// break omitted

                
case T_WHITESPACE:
                case 
T_CONSTANT_ENCAPSED_STRING:
                    
$line += substr_count($token[1], "\n");
                    break;
            }
        }

        return 
$result;
    }


    private static function 
fetch(&$tokens$take)
    {
        
$res null;
        while (
$token current($tokens)) {
            list(
$token$s) = is_array($token) ? $token : [$token$token];
            if (
in_array($token, (array) $taketrue)) {
                
$res .= $s;
            } elseif (!
in_array($token, [T_DOC_COMMENTT_WHITESPACET_COMMENT], true)) {
                break;
            }
            
next($tokens);
        }
        return 
$res;
    }
}
0 %
CodeCoverage/Generators/HtmlGenerator.php
<?php

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

namespace Tester\CodeCoverage\Generators;


/**
 * Code coverage report generator.
 */
class HtmlGenerator extends AbstractGenerator
{
    
/** @var array */
    
public static $classes = [
        
self::CODE_TESTED => 't'// tested
        
self::CODE_UNTESTED => 'u'// untested
        
self::CODE_DEAD => 'dead'// dead code
    
];

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

    
/** @var array */
    
private $files = [];


    
/**
     * @param  string  path to coverage.dat file
     * @param  string  path to source file/directory
     * @param  string
     */
    
public function __construct($file$source null$title null)
    {
        
parent::__construct($file$source);
        
$this->title $title;
    }


    protected function 
renderSelf()
    {
        
$this->setupHighlight();
        
$this->parse();

        
$title $this->title;
        
$classes self::$classes;
        
$files $this->files;
        
$coveredPercent $this->getCoveredPercent();

        include 
__DIR__ '/template.phtml';
    }


    private function 
setupHighlight()
    {
        
ini_set('highlight.comment''hc');
        
ini_set('highlight.default''hd');
        
ini_set('highlight.html''hh');
        
ini_set('highlight.keyword''hk');
        
ini_set('highlight.string''hs');
    }


    private function 
parse()
    {
        if (
count($this->files) > 0) {
            return;
        }

        
$this->files = [];
        foreach (
$this->getSourceIterator() as $entry) {
            
$entry = (string) $entry;

            
$coverage $covered $total 0;
            
$loaded = !empty($this->data[$entry]);
            
$lines = [];
            if (
$loaded) {
                
$lines $this->data[$entry];
                foreach (
$lines as $flag) {
                    if (
$flag >= self::CODE_UNTESTED) {
                        
$total++;
                    }
                    if (
$flag >= self::CODE_TESTED) {
                        
$covered++;
                    }
                }
                
$coverage round($covered 100 $total);
                
$this->totalSum += $total;
                
$this->coveredSum += $covered;
            } else {
                
$this->totalSum += count(file($entryFILE_SKIP_EMPTY_LINES));
            }

            
$light $total $total count(file($entry)) < 50;
            
$this->files[] = (object) [
                
'name' => str_replace((is_dir($this->source) ? $this->source dirname($this->source)) . DIRECTORY_SEPARATOR''$entry),
                
'file' => $entry,
                
'lines' => $lines,
                
'coverage' => $coverage,
                
'total' => $total,
                
'class' => $light 'light' : ($loaded null 'not-loaded'),
            ];
        }
    }
}
59 %
CodeCoverage/Generators/AbstractGenerator.php
<?php

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

namespace Tester\CodeCoverage\Generators;


/**
 * Code coverage report generator.
 */
abstract class AbstractGenerator
{
    const
        
CODE_DEAD = -2,
        
CODE_UNTESTED = -1,
        
CODE_TESTED 1;

    
/** @var array */
    
public $acceptFiles = ['php''phpc''phpt''phtml'];

    
/** @var array */
    
protected $data;

    
/** @var string */
    
protected $source;

    
/** @var int */
    
protected $totalSum 0;

    
/** @var int */
    
protected $coveredSum 0;


    
/**
     * @param  string  path to coverage.dat file
     * @param  string  path to covered source file or directory
     */
    
public function __construct($file$source null)
    {
        if (!
is_file($file)) {
            throw new \
Exception("File '$file' is missing.");
        }

        
$this->data = @unserialize(file_get_contents($file)); // @ is escalated to exception
        
if (!is_array($this->data)) {
            throw new \
Exception("Content of file '$file' is invalid.");
        }

        if (!
$source) {
            
$source key($this->data);
            for (
$i 0$i strlen($source); $i++) {
                foreach (
$this->data as $s => $foo) {
                    if (!isset(
$s[$i]) || $source[$i] !== $s[$i]) {
                        
$source substr($source0$i);
                        break 
2;
                    }
                }
            }
            
$source dirname($source 'x');

        } elseif (!
file_exists($source)) {
            throw new \
Exception("File or directory '$source' is missing.");
        }

        
$this->source realpath($source);
    }


    public function 
render($file null)
    {
        
$handle $file ? @fopen($file'w') : STDOUT// @ is escalated to exception
        
if (!$handle) {
            throw new \
Exception("Unable to write to file '$file'.");
        }

        
ob_start(function ($buffer) use ($handle) { fwrite($handle$buffer); }, 4096);
        try {
            
$this->renderSelf();
        } catch (\
Exception $e) {
        }
        
ob_end_flush();
        
fclose($handle);

        if (isset(
$e)) {
            if (
$file) {
                
unlink($file);
            }
            throw 
$e;
        }
    }


    
/**
     * @return float
     */
    
public function getCoveredPercent()
    {
        return 
$this->totalSum $this->coveredSum 100 $this->totalSum 0;
    }


    
/**
     * @return \CallbackFilterIterator
     */
    
protected function getSourceIterator()
    {
        
$iterator is_dir($this->source)
            ? new \
RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->source))
            : new \
ArrayIterator([new \SplFileInfo($this->source)]);

        return new \
CallbackFilterIterator($iterator, function (\SplFileInfo $file) {
            return 
$file->getBasename()[0] !== '.'  // . or .. or .gitignore
                
&& in_array($file->getExtension(), $this->acceptFilestrue);
        });
    }


    abstract protected function 
renderSelf();
}
99 %
CodeCoverage/Generators/CloverXMLGenerator.php
<?php

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

namespace Tester\CodeCoverage\Generators;

use 
DOMDocument;
use 
DOMElement;
use 
Tester\CodeCoverage\PhpParser;


class 
CloverXMLGenerator extends AbstractGenerator
{
    private static 
$metricAttributesMap = [
        
'packageCount' => 'packages',
        
'fileCount' => 'files',
        
'linesOfCode' => 'loc',
        
'linesOfNonCommentedCode' => 'ncloc',
        
'classCount' => 'classes',
        
'methodCount' => 'methods',
        
'coveredMethodCount' => 'coveredmethods',
        
'statementCount' => 'statements',
        
'coveredStatementCount' => 'coveredstatements',
        
'elementCount' => 'elements',
        
'coveredElementCount' => 'coveredelements',
        
'conditionalCount' => 'conditionals',
        
'coveredConditionalCount' => 'coveredconditionals',
    ];


    public function 
__construct($file$source null)
    {
        if (!
extension_loaded('dom')) {
            throw new \
LogicException('CloverXML generator requires DOM extension to be loaded.');
        }
        
parent::__construct($file$source);
    }


    protected function 
renderSelf()
    {
        
$time = (string) time();
        
$parser = new PhpParser;

        
$doc = new DOMDocument;
        
$doc->formatOutput true;

        
$elCoverage $doc->appendChild($doc->createElement('coverage'));
        
$elCoverage->setAttribute('generated'$time);

        
// TODO: @name
        
$elProject $elCoverage->appendChild($doc->createElement('project'));
        
$elProject->setAttribute('timestamp'$time);
        
$elProjectMetrics $elProject->appendChild($doc->createElement('metrics'));

        
$projectMetrics = (object) [
            
'packageCount' => 0,
            
'fileCount' => 0,
            
'linesOfCode' => 0,
            
'linesOfNonCommentedCode' => 0,
            
'classCount' => 0,
            
'methodCount' => 0,
            
'coveredMethodCount' => 0,
            
'statementCount' => 0,
            
'coveredStatementCount' => 0,
            
'elementCount' => 0,
            
'coveredElementCount' => 0,
            
'conditionalCount' => 0,
            
'coveredConditionalCount' => 0,
        ];

        foreach (
$this->getSourceIterator() as $file) {
            
$file = (string) $file;

            
$projectMetrics->fileCount++;

            if (empty(
$this->data[$file])) {
                
$coverageData null;
                
$this->totalSum += count(file($fileFILE_SKIP_EMPTY_LINES));
            } else {
                
$coverageData $this->data[$file];
            }

            
// TODO: split to <package> by namespace?
            
$elFile $elProject->appendChild($doc->createElement('file'));
            
$elFile->setAttribute('name'$file);
            
$elFileMetrics $elFile->appendChild($doc->createElement('metrics'));

            
$code $parser->parse(file_get_contents($file));

            
$fileMetrics = (object) [
                
'linesOfCode' => $code->linesOfCode,
                
'linesOfNonCommentedCode' => $code->linesOfCode $code->linesOfComments,
                
'classCount' => count($code->classes) + count($code->traits),
                
'methodCount' => 0,
                
'coveredMethodCount' => 0,
                
'statementCount' => 0,
                
'coveredStatementCount' => 0,
                
'elementCount' => 0,
                
'coveredElementCount' => 0,
                
'conditionalCount' => 0,
                
'coveredConditionalCount' => 0,
            ];

            foreach (
array_merge($code->classes$code->traits) as $name => $info) { // TODO: interfaces?
                
$elClass $elFile->appendChild($doc->createElement('class'));
                if ((
$tmp strrpos($name'\\')) === false) {
                    
$elClass->setAttribute('name'$name);
                } else {
                    
$elClass->setAttribute('namespace'substr($name0$tmp));
                    
$elClass->setAttribute('name'substr($name$tmp 1));
                }

                
$elClassMetrics $elClass->appendChild($doc->createElement('metrics'));
                
$classMetrics $this->calculateClassMetrics($info$coverageData);
                
self::setMetricAttributes($elClassMetrics$classMetrics);
                
self::appendMetrics($fileMetrics$classMetrics);
            }
            
self::setMetricAttributes($elFileMetrics$fileMetrics);


            foreach ((array) 
$coverageData as $line => $count) {
                if (
$count === self::CODE_DEAD) {
                    continue;
                }

                
// Line type can be 'method' but Xdebug does not report such lines as executed.
                
$elLine $elFile->appendChild($doc->createElement('line'));
                
$elLine->setAttribute('num', (string) $line);
                
$elLine->setAttribute('type''stmt');
                
$elLine->setAttribute('count', (string) max(0$count));

                
$this->totalSum++;
                
$this->coveredSum += $count 0;
            }

            
self::appendMetrics($projectMetrics$fileMetrics);
        }

        
// TODO: What about reported (covered) lines outside of class/trait definition?
        
self::setMetricAttributes($elProjectMetrics$projectMetrics);

        echo 
$doc->saveXML();
    }


    
/**
     * @return \stdClass
     */
    
private function calculateClassMetrics(\stdClass $info, array $coverageData null)
    {
        
$stats = (object) [
            
'methodCount' => count($info->methods),
            
'coveredMethodCount' => 0,
            
'statementCount' => 0,
            
'coveredStatementCount' => 0,
            
'conditionalCount' => 0,
            
'coveredConditionalCount' => 0,
            
'elementCount' => null,
            
'coveredElementCount' => null,
        ];

        foreach (
$info->methods as $name => $methodInfo) {
            list(
$lineCount$coveredLineCount) = $this->analyzeMethod($methodInfo$coverageData);

            
$stats->statementCount += $lineCount;

            if (
$coverageData !== null) {
                
$stats->coveredMethodCount += $lineCount === $coveredLineCount 0;
                
$stats->coveredStatementCount += $coveredLineCount;
            }
        }

        
$stats->elementCount $stats->methodCount $stats->statementCount;
        
$stats->coveredElementCount $stats->coveredMethodCount $stats->coveredStatementCount;

        return 
$stats;
    }


    
/**
     * @return array
     */
    
private static function analyzeMethod(\stdClass $info, array $coverageData null)
    {
        
$count 0;
        
$coveredCount 0;

        if (
$coverageData === null) { // Never loaded file
            
$count max(1$info->end $info->start 2);
        } else {
            for (
$i $info->start$i <= $info->end$i++) {
                if (isset(
$coverageData[$i]) && $coverageData[$i] !== self::CODE_DEAD) {
                    
$count++;
                    if (
$coverageData[$i] > 0) {
                        
$coveredCount++;
                    }
                }
            }
        }

        return [
$count$coveredCount];
    }


    private static function 
appendMetrics(\stdClass $summary, \stdClass $add)
    {
        foreach (
$add as $name => $value) {
            
$summary->{$name} += $value;
        }
    }


    private static function 
setMetricAttributes(DOMElement $element, \stdClass $metrics)
    {
        foreach (
$metrics as $name => $value) {
            
$element->setAttribute(self::$metricAttributesMap[$name], (string) $value);
        }
    }
}
0 %
CodeCoverage/Generators/template.phtml
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="robots" content="noindex,noarchive">
    <meta name="generator" content="Nette Tester">

    <title><?= $title htmlspecialchars("$title - ") : ''?>Code coverage</title>

    <style type="text/css">
    html {
        font: 14px/1.5 Verdana,"Geneva CE",lucida,sans-serif;
        border-top: 4.7em solid #f4ebdb;
    }

    body {
        max-width: 990px;
        margin: -4.7em auto 0;
        background: #fcfaf5;
        color: #333;
    }

    footer {
        margin-left: .5em;
    }

    h1 {
        font-family: "Trebuchet MS","Geneva CE",lucida,sans-serif;
        font-size: 1.9em;
        margin: .5em .5em 1.5em;
        color: #7a7772;
        text-shadow: 1px 1px 0 white;
    }

    div.code {
        background: white;
        border: 1px dotted silver;
        padding: .4em 0;
        display: none;
        color: #333;
        overflow: auto;
    }

    code,
    div.code {
        font: 13px/1.3 monospace;
    }

    div.code > div {
        float: left;
        min-width: 100%;
        position: relative;
    }

    aside {
        min-width: 100%;
        position: absolute;
    }

    aside div {
        white-space: pre;
        padding-left: .7em;
    }

    aside a {
        color: #c0c0c0;
    }

    aside a:hover {
        color: inherit;
        font-weight: bold;
        text-decoration: none;
    }

    code {
        display: block;
        white-space: nowrap;
        position: relative;
    }

    a {
        color: #006aeb;
        text-decoration: none;
    }

    a:active,
    a:hover {
        text-decoration: underline;
    }

    td {
        vertical-align: middle;
    }

    small {
        color: gray;
    }

    .number {
        text-align: right;
        width: 50px;
    }

    .bar {
        border: 1px solid #acacac;
        background: #e50400;
        width: 35px;
        height: 1em;
    }

    .bar div {
        background: #1a7e1e;
        height: 1em;
    }

    .light td {
        opacity: .5;
    }

    .light td * {
        color: gray;
    }

    .not-loaded td * {
        color: red;
    }

    .t { background-color: #e0ffe0; }
    .u { background-color: #ffe0e0; }

    code .hc { color: #929292; }
    code .hd { color: #333; }
    code .hh { color: #06B; }
    code .hk { color: #e71818; }
    code .hs { color: #008000; }
    </style>
</head>

<body>
    <h1><?= $title htmlspecialchars("$title - ") : ''?>Code coverage <?= round($coveredPercent?>&nbsp;%</h1>

    <?php foreach ($files as $id => $info): ?>
    <div>
        <table>
        <tr<?= $info->class " class='$info->class'" '' ?>>
            <td class="number"><small><?= $info->coverage ?> %</small></td>
            <td><div class="bar"><div style="width: <?= $info->coverage ?>%"></div></div></td>
            <td><a href="#F<?= $id ?>" class="toggle"><?= $info->name ?></a></td>
        </tr>
        </table>

        <div class="code" id="F<?= $id ?>">
        <div>
            <aside>
            <?php
            $code 
file_get_contents($info->file);
            
$lineCount substr_count($code"\n") + 1;
            
$digits ceil(log10($lineCount)) + 1;

            
$prevClass null;
            
$closeTag $buffer '';
            for (
$i 1$i $lineCount$i++) {
                
$class = isset($info->lines[$i]) && isset($classes[$info->lines[$i]])
                    ? 
$classes[$info->lines[$i]]
                    : 
'';

                if (
$class !== $prevClass) {
                    echo 
rtrim($buffer) . $closeTag;
                    
$buffer '';
                    
$closeTag '</div>';
                    echo 
'<div' . ($class " class='$class'" '') . '>';
                }

                
$buffer .= "<a href='#F{$id}L{$i}' id='F{$id}L{$i}'>" sprintf("%{$digits}s"$i) . "</a>\n";
                
$prevClass $class;
            }
            echo 
$buffer $closeTag;

            
$code strtr(highlight_string($codetrue), [
                
'<code>' => "<code style='margin-left: {$digits}em'>",
                
'<span style="color: ' => '<span class="',
            ]);
            
?>
            </aside>
            <?= $code ?>
        </div>
        </div>
    </div>
    <?php endforeach ?>

    <footer>
        <p>Generated by <a href="https://tester.nette.org">Nette Tester</a> at <?= @date('Y/m/d H:i:s'// @ timezone may not be set ?></p>
    </footer>

    <script>
    document.body.addEventListener('click', function (e) {
        if (e.target.className === 'toggle') {
            var el = document.getElementById(e.target.href.split('#')[1]);
            if (el.style.display === 'block') {
                el.style.display = 'none';
            } else {
                el.style.display = 'block';
            }
            e.preventDefault();
        }
    });

    if (el = document.getElementById(window.location.hash.replace(/^#|L\d+$/g, ''))) {
        el.style.display = 'block';
    }
    </script>
</body>
</html>
19 %
CodeCoverage/Collector.php
<?php

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

namespace Tester\CodeCoverage;


/**
 * Code coverage collector.
 */
class Collector
{
    
/** @var resource */
    
private static $file;

    
/** @var string */
    
private static $collector;


    
/**
     * @return bool
     */
    
public static function isStarted()
    {
        return 
self::$file !== null;
    }


    
/**
     * Starts gathering the information for code coverage.
     * @param  string
     * @return void
     * @throws \LogicException
     */
    
public static function start($file)
    {
        if (
self::isStarted()) {
            throw new \
LogicException('Code coverage collector has been already started.');
        }
        
self::$file fopen($file'c+');

        if (
defined('PHPDBG_VERSION') && PHP_VERSION_ID >= 70000) {
            
phpdbg_start_oplog();
            
self::$collector 'collectPhpDbg';

        } elseif (
extension_loaded('xdebug')) {
            
xdebug_start_code_coverage(XDEBUG_CC_UNUSED XDEBUG_CC_DEAD_CODE);
            
self::$collector 'collectXdebug';

        } else {
            
$alternative PHP_VERSION_ID >= 70000 ' or phpdbg SAPI' '';
            throw new \
LogicException("Code coverage functionality requires Xdebug extension$alternative.");
        }

        
register_shutdown_function(function () {
            
register_shutdown_function([__CLASS__'save']);
        });
    }


    
/**
     * Flushes all gathered information. Effective only with PHPDBG collector.
     */
    
public static function flush()
    {
        if (
self::isStarted() && self::$collector === 'collectPhpDbg') {
            
self::save();
        }
    }


    
/**
     * Saves information about code coverage. Can be called repeatedly to free memory.
     * @return void
     * @throws \LogicException
     */
    
public static function save()
    {
        if (!
self::isStarted()) {
            throw new \
LogicException('Code coverage collector has not been started.');
        }

        list(
$positive$negative) = call_user_func([__CLASS__self::$collector]);

        
flock(self::$fileLOCK_EX);
        
fseek(self::$file0);
        
$rawContent stream_get_contents(self::$file);
        
$original $rawContent unserialize($rawContent) : [];
        
$coverage array_replace_recursive($negative$original$positive);

        
fseek(self::$file0);
        
ftruncate(self::$file0);
        
fwrite(self::$fileserialize($coverage));
        
flock(self::$fileLOCK_UN);
    }


    
/**
     * Collects information about code coverage.
     * @return array
     */
    
private static function collectXdebug()
    {
        
$positive $negative = [];

        foreach (
xdebug_get_code_coverage() as $file <