Skip to content

Commit 1841369

Browse files
committed
Mutation testing + more UT
1 parent 0843757 commit 1841369

19 files changed

+268
-21
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Makefile text
2222
/.unused.php export-ignore
2323
/CHANGELOG.md export-ignore
2424
/CONTRIBUTING.md export-ignore
25+
/infection.json export-ignore
2526
/Makefile export-ignore
2627
/phpmd.xml export-ignore
2728
/phpstan.neon export-ignore

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
/.php_cs.cache
66
/.phpunit.cache/
77
/.phpunit.result.cache
8+
/infection.log

.php_cs

-2
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,6 @@ return PhpCsFixer\Config::create()
256256
'no_php4_constructor' => true,
257257
// Adds or removes `?` before type declarations for parameters with a default `null` value.
258258
'nullable_type_declaration_for_default_null_value' => true,
259-
// A return statement wishing to return `void` should not return `null`.
260-
'simplified_null_return' => true,
261259
// Changes doc blocks from single to multi line, or reversed. Works for class constants, properties and methods only.
262260
'phpdoc_line_span' => ['const'=>'multi','property'=>'single'],
263261
// Function `implode` must be called with 2 arguments in the documented order.

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Add **Deprecated** section in `CHANGELOG.md`
1313
- Add Redis pipeline support
1414
- Prefix properties name with `@` in Aggregation's SortBy (keep BC)
15-
- Add more unit tests (Aggregation options, Field creation)
15+
- Add more unit tests (Aggregation options, Field creation, DataHelper, RedisHelper)
16+
- (dev) Add mutation testing
1617

1718
### Changed
1819

CONTRIBUTING.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,20 @@ The application use PSR-12 code conventions, strong typing.
3434

3535
The source code must be, at least, compatible with **PHP 7.2**.
3636

37-
Check your code by running the command:
37+
Check your code by running the commands:
3838
```sh
3939
make analyze
40+
make test
4041
```
4142
The command will output any information worth knowing. No error should be left.
4243

44+
If Xdebug is installed, you can also run the commands:
45+
```sh
46+
make coverage
47+
make mutation-test
48+
```
49+
Ideally, coverage should only increase, and **Covered Code MSI** shouldn't be below 95%
50+
4351
----
4452

4553
Thanks!

Makefile

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: analyze fix-code test coverage
1+
.PHONY: analyze fix-code test coverage mutation-test clean
22

33
analyze: | vendor
44
$(COMPOSER) exec -v parallel-lint -- src
@@ -24,6 +24,14 @@ coverage: | vendor
2424
@if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi
2525
$(COMPOSER) exec -v phpunit -- --coverage-text --color
2626

27+
mutation-test: | vendor
28+
@if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi
29+
$(COMPOSER) exec -v infection -- --only-covered --min-covered-msi=95
30+
31+
clean:
32+
rm -rf .phpunit.cache vendor
33+
rm -f .php_cs.cache .phpunit.result.cache composer.phar composer.lock infection.log
34+
2735
vendor: composer.json
2836
$(COMPOSER) install --optimize-autoloader --no-suggest --prefer-dist
2937
touch vendor

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"enlightn/security-checker": "^1.5",
2525
"ergebnis/composer-normalize": "^2.13",
2626
"friendsofphp/php-cs-fixer": "^2.16",
27+
"infection/infection": "^0.18.2",
2728
"insolita/unused-scanner": "^2.2",
2829
"phan/phan": "^3.2",
2930
"php-parallel-lint/php-parallel-lint": "^1.2",

infection.json

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"source": {
3+
"directories": [
4+
"src"
5+
]
6+
},
7+
"logs": {
8+
"text": "infection.log"
9+
},
10+
"mutators": {
11+
"@default": true,
12+
"global-ignoreSourceCodeByRegex": [
13+
"throw new RuntimeException\\(sprintf\\('The assertion of %s::%s at line %d failed\\.'.+",
14+
"for \\(.+\\) \\{"
15+
],
16+
"DecrementInteger": {
17+
"ignore": [
18+
"MacFJA\\RediSearch\\Aggregate\\Exception\\UnknownSortDirectionException",
19+
"MacFJA\\RediSearch\\Search\\Exception\\UnknownUnitException"
20+
]
21+
},
22+
"OneZeroInteger": {
23+
"ignore": [
24+
"MacFJA\\RediSearch\\Aggregate\\Exception\\UnknownSortDirectionException",
25+
"MacFJA\\RediSearch\\Search\\Exception\\UnknownUnitException"
26+
]
27+
}
28+
}
29+
}

src/Aggregate/Exception/UnknownSortDirectionException.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class UnknownSortDirectionException extends UnexpectedValueException
3333
*/
3434
public function __construct(array $directions, int $code = 0, ?Throwable $previous = null)
3535
{
36-
$message = sprintf('Sort By direction can only be "ASC" or "DESC". Provided: %s', implode(', ', $directions));
36+
$message = sprintf('Sort By direction can only be "ASC" or "DESC". Provided: %s.', implode(', ', $directions));
3737
parent::__construct($message, $code, $previous);
3838
}
3939
}

src/Aggregate/SortBy.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class SortBy implements \MacFJA\RediSearch\PartialQuery
3939

4040
public const SORT_DESC = 'DESC';
4141

42+
private const SORT_ACCEPTABLE_VALUES = [self::SORT_ASC, self::SORT_DESC];
43+
4244
/**
4345
* @psalm-var array<string,"ASC"|"DESC">
4446
* @phpstan-var array<string,"ASC"|"DESC">
@@ -61,10 +63,10 @@ class SortBy implements \MacFJA\RediSearch\PartialQuery
6163
public function __construct(array $properties, ?int $max = null)
6264
{
6365
DataHelper::assertArrayOf(array_keys($properties), 'string');
64-
$allValues = array_unique(array_merge($properties, ['ASC', 'DESC']));
66+
$allValues = array_unique(array_merge($properties, self::SORT_ACCEPTABLE_VALUES));
6567
DataHelper::assert(
6668
2 === count($allValues),
67-
new UnknownSortDirectionException(array_diff($allValues, ['ASC', 'DESC']))
69+
new UnknownSortDirectionException(array_diff($allValues, self::SORT_ACCEPTABLE_VALUES))
6870

6971
);
7072
DataHelper::assert(null === $max || $max >= 0, new OutOfRangeException('MAX such by greater or equals to 0'));

src/Helper/RedisHelper.php

+11-5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use function array_filter;
2828
use function array_keys;
2929
use function array_merge;
30+
use function array_pop;
3031
use function array_shift;
3132
use function assert;
3233
use function count;
@@ -125,6 +126,9 @@ public static function buildQueryPartial(array $query, array $partials): array
125126
*/
126127
public static function getPairs(array $notKeyedArray): array
127128
{
129+
if (count($notKeyedArray) % 2 > 0) {
130+
array_pop($notKeyedArray);
131+
}
128132
$entries = array_chunk($notKeyedArray, 2);
129133
$keyed = array_combine(array_column($entries, 0), array_column($entries, 1));
130134
assert(is_array($keyed));
@@ -133,14 +137,16 @@ public static function getPairs(array $notKeyedArray): array
133137
}
134138

135139
/**
136-
* @param array<float|int|string> $notKeyedArray
140+
* @param array<bool|float|int|string> $notKeyedArray
141+
*
142+
* @return null|bool|float|int|string
137143
*/
138-
public static function getValue(array $notKeyedArray, string $key): ?string
144+
public static function getValue(array $notKeyedArray, string $key)
139145
{
140-
while (count($notKeyedArray) > 0) {
146+
while (count($notKeyedArray) >= 2) {
141147
$shifted = array_shift($notKeyedArray);
142-
if ($shifted === $key && count($notKeyedArray) > 0) {
143-
return (string) array_shift($notKeyedArray);
148+
if ($shifted === $key) {
149+
return array_shift($notKeyedArray);
144150
}
145151
}
146152

src/Index/InfoResult.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ public function getFieldsAsObject(): array
171171
case 'TAG':
172172
return new TagField(
173173
$name,
174-
RedisHelper::getValue($field, 'SEPARATOR'),
174+
/** @phan-suppress-next-line PhanPartialTypeMismatchArgument */
175+
DataHelper::nullOrCast(RedisHelper::getValue($field, 'SEPARATOR'), 'string'),
175176
in_array('SORTABLE', $field, true),
176177
in_array('NOINDEX', $field, true)
177178
);
@@ -181,7 +182,8 @@ public function getFieldsAsObject(): array
181182
in_array('NOSTEM', $field, true),
182183
/** @phan-suppress-next-line PhanPartialTypeMismatchArgument */
183184
DataHelper::nullOrCast(RedisHelper::getValue($field, 'WEIGHT'), 'float'),
184-
RedisHelper::getValue($field, 'PHONETIC'),
185+
/** @phan-suppress-next-line PhanPartialTypeMismatchArgument */
186+
DataHelper::nullOrCast(RedisHelper::getValue($field, 'PHONETIC'), 'string'),
185187
in_array('SORTABLE', $field, true),
186188
in_array('NOINDEX', $field, true)
187189
);

tests/Aggregate/GroupByTest.php

+13
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use MacFJA\RediSearch\Aggregate\GroupBy;
2525
use MacFJA\RediSearch\Aggregate\Reducer;
2626
use PHPUnit\Framework\TestCase;
27+
use RuntimeException;
2728
use Tests\MacFJA\RediSearch\support\Assertion;
2829

2930
/**
@@ -76,4 +77,16 @@ public function testNoReducer(): void
7677
new GroupBy(['name', 'age'], [])
7778
);
7879
}
80+
81+
public function testInvalidPropertiesDataType(): void
82+
{
83+
$this->expectException(RuntimeException::class);
84+
new GroupBy([1, false], []);
85+
}
86+
87+
public function testInvalidReducerDataType(): void
88+
{
89+
$this->expectException(RuntimeException::class);
90+
new GroupBy([], ['foo', 1, false]);
91+
}
7992
}

tests/Aggregate/ReducerTest.php

+8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
use MacFJA\RediSearch\Aggregate\Reducer;
2525
use PHPUnit\Framework\TestCase;
26+
use RuntimeException;
27+
use stdClass;
2628
use Tests\MacFJA\RediSearch\support\Assertion;
2729

2830
/**
@@ -38,6 +40,12 @@ class ReducerTest extends TestCase
3840
{
3941
use Assertion;
4042

43+
public function testInvalidArgumentsDataType(): void
44+
{
45+
$this->expectException(RuntimeException::class);
46+
new Reducer('foo', [new stdClass()]);
47+
}
48+
4149
/**
4250
* @covers ::count
4351
*/

tests/Aggregate/SortByTest.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use MacFJA\RediSearch\Aggregate\SortBy;
2626
use OutOfRangeException;
2727
use PHPUnit\Framework\TestCase;
28+
use RuntimeException;
2829
use Tests\MacFJA\RediSearch\support\Assertion;
2930

3031
/**
@@ -48,6 +49,10 @@ public function testNominal(): void
4849
'SORTBY 4 @name ASC @age DESC MAX 20',
4950
new SortBy(['name' => SortBy::SORT_ASC, 'age' => SortBy::SORT_DESC], 20)
5051
);
52+
self::assertSameQuery(
53+
'SORTBY 4 @name ASC @age DESC MAX 20',
54+
new SortBy(['name' => SortBy::SORT_ASC, '@age' => SortBy::SORT_DESC], 20)
55+
);
5156
self::assertSameQuery(
5257
'SORTBY 2 @name ASC', new SortBy(['name' => SortBy::SORT_ASC]));
5358
}
@@ -69,7 +74,13 @@ public function testInvalidMax(): void
6974
public function testInvalidSort(): void
7075
{
7176
$this->expectException(UnknownSortDirectionException::class);
72-
$this->expectExceptionMessage('Sort By direction can only be "ASC" or "DESC". Provided: UP, DOWN');
77+
$this->expectExceptionMessage('Sort By direction can only be "ASC" or "DESC". Provided: UP, DOWN.');
7378
new SortBy(['foo' => 'UP', 'bar' => 'DOWN']);
7479
}
80+
81+
public function testInvalidSortProperties(): void
82+
{
83+
$this->expectException(RuntimeException::class);
84+
new SortBy([1 => 'ASC', false => 'DESC']);
85+
}
7586
}

tests/Helper/DataHelperTest.php

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* Copyright MacFJA
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
9+
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
10+
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
11+
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
14+
* Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
17+
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
namespace Tests\MacFJA\RediSearch\Helper;
23+
24+
use InvalidArgumentException;
25+
use MacFJA\RediSearch\Helper\DataHelper;
26+
use PHPUnit\Framework\TestCase;
27+
use RuntimeException;
28+
use stdClass;
29+
30+
/**
31+
* @coversDefaultClass \MacFJA\RediSearch\Helper\DataHelper
32+
*/
33+
class DataHelperTest extends TestCase
34+
{
35+
/**
36+
* @covers ::assert
37+
*/
38+
public function testAssertTrue(): void
39+
{
40+
DataHelper::assert(true);
41+
$this->expectNotToPerformAssertions();
42+
}
43+
44+
/**
45+
* @covers ::assert
46+
*/
47+
public function testAssertFalse(): void
48+
{
49+
$this->expectException(RuntimeException::class);
50+
$this->expectExceptionMessage('The assertion of Tests\MacFJA\RediSearch\Helper\DataHelperTest::testAssertFalse at line ');
51+
52+
DataHelper::assert(false);
53+
}
54+
55+
/**
56+
* @covers ::assert
57+
*/
58+
public function testAssertFalseCustomException1(): void
59+
{
60+
$this->expectException(InvalidArgumentException::class);
61+
62+
DataHelper::assert(false, InvalidArgumentException::class);
63+
}
64+
65+
/**
66+
* @covers ::assert
67+
*/
68+
public function testAssertFalseCustomException2(): void
69+
{
70+
$exception = new InvalidArgumentException('not valid');
71+
$this->expectException(InvalidArgumentException::class);
72+
$this->expectExceptionMessage('not valid');
73+
74+
DataHelper::assert(false, $exception);
75+
}
76+
77+
/**
78+
* @covers ::assert
79+
*/
80+
public function testAssertFalseWrongCustomException(): void
81+
{
82+
$this->expectException(RuntimeException::class);
83+
84+
DataHelper::assert(false, stdClass::class);
85+
}
86+
87+
/**
88+
* @covers ::assert
89+
*/
90+
public function testAssertEmbed1(): void
91+
{
92+
$runner = function () {
93+
$this->expectException(RuntimeException::class);
94+
$this->expectExceptionMessage('The assertion of Tests\MacFJA\RediSearch\Helper\DataHelperTest::Tests\MacFJA\RediSearch\Helper\{closure} at line ');
95+
DataHelper::assert(false);
96+
};
97+
$runner();
98+
}
99+
100+
/**
101+
* @covers ::assert
102+
*/
103+
public function testAssertEmbed2(): void
104+
{
105+
$runner = function () {
106+
$this->expectException(RuntimeException::class);
107+
$this->expectExceptionMessage('The assertion of Tests\MacFJA\RediSearch\Helper\DataHelperTest::testAssertEmbed2 at line ');
108+
DataHelper::assert(false, null, 2);
109+
};
110+
$runner();
111+
}
112+
}

0 commit comments

Comments
 (0)