Browse Source

More testing; Handler 100% coverage

2.1.0
Dustin Wilson 2 years ago
parent
commit
fc960e3561
  1. 3
      composer.json
  2. 44
      composer.lock
  3. 7
      lib/Catcher.php
  4. 2
      lib/Catcher/Error.php
  5. 6
      lib/Catcher/HTMLHandler.php
  6. 80
      lib/Catcher/Handler.php
  7. 5
      lib/Catcher/JSONHandler.php
  8. 39
      tests/cases/TestCatcher.php
  9. 123
      tests/cases/TestHandler.php
  10. 59
      tests/cases/TestPlainTextHandler.php
  11. 19
      tests/docroot/testDispatch.php
  12. 1
      tests/phpunit.dist.xml

3
composer.json

@ -25,6 +25,7 @@
"mensbeam/html-dom": "^1.0",
"phpunit/phpunit": "^9.5",
"nikic/php-parser": "^4.15",
"eloquent/phony-phpunit": "^7.1"
"eloquent/phony-phpunit": "^7.1",
"giberti/phpunit-local-server": "^3.0"
}
}

44
composer.lock

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "276b1ae35ddd23e0ebc736f079ecf98f",
"content-hash": "426fded11213b92f432b1083735aeae4",
"packages": [
{
"name": "psr/log",
@ -275,6 +275,48 @@
},
"time": "2020-12-21T09:36:47+00:00"
},
{
"name": "giberti/phpunit-local-server",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/giberti/phpunit-local-server.git",
"reference": "61d07cb083fe5a354d95d84f357c63ba30d74862"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/giberti/phpunit-local-server/zipball/61d07cb083fe5a354d95d84f357c63ba30d74862",
"reference": "61d07cb083fe5a354d95d84f357c63ba30d74862",
"shasum": ""
},
"require": {
"ext-posix": "*",
"php": "^7.3 || ^8.0",
"phpunit/phpunit": "^8.0 || ^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Giberti\\PHPUnitLocalServer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Erik Giberti",
"homepage": "https://github.com/giberti"
}
],
"description": "A local HTTP server for PHPUnit tests",
"support": {
"issues": "https://github.com/giberti/phpunit-local-server/issues",
"source": "https://github.com/giberti/phpunit-local-server/tree/v3.0.0"
},
"time": "2022-05-01T01:31:15+00:00"
},
{
"name": "mensbeam/framework",
"version": "1.0.5",

7
lib/Catcher.php

@ -8,6 +8,7 @@
declare(strict_types=1);
namespace MensBeam\Foundation;
use MensBeam\Foundation\Catcher\{
Error,
Handler,
PlainTextHandler,
ThrowableController,
@ -218,6 +219,10 @@ class Catcher {
if (in_array($error['type'], [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_CORE_WARNING, \E_COMPILE_ERROR, \E_COMPILE_WARNING ])) {
$this->handleError($error['type'], $error['message'], $error['file'], $error['line']);
}
} else {
foreach ($this->handlers as $h) {
$h->dispatch();
}
}
}
@ -225,7 +230,7 @@ class Catcher {
/** Exists so the method may be replaced when mocking in tests */
protected function exit(int $status): void {
// This won't be shown as executed in code coverage
exit($status); //@codeCoverageIgnore
exit($status); // @codeCoverageIgnore
}
/** Exists so the method may be replaced when mocking in tests */

2
lib/Error.php → lib/Catcher/Error.php

@ -6,7 +6,7 @@
*/
declare(strict_types=1);
namespace MensBeam\Foundation;
namespace MensBeam\Foundation\Catcher;
class Error extends \Error {
public function __construct(string $message = '', int $code = 0, string $file = '', int $line = 0, ?\Throwable $previous = null) {

6
lib/Catcher/HTMLHandler.php

@ -76,15 +76,19 @@ class HTMLHandler extends Handler {
protected function dispatchCallback(): void {
$body = $this->_document->getElementsByTagName('body')[0];
$allSilent = true;
foreach ($this->outputBuffer as $o) {
if ($o->outputCode & self::SILENT) {
continue;
}
$allSilent = false;
$body->appendChild($o->output);
}
$this->print($this->_document->saveHTML());
if (!$allSilent) {
$this->print($this->_document->saveHTML());
}
}
protected function handleCallback(ThrowableController $controller): HandlerOutput {

80
lib/Catcher/Handler.php

@ -32,12 +32,6 @@ abstract class Handler {
* @var HandlerOutput[]
*/
protected array $outputBuffer = [];
/**
* Array of option property names; used when overloading
*
* @var string[]
*/
protected array $optionNames;
/** The number of backtrace frames in which to print arguments; defaults to 5 */
protected int $_backtraceArgFrameLimit = 5;
@ -55,12 +49,14 @@ abstract class Handler {
* is true the handler will output nothing
*/
protected bool $_forceOutputNow = false;
/** The HTTP code to be sent */
/** The HTTP code to be sent; possible values: 200, 400-599 */
protected int $_httpCode = 500;
/** If true the handler will output backtraces; defaults to false */
protected bool $_outputBacktrace = false;
/** If true the handler will output previous throwables; defaults to true */
protected bool $_outputPrevious = true;
/** When the SAPI is cli output errors to stderr; defaults to true */
protected bool $_outputToStderr = true;
/** If true the handler will be silent and won't output */
protected bool $_silent = false;
@ -70,52 +66,15 @@ abstract class Handler {
public function __construct(array $options = []) {
foreach ($options as $key => $value) {
$key = "_$key";
if ($key === '_httpCode' && is_int($value) && ($value < 400 || $value >= 600)) {
throw new \InvalidArgumentException('Option "httpCode" can only be an integer between 400 and 599');
if ($key === '_httpCode' && is_int($value) && $value !== 200 && max(400, min($value, 600)) !== $value) {
throw new \InvalidArgumentException('Option "httpCode" can only be an integer of 200 or 400-599');
}
$this->$key = $value;
}
$properties = (new \ReflectionClass($this))->getProperties(\ReflectionProperty::IS_PROTECTED);
$this->optionNames = [];
foreach ($properties as $p) {
$name = $p->getName();
if ($name[0] === '_') {
$this->optionNames[] = $name;
}
}
}
/*protected function __construct(ThrowableController $controller, array $data = []) {
$this->controller = $controller;
$this->data = $data;
if (!self::$_silent) {
$this->outputCode = (!self::$_forceOutputNow) ? self::OUTPUT : self::OUTPUT_NOW;
} else {
$this->outputCode = self::SILENT;
}
if ($forceContinue) {
$this->outputCode |= self::CONTINUE;
return;
} elseif ($forceExit) {
$this->outputCode |= self::EXIT;
return;
}
if ($this->outputCode !== self::SILENT) {
$throwable = $controller->getThrowable();
if ($throwable instanceof \Exception || in_array($throwable->getCode(), [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_COMPILE_ERROR, \E_USER_ERROR ])) {
$this->outputCode |= self::EXIT;
return;
}
}
$this->outputCode |= ($this->outputCode === self::SILENT) ? self::CONTINUE : self::BREAK;
return;
}*/
public function dispatch(): void {
@ -125,11 +84,15 @@ abstract class Handler {
// Send the headers if possible and necessary
if (isset($_SERVER['REQUEST_URI'])) {
// Can't figure out a way to test coverage here, but the logic is tested thoroughly
// when running tests in HTTP
// @codeCoverageIgnoreStart
if (!headers_sent()) {
header_remove('location');
header(sprintf('Content-type: %s; charset=%s', static::CONTENT_TYPE, $this->_charset));
}
http_response_code($this->_httpCode);
// @codeCoverageIgnoreEnd
}
$this->dispatchCallback();
@ -145,11 +108,6 @@ abstract class Handler {
abstract protected function dispatchCallback(): void;
/*protected function createOutput(mixed $output): HandlerOutput {
return new HandlerOutput($this->getControlCode(), $this->getOutputCode(), $output);
}*/
protected function getControlCode(): int {
$code = self::CONTINUE;
if ($this->_forceBreak) {
@ -177,24 +135,12 @@ abstract class Handler {
abstract protected function handleCallback(ThrowableController $controller): HandlerOutput;
protected function print(string $string): void {
if (strtolower(\PHP_SAPI) === 'cli') {
fprintf(\STDERR, "$string\n");
$string = "$string\n";
if (strtolower(\PHP_SAPI) === 'cli' && $this->_outputToStderr) {
// Can't test this in code coverage without printing errors to STDERR
fwrite(\STDERR, $string); // @codeCoverageIgnore
} else {
echo $string;
}
}
/*public function __get(string $name): mixed {
$name = "_$name";
if (in_array($name, $this->optionNames)) {
return $this->$name;
}
}
public function __set(string $name, mixed $value): void {
$name = "_$name";
if (in_array($name, $this->optionNames)) {
$this->$name = $value;
}
}*/
}

5
lib/Catcher/JSONHandler.php

@ -32,8 +32,11 @@ class JSONHandler extends Handler {
$errors[] = $o->output;
}
if (count($errors) === 0) {
return;
}
$output['errors'] = $errors;
$this->print(json_encode($output, \JSON_PARTIAL_OUTPUT_ON_ERROR));
}

39
tests/cases/TestCatcher.php

@ -6,15 +6,13 @@
*/
declare(strict_types=1);
namespace MensBeam\Foundation\Catcher\TestCase;
use MensBeam\Foundation\{
Catcher,
Error
};
namespace MensBeam\Foundation\Catcher\Test;
use MensBeam\Foundation\Catcher;
use MensBeam\Foundation\Catcher\{
PlainTextHandler,
Error,
HTMLHandler,
JSONHandler
JSONHandler,
PlainTextHandler
};
use Eloquent\Phony\Phpunit\Phony;
@ -60,7 +58,7 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
* @covers \MensBeam\Foundation\Catcher::pushHandler
* @covers \MensBeam\Foundation\Catcher::register
* @covers \MensBeam\Foundation\Catcher::unregister
* @covers \MensBeam\Foundation\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
@ -296,7 +294,7 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
* @covers \MensBeam\Foundation\Catcher::pushHandler
* @covers \MensBeam\Foundation\Catcher::register
* @covers \MensBeam\Foundation\Catcher::unregister
* @covers \MensBeam\Foundation\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
@ -339,6 +337,25 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
error_reporting($er);
$c->unregister();
$h1 = Phony::partialMock(PlainTextHandler::class, [ [ 'silent' => true ] ]);
$h2 = Phony::partialMock(HTMLHandler::class, [ [ 'silent' => true ] ]);
$h3 = Phony::partialMock(JSONHandler::class, [ [ 'silent' => true ] ]);
$h = Phony::partialMock(Catcher::class, [
$h1->get(),
$h2->get(),
$h3->get()
]);
$c = $h->get();
trigger_error('Ook!', \E_USER_ERROR);
$h1->dispatch->called();
$h2->dispatch->called();
$h3->dispatch->called();
$c->unregister();
}
/**
@ -350,7 +367,7 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
* @covers \MensBeam\Foundation\Catcher::pushHandler
* @covers \MensBeam\Foundation\Catcher::register
* @covers \MensBeam\Foundation\Catcher::unregister
* @covers \MensBeam\Foundation\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
@ -394,7 +411,7 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
* @covers \MensBeam\Foundation\Catcher::pushHandler
* @covers \MensBeam\Foundation\Catcher::register
* @covers \MensBeam\Foundation\Catcher::unregister
* @covers \MensBeam\Foundation\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode

123
tests/cases/TestHandler.php

@ -0,0 +1,123 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Foundation\Catcher\Test;
use MensBeam\Foundation\Catcher;
use MensBeam\Foundation\Catcher\{
Error,
HTMLHandler,
JSONHandler,
PlainTextHandler
};
class TestHandler extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
*/
public function testMethod___construct__exception(): void {
$this->expectException(\InvalidArgumentException::class);
$c = new Catcher(new PlainTextHandler([ 'httpCode' => 42 ]));
}
/**
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
*
* @covers \MensBeam\Foundation\Catcher::__construct
* @covers \MensBeam\Foundation\Catcher::handleError
* @covers \MensBeam\Foundation\Catcher::handleThrowable
* @covers \MensBeam\Foundation\Catcher::pushHandler
* @covers \MensBeam\Foundation\Catcher::register
* @covers \MensBeam\Foundation\Catcher::unregister
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
*/
public function testMethod__getControlCode(): void {
// Just need to test forceExit for coverage purposes
$c = new Catcher(new PlainTextHandler([ 'forceExit' => true, 'silent' => true ]));
trigger_error('Ook!', \E_USER_ERROR);
$this->assertEquals(\E_USER_ERROR, $c->getLastThrowable()->getCode());
$c->unregister();
}
/**
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
*
* @covers \MensBeam\Foundation\Catcher::__construct
* @covers \MensBeam\Foundation\Catcher::handleError
* @covers \MensBeam\Foundation\Catcher::handleThrowable
* @covers \MensBeam\Foundation\Catcher::pushHandler
* @covers \MensBeam\Foundation\Catcher::register
* @covers \MensBeam\Foundation\Catcher::unregister
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
*/
public function testMethod__getOutputCode(): void {
// Just need to test forceOutputNow for coverage purposes
$c = new Catcher(new PlainTextHandler([ 'forceOutputNow' => true, 'silent' => true ]));
trigger_error('Ook!', \E_USER_ERROR);
$this->assertEquals(\E_USER_ERROR, $c->getLastThrowable()->getCode());
$c->unregister();
}
/**
* @covers \MensBeam\Foundation\Catcher\Handler::print
*
* @covers \MensBeam\Foundation\Catcher::__construct
* @covers \MensBeam\Foundation\Catcher::handleError
* @covers \MensBeam\Foundation\Catcher::handleThrowable
* @covers \MensBeam\Foundation\Catcher::pushHandler
* @covers \MensBeam\Foundation\Catcher::register
* @covers \MensBeam\Foundation\Catcher::unregister
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
*/
public function testMethod__print(): void {
// Just need to test forceOutputNow for coverage purposes
$c = new Catcher(new PlainTextHandler([ 'forceOutputNow' => true, 'outputToStderr' => false ]));
ob_start();
trigger_error('Ook!', \E_USER_NOTICE);
ob_end_clean();
$this->assertEquals(\E_USER_NOTICE, $c->getLastThrowable()->getCode());
$c->unregister();
}
}

59
tests/cases/TestPlainTextHandler.php

@ -0,0 +1,59 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Foundation\Catcher\Test;
use MensBeam\Foundation\Catcher;
use MensBeam\Foundation\Catcher\{
Error,
HTMLHandler,
JSONHandler,
PlainTextHandler
};
class TestPlainTextHandler extends \Giberti\PHPUnitLocalServer\LocalServerTestCase {
public static function setupBeforeClass(): void {
static::createServerWithDocroot('./tests/docroot');
}
/**
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
*/
public function testMethod___construct__exception(): void {
$this->expectException(\InvalidArgumentException::class);
$c = new Catcher(new PlainTextHandler([ 'httpCode' => 42 ]));
}
/**
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
*
* @covers \MensBeam\Foundation\Catcher::__construct
* @covers \MensBeam\Foundation\Catcher::handleError
* @covers \MensBeam\Foundation\Catcher::handleThrowable
* @covers \MensBeam\Foundation\Catcher::pushHandler
* @covers \MensBeam\Foundation\Catcher::register
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\Handler::print
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
*/
public function testMethod__dispatch(): void {
$url = $this->getLocalServerUrl() . '/testDispatch.php';
$this->assertEquals(1, preg_match('/^\[[0-9:]+\]\s+Warning/', file_get_contents($url)));
}
}

19
tests/docroot/testDispatch.php

@ -0,0 +1,19 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Foundation\Catcher\Test;
use MensBeam\Foundation\Catcher;
use MensBeam\Foundation\Catcher\{
Error,
PlainTextHandler
};
require_once('../../vendor/autoload.php');
Catcher::$preventExit = true;
$c = new Catcher(new PlainTextHandler([ 'httpCode' => 200 ]));
trigger_error('Ook!', \E_USER_WARNING);

1
tests/phpunit.dist.xml

@ -18,6 +18,7 @@
<testsuites>
<testsuite name="Main">
<file>cases/TestCatcher.php</file>
<file>cases/TestHandler.php</file>
</testsuite>
</testsuites>
</phpunit>
Loading…
Cancel
Save