diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4645e0b1..b24f9135 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -202,7 +202,7 @@ jobs: strategy: matrix: php-version: ['8.0', '8.1'] - test-group: ['unit', 'db'] + test-group: ['unit', 'db', 'api'] steps: - name: Checkout code uses: actions/checkout@v2 @@ -222,8 +222,8 @@ jobs: run: composer infect:ci:unit env: INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }} - - if: ${{ matrix.test-group == 'db' }} - run: composer infect:ci:db + - if: ${{ matrix.test-group != 'unit' }} + run: composer infect:ci:${{ matrix.test-group }} upload-coverage: needs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 24ef65b5..d0cbf02b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this The official docker image has also been updated to use PHP 8.1 by default. ### Changed +* [#844](https://github.com/shlinkio/shlink/issues/844) Added mutation checks to API tests. * [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6. * [#1223](https://github.com/shlinkio/shlink/issues/1223) Updated to phpstan 1.0. * Added `domain` field to `DeleteShortUrlException` exception. diff --git a/composer.json b/composer.json index bb9319c1..615173c0 100644 --- a/composer.json +++ b/composer.json @@ -74,7 +74,7 @@ "phpunit/phpunit": "^9.5", "roave/security-advisories": "dev-master", "shlinkio/php-coding-standard": "~2.2.0", - "shlinkio/shlink-test-utils": "^2.4", + "shlinkio/shlink-test-utils": "^2.5", "symfony/var-dumper": "^6.0", "veewee/composer-run-parallel": "^1.1" }, @@ -113,7 +113,7 @@ ], "ci:parallel": [ "@parallel cs stan swagger:validate test:unit:ci test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms", - "@parallel test:api infect:ci:unit infect:ci:db" + "@parallel infect:test:api infect:ci:unit infect:ci:db" ], "cs": "phpcs", "cs:fix": "phpcbf", @@ -130,7 +130,7 @@ ], "test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox", "test:unit:ci": "@test:unit --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml", - "test:unit:pretty": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-html build/coverage-unit-html", + "test:unit:pretty": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-html build/coverage-unit/coverage-html", "test:db": "@parallel test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms", "test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml", "test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov --coverage-xml=build/coverage-db/coverage-xml --log-junit=build/coverage-db/junit.xml", @@ -142,15 +142,20 @@ "infect:ci:base": "infection --threads=4 --log-verbosity=default --only-covered --only-covering-test-cases --skip-initial-tests", "infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=83", "infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json", - "infect:ci": "@parallel infect:ci:unit infect:ci:db", + "infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json", + "infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api", "infect:test": [ - "@parallel test:unit:ci test:db:sqlite:ci", + "@parallel test:unit:ci test:db:sqlite:ci test:api", "@infect:ci" ], "infect:test:unit": [ "@test:unit:ci", "@infect:ci:unit" ], + "infect:test:api": [ + "@test:api", + "@infect:ci:api" + ], "swagger:validate": "php-openapi validate docs/swagger/swagger.json", "swagger:inline": "php-openapi inline docs/swagger/swagger.json docs/swagger/swagger-inlined.json", "clean:dev": "rm -f data/database.sqlite && rm -f config/params/generated_config.php" diff --git a/config/test/bootstrap_api_tests.php b/config/test/bootstrap_api_tests.php index 8d22d029..52c9d4fb 100644 --- a/config/test/bootstrap_api_tests.php +++ b/config/test/bootstrap_api_tests.php @@ -20,8 +20,7 @@ $config = $container->get('config'); $em = $container->get(EntityManager::class); $httpClient = $container->get('shlink_test_api_client'); -// Start code coverage collecting on swoole process, and stop it when process shuts down -$httpClient->request('GET', sprintf('http://%s:%s/api-tests/start-coverage', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT)); +// Dump code coverage when process shuts down register_shutdown_function(function () use ($httpClient): void { $httpClient->request( 'GET', diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index 68d1011c..ab7a910f 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -8,13 +8,16 @@ use GuzzleHttp\Client; use Laminas\ConfigAggregator\ConfigAggregator; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\ServiceManager\Factory\InvokableFactory; -use Laminas\Stdlib\Glob; use Monolog\Handler\StreamHandler; use Monolog\Logger; use PHPUnit\Runner\Version; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\Driver\Selector; use SebastianBergmann\CodeCoverage\Filter; +use SebastianBergmann\CodeCoverage\Report\Html\Facade as Html; use SebastianBergmann\CodeCoverage\Report\PHP; use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml; @@ -29,9 +32,8 @@ use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT; $isApiTest = env('TEST_ENV') === 'api'; if ($isApiTest) { $filter = new Filter(); - foreach (Glob::glob(__DIR__ . '/../../module/*/src') as $item) { - $filter->includeDirectory($item); - } + $filter->includeDirectory(__DIR__ . '/../../module/Core/src'); + $filter->includeDirectory(__DIR__ . '/../../module/Rest/src'); $coverage = new CodeCoverage((new Selector())->forLineCoverage($filter), $filter); } @@ -113,26 +115,19 @@ return [ ], 'routes' => !$isApiTest ? [] : [ - [ - 'name' => 'start_collecting_coverage', - 'path' => '/api-tests/start-coverage', - 'middleware' => middleware(static function () use (&$coverage) { - if ($coverage) { // @phpstan-ignore-line - $coverage->start('API tests'); - } - return new EmptyResponse(); - }), - 'allowed_methods' => ['GET'], - ], [ 'name' => 'dump_coverage', 'path' => '/api-tests/stop-coverage', 'middleware' => middleware(static function () use (&$coverage) { + // TODO I have tried moving this block to a listener so that it's invoked automatically, + // but then the coverage is generated empty ¯\_(ツ)_/¯ if ($coverage) { // @phpstan-ignore-line $basePath = __DIR__ . '/../../build/coverage-api'; - $coverage->stop(); + + // TODO Generate these coverages dynamically based on CLI options (new PHP())->process($coverage, $basePath . '.cov'); (new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml'); + (new Html())->process($coverage, $basePath . '/coverage-html'); } return new EmptyResponse(); @@ -141,6 +136,24 @@ return [ ], ], + 'middleware_pipeline' => !$isApiTest ? [] : [ + 'capture_code_coverage' => [ + 'middleware' => middleware(static function ( + ServerRequestInterface $req, + RequestHandlerInterface $handler, + ) use (&$coverage): ResponseInterface { + $coverage?->start($req->getHeaderLine('x-coverage-id')); + + try { + return $handler->handle($req); + } finally { + $coverage?->stop(); + } + }), + 'priority' => 9999, + ], + ], + 'mercure' => [ 'public_hub_url' => null, 'internal_hub_url' => null, diff --git a/infection-api.json b/infection-api.json new file mode 100644 index 00000000..398cd653 --- /dev/null +++ b/infection-api.json @@ -0,0 +1,23 @@ +{ + "source": { + "directories": [ + "module/*/src" + ] + }, + "timeout": 5, + "logs": { + "text": "build/infection-api/infection-log.txt", + "summary": "build/infection-api/summary-log.txt", + "debug": "build/infection-api/debug-log.txt" + }, + "tmpDir": "build/infection-api/temp", + "phpUnit": { + "configDir": "." + }, + "testFrameworkOptions": "--configuration=phpunit-api.xml", + "mutators": { + "@default": true, + "IdenticalEqual": false, + "NotIdenticalNotEqual": false + } +} diff --git a/phpunit-api.xml b/phpunit-api.xml index 38a53ca4..6dd527de 100644 --- a/phpunit-api.xml +++ b/phpunit-api.xml @@ -13,7 +13,8 @@ - ./module/*/src + ./module/Core/src + ./module/Rest/src