Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new assert json tests #436

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
},
"require-dev": {
"ext-simplexml": "*",
"phpstan/phpstan": "^0.12"
"phpstan/phpstan": "^1.0"
},
"autoload": {
"classmap": ["src/"]
Expand Down
14 changes: 10 additions & 4 deletions src/Framework/Assert.php
Original file line number Diff line number Diff line change
Expand Up @@ -481,23 +481,29 @@ public static function matchFile(string $file, $actual, string $description = nu
throw new \Exception("Unable to read file '$file'.");

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

} elseif (!self::isMatching($pattern, $actual)) {
if (self::$expandPatterns) {
[$pattern, $actual] = self::expandMatchingPatterns($pattern, $actual);
}
self::fail(self::describe('%1 should match %2', $description), $actual, $pattern);
self::fail(self::describe('%1 should match %2', $description), $actual, $pattern, null, basename($file));
}
}


/**
* Assertion that fails.
*/
public static function fail(string $message, $actual = null, $expected = null, \Throwable $previous = null): void
{
public static function fail(
string $message,
$actual = null,
$expected = null,
\Throwable $previous = null,
string $outputName = null
): void {
$e = new AssertException($message, $expected, $actual, $previous);
$e->outputName = $outputName;
if (self::$onFailure) {
(self::$onFailure)($e);
} else {
Expand Down
2 changes: 2 additions & 0 deletions src/Framework/AssertException.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class AssertException extends \Exception

public $expected;

public $outputName;


public function __construct(string $message, $expected, $actual, \Throwable $previous = null)
{
Expand Down
105 changes: 74 additions & 31 deletions src/Framework/Dumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,6 @@ class Dumper
*/
public static function toLine($var): string
{
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';

Expand All @@ -62,9 +51,7 @@ public static function toLine($var): string
} elseif (strlen($var) > self::$maxLength) {
$var = substr($var, 0, self::$maxLength) . '...';
}
return preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $var) || preg_last_error()
? '"' . strtr($var, $table) . '"'
: "'$var'";
return self::encodeStringLine($var);

} elseif (is_array($var)) {
$out = '';
Expand Down Expand Up @@ -146,20 +133,10 @@ private static function _toPhp(&$var, array &$list = [], int $level = 0, int &$l
} 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_string($var)) {
$res = self::encodeStringPhp($var);
$line += substr_count($res, "\n");
return $res;

} elseif (is_array($var)) {
$space = str_repeat("\t", $level);
Expand Down Expand Up @@ -242,9 +219,72 @@ private static function _toPhp(&$var, array &$list = [], int $level = 0, int &$l
return '/* resource ' . get_resource_type($var) . ' */';

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


private static function encodeStringPhp(string $s): string
{
static $special = [
"\r" => '\r',
"\n" => '\n',
"\t" => "\t",
"\e" => '\e',
'\\' => '\\\\',
];
$utf8 = preg_match('##u', $s);
$escaped = preg_replace_callback(
$utf8 ? '#[\p{C}\\\\]#u' : '#[\x00-\x1F\x7F-\xFF\\\\]#',
function ($m) use ($special) {
return $special[$m[0]] ?? (strlen($m[0]) === 1
? '\x' . str_pad(strtoupper(dechex(ord($m[0]))), 2, '0', STR_PAD_LEFT) . ''
: '\u{' . strtoupper(ltrim(dechex(self::utf8Ord($m[0])), '0')) . '}');
},
$s
);
return $s === str_replace('\\\\', '\\', $escaped)
? "'" . preg_replace('#\'|\\\\(?=[\'\\\\]|$)#D', '\\\\$0', $s) . "'"
: '"' . addcslashes($escaped, '"$') . '"';
}


private static function encodeStringLine(string $s): string
{
static $special = [
"\r" => "\\r\r",
"\n" => "\\n\n",
"\t" => "\\t\t",
"\e" => '\\e',
"'" => "'",
];
$utf8 = preg_match('##u', $s);
$escaped = preg_replace_callback(
$utf8 ? '#[\p{C}\']#u' : '#[\x00-\x1F\x7F-\xFF\']#',
function ($m) use ($special) {
return "\e[22m"
. ($special[$m[0]] ?? (strlen($m[0]) === 1
? '\x' . str_pad(strtoupper(dechex(ord($m[0]))), 2, '0', STR_PAD_LEFT)
: '\u{' . strtoupper(ltrim(dechex(self::utf8Ord($m[0])), '0')) . '}'))
. "\e[1m";
},
$s
);
return "'" . $escaped . "'";
}


private static function utf8Ord(string $c): int
{
$ord0 = ord($c[0]);
if ($ord0 < 0x80) {
return $ord0;
} elseif ($ord0 < 0xE0) {
return ($ord0 << 6) + ord($c[1]) - 0x3080;
} elseif ($ord0 < 0xF0) {
return ($ord0 << 12) + (ord($c[1]) << 6) + ord($c[2]) - 0xE2080;
} else {
return ($ord0 << 18) + (ord($c[1]) << 12) + (ord($c[2]) << 6) + ord($c[3]) - 0x3C82080;
}
}

Expand All @@ -265,6 +305,9 @@ public static function dumpException(\Throwable $e): string
if ($e instanceof AssertException) {
$expected = $e->expected;
$actual = $e->actual;
$testFile = $e->outputName
? dirname($testFile) . '/' . $e->outputName . '.foo'
: $testFile;

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 || preg_match('#[\x00-\x1F]#', $actual)))
Expand Down
23 changes: 22 additions & 1 deletion src/Runner/CliTester.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public function run(): ?int
$runner->setEnvironmentVariable(Environment::RUNNER, '1');
$runner->setEnvironmentVariable(Environment::COLORS, (string) (int) Environment::$useColors);

$this->installInterruptHandler();

if ($this->options['--coverage']) {
$coverageFile = $this->prepareCodeCoverage($runner);
}
Expand Down Expand Up @@ -360,7 +362,9 @@ private function setupErrors(): void
});

set_exception_handler(function (\Throwable $e) {
$this->displayException($e);
if (!$e instanceof InterruptException) {
$this->displayException($e);
}
exit(2);
});
}
Expand All @@ -374,4 +378,21 @@ private function displayException(\Throwable $e): void
: Dumper::color('white/red', 'Error: ' . $e->getMessage());
echo "\n";
}


private function installInterruptHandler(): void
{
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGINT, function (): void {
pcntl_signal(SIGINT, SIG_DFL);
throw new InterruptException;
});
pcntl_async_signals(true);

} elseif (function_exists('sapi_windows_set_ctrl_handler') && PHP_SAPI === 'cli') {
sapi_windows_set_ctrl_handler(function (): void {
throw new InterruptException;
});
}
}
}
80 changes: 23 additions & 57 deletions src/Runner/Runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,36 +125,37 @@ public function run(): bool

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

$this->installInterruptHandler();
$async = $this->threadCount > 1 && count($this->jobs) > 1;

while (($this->jobs || $running) && !$this->isInterrupted()) {
while ($threads && $this->jobs) {
$running[] = $job = array_shift($this->jobs);
$job->setEnvironmentVariable(Environment::THREAD, (string) array_shift($threads));
$job->run($async ? $job::RUN_ASYNC : 0);
}

if ($async) {
usleep(Job::RUN_USLEEP); // stream_select() doesn't work with proc_open()
}
try {
while (($this->jobs || $running) && !$this->interrupted) {
while ($threads && $this->jobs) {
$running[] = $job = array_shift($this->jobs);
$job->setEnvironmentVariable(Environment::THREAD, (string) array_shift($threads));
$job->run($async ? $job::RUN_ASYNC : 0);
}

foreach ($running as $key => $job) {
if ($this->isInterrupted()) {
break 2;
if ($async) {
usleep(Job::RUN_USLEEP); // stream_select() doesn't work with proc_open()
}

if (!$job->isRunning()) {
$threads[] = $job->getEnvironmentVariable(Environment::THREAD);
$this->testHandler->assess($job);
unset($running[$key]);
foreach ($running as $key => $job) {
if ($this->interrupted) {
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();
} finally {
foreach ($this->outputHandlers as $handler) {
$handler->end();
}
}

return $this->result;
Expand Down Expand Up @@ -235,41 +236,6 @@ public function getInterpreter(): PhpInterpreter
}


private function installInterruptHandler(): void
{
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGINT, function (): void {
pcntl_signal(SIGINT, SIG_DFL);
$this->interrupted = true;
});
} elseif (function_exists('sapi_windows_set_ctrl_handler') && PHP_SAPI === 'cli') {
sapi_windows_set_ctrl_handler(function () {
$this->interrupted = true;
});
}
}


private function removeInterruptHandler(): void
{
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGINT, SIG_DFL);
} elseif (function_exists('sapi_windows_set_ctrl_handler') && PHP_SAPI === 'cli') {
sapi_windows_set_ctrl_handler(null);
}
}


private function isInterrupted(): bool
{
if (function_exists('pcntl_signal_dispatch')) {
pcntl_signal_dispatch();
}

return $this->interrupted;
}


private function getLastResult(Test $test): int
{
$signature = $test->getSignature();
Expand Down
15 changes: 15 additions & 0 deletions src/Runner/exceptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

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

declare(strict_types=1);

namespace Tester\Runner;


class InterruptException extends \Exception
{
}
1 change: 1 addition & 0 deletions src/tester.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

declare(strict_types=1);

require __DIR__ . '/Runner/exceptions.php';
require __DIR__ . '/Runner/Test.php';
require __DIR__ . '/Runner/PhpInterpreter.php';
require __DIR__ . '/Runner/Runner.php';
Expand Down
25 changes: 25 additions & 0 deletions tests/Framework/Assert.json.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

use Tester\Assert;

require __DIR__ . '/../bootstrap.php';

$validJSON = '{"foo": "bar"}';
$notJSON = [0, 1, true, false];
$notValidJSON = ['"foo": "bar"'];

foreach ($notJSON as $value) {
Assert::exception(function () use ($value) {
Assert::json($value);
}, Tester\AssertException::class, '%a% should be string');
}

foreach ($notValidJSON as $value) {
Assert::exception(function () use ($value) {
Assert::json($value);
}, Tester\AssertException::class, '%a% should be valid JSON');
}

Assert::json($validJSON);