From a7d0e6a260fab66abdb1c15ec91da3c193a067fd Mon Sep 17 00:00:00 2001 From: Dustin Wilson Date: Mon, 3 Apr 2023 22:14:44 -0500 Subject: [PATCH] 100% coverage --- composer.json | 3 +- composer.lock | 53 ++++++++++++++++- lib/Logger.php | 13 ++++- lib/Logger/Handler.php | 8 ++- lib/Logger/StreamHandler.php | 14 +++-- tests/Bootstrap.php | 2 +- tests/cases/TestHandler.php | 11 ++++ tests/cases/TestLogger.php | 17 +++++- tests/cases/TestStreamHandler.php | 95 +++++++++++++++++++++++++++++++ 9 files changed, 201 insertions(+), 15 deletions(-) create mode 100644 tests/cases/TestStreamHandler.php diff --git a/composer.json b/composer.json index 7a46b25..773b717 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "require-dev": { "ext-pcov": "*", "docopt/docopt": "^1.0", - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.0", + "mikey179/vfsstream": "^1.6" } } diff --git a/composer.lock b/composer.lock index f3324e9..90982e9 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": "72b6aa958e055d50217e8d98aa9f070a", + "content-hash": "3d02f5002c6b62190718aa8c70a8acd1", "packages": [ { "name": "mensbeam/filesystem", @@ -374,6 +374,57 @@ }, "time": "2023-03-22T12:31:48+00:00" }, + { + "name": "mikey179/vfsstream", + "version": "v1.6.11", + "source": { + "type": "git", + "url": "https://github.com/bovigo/vfsStream.git", + "reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f", + "reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.5|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Frank Kleine", + "homepage": "http://frankkleine.de/", + "role": "Developer" + } + ], + "description": "Virtual file system to mock the real file system in unit tests.", + "homepage": "http://vfs.bovigo.org/", + "support": { + "issues": "https://github.com/bovigo/vfsStream/issues", + "source": "https://github.com/bovigo/vfsStream/tree/master", + "wiki": "https://github.com/bovigo/vfsStream/wiki" + }, + "time": "2022-02-23T02:02:42+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.11.1", diff --git a/lib/Logger.php b/lib/Logger.php index b5f5091..05baa9b 100644 --- a/lib/Logger.php +++ b/lib/Logger.php @@ -40,8 +40,13 @@ class Logger implements LoggerInterface { public function __construct(?string $channel = null, Handler ...$handlers) { $this->setChannel($channel); + // Set some handlers if no handlers are set, printing lower levels to stderr, + // higher to stdout. if (count($handlers) === 0) { - $handlers[] = new StreamHandler('php://stdout'); + $handlers = [ + new StreamHandler(stream: 'php://stderr', levels: [ 0, 1, 2, 3 ]), + new StreamHandler(stream: 'php://stdout', levels: [ 4, 5, 6, 7 ]) + ]; } $this->handlers = $handlers; @@ -116,12 +121,16 @@ class Logger implements LoggerInterface { * @throws \MensBeam\Logger\InvalidArgumentException */ public function log($level, string|\Stringable $message, array $context = []): void { + if ($level instanceof Level) { + $level = $level->value; + } + // Because the interface won't allow limiting $level to just int|string this is // necessary. if (!is_int($level) && !is_string($level)) { $type = gettype($level); $type = ($type === 'object') ? $level::class : $type; - throw new InvalidArgumentException(sprintf('Argument #1 ($level) must be of type int|string, %s given', $type)); + throw new InvalidArgumentException(sprintf('Argument #1 ($level) must be of type int|%s|string, %s given', Level::class, $type)); } // If the level is a string convert it to a RFC5424 level integer. diff --git a/lib/Logger/Handler.php b/lib/Logger/Handler.php index ed7ac46..2726e12 100644 --- a/lib/Logger/Handler.php +++ b/lib/Logger/Handler.php @@ -64,7 +64,11 @@ abstract class Handler { $this->$name = $value; } - public function __invoke(int $level, string $channel, string $message, array $context = []): void { + public function __invoke(int $level, ?string $channel, string $message, array $context = []): void { + if (!in_array($level, $this->levels)) { + return; + } + $datetime = \DateTimeImmutable::createFromFormat('U.u', (string)microtime(true))->format($this->_datetimeFormat); $message = trim($message); @@ -73,7 +77,7 @@ abstract class Handler { $message = $t($message, $context); } - $this->invokeCallback($datetime, $level, $channel, $message, $context); + $this->invokeCallback($datetime, $level, $channel ?? '', $message, $context); } diff --git a/lib/Logger/StreamHandler.php b/lib/Logger/StreamHandler.php index 9f8aa36..5dabf6c 100644 --- a/lib/Logger/StreamHandler.php +++ b/lib/Logger/StreamHandler.php @@ -28,7 +28,7 @@ class StreamHandler extends Handler { // The memory limit is in a shorthand format // (https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes), so we // need it as a integer representation in bytes. - if (preg_match('/^\s*(?\d+)(?:\.\d+)?\s*(?[gkm])\s*$/i', strtolower(ini_get('memory_limit')), $matches) === 1) { + if (preg_match('/^\s*(?\d+)(?:\.\d+)?\s*(?[gkm])\s*$/i', ini_get('memory_limit'), $matches) === 1) { $num = (int)$matches['num']; switch ($matches['unit'] ?? '') { case 'g': $num *= 1024; @@ -61,6 +61,12 @@ class StreamHandler extends Handler { throw new InvalidArgumentException(sprintf('Argument #1 ($stream) must be of type resource|string, %s given', $type)); } + + // Bad dog, no biscuit! + if (($options['entryFormat'] ?? null) === '') { + $options['entryFormat'] = $this->_entryFormat; + } + parent::__construct($levels, $options); } @@ -73,11 +79,7 @@ class StreamHandler extends Handler { protected function invokeCallback(string $datetime, int $level, string $channel, string $message, array $context = []): void { - if (!in_array($level, $this->levels)) { - return; - } - - // Do output formatting here. + // Do entry formatting here. $output = trim(preg_replace_callback('/%([a-z_]+)%/', function($m) use ($datetime, $level, $channel, $message) { switch ($m[1]) { case 'channel': return $channel; diff --git a/tests/Bootstrap.php b/tests/Bootstrap.php index 61d823d..36bb5e6 100644 --- a/tests/Bootstrap.php +++ b/tests/Bootstrap.php @@ -2,7 +2,7 @@ declare(strict_types=1); namespace MensBeam\Logger\Test; -ini_set('memory_limit', '-1'); +ini_set('memory_limit', '2G'); ini_set('zend.assertions', '1'); ini_set('assert.exception', 'true'); error_reporting(\E_ALL); diff --git a/tests/cases/TestHandler.php b/tests/cases/TestHandler.php index d8762d8..1d619ba 100644 --- a/tests/cases/TestHandler.php +++ b/tests/cases/TestHandler.php @@ -83,6 +83,17 @@ class TestHandler extends ErrorHandlingTestCase { rewind($s); $o = stream_get_contents($s); $this->assertEquals(1, preg_match('/^' . (new \DateTimeImmutable())->format('Y-m-d') . ' ook ERROR Ook! Eek!\n/', $o)); + fclose($s); + } + + public function testInvocationWithUnsupportedLevel(): void { + $s = fopen('php://memory', 'r+'); + $l = new Logger('ook', new StreamHandler(stream: $s, levels: [ 0 ])); + $l->error('ook'); + rewind($s); + $o = stream_get_contents($s); + $this->assertSame('', $o); + fclose($s); } diff --git a/tests/cases/TestLogger.php b/tests/cases/TestLogger.php index 0e513d5..ebaaee6 100644 --- a/tests/cases/TestLogger.php +++ b/tests/cases/TestLogger.php @@ -10,6 +10,7 @@ namespace MensBeam\Logger\Test; use MensBeam\Logger; use MensBeam\Logger\{ InvalidArgumentException, + Level, StreamHandler }; @@ -24,10 +25,14 @@ class TestLogger extends \PHPUnit\Framework\TestCase { public function testDefaultHandler(): void { $l = new Logger(); $h = $l->getHandlers(); - $this->assertEquals(1, count($h)); + $this->assertEquals(2, count($h)); $this->assertInstanceOf(StreamHandler::class, $h[0]); + $this->assertInstanceOf(StreamHandler::class, $h[1]); $s = $h[0]->getStream(); $this->assertIsString($s); + $this->assertSame('php://stderr', $s); + $s = $h[1]->getStream(); + $this->assertIsString($s); $this->assertSame('php://stdout', $s); } @@ -49,7 +54,15 @@ class TestLogger extends \PHPUnit\Framework\TestCase { $l->$levelName('Ook!'); rewind($s); $o = stream_get_contents($s); - $this->assertEquals(1, preg_match('/^' . (new \DateTimeImmutable())->format('M d') . ' \d{2}:\d{2}:\d{2} ook ' . strtoupper($levelName) . ' Ook!\n/', $o)); + $regex = '/^' . (new \DateTimeImmutable())->format('M d') . ' \d{2}:\d{2}:\d{2} ook ' . strtoupper($levelName) . ' Ook!\n/'; + $this->assertEquals(1, preg_match($regex, $o)); + + // Try it again with Level enum + $l->log(constant(sprintf('%s::%s', Level::class, ucfirst($levelName))), 'Ook!'); + rewind($s); + $o = stream_get_contents($s); + $this->assertEquals(1, preg_match($regex, $o)); + fclose($s); } /** @dataProvider provideFatalErrorTests */ diff --git a/tests/cases/TestStreamHandler.php b/tests/cases/TestStreamHandler.php new file mode 100644 index 0000000..d2cc3d4 --- /dev/null +++ b/tests/cases/TestStreamHandler.php @@ -0,0 +1,95 @@ +format('M d') . ' \d{2}:\d{2}:\d{2} ook ERROR Ook!\nEek!\n/'; + $this->assertEquals(1, preg_match($regex, $closure())); + } + + public function testFatalErrors(): void { + $this->expectException(InvalidArgumentException::class); + new StreamHandler(42); + } + + public function testGetStream(): void { + $h = new StreamHandler('ook'); + $this->assertSame(CWD . '/ook', $h->getStream()); + } + + /** @dataProvider provideEntryFormattingTests */ + public function testEntryFormatting(string $entryFormat, string $regex): void { + $s = fopen('php://memory', 'r+'); + $h = new StreamHandler(stream: $s, options: [ + 'entryFormat' => $entryFormat + ]); + $h(Level::Error->value, 'ook', 'ook'); + rewind($s); + $o = stream_get_contents($s); + $this->assertEquals(1, preg_match($regex, $o)); + fclose($s); + } + + + public static function provideResourceTypesTests(): iterable { + $iterable = [ + function (): string { + $s = fopen('php://memory', 'r+'); + $h = new StreamHandler($s); + $h(Level::Error->value, 'ook', "Ook!\nEek!"); + rewind($s); + $o = stream_get_contents($s); + fclose($s); + return $o; + }, + function (): string { + $f = tempnam(sys_get_temp_dir(), 'logger'); + $h = new StreamHandler("file://$f"); + $h(Level::Error->value, 'ook', "Ook!\nEek!"); + $o = file_get_contents($f); + unlink($f); + return $o; + }, + function (): string { + $v = vfsStream::setup('ook', 0777, [ 'ook.log' => '' ]); + $f = $v->url() . '/ook.log'; + $h = new StreamHandler($f); + $h(Level::Error->value, 'ook', "Ook!\nEek!"); + return file_get_contents($f); + } + ]; + + foreach ($iterable as $i) { + yield [ $i ]; + } + } + + public static function provideEntryFormattingTests(): iterable { + $iterable = [ + [ '%ook%', '/\n/' ], + [ '%channel% %channel% %channel% %channel% %level% %level_name%', '/ook ook ook ook 3 ERROR\n/' ], + [ '', '/^' . (new \DateTimeImmutable())->format('M d') . ' \d{2}:\d{2}:\d{2} ook ERROR ook\n/' ] + ]; + + foreach ($iterable as $i) { + yield $i; + } + } +} \ No newline at end of file