mirror of
https://github.com/dg/dibi.git
synced 2025-08-03 20:57:36 +02:00
401 lines
9.6 KiB
PHP
401 lines
9.6 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Nette Framework
|
|
*
|
|
* @copyright Copyright (c) 2004 David Grudl
|
|
* @license http://nette.org/license Nette license
|
|
* @link http://nette.org
|
|
* @category Nette
|
|
* @package Nette\Test
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
* Single test case.
|
|
*
|
|
* @author David Grudl
|
|
* @package Nette\Test
|
|
*/
|
|
class TestCase
|
|
{
|
|
/** @var string test file */
|
|
private $file;
|
|
|
|
/** @var array test file multiparts */
|
|
private $sections;
|
|
|
|
/** @var string test output */
|
|
private $output;
|
|
|
|
/** @var string output headers in raw format */
|
|
private $headers;
|
|
|
|
/** @var string PHP command line */
|
|
private $cmdLine;
|
|
|
|
/** @var string PHP command line */
|
|
private $phpVersion;
|
|
|
|
/** @var array */
|
|
private static $cachedPhp;
|
|
|
|
|
|
|
|
/**
|
|
* @param string test file name
|
|
* @param string PHP command line
|
|
* @return void
|
|
*/
|
|
public function __construct($testFile)
|
|
{
|
|
$this->file = (string) $testFile;
|
|
$this->sections = self::parseSections($this->file);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Runs single test.
|
|
* @return void
|
|
*/
|
|
public function run()
|
|
{
|
|
// pre-skip?
|
|
$options = $this->sections['options'];
|
|
if (isset($options['skip'])) {
|
|
$message = $options['skip'] ? $options['skip'] : 'No message.';
|
|
throw new TestCaseException($message, TestCaseException::SKIPPED);
|
|
|
|
} elseif (isset($options['phpversion'])) {
|
|
$operator = '>=';
|
|
if (preg_match('#^(<=|le|<|lt|==|=|eq|!=|<>|ne|>=|ge|>|gt)#', $options['phpversion'], $matches)) {
|
|
$options['phpversion'] = trim(substr($options['phpversion'], strlen($matches[1])));
|
|
$operator = $matches[1];
|
|
}
|
|
if (version_compare($options['phpversion'], $this->phpVersion, $operator)) {
|
|
throw new TestCaseException("Requires PHP $operator $options[phpversion].", TestCaseException::SKIPPED);
|
|
}
|
|
}
|
|
|
|
$this->execute();
|
|
$output = $this->output;
|
|
$headers = array_change_key_case(self::parseLines($this->headers, ':'), CASE_LOWER);
|
|
$tests = 0;
|
|
|
|
// post-skip?
|
|
if (isset($headers['x-nette-test-skip'])) {
|
|
throw new TestCaseException($headers['x-nette-test-skip'], TestCaseException::SKIPPED);
|
|
}
|
|
|
|
// compare output
|
|
$expectedOutput = $this->getExpectedOutput();
|
|
if ($expectedOutput !== NULL) {
|
|
$tests++;
|
|
$binary = (bool) preg_match('#[\x00-\x08\x0B\x0C\x0E-\x1F]#', $expectedOutput);
|
|
if ($binary) {
|
|
if ($expectedOutput !== $output) {
|
|
throw new TestCaseException("Binary output doesn't match.");
|
|
}
|
|
} else {
|
|
$output = self::normalize($output, isset($options['keeptrailingspaces']));
|
|
$expectedOutput = self::normalize($expectedOutput, isset($options['keeptrailingspaces']));
|
|
if (!$this->compare($output, $expectedOutput)) {
|
|
throw new TestCaseException("Output doesn't match.");
|
|
}
|
|
}
|
|
}
|
|
|
|
// compare headers
|
|
$expectedHeaders = $this->getExpectedHeaders();
|
|
if ($expectedHeaders !== NULL) {
|
|
$tests++;
|
|
$expectedHeaders = self::normalize($expectedHeaders, FALSE);
|
|
$expectedHeaders = array_change_key_case(self::parseLines($expectedHeaders, ':'), CASE_LOWER);
|
|
foreach ($expectedHeaders as $name => $header) {
|
|
if (!isset($headers[$name])) {
|
|
throw new TestCaseException("Missing header '$name'.");
|
|
|
|
} elseif (!$this->compare($headers[$name], $header)) {
|
|
throw new TestCaseException("Header '$name' doesn't match.");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$tests) { // expecting no output
|
|
if (trim($output) !== '') {
|
|
throw new TestCaseException("Empty output doesn't match.");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Sets PHP command line.
|
|
* @param string
|
|
* @param string
|
|
* @param string
|
|
* @return TestCase provides a fluent interface
|
|
*/
|
|
public function setPhp($binary, $args, $environment)
|
|
{
|
|
if (isset(self::$cachedPhp[$binary])) {
|
|
$this->phpVersion = self::$cachedPhp[$binary];
|
|
|
|
} else {
|
|
exec($environment . escapeshellarg($binary) . ' -v', $output, $res);
|
|
if ($res !== 0 && $res !== 255) {
|
|
throw new Exception("Unable to execute '$binary -v'.");
|
|
}
|
|
|
|
if (!preg_match('#^PHP (\S+).*cli#i', $output[0], $matches)) {
|
|
throw new Exception("Unable to detect PHP version (output: $output[0]).");
|
|
}
|
|
|
|
$this->phpVersion = self::$cachedPhp[$binary] = $matches[1];
|
|
}
|
|
|
|
$this->cmdLine = $environment . escapeshellarg($binary) . $args;
|
|
return $this;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Execute test.
|
|
* @return array
|
|
*/
|
|
private function execute()
|
|
{
|
|
$this->headers = $this->output = NULL;
|
|
|
|
$tempFile = tempnam('', 'tmp');
|
|
if (!$tempFile) {
|
|
throw new Exception("Unable to create temporary file.");
|
|
}
|
|
|
|
$command = $this->cmdLine;
|
|
if (isset($this->sections['options']['phpini'])) {
|
|
foreach (explode(';', $this->sections['options']['phpini']) as $item) {
|
|
$command .= " -d " . escapeshellarg(trim($item));
|
|
}
|
|
}
|
|
$command .= ' ' . escapeshellarg($this->file) . ' > ' . escapeshellarg($tempFile);
|
|
|
|
chdir(dirname($this->file));
|
|
exec($command, $foo, $res);
|
|
if ($res === 255) {
|
|
// exit_status 255 => parse or fatal error
|
|
|
|
} elseif ($res !== 0) {
|
|
throw new Exception("Unable to execute '$command'.");
|
|
|
|
}
|
|
|
|
$this->output = file_get_contents($tempFile);
|
|
unlink($tempFile);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Returns test file section.
|
|
* @return string
|
|
*/
|
|
public function getSection($name)
|
|
{
|
|
return isset($this->sections[$name]) ? $this->sections[$name] : NULL;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Returns test name.
|
|
* @return string
|
|
*/
|
|
public function getName()
|
|
{
|
|
return $this->sections['options']['name'];
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Returns test output.
|
|
* @return string
|
|
*/
|
|
public function getOutput()
|
|
{
|
|
return $this->output;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Returns output headers.
|
|
* @return string
|
|
*/
|
|
public function getHeaders()
|
|
{
|
|
return $this->headers;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Returns expected output.
|
|
* @return string
|
|
*/
|
|
public function getExpectedOutput()
|
|
{
|
|
if (isset($this->sections['expect'])) {
|
|
return $this->sections['expect'];
|
|
|
|
} elseif (is_file($expFile = str_replace('.phpt', '', $this->file) . '.expect')) {
|
|
return file_get_contents($expFile);
|
|
|
|
} else {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Returns expected headers.
|
|
* @return string
|
|
*/
|
|
public function getExpectedHeaders()
|
|
{
|
|
return $this->getSection('expectheaders');
|
|
}
|
|
|
|
|
|
|
|
/********************* helpers ****************d*g**/
|
|
|
|
|
|
|
|
/**
|
|
* Splits file into sections.
|
|
* @param string file
|
|
* @return array
|
|
*/
|
|
public static function parseSections($testFile)
|
|
{
|
|
$content = file_get_contents($testFile);
|
|
$sections = array(
|
|
'options' => array(),
|
|
);
|
|
|
|
// phpDoc
|
|
$phpDoc = preg_match('#^/\*\*(.*?)\*/#ms', $content, $matches) ? trim($matches[1]) : '';
|
|
preg_match_all('#^\s*\*\s*@(\S+)(.*)#mi', $phpDoc, $matches, PREG_SET_ORDER);
|
|
foreach ($matches as $match) {
|
|
$sections['options'][strtolower($match[1])] = isset($match[2]) ? trim($match[2]) : TRUE;
|
|
}
|
|
$sections['options']['name'] = preg_match('#^\s*\*\s*TEST:(.*)#mi', $phpDoc, $matches) ? trim($matches[1]) : $testFile;
|
|
|
|
// file parts
|
|
$tmp = preg_split('#^-{3,}([^\s-]+)-{1,}(?:\r?\n|$)#m', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
|
|
$i = 1;
|
|
while (isset($tmp[$i])) {
|
|
$sections[strtolower($tmp[$i])] = $tmp[$i+1];
|
|
$i += 2;
|
|
}
|
|
return $sections;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Splits HTTP headers into array.
|
|
* @param string
|
|
* @param string
|
|
* @return array
|
|
*/
|
|
public static function parseLines($raw, $separator)
|
|
{
|
|
$headers = array();
|
|
foreach (explode("\r\n", $raw) as $header) {
|
|
$a = strpos($header, $separator);
|
|
if ($a !== FALSE) {
|
|
$headers[trim(substr($header, 0, $a))] = (string) trim(substr($header, $a + 1));
|
|
}
|
|
}
|
|
return $headers;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Compares results.
|
|
* @param string
|
|
* @param string
|
|
* @return bool
|
|
*/
|
|
public static function compare($left, $right)
|
|
{
|
|
$right = strtr($right, array(
|
|
'%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
|
|
'%ns%'=> '(?:[_0-9a-zA-Z\\\\]+\\\\|N)?',// PHP namespace
|
|
'%[^' => '[^', // reg-exp
|
|
'%[' => '[', // reg-exp
|
|
']%' => ']+', // reg-exp
|
|
|
|
'.' => '\.', '\\' => '\\\\', '+' => '\+', '*' => '\*', '?' => '\?', '[' => '\[', '^' => '\^', ']' => '\]', '$' => '\$', '(' => '\(', ')' => '\)', // preg quote
|
|
'{' => '\{', '}' => '\}', '=' => '\=', '!' => '\!', '>' => '\>', '<' => '\<', '|' => '\|', ':' => '\:', '-' => '\-', "\x00" => '\000', '#' => '\#', // preg quote
|
|
));
|
|
|
|
return (bool) preg_match("#^$right$#s", $left);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Normalizes whitespace
|
|
* @param string
|
|
* @param bool
|
|
* @return string
|
|
*/
|
|
public static function normalize($s, $keepTrailingSpaces)
|
|
{
|
|
$s = str_replace("\n", PHP_EOL, str_replace("\r\n", "\n", $s)); // normalize EOL
|
|
if (!$keepTrailingSpaces) {
|
|
$s = preg_replace("#[\t ]+(\r?\n)#", '$1', $s); // multiline right trim
|
|
$s = rtrim($s); // ending trim
|
|
}
|
|
return $s;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Single test exception.
|
|
*
|
|
* @author David Grudl
|
|
* @package Nette\Test
|
|
*/
|
|
class TestCaseException extends Exception
|
|
{
|
|
const SKIPPED = 1;
|
|
|
|
}
|