From 579ed4940b50f5d71c63fff3a8819251c8660b44 Mon Sep 17 00:00:00 2001 From: Hugo FOUGERES Date: Wed, 8 Mar 2023 10:48:02 +0100 Subject: [PATCH] Try mutation testing --- .github/workflows/tests.yaml | 21 ++++ .travis.yml | 3 +- composer.json | 10 +- infection.json | 13 +++ phpunit.xml | 2 + src/Data/Json/JsonData.php | 2 +- src/Rule/Json/Check/DateFormatCheckTrait.php | 2 +- src/Rule/Json/Check/EmptyCheckTrait.php | 4 +- src/Rule/Json/Check/EnumCheckTrait.php | 2 +- src/Rule/Json/Check/LengthCheckTrait.php | 2 +- src/Rule/Json/Check/MaximumCheckTrait.php | 2 +- src/Rule/Json/Check/MinimumCheckTrait.php | 2 +- src/Rule/Json/Check/PatternCheckTrait.php | 2 +- src/Rule/Json/Factory/JsonRuleFactory.php | 2 +- src/Rule/Json/FloatRule.php | 4 +- src/Rule/Json/IntegerRule.php | 4 +- src/Rule/Json/NumericRule.php | 4 +- src/Rule/Json/StringRule.php | 6 +- src/Rule/Json/TypedListRule.php | 15 +-- src/Schema/Json/JsonSchema.php | 1 + tests/Rule/Json/TypedListRuleTest.php | 115 ++++++++++++++++++- tests/Schema/Json/JsonSchemaTest.php | 28 +++++ tests/Schema/Json/OneLevelJsonSchemaTest.php | 9 +- 23 files changed, 217 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/tests.yaml create mode 100644 infection.json create mode 100644 tests/Schema/Json/JsonSchemaTest.php diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..b5cf502 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,21 @@ +name: Run tests +on: [ push ] +jobs: + run-mutation-tests: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + tools: composer:v2 + coverage: xdebug + + - name: Composer install + run: composer install + + - name: Run mutation tests + run: vendor/bin/infection --show-mutations \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 51ff587..0aa2562 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,7 @@ env: - XDEBUG_MODE=coverage php: - - 7.4 - - 8.0 + - 8.1 before_script: - travis_retry composer self-update diff --git a/composer.json b/composer.json index 147e6d5..d91646e 100644 --- a/composer.json +++ b/composer.json @@ -11,11 +11,12 @@ ], "minimum-stability": "stable", "require": { - "php": "^7.4|^8.0", + "php": ">=8.1", "ext-json": "*" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^9.5", + "infection/infection": "^0.26.16" }, "autoload": { "psr-4": { @@ -26,5 +27,10 @@ "psr-4": { "hunomina\\DataValidator\\Test\\": "tests/" } + }, + "config": { + "allow-plugins": { + "infection/extension-installer": true + } } } diff --git a/infection.json b/infection.json new file mode 100644 index 0000000..f47cef2 --- /dev/null +++ b/infection.json @@ -0,0 +1,13 @@ +{ + "$schema": "vendor/infection/infection/resources/schema.json", + "source": { + "directories": [ + "src" + ] + }, + "mutators": { + "@default": true, + "Concat": false, + "ConcatOperandRemoval": false + } +} \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index da30a9e..e7b2855 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,6 +8,8 @@ forceCoversAnnotation="true" stderr="true" verbose="true" + executionOrder="random" + resolveDependencies="true" > diff --git a/src/Data/Json/JsonData.php b/src/Data/Json/JsonData.php index 051d656..619ac9e 100644 --- a/src/Data/Json/JsonData.php +++ b/src/Data/Json/JsonData.php @@ -89,7 +89,7 @@ public function offsetExists($offset): bool * @return mixed|null * @codeCoverageIgnore */ - public function offsetGet($offset) + public function offsetGet($offset): mixed { if ($this->offsetExists($offset)) { return $this->data[$offset]; diff --git a/src/Rule/Json/Check/DateFormatCheckTrait.php b/src/Rule/Json/Check/DateFormatCheckTrait.php index 79a6815..734610f 100644 --- a/src/Rule/Json/Check/DateFormatCheckTrait.php +++ b/src/Rule/Json/Check/DateFormatCheckTrait.php @@ -31,7 +31,7 @@ public function setDateFormat(?string $dateFormat): void * @return bool * Return true if the string data match the date format $this->dateFormat */ - public function validateDateFormat(string $data): bool + private function validateDateFormat(string $data): bool { if ($this->dateFormat === null) { return true; diff --git a/src/Rule/Json/Check/EmptyCheckTrait.php b/src/Rule/Json/Check/EmptyCheckTrait.php index 669469d..bebffbd 100644 --- a/src/Rule/Json/Check/EmptyCheckTrait.php +++ b/src/Rule/Json/Check/EmptyCheckTrait.php @@ -30,5 +30,5 @@ public function setEmpty(bool $empty): void * Return true if the value is "empty" (string, list) and can * Or if the value is not empty */ - abstract public function validateEmptiness($data): bool; -} \ No newline at end of file + abstract private function validateEmptiness($data): bool; +} diff --git a/src/Rule/Json/Check/EnumCheckTrait.php b/src/Rule/Json/Check/EnumCheckTrait.php index b3f558e..597031d 100644 --- a/src/Rule/Json/Check/EnumCheckTrait.php +++ b/src/Rule/Json/Check/EnumCheckTrait.php @@ -35,7 +35,7 @@ public function setEnum(?array $enum): void * @return bool * Return true if the data can be found in $this->enum */ - public function validateEnum($data): bool + private function validateEnum($data): bool { if ($this->enum === null) { return true; diff --git a/src/Rule/Json/Check/LengthCheckTrait.php b/src/Rule/Json/Check/LengthCheckTrait.php index 40bdba9..151e544 100644 --- a/src/Rule/Json/Check/LengthCheckTrait.php +++ b/src/Rule/Json/Check/LengthCheckTrait.php @@ -35,5 +35,5 @@ public function setLength(?int $length): void * @return bool * Return true if the data length (regardless of the type, could be a string, an array, ...) equals $this->length */ - abstract public function validateLength($data): bool; + abstract private function validateLength($data): bool; } \ No newline at end of file diff --git a/src/Rule/Json/Check/MaximumCheckTrait.php b/src/Rule/Json/Check/MaximumCheckTrait.php index 7df0c46..b22c531 100644 --- a/src/Rule/Json/Check/MaximumCheckTrait.php +++ b/src/Rule/Json/Check/MaximumCheckTrait.php @@ -29,5 +29,5 @@ public function setMaximum(?float $maximum): void * Return true if the value is lower or equals to $this->maximum * For integer check => Cast the parameter */ - abstract public function validateMaximum($data): bool; + abstract private function validateMaximum($data): bool; } \ No newline at end of file diff --git a/src/Rule/Json/Check/MinimumCheckTrait.php b/src/Rule/Json/Check/MinimumCheckTrait.php index 6bfe456..24e0d59 100644 --- a/src/Rule/Json/Check/MinimumCheckTrait.php +++ b/src/Rule/Json/Check/MinimumCheckTrait.php @@ -29,5 +29,5 @@ public function setMinimum(?float $minimum): void * Return true if the value is greater or equals to $this->minimum * For integer check => Cast the parameter */ - abstract public function validateMinimum($data): bool; + abstract private function validateMinimum($data): bool; } \ No newline at end of file diff --git a/src/Rule/Json/Check/PatternCheckTrait.php b/src/Rule/Json/Check/PatternCheckTrait.php index 4724737..d872051 100644 --- a/src/Rule/Json/Check/PatternCheckTrait.php +++ b/src/Rule/Json/Check/PatternCheckTrait.php @@ -30,7 +30,7 @@ public function setPattern(?string $pattern): void * Return true if the string data match the pattern * Regular expressions only apply to string that's why this method is not abstract */ - public function validatePattern(string $data): bool + private function validatePattern(string $data): bool { if ($this->pattern === null) { return true; diff --git a/src/Rule/Json/Factory/JsonRuleFactory.php b/src/Rule/Json/Factory/JsonRuleFactory.php index 34c1387..a35be23 100644 --- a/src/Rule/Json/Factory/JsonRuleFactory.php +++ b/src/Rule/Json/Factory/JsonRuleFactory.php @@ -32,7 +32,7 @@ public static function create(string $type, array $options): JsonRule { try { $rule = self::getRuleObjectFromType($type); - } catch (InvalidRuleException $e) { + } catch (InvalidRuleException) { throw new InvalidRuleException('Invalid rule type : `' . $type . '`', InvalidRuleException::INVALID_RULE_TYPE); } diff --git a/src/Rule/Json/FloatRule.php b/src/Rule/Json/FloatRule.php index c21ef37..fee7ce7 100644 --- a/src/Rule/Json/FloatRule.php +++ b/src/Rule/Json/FloatRule.php @@ -52,7 +52,7 @@ public function validate($data): bool * @param $data * @return bool */ - public function validateMaximum($data): bool + private function validateMaximum($data): bool { if ($this->maximum === null) { return true; @@ -65,7 +65,7 @@ public function validateMaximum($data): bool * @param $data * @return bool */ - public function validateMinimum($data): bool + private function validateMinimum($data): bool { if ($this->minimum === null) { return true; diff --git a/src/Rule/Json/IntegerRule.php b/src/Rule/Json/IntegerRule.php index 01c04fe..22ed13e 100644 --- a/src/Rule/Json/IntegerRule.php +++ b/src/Rule/Json/IntegerRule.php @@ -52,7 +52,7 @@ public function validate($data): bool * @param $data * @return bool */ - public function validateMaximum($data): bool + private function validateMaximum($data): bool { if ($this->maximum === null) { return true; @@ -65,7 +65,7 @@ public function validateMaximum($data): bool * @param $data * @return bool */ - public function validateMinimum($data): bool + private function validateMinimum($data): bool { if ($this->minimum === null) { return true; diff --git a/src/Rule/Json/NumericRule.php b/src/Rule/Json/NumericRule.php index d51f6bf..ab28991 100644 --- a/src/Rule/Json/NumericRule.php +++ b/src/Rule/Json/NumericRule.php @@ -52,7 +52,7 @@ public function validate($data): bool * @param float $data * @return bool */ - public function validateMaximum($data): bool + private function validateMaximum($data): bool { if ($this->maximum === null) { return true; @@ -65,7 +65,7 @@ public function validateMaximum($data): bool * @param float $data * @return bool */ - public function validateMinimum($data): bool + private function validateMinimum($data): bool { if ($this->minimum === null) { return true; diff --git a/src/Rule/Json/StringRule.php b/src/Rule/Json/StringRule.php index ee42dd9..1df6353 100644 --- a/src/Rule/Json/StringRule.php +++ b/src/Rule/Json/StringRule.php @@ -64,7 +64,7 @@ public function validate($data): bool * @param $data * @return bool */ - public function validateEmptiness($data): bool + private function validateEmptiness($data): bool { if ($this->empty === false) { // can not be an empty value return $data !== ''; @@ -77,7 +77,7 @@ public function validateEmptiness($data): bool * @param $data * @return bool */ - public function validateLength($data): bool + private function validateLength($data): bool { if ($this->length === null) { return true; @@ -93,4 +93,4 @@ public function getType(): string { return self::STRING_TYPE; } -} \ No newline at end of file +} diff --git a/src/Rule/Json/TypedListRule.php b/src/Rule/Json/TypedListRule.php index 731eac3..c322efb 100644 --- a/src/Rule/Json/TypedListRule.php +++ b/src/Rule/Json/TypedListRule.php @@ -26,11 +26,8 @@ class TypedListRule extends JsonRule use EnumCheckTrait; use EmptyCheckTrait; - private JsonRule $childRule; - - public function __construct(JsonRule $childRule) + public function __construct(private JsonRule $childRule) { - $this->childRule = $childRule; } /** @@ -81,7 +78,7 @@ public function validate($data): bool /** * @inheritDoc */ - public function validateEmptiness($data): bool + private function validateEmptiness($data): bool { if (($this->empty === false) && $this->minimum !== 0) { return count($data) !== 0; @@ -106,7 +103,7 @@ public function setLength(?int $length): void /** * @inheritDoc */ - public function validateLength($data): bool + private function validateLength($data): bool { if ($this->length === null) { return true; @@ -131,7 +128,7 @@ public function setMaximum(?float $maximum): void * @param array $data * @return bool */ - public function validateMaximum($data): bool + private function validateMaximum($data): bool { if ($this->maximum === null) { return true; @@ -156,7 +153,7 @@ public function setMinimum(?float $minimum): void * @param array $data * @return bool */ - public function validateMinimum($data): bool + private function validateMinimum($data): bool { if ($this->minimum === null) { return true; @@ -180,4 +177,4 @@ public function getType(): string { return $this->childRule->getType() . self::LIST_TYPE_SUFFIX; } -} \ No newline at end of file +} diff --git a/src/Schema/Json/JsonSchema.php b/src/Schema/Json/JsonSchema.php index 1b6e862..49b4e18 100644 --- a/src/Schema/Json/JsonSchema.php +++ b/src/Schema/Json/JsonSchema.php @@ -207,6 +207,7 @@ private function validateObject(JsonData $dataType): bool } catch (InvalidDataException $e) { throw new InvalidDataException('`' . $field . '` does not validate the schema. ' . $e->getMessage(), $e->getCode(), $e); } + break; } } diff --git a/tests/Rule/Json/TypedListRuleTest.php b/tests/Rule/Json/TypedListRuleTest.php index 23c0bf1..d4a6802 100644 --- a/tests/Rule/Json/TypedListRuleTest.php +++ b/tests/Rule/Json/TypedListRuleTest.php @@ -3,6 +3,7 @@ namespace hunomina\DataValidator\Test\Rule\Json; use hunomina\DataValidator\Exception\Json\InvalidDataException; +use hunomina\DataValidator\Exception\Json\InvalidRuleException; use hunomina\DataValidator\Rule\Json\BooleanRule; use hunomina\DataValidator\Rule\Json\CharacterRule; use hunomina\DataValidator\Rule\Json\FloatRule; @@ -127,6 +128,118 @@ private static function InvalidNumericList(): array return [new NumericRule(), [1, 2.2, '3'], false]; } + public function testThrowsOnInvalidSetMinimumParameter(): void + { + $rule = new TypedListRule(new IntegerRule()); + + try { + $rule->setMinimum(-1); + } catch (Throwable $t) { + self::assertInstanceOf(InvalidRuleException::class, $t); + self::assertSame(InvalidRuleException::INVALID_LIST_MIN_RULE, $t->getCode()); + } + } + + public function testThrowsOnInvalidSetMaximumParameter(): void + { + $rule = new TypedListRule(new IntegerRule()); + + try { + $rule->setMaximum(-1); + } catch (Throwable $t) { + self::assertInstanceOf(InvalidRuleException::class, $t); + self::assertSame(InvalidRuleException::INVALID_LIST_MAX_RULE, $t->getCode()); + } + } + + /** + * @dataProvider provideValuesForMinimumCheck + */ + public function testMinimumCheck(int $minimum, array $data, bool $expectedResult): void + { + $rule = new TypedListRule(new StringRule()); + $rule->setMinimum($minimum); + + if (!$expectedResult) { + try { + $rule->validate($data); + } catch (Throwable $t) { + // exception thrown by the invalid list element (scalar type) + self::assertInstanceOf(InvalidDataException::class, $t); + self::assertEquals(InvalidDataException::INVALID_MIN_VALUE, $t->getCode()); + } + } else { + self::assertTrue($rule->validate($data)); + } + } + + public function provideValuesForMinimumCheck(): array + { + $data = ['first', 'second', 'third']; + + return [ + 'lower' => [ + 'minimum' => 4, + 'data' => $data, + 'expectedResult' => false, + ], + 'exact' => [ + 'minimum' => 3, + 'data' => $data, + 'expectedResult' => true, + ], + 'greater' => [ + 'minimum' => 2, + 'data' => $data, + 'expectedResult' => true, + ], + ]; + } + + /** + * @dataProvider provideValuesForMaximumCheck + */ + public function testMaximumCheck(int $maximum, array $data, bool $expectedResult): void + { + $rule = new TypedListRule(new StringRule()); + $rule->setMaximum($maximum); + + if (!$expectedResult) { + try { + $rule->validate($data); + } catch (Throwable $t) { + // exception thrown by the invalid list element (scalar type) + self::assertInstanceOf(InvalidDataException::class, $t); + self::assertEquals(InvalidDataException::INVALID_MAX_VALUE, $t->getCode()); + } + } else { + self::assertTrue($rule->validate($data)); + } + } + + public function provideValuesForMaximumCheck(): array + { + $data = ['first', 'second', 'third']; + + return [ + 'lower' => [ + 'maximum' => 4, + 'data' => $data, + 'expectedResult' => true, + ], + 'exact' => [ + 'maximum' => 3, + 'data' => $data, + 'expectedResult' => true, + ], + 'greater' => [ + 'maximum' => 2, + 'data' => $data, + 'expectedResult' => false, + ], + ]; + } + /** * @throws InvalidDataException */ @@ -160,4 +273,4 @@ public function getScalarRules(): array [new BooleanRule()] ]; } -} \ No newline at end of file +} diff --git a/tests/Schema/Json/JsonSchemaTest.php b/tests/Schema/Json/JsonSchemaTest.php new file mode 100644 index 0000000..af2a47c --- /dev/null +++ b/tests/Schema/Json/JsonSchemaTest.php @@ -0,0 +1,28 @@ + ['type' => JsonRule::CHAR_TYPE]]); + + try { + $schema->validate(new JsonData(['test' => 'notACharacter'])); + } catch (Throwable $t) { + self::assertInstanceOf(InvalidDataException::class, $t); + $previous = $t->getPrevious(); + self::assertInstanceOf(InvalidDataException::class, $previous); + self::assertStringContainsString($previous->getMessage(), $t->getMessage()); + self::assertSame($previous->getCode(), $t->getCode()); + } + } +} diff --git a/tests/Schema/Json/OneLevelJsonSchemaTest.php b/tests/Schema/Json/OneLevelJsonSchemaTest.php index da3a44f..178375a 100644 --- a/tests/Schema/Json/OneLevelJsonSchemaTest.php +++ b/tests/Schema/Json/OneLevelJsonSchemaTest.php @@ -2,7 +2,6 @@ namespace hunomina\DataValidator\Test\Schema\Json; -use hunomina\DataValidator\Exception\Json\InvalidSchemaException; use hunomina\DataValidator\Rule\Json\BooleanRule; use hunomina\DataValidator\Rule\Json\CharacterRule; use hunomina\DataValidator\Rule\Json\FloatRule; @@ -136,10 +135,6 @@ public function testNullRuleOnSetSchema(): void 'null' => ['type' => JsonRule::STRING_TYPE] ]); - $schema2 = new JsonSchema([ - 'null' => ['type' => JsonRule::STRING_TYPE, 'null' => true], - ]); - self::assertCount(1, $schema->getRules()); self::assertCount(0, $schema->getChildren()); @@ -147,6 +142,10 @@ public function testNullRuleOnSetSchema(): void self::assertInstanceOf(StringRule::class, $schema->getRules()['null']); self::assertFalse($schema->getRules()['null']->canBeNull()); + $schema2 = new JsonSchema([ + 'null' => ['type' => JsonRule::STRING_TYPE, 'null' => true], + ]); + self::assertCount(1, $schema2->getRules()); self::assertCount(0, $schema2->getChildren());