diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..58f71c5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,91 @@ +name: CI + +on: + pull_request: + push: + +jobs: + test: + name: "PHPUnit PHP ${{ matrix.php }}" + + strategy: + matrix: + php: [ 8.0, 8.1, rc ] + include: + - php: '8.0' + coverage: true + + runs-on: ubuntu-latest + + steps: + - name: Setup PHP + if: matrix.coverage + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring + tools: composer, cs2pr + coverage: pcov + + - uses: actions/checkout@v2 + + - name: Cache Composer cache + uses: actions/cache@v2 + with: + path: ~/.composer/cache + key: composer-cache-php${{ matrix.php }} + + - name: Composer install + run: composer install --no-progress --no-suggest --no-interaction --prefer-dist --optimize-autoloader + + - name: Enable code coverage + if: matrix.coverage + run: echo "COVERAGE=1" >> $GITHUB_ENV + + - name: PHPUnit + run: | + mkdir -p build/logs/phpunit + if [ "$COVERAGE" = '1' ]; then + vendor/bin/phpunit --coverage-clover build/logs/phpunit/clover.xml --log-junit build/logs/phpunit/junit.xml --colors=always + else + vendor/bin/phpunit --log-junit build/logs/phpunit/junit.xml --colors=always + fi + + - name: Upload coverage results to Codecov + if: matrix.coverage + uses: codecov/codecov-action@v1 + with: + name: phpunit-php${{ matrix.php }} + flags: phpunit + fail_ci_if_error: true + continue-on-error: true + + static-analysis: + name: "Static Analysis" + + runs-on: ubuntu-latest + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.0 + extensions: mbstring + tools: composer, cs2pr + + - uses: actions/checkout@v2 + + - name: Cache Composer cache + uses: actions/cache@v2 + with: + path: ~/.composer/cache + key: composer-cache-php8.0 + + - name: Composer install + run: composer install --no-progress --no-suggest --no-interaction --prefer-dist --optimize-autoloader + + - name: Psalm + run: php vendor/bin/psalm --threads=2 --output-format=github --shepherd --stats + + - name: PHPStan + run: php vendor/bin/phpstan analyse --error-format=checkstyle --no-progress src/ tests/ | cs2pr diff --git a/.gitignore b/.gitignore index fae9722..214a429 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ !.* composer.phar +composer.lock +.phpunit.result.cache vendor/ diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 18ed129..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,17 +0,0 @@ -build: true -inherit: true - -tools: - external_code_coverage: true - php_code_sniffer: true - php_cpd: true - php_cs_fixer: true - php_loc: true - php_mess_detector: true - php_pdepend: true - php_analyzer: true - sensiolabs_security_checker: true - -filter: - excluded_paths: - - 'vendor/*' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9db711f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: php - -php: - - 7.1 - - 7.2 - - 7.3 - -sudo: false - -install: travis_retry composer install - -script: composer ci - -after_success: - - if [[ "`phpenv version-name`" != "7.1" ]]; then exit 0; fi - - vendor/bin/phpunit --coverage-clover coverage.clover - - wget https://scrutinizer-ci.com/ocular.phar - - php ocular.phar code-coverage:upload --format=php-clover coverage.clover - -cache: - directories: - - $HOME/.composer/cache diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..653ff9d --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +.PHONY: ci cs test phpunit psalm phpstan stan + +ci: phpstan phpunit psalm +cs: phpstan psalm +test: phpunit + +phpunit: + php ./vendor/bin/phpunit -c phpunit.xml.dist + +coverage-html: + php ./vendor/bin/phpunit -c phpunit.xml.dist --coverage-html=./build/coverage/html + +psalm: + ./vendor/bin/psalm + +phpstan: + ./vendor/bin/phpstan analyse -c phpstan.neon --no-progress + +stan: phpstan + diff --git a/README.md b/README.md index 90114cf..acad6a5 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # PSR Log Test Doubles -[![Build Status](https://secure.travis-ci.org/wmde/PsrLogTestDoubles.png?branch=master)](http://travis-ci.org/wmde/PsrLogTestDoubles) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/wmde/PsrLogTestDoubles/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/wmde/PsrLogTestDoubles/?branch=master) -[![Code Coverage](https://scrutinizer-ci.com/g/wmde/PsrLogTestDoubles/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/wmde/PsrLogTestDoubles/?branch=master) +[![Build Status](https://img.shields.io/github/workflow/status/wmde/PsrLogTestDoubles/CI)](https://github.com/wmde/PsrLogTestDoubles/actions?query=workflow%3ACI) +[![codecov](https://codecov.io/gh/wmde/PsrLogTestDoubles/branch/master/graph/badge.svg)](https://codecov.io/gh/wmde/PsrLogTestDoubles) +[![Type Coverage](https://shepherd.dev/github/wmde/PsrLogTestDoubles/coverage.svg)](https://shepherd.dev/github/wmde/PsrLogTestDoubles) +[![Psalm level](https://shepherd.dev/github/wmde/PsrLogTestDoubles/level.svg)](psalm.xml) [![Latest Stable Version](https://poser.pugx.org/wmde/psr-log-test-doubles/version.png)](https://packagist.org/packages/wmde/psr-log-test-doubles) [![Download count](https://poser.pugx.org/wmde/psr-log-test-doubles/d/total.png)](https://packagist.org/packages/wmde/psr-log-test-doubles) @@ -59,6 +60,26 @@ $this->assertSame( LogLevel::ERROR, $firstLogCall->getLevel() ); ## Release notes +### 3.2.0 (2022-03-28) + +* Added `LogCall::isError` +* Added `LogCall::withoutContext` +* Added `LogCalls::filter` +* Added `LogCalls::getErrors` +* Added `LogCalls::map` +* Added `LogCalls::withoutContexts` + +### 3.1.0 (2022-01-26) + +* Added `LogCalls::getLastCall` + +### 3.0.0 (2022-01-26) + +* Added support for `psr/log` 2.x and 3.x +* Changed minimum PHP version from PHP 7.1 to 8.0 +* Added several property, parameter and return types +* Added Psalm and PHPStan CI and compliance with level 1 checks + ### 2.2.0 (2017-05-23) * Added `LoggerSpy::getFirstLogCall` convenience method diff --git a/composer.json b/composer.json index 4b31f79..70de7e9 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,13 @@ "description": "Test Doubles for the PSR-3 Logger Interface", "homepage": "https://github.com/wmde/PsrLogTestDoubles", "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "Jeroen De Dauw", + "email": "jeroendedauw@gmail.com", + "homepage": "https://www.EntropyWins.wtf" + } + ], "keywords": [ "log", "psr", @@ -20,40 +27,22 @@ "test" ], "require": { - "php": ">=7.1", - "psr/log": "~1.0" + "php": ">=8.0", + "psr/log": "~3.0|~2.0" }, "require-dev": { - "phpunit/phpunit": "~6.1", - "squizlabs/php_codesniffer": "~2.5", - "mediawiki/mediawiki-codesniffer": "~0.6.0", - "ockcyp/covers-validator": "~0.4" + "phpunit/phpunit": "~9.5", + "vimeo/psalm": "^4.18.1", + "phpstan/phpstan": "^1.4.2" }, "autoload": { "psr-4": { "WMDE\\PsrLogTestDoubles\\": "src/" } }, - "extra": { - "branch-alias": { - "dev-master": "2.1.x-dev" + "autoload-dev": { + "psr-4": { + "WMDE\\PsrLogTestDoubles\\Tests\\": "tests/" } - }, - "scripts": { - "ci": [ - "@test", - "@cs" - ], - "test": [ - "composer validate --no-interaction", - "vendor/bin/covers-validator", - "vendor/bin/phpunit" - ], - "cs": [ - "@phpcs" - ], - "phpcs": [ - "vendor/bin/phpcs src/ tests/ --standard=phpcs.xml --extensions=php -sp" - ] } } diff --git a/phpcs.xml b/phpcs.xml deleted file mode 100644 index 7dd5865..0000000 --- a/phpcs.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - - - - - - - - - - - - - - - 0 - - - - - - - - - - - - - - - - - - 0 - - - - - - - - - - - - - - - - - - - - 0 - - - - diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..c8b9993 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 1 + paths: + - src + - tests diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..fafb1f5 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/src/LogCall.php b/src/LogCall.php index 8d5626b..b00c0ff 100644 --- a/src/LogCall.php +++ b/src/LogCall.php @@ -4,6 +4,8 @@ namespace WMDE\PsrLogTestDoubles; +use Psr\Log\LogLevel; + /** * Value object representing a call to the logger * @@ -12,25 +14,19 @@ */ class LogCall { - private $level; - private $message; - private $context; + private const ERROR_LEVELS = [ LogLevel::ERROR, LogLevel::CRITICAL, LogLevel::ALERT, LogLevel::EMERGENCY ]; - /** - * @param mixed $level Typically one of the @see LogLevel constants - * @param string $message - * @param array $context - */ - public function __construct( $level, string $message, array $context = [] ) { + private mixed $level; + private string $message; + private array $context; + + public function __construct( mixed $level, string $message, array $context = [] ) { $this->level = $level; $this->message = $message; $this->context = $context; } - /** - * @return mixed Typically one of the @see LogLevel constants - */ - public function getLevel() { + public function getLevel(): mixed { return $this->level; } @@ -42,4 +38,19 @@ public function getContext(): array { return $this->context; } + /** + * Returns if the log level is error or above. + * @since 3.2 + */ + public function isError(): bool { + return in_array( $this->level, self::ERROR_LEVELS ); + } + + /** + * @since 3.2 + */ + public function withoutContext(): self { + return new self( $this->level, $this->message, [] ); + } + } diff --git a/src/LogCalls.php b/src/LogCalls.php index 323b4f4..c3e5090 100644 --- a/src/LogCalls.php +++ b/src/LogCalls.php @@ -4,6 +4,8 @@ namespace WMDE\PsrLogTestDoubles; +use Psr\Log\LogLevel; + /** * Immutable and ordered collection of LogCall objects * @@ -14,16 +16,16 @@ */ class LogCalls implements \IteratorAggregate, \Countable { - private $calls; - /** - * @param LogCall[] $calls + * @var LogCall[] */ + private array $calls; + public function __construct( LogCall... $calls ) { $this->calls = $calls; } - public function getIterator() { + public function getIterator(): \ArrayIterator { return new \ArrayIterator( $this->calls ); } @@ -40,15 +42,62 @@ function( LogCall $logCall ) { } public function getFirstCall(): ?LogCall { - return empty( $this->calls ) ? null : $this->calls[0]; + return $this->calls[0] ?? null; + } + + /** + * @since 3.1 + */ + public function getLastCall(): ?LogCall { + return $this->calls[count( $this->calls ) - 1] ?? null; } /** * @since 2.1 * @return int */ - public function count() { + public function count(): int { return count( $this->calls ); } + /** + * @since 3.2 + * @param callable(LogCall):bool $filter + * @return self + */ + public function filter( callable $filter ): self { + return new self( ...array_filter( $this->calls, $filter ) ); + } + + /** + * Returns log calls with log level ERROR and above. + * @since 3.2 + */ + public function getErrors(): self { + return $this->filter( fn( LogCall $call ) => $call->isError() ); + } + + /** + * @since 3.2 + * @param callable(LogCall):LogCall $map + * @return self + */ + public function map( callable $map ): self { + $calls = []; + + foreach ( $this->calls as $call ) { + $calls[] = $map( $call ); + } + + return new self( ...$calls ); + } + + /** + * Returns a copy of the log calls with their contexts removed. + * @since 3.2 + */ + public function withoutContexts(): self { + return $this->map( fn( LogCall $call ) => $call->withoutContext() ); + } + } diff --git a/src/LoggerSpy.php b/src/LoggerSpy.php index ce89e66..770b98b 100644 --- a/src/LoggerSpy.php +++ b/src/LoggerSpy.php @@ -12,13 +12,16 @@ */ class LoggerSpy extends AbstractLogger { - private $logCalls = []; + /** + * @var array + */ + private array $logCalls = []; /** - * @since 1.0 + * Signature changed in 3.0 */ - public function log( $level, $message, array $context = [] ) { - $this->logCalls[] = new LogCall( $level, $message, $context ); + public function log( $level, string|\Stringable $message, array $context = [] ): void { + $this->logCalls[] = new LogCall( $level, (string)$message, $context ); } /** @@ -39,7 +42,7 @@ public function getFirstLogCall(): ?LogCall { * @since 1.1 * @throws AssertionException */ - public function assertNoLoggingCallsWhereMade() { + public function assertNoLoggingCallsWhereMade(): void { if ( !empty( $this->logCalls ) ) { throw new AssertionException( 'Logger calls where made while non where expected: ' . var_export( $this->logCalls, true ) diff --git a/tests/LogCallTest.php b/tests/LogCallTest.php new file mode 100644 index 0000000..d8e7377 --- /dev/null +++ b/tests/LogCallTest.php @@ -0,0 +1,45 @@ + + */ +class LogCallTest extends TestCase { + + public function testIsNotError(): void { + $this->assertFalse( ( new LogCall( LogLevel::DEBUG, '' ) )->isError() ); + $this->assertFalse( ( new LogCall( LogLevel::NOTICE, '' ) )->isError() ); + } + + public function testIsError(): void { + $this->assertTrue( ( new LogCall( LogLevel::ERROR, '' ) )->isError() ); + $this->assertTrue( ( new LogCall( LogLevel::EMERGENCY, '' ) )->isError() ); + $this->assertTrue( ( new LogCall( LogLevel::ALERT, '' ) )->isError() ); + $this->assertTrue( ( new LogCall( LogLevel::CRITICAL, '' ) )->isError() ); + } + + public function testWithoutContextRemovesContext(): void { + $call = new LogCall( LogLevel::DEBUG, 'whatever', [ 'foo' => 'bar' ] ); + + $this->assertEquals( + new LogCall( LogLevel::DEBUG, 'whatever', [] ), + $call->withoutContext() + ); + + $this->assertSame( + [ 'foo' => 'bar' ], + $call->getContext() + ); + } + +} diff --git a/tests/LogCallsTest.php b/tests/LogCallsTest.php index 927204b..ac4c3cd 100644 --- a/tests/LogCallsTest.php +++ b/tests/LogCallsTest.php @@ -18,11 +18,11 @@ */ class LogCallsTest extends TestCase { - public function testWhenThereAreNoLogCalls_getMessagesReturnsEmptyArray() { + public function testWhenThereAreNoLogCalls_getMessagesReturnsEmptyArray(): void { $this->assertSame( [], ( new LogCalls() )->getMessages() ); } - public function testWhenMultipleThingsAreLogged_getLogMessagesReturnsAllMessages() { + public function testWhenMultipleThingsAreLogged_getLogMessagesReturnsAllMessages(): void { $logCalls = new LogCalls( new LogCall( LogLevel::INFO, 'And so it begins', [ 'year' => 2258 ] ), new LogCall( LogLevel::ALERT, "There's a hole in your mind" ), @@ -39,18 +39,22 @@ public function testWhenMultipleThingsAreLogged_getLogMessagesReturnsAllMessages ); } - public function testCanUseCollectionAsTraversable() { - $logCalls = new LogCalls( new LogCall( LogLevel::INFO, 'And so it begins' ) ); - - $this->assertContains( new LogCall( LogLevel::INFO, 'And so it begins' ), $logCalls, '', false, false ); - $this->assertNotContains( new LogCall( LogLevel::DEBUG, 'And so it begins' ), $logCalls, '', false, false ); + public function testCanUseCollectionAsTraversable(): void { + $this->assertEquals( + [ + new LogCall( LogLevel::INFO, 'And so it begins' ) + ], + iterator_to_array( + new LogCalls( new LogCall( LogLevel::INFO, 'And so it begins' ) ) + ) + ); } - public function testWhenThereAreNoLogCalls_getFirstCallReturnsNull() { + public function testWhenThereAreNoLogCalls_getFirstCallReturnsNull(): void { $this->assertNull( ( new LogCalls() )->getFirstCall() ); } - public function testWhenMultipleThingsAreLogged_getFirstCallReturnsTheFirst() { + public function testWhenMultipleThingsAreLogged_getFirstCallReturnsTheFirst(): void { $logCalls = new LogCalls( new LogCall( LogLevel::INFO, 'And so it begins', [ 'year' => 2258 ] ), new LogCall( LogLevel::ALERT, "There's a hole in your mind" ) @@ -61,7 +65,7 @@ public function testWhenMultipleThingsAreLogged_getFirstCallReturnsTheFirst() { $this->assertSame( [ 'year' => 2258 ], $logCalls->getFirstCall()->getContext() ); } - public function testImplementsCountable() { + public function testImplementsCountable(): void { $logCalls = new LogCalls( new LogCall( LogLevel::INFO, 'And so it begins', [ 'year' => 2258 ] ), new LogCall( LogLevel::ALERT, "There's a hole in your mind" ), @@ -71,4 +75,84 @@ public function testImplementsCountable() { $this->assertCount( 3, $logCalls ); } + public function testGetLastCallReturnsLastCall(): void { + $logCalls = new LogCalls( + new LogCall( LogLevel::INFO, 'And so it begins', [ 'year' => 2258 ] ), + new LogCall( LogLevel::ALERT, "There's a hole in your mind" ) + ); + + $this->assertSame( + "There's a hole in your mind", + $logCalls->getLastCall()->getMessage() + ); + } + + public function testWhenThereAreNoLogCalls_getLastCallReturnsNull(): void { + $this->assertNull( ( new LogCalls() )->getLastCall() ); + } + + public function testFilter(): void { + $logCalls = new LogCalls( + new LogCall( LogLevel::INFO, 'And so it begins', [ 'year' => 2258 ] ), + new LogCall( LogLevel::ALERT, "There's a hole in your mind" ), + new LogCall( LogLevel::INFO, 'And so it begins' ), + new LogCall( LogLevel::CRITICAL, 'Enemy sighted' ), + ); + + $this->assertEquals( + new LogCalls( + new LogCall( LogLevel::INFO, 'And so it begins', [ 'year' => 2258 ] ), + new LogCall( LogLevel::INFO, 'And so it begins' ), + ), + $logCalls->filter( fn( LogCall $call ) => $call->getLevel() === LogLevel::INFO ) + ); + } + + public function testGetErrors(): void { + $logCalls = new LogCalls( + new LogCall( LogLevel::INFO, 'And so it begins', [ 'year' => 2258 ] ), + new LogCall( LogLevel::ALERT, "There's a hole in your mind" ), + new LogCall( LogLevel::INFO, 'And so it begins' ), + new LogCall( LogLevel::CRITICAL, 'Enemy sighted' ), + ); + + $this->assertEquals( + new LogCalls( + new LogCall( LogLevel::ALERT, "There's a hole in your mind" ), + new LogCall( LogLevel::CRITICAL, 'Enemy sighted' ), + ), + $logCalls->getErrors() + ); + } + + public function testMap(): void { + $logCalls = new LogCalls( + new LogCall( LogLevel::INFO, 'And so it begins', [ 'year' => 2258 ] ), + new LogCall( LogLevel::ALERT, "There's a hole in your mind", [ 'year' => 2260 ] ), + ); + + $this->assertEquals( + new LogCalls( + new LogCall( LogLevel::INFO, 'And so it begins' ), + new LogCall( LogLevel::ALERT, "There's a hole in your mind" ), + ), + $logCalls->map( fn( LogCall $call ) => $call->withoutContext() ) + ); + } + + public function testWithoutContexts(): void { + $logCalls = new LogCalls( + new LogCall( LogLevel::INFO, 'And so it begins', [ 'year' => 2258 ] ), + new LogCall( LogLevel::ALERT, "There's a hole in your mind", [ 'year' => 2260 ] ), + ); + + $this->assertEquals( + new LogCalls( + new LogCall( LogLevel::INFO, 'And so it begins' ), + new LogCall( LogLevel::ALERT, "There's a hole in your mind" ), + ), + $logCalls->withoutContexts() + ); + } + } diff --git a/tests/LoggerSpyTest.php b/tests/LoggerSpyTest.php index 84a59b9..770892f 100644 --- a/tests/LoggerSpyTest.php +++ b/tests/LoggerSpyTest.php @@ -19,13 +19,13 @@ */ class LoggerSpyTest extends TestCase { - public function testWhenNothingIsLogged_getLogCallsReturnsEmptyArray() { + public function testWhenNothingIsLogged_getLogCallsReturnsEmptyArray(): void { $loggerSpy = new LoggerSpy(); $this->assertEquals( new LogCalls(), $loggerSpy->getLogCalls() ); } - public function testWhenLogIsCalled_getLogCallsReturnsAllCalls() { + public function testWhenLogIsCalled_getLogCallsReturnsAllCalls(): void { $loggerSpy = new LoggerSpy(); $loggerSpy->log( LogLevel::INFO, 'And so it begins', [ 'year' => 2258 ] ); @@ -40,7 +40,7 @@ public function testWhenLogIsCalled_getLogCallsReturnsAllCalls() { ); } - public function testWhenShotcutMethodsAreCalled_getLogCallsReturnsAllCalls() { + public function testWhenShortcutMethodsAreCalled_getLogCallsReturnsAllCalls(): void { $loggerSpy = new LoggerSpy(); $loggerSpy->info( 'And so it begins', [ 'year' => 2258 ] ); @@ -55,7 +55,7 @@ public function testWhenShotcutMethodsAreCalled_getLogCallsReturnsAllCalls() { ); } - public function testWhenLoggerWasCalled_assertNoCallsThrowsException() { + public function testWhenLoggerWasCalled_assertNoCallsThrowsException(): void { $loggerSpy = new LoggerSpy(); $loggerSpy->alert( "There's a hole in your mind" ); @@ -63,7 +63,7 @@ public function testWhenLoggerWasCalled_assertNoCallsThrowsException() { $loggerSpy->assertNoLoggingCallsWhereMade(); } - public function testWhenLoggerWasNotCalled_assertNoCallsDoesNotThrowException() { + public function testWhenLoggerWasNotCalled_assertNoCallsDoesNotThrowException(): void { $loggerSpy = new LoggerSpy(); $loggerSpy->assertNoLoggingCallsWhereMade(); @@ -75,7 +75,7 @@ public function testWhenThereAreNoLogCalls_getFirstLogCallReturnsNull() { $this->assertNull( ( new LoggerSpy() )->getFirstLogCall() ); } - public function testWhenMultipleThingsAreLogged_getFirstLogCallReturnsTheFirst() { + public function testWhenMultipleThingsAreLogged_getFirstLogCallReturnsTheFirst(): void { $loggerSpy = new LoggerSpy(); $loggerSpy->info( 'And so it begins', [ 'year' => 2258 ] );