From 3af91342e16609536055a0bcf4078d11c403e140 Mon Sep 17 00:00:00 2001 From: Dustin Wilson Date: Sun, 2 Apr 2023 00:12:08 -0500 Subject: [PATCH] Added tests for Logger\Handler, Logger\Level --- composer.json | 5 + composer.lock | 18 ++-- lib/Logger/Handler.php | 71 ++++++++----- tests/cases/TestHandler.php | 151 ++++++++++++++++++++++++++++ tests/cases/TestLevel.php | 37 +++++++ tests/cases/TestLogger.php | 6 +- tests/lib/Error.php | 11 ++ tests/lib/ErrorHandlingTestCase.php | 31 ++++++ 8 files changed, 294 insertions(+), 36 deletions(-) create mode 100644 tests/cases/TestHandler.php create mode 100644 tests/cases/TestLevel.php create mode 100644 tests/lib/Error.php create mode 100644 tests/lib/ErrorHandlingTestCase.php diff --git a/composer.json b/composer.json index bfb5b92..7a46b25 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,11 @@ "MensBeam\\": "lib/" } }, + "autoload-dev": { + "psr-4": { + "MensBeam\\Logger\\Test\\": "tests/lib/" + } + }, "authors": [ { "name": "Dustin Wilson", diff --git a/composer.lock b/composer.lock index 713ec80..f3324e9 100644 --- a/composer.lock +++ b/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": "2bcb4ee6bff85959554f876637b58d0e", + "content-hash": "72b6aa958e055d50217e8d98aa9f070a", "packages": [ { "name": "mensbeam/filesystem", @@ -864,16 +864,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.0.18", + "version": "10.0.19", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "582563ed2edc62d1455cdbe00ea49fe09428eef3" + "reference": "20c23e85c86e5c06d63538ba464e8054f4744e62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/582563ed2edc62d1455cdbe00ea49fe09428eef3", - "reference": "582563ed2edc62d1455cdbe00ea49fe09428eef3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/20c23e85c86e5c06d63538ba464e8054f4744e62", + "reference": "20c23e85c86e5c06d63538ba464e8054f4744e62", "shasum": "" }, "require": { @@ -945,7 +945,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.0.18" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.0.19" }, "funding": [ { @@ -961,7 +961,7 @@ "type": "tidelift" } ], - "time": "2023-03-22T06:15:31+00:00" + "time": "2023-03-27T11:46:33+00:00" }, { "name": "sebastian/cli-parser", @@ -1931,6 +1931,8 @@ "platform": { "php": ">=8.1" }, - "platform-dev": [], + "platform-dev": { + "ext-pcov": "*" + }, "plugin-api-version": "2.3.0" } diff --git a/lib/Logger/Handler.php b/lib/Logger/Handler.php index 7ec8d44..ed7ac46 100644 --- a/lib/Logger/Handler.php +++ b/lib/Logger/Handler.php @@ -19,37 +19,24 @@ abstract class Handler { public function __construct(array $levels = [ 0, 1, 2, 3, 4, 5, 6, 7 ], array $options = []) { - $levelsCount = count($levels); - if ($levelsCount > 8) { - throw new InvalidArgumentException(sprintf('Argument #%s ($levels) cannot have more than 8 values', $this->getParamPosition())); - } - if (count($levels) === 0) { - throw new InvalidArgumentException(sprintf('Argument #%s ($levels) must not be empty', $this->getParamPosition())); - } - - $levels = array_unique($levels, \SORT_NUMERIC); - foreach ($levels as $k => $v) { - if (!is_int($v)) { - $type = gettype($v); - $type = ($type === 'object') ? $v::class : $type; - throw new InvalidArgumentException(sprintf('Value #%s of argument #%s ($levels) must be of type int, %s given', $k, $this->getParamPosition(), $type)); - } - - if ($v < 0 || $v > 7) { - throw new RangeException(sprintf('Argument #%s ($levels) cannot be %s; it is not in the range 0 - 7', $this->getParamPosition(), $v)); - } - } - - $this->levels = array_values($levels); + $this->levels = $this->verifyLevels($levels); + $class = get_class($this); foreach ($options as $key => $value) { - $key = "_$key"; - $this->$key = $value; + $name = "_$key"; + if (!property_exists($class, $name)) { + trigger_error(sprintf('Undefined option in %s: %s', $class, $key), \E_USER_WARNING); + continue; + } + $this->$name = $value; } } + public function getLevels(): array { + return $this->levels; + } public function getOption(string $name): mixed { $class = get_class($this); @@ -62,10 +49,15 @@ abstract class Handler { return $this->$name; } + public function setLevels(int ...$levels): void { + $this->levels = $this->verifyLevels($levels, false); + } + public function setOption(string $name, mixed $value): void { $class = get_class($this); if (!property_exists($class, "_$name")) { trigger_error(sprintf('Undefined option in %s: %s', $class, $name), \E_USER_WARNING); + return; } $name = "_$name"; @@ -88,6 +80,35 @@ abstract class Handler { abstract protected function invokeCallback(string $datetime, int $level, string $channel, string $message, array $context = []): void; + protected function verifyLevels(array $levels, bool $constructor = true): array { + $levelsCount = count($levels); + if (count($levels) === 0) { + throw new InvalidArgumentException(sprintf('Argument #%s ($levels) must not be empty', ($constructor) ? $this->getParamPosition() : 1)); + } + + foreach ($levels as $k => $v) { + if ($v instanceof Level) { + $levels[$k] = $v = $v->value; + } + + if (!is_int($v)) { + $type = gettype($v); + $type = ($type === 'object') ? $v::class : $type; + $levelClassName = Level::class; + throw new InvalidArgumentException(sprintf('Value #%s of argument #%s ($levels) must be of type int|%s, %s given', $k + 1, ($constructor) ? $this->getParamPosition() : 1, $levelClassName, $type)); + } + + if ($v < 0 || $v > 7) { + throw new RangeException(sprintf('Value #%s of argument #%s ($levels) cannot be %s; it is not in the range 0 - 7', $k + 1, ($constructor) ? $this->getParamPosition() : 1, $v)); + } + } + + $levels = array_unique($levels, \SORT_NUMERIC); + sort($levels, \SORT_NUMERIC); + return array_values($levels); + } + + private function getParamPosition(): int { $params = (new \ReflectionClass(get_called_class()))->getConstructor()->getParameters(); foreach ($params as $k => $p) { @@ -96,6 +117,6 @@ abstract class Handler { } } - return -1; + return -1; // @codeCoverageIgnore } } \ No newline at end of file diff --git a/tests/cases/TestHandler.php b/tests/cases/TestHandler.php new file mode 100644 index 0000000..d8762d8 --- /dev/null +++ b/tests/cases/TestHandler.php @@ -0,0 +1,151 @@ +assertSame([ 0, 1, 2, 3, 4, 5, 6, 7 ], $h->getLevels()); + } + + public function testOptions(): void { + $h = new StreamHandler(options: [ + 'bubbles' => false, + 'datetimeFormat' => 'Y-m-d\TH:i:sP' + ]); + $this->assertFalse($h->getOption('bubbles')); + $this->assertSame('Y-m-d\TH:i:sP', $h->getOption('datetimeFormat')); + $h->setOption('bubbles', true); + $h->setOption('datetimeFormat', 'Y-m-d'); + $this->assertTrue($h->getOption('bubbles')); + $this->assertSame('Y-m-d', $h->getOption('datetimeFormat')); + } + + /** @dataProvider provideFatalErrorTests */ + public function testFatalErrors(string $throwableClassName, int $code, string $message, \Closure $closure): void { + $this->expectException($throwableClassName); + $this->expectExceptionMessage($message); + if ($throwableClassName === Error::class) { + $this->expectExceptionCode($code); + } + + $closure(new StreamHandler()); + } + + /** @dataProvider provideNonFatalErrorTests */ + public function testNonFatalErrors(int $code, string $message, \Closure $closure): void { + $closure(new StreamHandler()); + $this->assertEquals($code, $this->lastError?->getCode()); + $this->assertSame($message, $this->lastError?->getMessage()); + } + + + public function testInvocation(): void { + $s = fopen('php://memory', 'r+'); + // Test setting the datetimeFormat and messageTransform options, showing + // a very simple example of using sprintf for interpolation. + $l = new Logger('ook', new StreamHandler(stream: $s, options: [ + 'datetimeFormat' => 'Y-m-d', + 'messageTransform' => function (string $message, array $context): string { + return vsprintf($message, $context); + } + ])); + $l->error('Ook! %s', [ 'Eek!' ]); + rewind($s); + $o = stream_get_contents($s); + $this->assertEquals(1, preg_match('/^' . (new \DateTimeImmutable())->format('Y-m-d') . ' ook ERROR Ook! Eek!\n/', $o)); + } + + + public static function provideFatalErrorTests(): iterable { + $iterable = [ + [ + InvalidArgumentException::class, + 0, + 'Argument #1 ($levels) must not be empty', + function (Handler $h): void { + $h->setLevels(); + } + ], + [ + InvalidArgumentException::class, + 0, + 'Value #5 of argument #2 ($levels) must be of type int|MensBeam\Logger\Level, string given', + function (Handler $h): void { + new StreamHandler(levels: [ 0, 1, 2, 3, '4', 5, 6, 7 ]); + } + ], + [ + RangeException::class, + 0, + 'Value #2 of argument #1 ($levels) cannot be 42; it is not in the range 0 - 7', + function (Handler $h): void { + $h->setLevels(0, 42); + } + ] + ]; + + foreach ($iterable as $i) { + yield $i; + } + } + + public static function provideNonFatalErrorTests(): iterable { + $iterable = [ + [ + \E_USER_WARNING, + 'Undefined option in ' . StreamHandler::class . ': ook', + function (Handler $h): void { + $h = new StreamHandler(options: [ 'ook' => 'eek' ]); + } + ], + [ + \E_USER_WARNING, + 'Undefined option in ' . StreamHandler::class . ': ook', + function (Handler $h): void { + $ook = $h->getOption('ook'); + } + ], + [ + \E_USER_WARNING, + 'Undefined option in ' . StreamHandler::class . ': ook', + function (Handler $h): void { + $ook = $h->setOption('ook', 'eek'); + } + ] + ]; + + foreach ($iterable as $i) { + yield $i; + } + } +} \ No newline at end of file diff --git a/tests/cases/TestLevel.php b/tests/cases/TestLevel.php new file mode 100644 index 0000000..9f4eccd --- /dev/null +++ b/tests/cases/TestLevel.php @@ -0,0 +1,37 @@ +assertSame($level, Level::fromPSR3($PSR3Level)); + $this->assertSame($PSR3Level, $level->toPSR3()); + } + + public static function provideConversionsTests(): iterable { + foreach ([ + [ LogLevel::EMERGENCY, Level::Emergency ], + [ LogLevel::ALERT, Level::Alert ], + [ LogLevel::CRITICAL, Level::Critical ], + [ LogLevel::ERROR, Level::Error ], + [ LogLevel::WARNING, Level::Warning ], + [ LogLevel::NOTICE, Level::Notice ], + [ LogLevel::INFO, Level::Info ], + [ LogLevel::DEBUG, Level::Debug ] + ] as $l) { + yield $l; + } + } +} \ No newline at end of file diff --git a/tests/cases/TestLogger.php b/tests/cases/TestLogger.php index dcb548c..0e513d5 100644 --- a/tests/cases/TestLogger.php +++ b/tests/cases/TestLogger.php @@ -52,8 +52,8 @@ class TestLogger extends \PHPUnit\Framework\TestCase { $this->assertEquals(1, preg_match('/^' . (new \DateTimeImmutable())->format('M d') . ' \d{2}:\d{2}:\d{2} ook ' . strtoupper($levelName) . ' Ook!\n/', $o)); } - /** @dataProvider provideErrorTests */ - public function testErrors(string $throwableClassName, \Closure $closure): void { + /** @dataProvider provideFatalErrorTests */ + public function testFatalErrors(string $throwableClassName, \Closure $closure): void { $this->expectException($throwableClassName); $closure(new Logger()); } @@ -65,7 +65,7 @@ class TestLogger extends \PHPUnit\Framework\TestCase { } } - public static function provideErrorTests(): iterable { + public static function provideFatalErrorTests(): iterable { $iterable = [ [ InvalidArgumentException::class, diff --git a/tests/lib/Error.php b/tests/lib/Error.php new file mode 100644 index 0000000..03352c4 --- /dev/null +++ b/tests/lib/Error.php @@ -0,0 +1,11 @@ +lastError = $e; + if (in_array($code, [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_COMPILE_ERROR, \E_USER_ERROR, \E_RECOVERABLE_ERROR ])) { + throw $e; + } + } +} \ No newline at end of file