From 7ddda411cab4890b6c9bc38903555ffce43d4785 Mon Sep 17 00:00:00 2001 From: Tomasz Kowalczyk Date: Fri, 7 Aug 2020 21:23:52 +0200 Subject: [PATCH] 100% code coverage, mutation score, and 99.1% type coverage --- .travis.yml | 2 +- Makefile | 6 ++--- composer.json | 2 +- src/Doctrine/PlatenumDoctrineType.php | 13 +++++++---- src/Enum/CallbackEnumTrait.php | 10 ++++----- src/Enum/EnumTrait.php | 25 ++++++++------------- src/Exception/PlatenumException.php | 2 +- tests/CallbackEnumTest.php | 16 ++++++++++++++ tests/DoctrineTest.php | 32 +++++++++++++++++++++++++++ tests/EnumTest.php | 14 +++++++++++- 10 files changed, 89 insertions(+), 33 deletions(-) diff --git a/.travis.yml b/.travis.yml index 694131c..c0effc3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ before_script: script: - vendor/bin/phpunit --coverage-text - vendor/bin/psalm --threads=8 --no-cache --shepherd - - vendor/bin/infection + - vendor/bin/infection -j2 - cat infection.log matrix: diff --git a/Makefile b/Makefile index f0c6f11..ede147f 100644 --- a/Makefile +++ b/Makefile @@ -11,13 +11,15 @@ test: test-phpunit test-phpunit: PHP_VERSION=7.4 docker-compose run --rm php php -v PHP_VERSION=7.4 docker-compose run --rm php php vendor/bin/phpunit --coverage-text + make test-infection make qa-psalm - make qa-infection test-phpunit-local: php -v php vendor/bin/phpunit --coverage-text php vendor/bin/psalm --no-cache php vendor/bin/infection +test-infection: + PHP_VERSION=7.4 docker-compose run --rm php php vendor/bin/infection -j2 travis: PHP_VERSION=7.1.3 make travis-job @@ -34,8 +36,6 @@ travis-job: qa-psalm: PHP_VERSION=7.4 docker-compose run --rm php php vendor/bin/psalm --no-cache -qa-infection: - PHP_VERSION=7.4 docker-compose run --rm php php vendor/bin/infection run-php: PHP_VERSION=7.4 docker-compose run --rm php php ${FILE} diff --git a/composer.json b/composer.json index dc903e8..b8d4ac8 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "ext-json": "*", "phpunit/phpunit": ">=6.0", "hirak/prestissimo": "^0.3.8", - "vimeo/psalm": "^3.10", + "vimeo/psalm": "^3.14", "doctrine/dbal": "^2.9", "doctrine/orm": "^2.7", "infection/infection": ">=0.13" diff --git a/src/Doctrine/PlatenumDoctrineType.php b/src/Doctrine/PlatenumDoctrineType.php index 6fc1ba0..a65d992 100644 --- a/src/Doctrine/PlatenumDoctrineType.php +++ b/src/Doctrine/PlatenumDoctrineType.php @@ -43,7 +43,8 @@ public static function registerString(string $alias, string $class): void { /** @psalm-suppress MissingClosureParamType */ $toString = function($value): string { - return (string)$value; + /** @psalm-suppress MixedArgument */ + return strval($value); }; $sql = function(array $declaration, AbstractPlatform $platform): string { return $platform->getVarcharTypeDeclarationSQL([]); @@ -86,14 +87,18 @@ private static function allTraitsOf(string $class): array $traits = []; do { - $traits = array_merge(class_uses($class, true), $traits); + foreach(class_uses($class, true) as $fqcn) { + $traits[] = $fqcn; + } } while($class = get_parent_class($class)); foreach ($traits as $trait => $same) { - $traits = array_merge(class_uses($trait, true), $traits); + foreach(class_uses($same, true) as $fqcn) { + $traits[] = $fqcn; + } } - return array_values(array_unique($traits)); + return $traits; } public function getName(): string diff --git a/src/Enum/CallbackEnumTrait.php b/src/Enum/CallbackEnumTrait.php index 0a5af8f..293b842 100644 --- a/src/Enum/CallbackEnumTrait.php +++ b/src/Enum/CallbackEnumTrait.php @@ -26,13 +26,11 @@ final public static function initialize(callable $callback): void final private static function resolve(): array { - if(false === array_key_exists(static::class, static::$callbacks)) { - throw PlatenumException::fromInvalidCallback(static::class); - } - if(false === is_callable(static::$callbacks[static::class])) { - throw PlatenumException::fromInvalidCallback(static::class); + $class = static::class; + if(false === (array_key_exists($class, static::$callbacks) && is_callable(static::$callbacks[$class]))) { + throw PlatenumException::fromInvalidCallback($class); } - return (static::$callbacks[static::class])(); + return (static::$callbacks[$class])(); } } diff --git a/src/Enum/EnumTrait.php b/src/Enum/EnumTrait.php index 8d3206c..f0468e4 100644 --- a/src/Enum/EnumTrait.php +++ b/src/Enum/EnumTrait.php @@ -102,9 +102,8 @@ final public function fromInstance(&$enum): void /* --- EXCEPTIONS --- */ - protected static function throwInvalidMemberException(string $member): void + private static function throwInvalidMemberException(string $member): void { - static::throwDefaultInvalidMemberException($member); } private static function throwDefaultInvalidMemberException(string $member): void @@ -113,9 +112,8 @@ private static function throwDefaultInvalidMemberException(string $member): void } /** @param mixed $value */ - protected static function throwInvalidValueException($value): void + private static function throwInvalidValueException($value): void { - static::throwDefaultInvalidValueException($value); } /** @param mixed $value */ @@ -188,29 +186,23 @@ final public function hasValue($value): bool /** @return int|string */ final public static function memberToValue(string $member) { - static::resolveMembers(); - - $class = static::class; if(false === static::memberExists($member)) { static::throwInvalidMemberException($member); static::throwDefaultInvalidMemberException($member); } - return static::$members[$class][$member]; + return static::$members[static::class][$member]; } /** @param int|string $value */ final public static function valueToMember($value): string { - static::resolveMembers(); - - $class = static::class; if(false === static::valueExists($value)) { static::throwInvalidValueException($value); static::throwDefaultInvalidValueException($value); } - return (string)array_search($value, static::$members[$class], true); + return (string)array_search($value, static::$members[static::class], true); } final public static function getMembers(): array @@ -243,13 +235,14 @@ final private static function resolveMembers(): void return; } + $throwMissingResolve = function(string $class): void { + throw PlatenumException::fromMissingResolve($class); + }; // reflection instead of method_exists because of PHP 7.4 bug #78632 // @see https://bugs.php.net/bug.php?id=78632 - if(false === (new \ReflectionClass($class))->hasMethod('resolve')) { - throw PlatenumException::fromMissingResolve($class); - } + $hasResolve = (new \ReflectionClass($class))->hasMethod('resolve'); /** @var array $members */ - $members = static::resolve(); + $members = $hasResolve ? static::resolve() : $throwMissingResolve($class); if(empty($members)) { throw PlatenumException::fromEmptyMembers($class); } diff --git a/src/Exception/PlatenumException.php b/src/Exception/PlatenumException.php index 8916157..3e152c2 100644 --- a/src/Exception/PlatenumException.php +++ b/src/Exception/PlatenumException.php @@ -17,7 +17,7 @@ public static function fromInvalidMember(string $fqcn, string $member, array $me /** @param mixed $value */ public static function fromInvalidValue(string $fqcn, $value): self { - return new self(sprintf('Enum `%s` does not contain any member with value `%s`.', $fqcn, is_scalar($value) ? (string)$value : gettype($value))); + return new self(sprintf('Enum `%s` does not contain any member with value `%s`.', $fqcn, is_scalar($value) ? strval($value) : gettype($value))); } /* --- GENERIC --- */ diff --git a/tests/CallbackEnumTest.php b/tests/CallbackEnumTest.php index a53dc7e..f0e86af 100644 --- a/tests/CallbackEnumTest.php +++ b/tests/CallbackEnumTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); namespace Thunder\Platenum\Tests; +use Thunder\Platenum\Enum\CallbackEnumTrait; use Thunder\Platenum\Exception\PlatenumException; /** @@ -42,4 +43,19 @@ public function testExceptionAlreadyInitialized(): void $this->expectExceptionMessage('Enum '.$class.' callback was already initialized.'); $class::initialize(function() use($members) { return $members; }); } + + public function testImpossibleExceptionCallbackNotCallable(): void + { + $class = $this->computeUniqueClassName('CallbackTrait'); + eval('final class '.$class.' implements \JsonSerializable { use '.CallbackEnumTrait::class.'; }'); + + $ref = new \ReflectionClass($class); + $callbacks = $ref->getProperty('callbacks'); + $callbacks->setAccessible(true); + $callbacks->setValue($class, [$class => 'invalid']); + + $this->expectException(PlatenumException::class); + $this->expectExceptionMessage('Enum '.$class.' requires static property $callback to be a valid callable returning members and values mapping.'); + $class::FIRST(); + } } diff --git a/tests/DoctrineTest.php b/tests/DoctrineTest.php index dd9a03b..477cf1f 100644 --- a/tests/DoctrineTest.php +++ b/tests/DoctrineTest.php @@ -3,6 +3,7 @@ namespace Thunder\Platenum\Tests; use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManager; use Doctrine\Persistence\Mapping\Driver\StaticPHPDriver; @@ -46,4 +47,35 @@ public function testCreateFromMember(): void $this->assertSame($entity->getStringValue(), $foundEntity->getStringValue()); $this->assertNull($foundEntity->getNullableValue()); } + + public function testDoctrineType(): void + { + PlatenumDoctrineType::registerInteger('intEnum0', DoctrineIntEnum::class); + $intType = PlatenumDoctrineType::getType('intEnum0'); + + $platform = new MySqlPlatform(); + $this->assertTrue($intType->requiresSQLCommentHint($platform)); + $this->assertSame('intEnum0', $intType->getName()); + $this->assertSame('INT', $intType->getSQLDeclaration([], $platform)); + + PlatenumDoctrineType::registerString('stringEnum0', DoctrineStringEnum::class); + $stringType = PlatenumDoctrineType::getType('stringEnum0'); + $this->assertSame('VARCHAR(255)', $stringType->getSQLDeclaration([], $platform)); + } + + public function testInvalidClass(): void + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('PlatenumDoctrineType allows only Platenum enumerations, `stdClass` given.'); + PlatenumDoctrineType::registerInteger('invalid', \stdClass::class); + } + + public function testDuplicateAlias(): void + { + PlatenumDoctrineType::registerString('enumX', DoctrineIntEnum::class); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Alias `'.DoctrineIntEnum::class.'` was already registered in PlatenumDoctrineType.'); + PlatenumDoctrineType::registerString('enumX', DoctrineIntEnum::class); + } } diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 06e8391..3965f26 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -147,7 +147,7 @@ public function testExceptionNonStringMember(): void public function testExceptionNonUniformValueType(): void { - $enum = $this->makeRawEnum(['FIRST' => 1, 'SECOND' => '2']); + $enum = $this->makeRawEnum(['FIRST' => 1, 'SECOND' => '2', 'THIRD' => '3']); $this->expectException(PlatenumException::class); $this->expectExceptionMessage('Enum `'.$enum.'` member values must be of the same type, `integer,string` given.'); @@ -213,6 +213,7 @@ public function testDifferentEnumsInequality(): void $enumA = $this->makeRawEnum(['FIRST' => 1, 'SECOND' => 2]); $enumB = $this->makeRawEnum(['FIRST' => 'first', 'SECOND' => 'second']); + $this->assertFalse($enumA::FIRST()->equals($enumB::FIRST())); $this->assertNotSame($enumA::FIRST(), $enumB::FIRST()); $this->assertNotSame($enumA::SECOND(), $enumB::SECOND()); } @@ -307,6 +308,17 @@ public function testValueToMember(): void $this->assertSame('FIRST', $stringEnum::valueToMember('first')); } + public function testImpossibleValueToMemberInvalidMemberTypeException(): void + { + $enum = $this->makeRawEnum(['FIRST' => 'first', 'SECOND' => 'second']); + $ref = new \ReflectionClass($enum); + $members = $ref->getProperty('members'); + $members->setAccessible(true); + $members->setValue($enum, [$enum => [0 => 'first']]); + + $this->assertSame('0', $enum::valueToMember('first')); + } + public function testJsonEncode(): void { $enum = $this->makeRawEnum(['FIRST' => 1, 'SECOND' => 2]);