diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index a1fa841..4375411 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -5,9 +5,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - # There is no code, we don't have to test old PHP versions php-versions: - - 7.4 - 8.0 - 8.1 dependency-levels: @@ -15,7 +13,7 @@ jobs: experimental: - false include: - - php-versions: 7.4 + - php-versions: 8.0 dependency-levels: 'lowest' experimental: false fail-fast: false @@ -30,7 +28,7 @@ jobs: php-version: ${{ matrix.php-versions }} - name: Validating PHP syntax - run: find ./tests/ -type f -name '*.php' -print0 | xargs -0 -L 1 -P 4 -- php -l + run: find ./{src,tests}/ -type f -name '*.php' -print0 | xargs -0 -L 1 -P 4 -- php -l - name: Validate composer.json and composer.lock run: composer validate diff --git a/CHANGELOG.md b/CHANGELOG.md index 1796ba6..88eefb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [1.1.0] - YYYY-MM-DD +### Added +- Import classes from [`psr/log`][] `v1.1.4`, for compatibility with `v2.0.0` and `v3.0.0`. + ## [1.0.0] - 2022-09-07 ### Changed - Compatible with PHP 7.4 and 8.x. Dropped support for lower versions as Test class is marked @requires PHP 7.4 diff --git a/README.md b/README.md index 8209eac..390cf5a 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,9 @@ If the project supports older versions of PHP: The version of `fig/log-test` that is installed after composer dependencies resolution varies with the version of `psr/log`. | psr/log | fig/log-test | | -|---------------|---------------|-------------------------------------------------| -| `^1.1.14` | `1.0.*` | Empty package, classes a provided by `psr/log`. | -| `^2.0\|^3.0` | `^1.1` | Imports test classes removed from `psr/log`. | +|---------------|--------------|-------------------------------------------------| +| `^1.1.14` | `1.0.*` | Empty package, classes a provided by `psr/log`. | +| `^2.0\|^3.0` | `^1.1` | Imports test classes removed from `psr/log`. | [`psr/log`]: https://packagist.org/packages/psr/log [PSR-3]: https://www.php-fig.org/psr/psr-3/ diff --git a/composer.json b/composer.json index 48b64bb..9bd189e 100644 --- a/composer.json +++ b/composer.json @@ -10,13 +10,18 @@ } ], "require": { - "php": "^7.4 | ^8.0", - "psr/log": "^1.1.1" + "php": "^8.0", + "psr/log": "^2.0 | ^3.0" }, "require-dev": { "phpunit/phpunit": "^8.0 | ^9.0", "squizlabs/php_codesniffer": "^3.6" }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, "autoload-dev": { "psr-4": { "Fig\\Log\\Tests\\": "tests" diff --git a/src/Test/DummyTest.php b/src/Test/DummyTest.php new file mode 100644 index 0000000..9638c11 --- /dev/null +++ b/src/Test/DummyTest.php @@ -0,0 +1,18 @@ + ". + * + * Example ->error('Foo') would yield "error Foo". + * + * @return string[] + */ + abstract public function getLogs(); + + public function testImplements() + { + $this->assertInstanceOf(LoggerInterface::class, $this->getLogger()); + } + + /** + * @dataProvider provideLevelsAndMessages + */ + public function testLogsAtAllLevels($level, $message) + { + $logger = $this->getLogger(); + $logger->{$level}($message, ['user' => 'Bob']); + $logger->log($level, $message, ['user' => 'Bob']); + + $expected = [ + $level . ' message of level ' . $level . ' with context: Bob', + $level . ' message of level ' . $level . ' with context: Bob', + ]; + $this->assertEquals($expected, $this->getLogs()); + } + + public function provideLevelsAndMessages() + { + return [ + LogLevel::EMERGENCY => [LogLevel::EMERGENCY, 'message of level emergency with context: {user}'], + LogLevel::ALERT => [LogLevel::ALERT, 'message of level alert with context: {user}'], + LogLevel::CRITICAL => [LogLevel::CRITICAL, 'message of level critical with context: {user}'], + LogLevel::ERROR => [LogLevel::ERROR, 'message of level error with context: {user}'], + LogLevel::WARNING => [LogLevel::WARNING, 'message of level warning with context: {user}'], + LogLevel::NOTICE => [LogLevel::NOTICE, 'message of level notice with context: {user}'], + LogLevel::INFO => [LogLevel::INFO, 'message of level info with context: {user}'], + LogLevel::DEBUG => [LogLevel::DEBUG, 'message of level debug with context: {user}'], + ]; + } + + public function testThrowsOnInvalidLevel() + { + $this->expectException(InvalidArgumentException::class); + $logger = $this->getLogger(); + $logger->log('invalid level', 'Foo'); + } + + public function testContextReplacement() + { + $logger = $this->getLogger(); + $logger->info('{Message {nothing} {user} {foo.bar} a}', ['user' => 'Bob', 'foo.bar' => 'Bar']); + + $expected = ['info {Message {nothing} Bob Bar a}']; + $this->assertEquals($expected, $this->getLogs()); + } + + public function testObjectCastToString() + { + $dummy = $this->createPartialMock(DummyTest::class, ['__toString']); + $dummy->expects($this->once()) + ->method('__toString') + ->will($this->returnValue('DUMMY')); + + $this->getLogger()->warning($dummy); + + $expected = ['warning DUMMY']; + $this->assertEquals($expected, $this->getLogs()); + } + + public function testContextCanContainAnything() + { + $closed = fopen('php://memory', 'r'); + fclose($closed); + + $context = [ + 'bool' => true, + 'null' => null, + 'string' => 'Foo', + 'int' => 0, + 'float' => 0.5, + 'nested' => ['with object' => new DummyTest()], + 'object' => new \DateTime(), + 'resource' => fopen('php://memory', 'r'), + 'closed' => $closed, + ]; + + $this->getLogger()->warning('Crazy context data', $context); + + $expected = ['warning Crazy context data']; + $this->assertEquals($expected, $this->getLogs()); + } + + public function testContextExceptionKeyCanBeExceptionOrOtherValues() + { + $logger = $this->getLogger(); + $logger->warning('Random message', ['exception' => 'oops']); + $logger->critical('Uncaught Exception!', ['exception' => new \LogicException('Fail')]); + + $expected = [ + 'warning Random message', + 'critical Uncaught Exception!' + ]; + $this->assertEquals($expected, $this->getLogs()); + } +} diff --git a/src/Test/TestLogger.php b/src/Test/TestLogger.php new file mode 100644 index 0000000..e3af87f --- /dev/null +++ b/src/Test/TestLogger.php @@ -0,0 +1,328 @@ + $level, + 'message' => $message, + 'context' => $context, + ]; + + $this->recordsByLevel[$record['level']][] = $record; + $this->records[] = $record; + } + + /** + * @param string $level + * @return bool + */ + public function hasRecords($level) + { + return isset($this->recordsByLevel[$level]); + } + + /** + * @param array $record + * @param string $level + * @return bool + */ + public function hasRecord($record, $level) + { + if (is_string($record)) { + $record = ['message' => $record]; + } + + return $this->hasRecordThatPasses(function ($rec) use ($record) { + if ($rec['message'] !== $record['message']) { + return false; + } + if (isset($record['context']) && $rec['context'] !== $record['context']) { + return false; + } + return true; + }, $level); + } + + /** + * @param string $message + * @param string $level + * @return bool + */ + public function hasRecordThatContains($message, $level) + { + return $this->hasRecordThatPasses(fn ($rec) => str_contains($rec['message'], $message), $level); + } + + /** + * @param string $regex + * @param string $level + * @return bool + */ + public function hasRecordThatMatches($regex, $level) + { + return $this->hasRecordThatPasses(fn ($rec) => preg_match($regex, $rec['message']) > 0, $level); + } + + /** + * @param callable $predicate + * @param string $level + * @return bool + */ + public function hasRecordThatPasses(callable $predicate, $level) + { + if (!isset($this->recordsByLevel[$level])) { + return false; + } + foreach ($this->recordsByLevel[$level] as $i => $rec) { + if ($predicate($rec, $i)) { + return true; + } + } + + return false; + } + + /** + * @deprecated Since psr/log-util 1.1 + */ + public function __call($method, $args) + { + @trigger_error(sprintf('Since psr/log-util 1.1: Method "%s" is deprecated and should not be called. Use method "%s" instead.', __FUNCTION__, $method), \E_USER_DEPRECATED); + + if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { + $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; + $level = strtolower($matches[2]); + if (method_exists($this, $genericMethod)) { + $args[] = $level; + return call_user_func_array([$this, $genericMethod], $args); + } + } + throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); + } + + public function hasEmergency($record): bool + { + return $this->hasRecord($record, 'emergency'); + } + + public function hasAlert($record): bool + { + return $this->hasRecord($record, 'alert'); + } + + public function hasCritical($record): bool + { + return $this->hasRecord($record, 'critical'); + } + + public function hasError($record): bool + { + return $this->hasRecord($record, 'error'); + } + + public function hasWarning($record): bool + { + return $this->hasRecord($record, 'warning'); + } + + public function hasNotice($record): bool + { + return $this->hasRecord($record, 'notice'); + } + + public function hasInfo($record): bool + { + return $this->hasRecord($record, 'info'); + } + + public function hasDebug($record): bool + { + return $this->hasRecord($record, 'debug'); + } + + public function hasEmergencyRecords(): bool + { + return $this->hasRecords('emergency'); + } + + public function hasAlertRecords(): bool + { + return $this->hasRecords('alert'); + } + + public function hasCriticalRecords(): bool + { + return $this->hasRecords('critical'); + } + + public function hasErrorRecords(): bool + { + return $this->hasRecords('error'); + } + + public function hasWarningRecords(): bool + { + return $this->hasRecords('warning'); + } + + public function hasNoticeRecords(): bool + { + return $this->hasRecords('notice'); + } + + public function hasInfoRecords(): bool + { + return $this->hasRecords('info'); + } + + public function hasDebugRecords(): bool + { + return $this->hasRecords('debug'); + } + + public function hasEmergencyThatContains($message): bool + { + return $this->hasRecordThatContains($message, 'emergency'); + } + + public function hasAlertThatContains($message): bool + { + return $this->hasRecordThatContains($message, 'alert'); + } + + public function hasCriticalThatContains($message): bool + { + return $this->hasRecordThatContains($message, 'critical'); + } + + public function hasErrorThatContains($message): bool + { + return $this->hasRecordThatContains($message, 'error'); + } + + public function hasWarningThatContains($message): bool + { + return $this->hasRecordThatContains($message, 'warning'); + } + + public function hasNoticeThatContains($message): bool + { + return $this->hasRecordThatContains($message, 'notice'); + } + + public function hasInfoThatContains($message): bool + { + return $this->hasRecordThatContains($message, 'info'); + } + + public function hasDebugThatContains($message): bool + { + return $this->hasRecordThatContains($message, 'debug'); + } + + public function hasEmergencyThatMatches(string $regex): bool + { + return $this->hasRecordThatMatches($regex, 'emergency'); + } + + public function hasAlertThatMatches(string $regex): bool + { + return $this->hasRecordThatMatches($regex, 'alert'); + } + + public function hasCriticalThatMatches(string $regex): bool + { + return $this->hasRecordThatMatches($regex, 'critical'); + } + + public function hasErrorThatMatches(string $regex): bool + { + return $this->hasRecordThatMatches($regex, 'error'); + } + + public function hasWarningThatMatches(string $regex): bool + { + return $this->hasRecordThatMatches($regex, 'warning'); + } + + public function hasNoticeThatMatches(string $regex): bool + { + return $this->hasRecordThatMatches($regex, 'notice'); + } + + public function hasInfoThatMatches(string $regex): bool + { + return $this->hasRecordThatMatches($regex, 'info'); + } + + public function hasDebugThatMatches(string $regex): bool + { + return $this->hasRecordThatMatches($regex, 'debug'); + } + + public function hasEmergencyThatPasses(callable $predicate): bool + { + return $this->hasRecordThatPasses($predicate, 'emergency'); + } + + public function hasAlertThatPasses(callable $predicate): bool + { + return $this->hasRecordThatPasses($predicate, 'alert'); + } + + public function hasCriticalThatPasses(callable $predicate): bool + { + return $this->hasRecordThatPasses($predicate, 'critical'); + } + + public function hasErrorThatPasses(callable $predicate): bool + { + return $this->hasRecordThatPasses($predicate, 'error'); + } + + public function hasWarningThatPasses(callable $predicate): bool + { + return $this->hasRecordThatPasses($predicate, 'warning'); + } + + public function hasNoticeThatPasses(callable $predicate): bool + { + return $this->hasRecordThatPasses($predicate, 'notice'); + } + + public function hasInfoThatPasses(callable $predicate): bool + { + return $this->hasRecordThatPasses($predicate, 'info'); + } + + public function hasDebugThatPasses(callable $predicate): bool + { + return $this->hasRecordThatPasses($predicate, 'debug'); + } + + public function reset() + { + $this->records = []; + $this->recordsByLevel = []; + } +} diff --git a/tests/Test/TestLoggerTest.php b/tests/Test/TestLoggerTest.php index ee84abd..986b0d7 100644 --- a/tests/Test/TestLoggerTest.php +++ b/tests/Test/TestLoggerTest.php @@ -8,7 +8,6 @@ /** * Test classes from psr/log 1.1.x * - * @requires PHP 7.4 * @covers \Psr\Log\Test\TestLogger * @covers \Psr\Log\Test\LoggerInterfaceTest * @@ -72,7 +71,7 @@ public function testHasRecord(string $level): void $this->assertTrue(call_user_func([$logger, $levelMethod], $record), $levelMethod.' without context'); $record = ['message' => $level.' Message', ['foo' => 'bar']]; - $this->assertTrue($logger->hasRecord($record, $level),'hasRecord with context'); + $this->assertTrue($logger->hasRecord($record, $level), 'hasRecord with context'); $this->assertTrue(call_user_func([$logger, $levelMethod], $record), $levelMethod.' with context'); $this->assertTrue(call_user_func([$logger, $levelMethod.'ThatContains'], 'Message'), $levelMethod.'ThatContains');