diff --git a/.github/workflows/build-changelog.yml b/.github/workflows/build-changelog.yml index cd0be51e..65b91518 100644 --- a/.github/workflows/build-changelog.yml +++ b/.github/workflows/build-changelog.yml @@ -6,10 +6,11 @@ on: - develop jobs: - update_release_draft: + update: + name: Update runs-on: ubuntu-latest steps: - - name: Run Release Drafter + - name: Run uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 69f92be8..68f7d6fa 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -46,7 +46,7 @@ jobs: - name: Install PHP dependencies run: | - if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpunit/phpunit johnkary/phpunit-speedtrap --dev; fi + if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpunit/phpunit atk4/ergebnis-phpunit-slow-test-detector --dev; fi if [ "${{ matrix.type }}" != "CodingStyle" ]; then composer remove --no-interaction --no-update friendsofphp/php-cs-fixer --dev; fi if [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpstan/\* --dev; fi composer update --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader @@ -54,7 +54,7 @@ jobs: - name: "Run tests (only for Phpunit)" if: startsWith(matrix.type, 'Phpunit') run: | - vendor/bin/phpunit --exclude-group none --no-coverage -v + vendor/bin/phpunit --exclude-group none --no-coverage --fail-on-warning --fail-on-risky $(if vendor/bin/phpunit --version | grep -q '^PHPUnit 9\.'; then echo -v; else echo --fail-on-notice --fail-on-deprecation --display-notices --display-deprecations --display-warnings --display-errors --display-incomplete --display-skipped; fi) - name: Check Coding Style (only for CodingStyle) if: matrix.type == 'CodingStyle' @@ -108,13 +108,13 @@ jobs: - name: Install PHP dependencies run: | - if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "Phpunit Lowest" ] && [ "${{ matrix.type }}" != "Phpunit Burn" ]; then composer remove --no-interaction --no-update phpunit/phpunit johnkary/phpunit-speedtrap --dev; fi + if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "Phpunit Lowest" ] && [ "${{ matrix.type }}" != "Phpunit Burn" ]; then composer remove --no-interaction --no-update phpunit/phpunit atk4/ergebnis-phpunit-slow-test-detector --dev; fi if [ "${{ matrix.type }}" != "CodingStyle" ]; then composer remove --no-interaction --no-update friendsofphp/php-cs-fixer --dev; fi if [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpstan/\* --dev; fi if [ -n "$LOG_COVERAGE" ]; then composer require --no-interaction --no-install phpunit/phpcov; fi composer update --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader if [ "${{ matrix.type }}" = "Phpunit Lowest" ]; then composer update --ansi --prefer-dist --prefer-lowest --prefer-stable --no-interaction --no-progress --optimize-autoloader; fi - if [ "${{ matrix.type }}" = "Phpunit Burn" ]; then sed -i 's~ *public function runBare(): void~public function runBare(): void { gc_collect_cycles(); gc_collect_cycles(); $memDiffs = array_fill(0, '"$(if [ \"$GITHUB_EVENT_NAME\" == \"schedule\" ]; then echo 1024; else echo 64; fi)"', 0); for ($i = -1; $i < count($memDiffs); ++$i) { $this->_runBare(); gc_collect_cycles(); gc_collect_cycles(); $mem = memory_get_usage(); if ($i !== -1) { $memDiffs[$i] = $mem - $memPrev; } $memPrev = $mem; rsort($memDiffs); if (array_sum($memDiffs) >= 4096 * 1024 || $memDiffs[2] > 0) { $this->onNotSuccessfulTest(new AssertionFailedError("Memory leak detected! (" . implode(" + ", array_map(static fn ($v) => number_format($v / 1024, 3, ".", " "), array_filter($memDiffs))) . " KB, " . ($i + 2) . " iterations)")); } } } private function _runBare(): void~' vendor/phpunit/phpunit/src/Framework/TestCase.php && cat vendor/phpunit/phpunit/src/Framework/TestCase.php | grep '_runBare('; fi + if [ "${{ matrix.type }}" = "Phpunit Burn" ]; then sed -i 's~public function runBare(): void~public function runBare(): void { gc_collect_cycles(); gc_collect_cycles(); $memDiffs = array_fill(0, '"$(if [ \"$GITHUB_EVENT_NAME\" == \"schedule\" ]; then echo 1024; else echo 64; fi)"', 0); for ($i = -1; $i < count($memDiffs); ++$i) { $this->_runBare(); gc_collect_cycles(); gc_collect_cycles(); $mem = memory_get_usage(); if ($i !== -1) { $memDiffs[$i] = $mem - $memPrev; } $memPrev = $mem; rsort($memDiffs); if (array_sum($memDiffs) >= 4096 * 1024 || $memDiffs[2] > 0) { $this->onNotSuccessfulTest(new AssertionFailedError("Memory leak detected! (" . implode(" + ", array_map(static fn ($v) => number_format($v / 1024, 3, ".", " "), array_filter($memDiffs))) . " KB, " . ($i + 2) . " iterations)")); } } } private function _runBare(): void~' vendor/phpunit/phpunit/src/Framework/TestCase.php && cat vendor/phpunit/phpunit/src/Framework/TestCase.php | grep '_runBare('; fi - name: Init run: | @@ -122,7 +122,7 @@ jobs: - name: "Run tests" run: | - php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) -v + php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) --fail-on-warning --fail-on-risky $(if vendor/bin/phpunit --version | grep -q '^PHPUnit 9\.'; then echo -v; else echo --fail-on-notice --fail-on-deprecation --display-notices --display-deprecations --display-warnings --display-errors --display-incomplete --display-skipped; fi) - name: Upload coverage logs 1/2 (only for coverage) if: env.LOG_COVERAGE diff --git a/composer.json b/composer.json index ff292524..adb9b726 100644 --- a/composer.json +++ b/composer.json @@ -40,14 +40,14 @@ "symfony/yaml": "^3.4 || ^4.4 || ^5.1 || ^6.0 || ^7.0" }, "require-dev": { + "atk4/ergebnis-phpunit-slow-test-detector": "^2.4", "ergebnis/composer-normalize": "^2.13", "friendsofphp/php-cs-fixer": "^3.0", - "johnkary/phpunit-speedtrap": "^3.3", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^9.5.5" + "phpunit/phpunit": "^9.5.5 || ^10.0" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/docs/container.md b/docs/container.md index 023a57cb..90500a16 100644 --- a/docs/container.md +++ b/docs/container.md @@ -231,7 +231,7 @@ $args = ['name' => 'child_name']; // obsolete, backward-compatible ``` Method will return the object. Will throw exception if child with same -name already exist. +name already exists. ::: :::{php:method} removeElement($shortName) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 6a8956de..62e7b462 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -20,3 +20,29 @@ parameters: path: 'tests/DynamicMethodTraitTest.php' message: '~^Call to an undefined method Atk4\\Core\\Tests\\(DynamicMethodMock|DynamicMethodWithoutHookMock)::\w+\(\)\.$~' count: 10 + + # remove once PHPUnit 9.x support is removed + - + path: 'src/Phpunit/TestCase.php' + message: '~^Access to constant (STATUS_INCOMPLETE|STATUS_SKIPPED) on an unknown class PHPUnit\\Runner\\BaseTestRunner\.$~' + count: 2 + - + path: 'src/Phpunit/TestCase.php' + message: '~^Call to an undefined method Atk4\\Core\\Phpunit\\TestCase::(getName|getStatus|getTestResultObject)\(\)\.$~' + count: 4 + - + path: 'src/Phpunit/TestCase.php' + message: '~^Call to an undefined static method PHPUnit\\Util\\Test::(getLinesToBeCovered|getLinesToBeUsed)\(\)\.$~' + count: 2 + - + path: 'tests/HookTraitTest.php' + message: '~^Call to an undefined method Atk4\\Core\\Tests\\HookTraitTest::getName\(\)\.$~' + count: 2 + - + path: 'tests/Phpunit/TestCaseTest.php' + message: '~^Call to an undefined method Atk4\\Core\\Tests\\Phpunit\\TestCaseTest::getStatus\(\)\.$~' + count: 1 + - + path: 'tests/Phpunit/TestCaseTest.php' + message: '~^Access to constant STATUS_INCOMPLETE on an unknown class PHPUnit\\Runner\\BaseTestRunner\.$~' + count: 1 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 77389849..6f14faf0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,14 +4,16 @@ tests - - - - + + + + src tests + + diff --git a/src/CollectionTrait.php b/src/CollectionTrait.php index 95f33f77..3beaeb30 100644 --- a/src/CollectionTrait.php +++ b/src/CollectionTrait.php @@ -27,10 +27,10 @@ trait CollectionTrait * * @param string $collection property name */ - public function _addIntoCollection(string $name, object $item, string $collection): object + protected function _addIntoCollection(string $name, object $item, string $collection): object { if (!isset($this->{$collection}) || !is_array($this->{$collection})) { - throw (new Exception('Collection does NOT exist')) + throw (new Exception('Collection does not exist')) ->addMoreInfo('collection', $collection); } @@ -41,11 +41,10 @@ public function _addIntoCollection(string $name, object $item, string $collectio } if ($this->_hasInCollection($name, $collection)) { - throw (new Exception('Element with the same name already exist in the collection')) + throw (new Exception('Element with the same name already exists in the collection')) ->addMoreInfo('collection', $collection) ->addMoreInfo('name', $name); } - $this->{$collection}[$name] = $item; // carry on reference to application if we have appScopeTraits set if ((TraitUtil::hasAppScopeTrait($this) && TraitUtil::hasAppScopeTrait($item)) @@ -69,6 +68,8 @@ public function _addIntoCollection(string $name, object $item, string $collectio } } + $this->{$collection}[$name] = $item; + return $item; } @@ -77,10 +78,10 @@ public function _addIntoCollection(string $name, object $item, string $collectio * * @param string $collection property name */ - public function _removeFromCollection(string $name, string $collection): void + protected function _removeFromCollection(string $name, string $collection): void { if (!$this->_hasInCollection($name, $collection)) { - throw (new Exception('Element is NOT in the collection')) + throw (new Exception('Element is not in the collection')) ->addMoreInfo('collection', $collection) ->addMoreInfo('name', $name); } @@ -94,7 +95,7 @@ public function _removeFromCollection(string $name, string $collection): void * * @param string $collectionName property name to be cloned */ - public function _cloneCollection(string $collectionName): void + protected function _cloneCollection(string $collectionName): void { $this->{$collectionName} = array_map(function ($item) { $item = clone $item; @@ -111,7 +112,7 @@ public function _cloneCollection(string $collectionName): void * * @param string $collection property name */ - public function _hasInCollection(string $name, string $collection): bool + protected function _hasInCollection(string $name, string $collection): bool { return isset($this->{$collection}[$name]); } @@ -119,11 +120,11 @@ public function _hasInCollection(string $name, string $collection): bool /** * @param string $collection property name */ - public function _getFromCollection(string $name, string $collection): object + protected function _getFromCollection(string $name, string $collection): object { $res = $this->{$collection}[$name] ?? null; if ($res === null) { - throw (new Exception('Element is NOT in the collection')) + throw (new Exception('Element is not in the collection')) ->addMoreInfo('collection', $collection) ->addMoreInfo('name', $name); } diff --git a/src/ContainerTrait.php b/src/ContainerTrait.php index e0989432..241c91ba 100644 --- a/src/ContainerTrait.php +++ b/src/ContainerTrait.php @@ -24,7 +24,7 @@ trait ContainerTrait /** * Returns unique element name based on desired name. */ - public function _uniqueElementName(string $desired): string + protected function _uniqueElementName(string $desired): string { if (!isset($this->_elementNameCounts[$desired])) { $this->_elementNameCounts[$desired] = 1; @@ -130,7 +130,7 @@ public function removeElement($shortName) } if (!isset($this->elements[$shortName])) { - throw (new Exception('Could not remove child from parent. Instead of destroy() try using removeField / removeColumn / ..')) + throw (new Exception('Child element not found')) ->addMoreInfo('parent', $this) ->addMoreInfo('name', $shortName); } diff --git a/src/DynamicMethodTrait.php b/src/DynamicMethodTrait.php index 919be2d6..6052d8b3 100644 --- a/src/DynamicMethodTrait.php +++ b/src/DynamicMethodTrait.php @@ -67,7 +67,7 @@ public function addMethod(string $name, \Closure $fx) } if ($this->hasMethod($name)) { - throw (new Exception('Registering method twice')) + throw (new Exception('Method is already defined')) ->addMoreInfo('name', $name); } diff --git a/src/Phpunit/TestCase.php b/src/Phpunit/TestCase.php index 2c578b6e..b19a416f 100644 --- a/src/Phpunit/TestCase.php +++ b/src/Phpunit/TestCase.php @@ -7,17 +7,43 @@ use Atk4\Core\WarnDynamicPropertyTrait; use PHPUnit\Framework\TestCase as BaseTestCase; use PHPUnit\Framework\TestResult; +use PHPUnit\Metadata\Api\CodeCoverage as CodeCoverageMetadata; use PHPUnit\Runner\BaseTestRunner; +use PHPUnit\Runner\CodeCoverage; use PHPUnit\Util\Test as TestUtil; -use SebastianBergmann\CodeCoverage\CodeCoverage; +use SebastianBergmann\CodeCoverage\CodeCoverage as CodeCoverageRaw; + +if (\PHP_VERSION_ID >= 8_01_00) { + trait Phpunit9xTestCaseTrait + { + protected function onNotSuccessfulTest(\Throwable $e): never + { + $this->_onNotSuccessfulTest($e); + } + } +} else { + trait Phpunit9xTestCaseTrait + { + protected function onNotSuccessfulTest(\Throwable $e): void + { + $this->_onNotSuccessfulTest($e); + } + } +} /** * Generic TestCase for PHPUnit tests for ATK4 repos. */ abstract class TestCase extends BaseTestCase { + use Phpunit9xTestCaseTrait; use WarnDynamicPropertyTrait; + final public static function isPhpunit9x(): bool + { + return (new \ReflectionClass(self::class))->hasMethod('getStatus'); + } + protected function setUp(): void { // rerun data providers to fix coverage when coverage for test files is enabled @@ -89,15 +115,15 @@ protected function tearDown(): void gc_collect_cycles(); // fix coverage for skipped/incomplete tests - // based on https://github.com/sebastianbergmann/phpunit/blob/9.5.21/src/Framework/TestResult.php#L830 - // and https://github.com/sebastianbergmann/phpunit/blob/9.5.21/src/Framework/TestResult.php#L857 - if (in_array($this->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE], true)) { - $coverage = $this->getTestResultObject()->getCodeCoverage(); + // based on https://github.com/sebastianbergmann/phpunit/blob/9.5.21/src/Framework/TestResult.php#L830 https://github.com/sebastianbergmann/phpunit/blob/10.4.2/src/Framework/TestRunner.php#L154 + // and https://github.com/sebastianbergmann/phpunit/blob/9.5.21/src/Framework/TestResult.php#L857 https://github.com/sebastianbergmann/phpunit/blob/10.4.2/src/Framework/TestRunner.php#L178 + if (self::isPhpunit9x() ? in_array($this->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE], true) : $this->status()->isSkipped() || $this->status()->isIncomplete()) { + $coverage = self::isPhpunit9x() ? $this->getTestResultObject()->getCodeCoverage() : (CodeCoverage::instance()->isActive() ? CodeCoverage::instance() : null); if ($coverage !== null) { - $coverageId = \Closure::bind(static fn () => $coverage->currentId, null, CodeCoverage::class)(); + $coverageId = self::isPhpunit9x() ? \Closure::bind(static fn () => $coverage->currentId, null, CodeCoverageRaw::class)() : (\Closure::bind(static fn () => $coverage->collecting, null, CodeCoverage::class)() ? $this : null); if ($coverageId !== null) { - $linesToBeCovered = TestUtil::getLinesToBeCovered(static::class, $this->getName(false)); - $linesToBeUsed = TestUtil::getLinesToBeUsed(static::class, $this->getName(false)); + $linesToBeCovered = self::isPhpunit9x() ? TestUtil::getLinesToBeCovered(static::class, $this->getName(false)) : (new CodeCoverageMetadata())->linesToBeCovered(static::class, $this->name()); + $linesToBeUsed = self::isPhpunit9x() ? TestUtil::getLinesToBeUsed(static::class, $this->getName(false)) : (new CodeCoverageMetadata())->linesToBeUsed(static::class, $this->name()); $coverage->stop(true, $linesToBeCovered, $linesToBeUsed); $coverage->start($coverageId); } @@ -131,7 +157,10 @@ private function releaseObjectsFromExceptionTrace(\Throwable $e): void } } - protected function onNotSuccessfulTest(\Throwable $e): void + /** + * @return never + */ + protected function _onNotSuccessfulTest(\Throwable $e): void { // release objects from uncaught exception as it is never released $this->releaseObjectsFromExceptionTrace($e); diff --git a/src/TraitUtil.php b/src/TraitUtil.php index d7300507..69a6a411 100644 --- a/src/TraitUtil.php +++ b/src/TraitUtil.php @@ -29,7 +29,7 @@ public static function hasTrait($class, string $traitName): bool // prevent mass use for other than internal use then we can decide // if we want to keep support this or replace with pure interfaces if (!str_starts_with($traitName, 'Atk4\Core\\')) { - throw new Exception(self::class . '::hasTrait is not intended for use with other than Atk4\Core\* traits'); + throw new Exception(self::class . '::hasTrait() method is not intended for use with other than Atk4\Core\* traits'); } $parentClass = get_parent_class($class); diff --git a/src/TranslatableTrait.php b/src/TranslatableTrait.php index 54a8ef7d..e354a186 100644 --- a/src/TranslatableTrait.php +++ b/src/TranslatableTrait.php @@ -12,7 +12,7 @@ trait TranslatableTrait { /** - * Translates the given message. + * Translate the given message. * * @param string $message The message to be translated * @param array $parameters Array of parameters used to translate message diff --git a/tests/CollectionMock.php b/tests/CollectionMock.php index f46bc280..0ee13aaf 100644 --- a/tests/CollectionMock.php +++ b/tests/CollectionMock.php @@ -15,7 +15,7 @@ class CollectionMock protected $fields = []; /** - * @param array|object|null $seed + * @param array|FieldMock|null $seed */ public function addField(string $name, $seed = null): FieldMock { diff --git a/tests/CollectionTraitTest.php b/tests/CollectionTraitTest.php index 44573508..ccf33b2f 100644 --- a/tests/CollectionTraitTest.php +++ b/tests/CollectionTraitTest.php @@ -5,7 +5,6 @@ namespace Atk4\Core\Tests; use Atk4\Core\AppScopeTrait; -use Atk4\Core\DiContainerTrait; use Atk4\Core\Exception; use Atk4\Core\InitializerTrait; use Atk4\Core\NameTrait; @@ -32,9 +31,6 @@ public function testBasic(): void self::assertFalse($m->hasField('name')); } - /** - * Test Trackable and AppScope. - */ public function testBasicWithApp(): void { $m = new CollectionMockWithApp(); @@ -60,14 +56,31 @@ public function testBasicWithApp(): void self::assertSame(40, strlen($longField->name)); } + public function testCloneCollection(): void + { + $m = new CollectionMock(); + $m->addField('a', [FieldMock::class]); + $m->addField('b', [FieldMockCustom::class]); + + $mCloned = clone $m; + \Closure::bind(static fn () => $m->_cloneCollection('fields'), null, CollectionMock::class)(); + + self::assertNotSame($m->getField('a'), $mCloned->getField('a')); + self::assertNotSame($m->getField('b'), $mCloned->getField('b')); + self::assertSame('b', $m->getField('b')->shortName); // @phpstan-ignore-line + self::assertSame('b', $mCloned->getField('b')->shortName); // @phpstan-ignore-line + } + /** * Bad collection name. */ public function testException1(): void { - $this->expectException(Exception::class); $m = new CollectionMock(); - $m->_addIntoCollection('foo', (object) [], ''); // empty collection name + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Collection does not exist'); + \Closure::bind(static fn () => $m->_addIntoCollection('foo', (object) [], ''), null, CollectionMock::class)(); // empty collection name } /** @@ -75,9 +88,11 @@ public function testException1(): void */ public function testException2(): void { - $this->expectException(Exception::class); $m = new CollectionMock(); - $m->_addIntoCollection('', (object) [], 'fields'); // empty object name + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Empty name is not supported'); + \Closure::bind(static fn () => $m->_addIntoCollection('', (object) [], 'fields'), null, CollectionMock::class)(); // empty object name } /** @@ -85,45 +100,46 @@ public function testException2(): void */ public function testException3(): void { - $this->expectException(Exception::class); $m = new CollectionMock(); - $m->_addIntoCollection('foo', (object) [], 'fields'); - $m->_addIntoCollection('foo', (object) [], 'fields'); // already exists + \Closure::bind(static fn () => $m->_addIntoCollection('foo', (object) [], 'fields'), null, CollectionMock::class)(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Element with the same name already exists in the collection'); + \Closure::bind(static fn () => $m->_addIntoCollection('foo', (object) [], 'fields'), null, CollectionMock::class)(); // already exists } /** - * Cannot remove non existent object. + * Cannot get non existent object. */ public function testException4(): void { - $this->expectException(Exception::class); $m = new CollectionMock(); - $m->_removeFromCollection('dont_exist', 'fields'); // do not exist + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Element is not in the collection'); + \Closure::bind(static fn () => $m->_getFromCollection('dont_exist', 'fields'), null, CollectionMock::class)(); // does not exist } /** - * Cannot get non existent object. + * Cannot remove non existent object. */ public function testException5(): void { - $this->expectException(Exception::class); $m = new CollectionMock(); - $m->_getFromCollection('dont_exist', 'fields'); // do not exist + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Element is not in the collection'); + \Closure::bind(static fn () => $m->_removeFromCollection('dont_exist', 'fields'), null, CollectionMock::class)(); // does not exist } - /** - * Cannot get non existent object. - */ public function testException6(): void { - $this->expectException(Exception::class); $m = new CollectionMock(); - $m->addField('test', new class() { - use DiContainerTrait; - use InitializerTrait; - /** @var string */ - public $name; + $this->expectException(Exception::class); + $this->expectExceptionMessage('Object was not initialized'); + $m->addField('test', new class() extends FieldMock { + use InitializerTrait; protected function init(): void {} }); diff --git a/tests/ConfigTraitTest.php b/tests/ConfigTraitTest.php index f39f641b..13a1d1b3 100644 --- a/tests/ConfigTraitTest.php +++ b/tests/ConfigTraitTest.php @@ -77,22 +77,28 @@ public function testFileRead(): void public function testFileReadException(): void { - $this->expectException(Exception::class); $m = new ConfigMock(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Cannot read config file'); $m->readConfig('unknown_file.php'); } public function testFileBadFormatException(): void { - $this->expectException(Exception::class); $m = new ConfigMock(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('File was read but has a bad format'); $m->readConfig($this->dir . '/config_bad_format.php'); } public function testWrongFileFormatException(): void { - $this->expectException(Exception::class); $m = new ConfigMock(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Unknown Format. Allowed formats: php, json, yml'); $m->readConfig($this->dir . '/config.yml', 'wrong-format'); } diff --git a/tests/ContainerTraitTest.php b/tests/ContainerTraitTest.php index 4b0b92f7..4715dc7e 100644 --- a/tests/ContainerTraitTest.php +++ b/tests/ContainerTraitTest.php @@ -142,6 +142,7 @@ public function __construct(string $name, bool $isLongName) self::assertSame('foo', $app->add($createTrackableMockFx('foo', true))->name); $this->expectException(Exception::class); + $this->expectExceptionMessage('Element has too long desired name'); self::assertSame(40, strlen($app->add($createTrackableMockFx(str_repeat('x', 100), true))->name)); } @@ -204,9 +205,11 @@ public function testArgs(): void public function testExceptionExists(): void { - $this->expectException(Exception::class); $m = new ContainerMock(); $m->add(new TrackableMock(), 'foo'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Element with requested name already exists'); $m->add(new TrackableMock(), 'foo'); } @@ -221,14 +224,14 @@ public function testDesiredName(): void public function testExceptionShortName(): void { - $this->expectException(Exception::class); $m1 = new ContainerMock(); $m2 = new ContainerMock(); $m1foo = $m1->add(new TrackableMock(), 'foo'); $m2foo = $m2->add(new TrackableMock(), 'foo'); - // will carry on short name and run into collision. - $m2->add($m1foo); + $this->expectException(Exception::class); + $this->expectExceptionMessage('Element with requested name already exists'); + $m2->add($m1foo); // will carry on short name and run into collision } public function testExceptionArg2(): void @@ -236,12 +239,13 @@ public function testExceptionArg2(): void $m = new ContainerMock(); if (\PHP_MAJOR_VERSION === 7) { - $this->expectWarning(); // @phpstan-ignore-line - $this->expectWarningMessage('array_diff_key(): Expected parameter 1 to be an array, int given'); // @phpstan-ignore-line - } else { - $this->expectException(\TypeError::class); - $this->expectExceptionMessage('array_diff_key(): Argument #1 ($array) must be of type array, int given'); + self::assertNotNull('Expecting E_WARNING is deprecated in PHPUnit 9'); // @phpstan-ignore-line + + return; } + + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('array_diff_key(): Argument #1 ($array) must be of type array, int given'); $m->add(new TrackableMock(), 123); // @phpstan-ignore-line } @@ -259,13 +263,16 @@ public function testException4(): void $m = new ContainerMock(); $this->expectException(Exception::class); + $this->expectExceptionMessage('Child element not found'); $m->getElement('dont_exist'); } public function testException5(): void { - $this->expectException(Exception::class); $m = new ContainerMock(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Child element not found'); $m->removeElement('dont_exist'); } } diff --git a/tests/DiContainerTraitTest.php b/tests/DiContainerTraitTest.php index c67ba5cf..368e0e85 100644 --- a/tests/DiContainerTraitTest.php +++ b/tests/DiContainerTraitTest.php @@ -16,20 +16,25 @@ public function testFromSeed(): void self::assertSame(StdSat2::class, get_class(StdSat::fromSeed([StdSat2::class]))); $this->expectException(Exception::class); + $this->expectExceptionMessage('Seed class is not a subtype of static class'); StdSat2::fromSeed([StdSat::class]); } public function testNoPropExStandard(): void { - $this->expectException(Exception::class); $m = new FactoryDiMock2(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Property for specified object is not defined'); $m->setDefaults(['not_exist' => 'qwerty']); } public function testNoPropExNumeric(): void { - $this->expectException(Exception::class); $m = new FactoryDiMock2(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Property for specified object is not defined'); $m->setDefaults([5 => 'qwerty']); // @phpstan-ignore-line } @@ -81,10 +86,14 @@ public function testPassively(): void public function testInstanceOfBeforeConstructor(): void { + $catchCalled = false; try { new FactoryDiMockConstructorMustNeverBeCalled(); } catch (\Error $e) { + $catchCalled = true; } + self::assertTrue($catchCalled); + $this->expectException(Exception::class); $this->expectExceptionMessage('Seed class is not a subtype of static class'); FactoryDiMockConstructorMustNeverBeCalled2::fromSeed([FactoryDiMockConstructorMustNeverBeCalled::class]); diff --git a/tests/DynamicMethodTraitTest.php b/tests/DynamicMethodTraitTest.php index 0714e345..ca6c25f6 100644 --- a/tests/DynamicMethodTraitTest.php +++ b/tests/DynamicMethodTraitTest.php @@ -71,6 +71,7 @@ public function testExceptionUndefinedWithoutHookTrait(): void $m = new DynamicMethodWithoutHookMock(); $this->expectException(\Error::class); + $this->expectExceptionMessage('Call to undefined method ' . DynamicMethodWithoutHookMock::class . '::unknownMethod()'); $m->unknownMethod(); } @@ -79,6 +80,7 @@ public function testExceptionAddWithoutHookTrait(): void $m = new DynamicMethodWithoutHookMock(); $this->expectException(Exception::class); + $this->expectExceptionMessage('Object must use HookTrait for dynamic method support'); $m->addMethod('sum', $this->createSumFx()); } @@ -101,6 +103,7 @@ public function testDoubleMethodException(): void $m->addMethod('sum', $this->createSumFx()); $this->expectException(Exception::class); + $this->expectExceptionMessage('Method is already defined'); $m->addMethod('sum', $this->createSumFx()); } diff --git a/tests/HookTraitTest.php b/tests/HookTraitTest.php index 27ca9281..af8aedbf 100644 --- a/tests/HookTraitTest.php +++ b/tests/HookTraitTest.php @@ -377,7 +377,7 @@ public function testOnHookDynamicBoundGetterException(): void $this->expectException(\TypeError::class); $this->expectExceptionMessage('New $this getter must be static'); $m->onHookDynamic('inc', function (HookMock $m) { - $this->getName(); // prevent PHP CS Fixer to make this anonymous function static + self::isPhpunit9x() ? $this->getName(false) : $this->name(); // prevent PHP CS Fixer to make this anonymous function static return $m; }, $m->makeIncrementResultFx()); @@ -411,7 +411,7 @@ public function testPassByReference(): void $value = 0; $m = new HookMock(); $m->onHookShort('inc', function ($ignore1st, int &$value) { - $this->getName(); // prevent PHP CS Fixer to make this anonymous function static + self::isPhpunit9x() ? $this->getName(false) : $this->name(); // prevent PHP CS Fixer to make this anonymous function static ++$value; }); diff --git a/tests/Phpunit/TestCaseTest.php b/tests/Phpunit/TestCaseTest.php index 8af95206..c8c956af 100644 --- a/tests/Phpunit/TestCaseTest.php +++ b/tests/Phpunit/TestCaseTest.php @@ -7,6 +7,7 @@ use Atk4\Core\Exception; use Atk4\Core\Phpunit\TestCase; use PHPUnit\Framework\TestCase as PhpunitTestCase; +use PHPUnit\Framework\TestStatus\TestStatus; use PHPUnit\Runner\BaseTestRunner; class TestCaseTest extends TestCase @@ -105,7 +106,7 @@ public function testObjectsAreReleasedFromUncaughtException(): void /** * @return iterable> */ - public function provideProviderAbCases(): iterable + public static function provideProviderAbCases(): iterable { yield ['a']; yield ['b']; @@ -126,7 +127,7 @@ public function testProviderCoverage(string $v): void /** * @return iterable> */ - public function provideProviderCoverageCases(): iterable + public static function provideProviderCoverageCases(): iterable { yield ['x']; ++self::$providerCoverageCallCounter; @@ -138,8 +139,8 @@ public function provideProviderCoverageCases(): iterable */ public function testCoverageImplForTestMarkedAsIncomplete(): void { - $testStatusOrig = \Closure::bind(fn () => $this->status, $this, PhpunitTestCase::class)(); - \Closure::bind(fn () => $this->status = BaseTestRunner::STATUS_INCOMPLETE, $this, PhpunitTestCase::class)(); + $testStatusOrig = self::isPhpunit9x() ? $this->getStatus() : $this->status(); + \Closure::bind(fn () => $this->status = TestCase::isPhpunit9x() ? BaseTestRunner::STATUS_INCOMPLETE : TestStatus::incomplete(), $this, PhpunitTestCase::class)(); try { $this->tearDown(); } finally { diff --git a/tests/TraitUtilTest.php b/tests/TraitUtilTest.php index 72769997..3854c9d0 100644 --- a/tests/TraitUtilTest.php +++ b/tests/TraitUtilTest.php @@ -4,10 +4,12 @@ namespace Atk4\Core\tests; +use Atk4\Core\Exception; use Atk4\Core\HookTrait; use Atk4\Core\NameTrait; use Atk4\Core\Phpunit\TestCase; use Atk4\Core\TraitUtil; +use PHPUnit\Framework\MockObject\Method as MockObjectMethodTrait; class TraitUtilTest extends TestCase { @@ -29,6 +31,13 @@ public function testHasTrait(): void self::assertTrue(TraitUtil::hasTrait(TraitUtilTestB::class, HookTrait::class)); self::assertTrue(TraitUtil::hasTrait(TraitUtilTestC::class, HookTrait::class)); } + + public function testHasTraitNoAtk4CoreException(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage(TraitUtil::class . '::hasTrait() method is not intended for use with other than Atk4\Core\* traits'); + TraitUtil::hasTrait(TraitUtilTestA::class, MockObjectMethodTrait::class); + } } class TraitUtilTestA {} diff --git a/tests/Translator/AdapterAppTest.php b/tests/Translator/AdapterAppTest.php index a81a1363..7e20f03c 100644 --- a/tests/Translator/AdapterAppTest.php +++ b/tests/Translator/AdapterAppTest.php @@ -7,7 +7,7 @@ use Atk4\Core\AppScopeTrait; use Atk4\Core\TranslatableTrait; -class AdapterAppTest extends AdapterBaseTest +class AdapterAppTest extends AdapterTestCase { public function getTranslatableMock(): object { diff --git a/tests/Translator/AdapterGenericTest.php b/tests/Translator/AdapterGenericTest.php index 5c3804c9..2cc2ade3 100644 --- a/tests/Translator/AdapterGenericTest.php +++ b/tests/Translator/AdapterGenericTest.php @@ -8,7 +8,7 @@ use Atk4\Core\Translator\Adapter\Generic; use Atk4\Core\Translator\Translator; -class AdapterGenericTest extends AdapterBaseTest +class AdapterGenericTest extends AdapterTestCase { public function getTranslatableMock(): object { diff --git a/tests/Translator/AdapterBaseTest.php b/tests/Translator/AdapterTestCase.php similarity index 98% rename from tests/Translator/AdapterBaseTest.php rename to tests/Translator/AdapterTestCase.php index 83b62e97..d9922a52 100644 --- a/tests/Translator/AdapterBaseTest.php +++ b/tests/Translator/AdapterTestCase.php @@ -8,7 +8,7 @@ use Atk4\Core\Translator\Adapter\Generic; use Atk4\Core\Translator\Translator; -abstract class AdapterBaseTest extends TestCase +abstract class AdapterTestCase extends TestCase { abstract public function getTranslatableMock(): object; diff --git a/tests/WarnDynamicPropertyTraitTest.php b/tests/WarnDynamicPropertyTraitTest.php index 7fd5157a..4faa622f 100644 --- a/tests/WarnDynamicPropertyTraitTest.php +++ b/tests/WarnDynamicPropertyTraitTest.php @@ -131,4 +131,4 @@ public function testGetSetWithWarningSuppressed(): void } } -class WarnError extends \Exception {} +class WarnError extends \ErrorException {}