diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d6c82e..65d7c94 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,13 +12,12 @@ jobs: strategy: matrix: php: - - "8.2" - - "8.3" + - "8.4" # dependencies: # - "lowest" # - "highest" include: - - php-version: "8.3" + - php-version: "8.4" composer-options: "--ignore-platform-reqs" steps: - name: Checkout diff --git a/.run/PHPUnit.run.xml b/.run/PHPUnit.run.xml index 4858ef6..fc83218 100644 --- a/.run/PHPUnit.run.xml +++ b/.run/PHPUnit.run.xml @@ -1,10 +1,5 @@ - - - - diff --git a/benchmarking/benchmark.php b/benchmarking/benchmark.php index 19ab6ae..4264973 100644 --- a/benchmarking/benchmark.php +++ b/benchmarking/benchmark.php @@ -364,4 +364,4 @@ fclose($fp); } -echo 'Total time: ' . round($totalTime) . 'ms' . PHP_EOL; \ No newline at end of file +echo 'Total time: ' . round($totalTime) . 'ms' . PHP_EOL; diff --git a/composer.json b/composer.json index 1654b19..a45640c 100644 --- a/composer.json +++ b/composer.json @@ -3,8 +3,8 @@ "description": "Multi-probe consistent hashing implementation for PHP", "type": "library", "require-dev": { - "phpunit/phpunit": "^11.2", - "phpstan/phpstan": "^1.11" + "phpunit/phpunit": "^11.5", + "phpstan/phpstan": "^2.1" }, "license": "GPL-3.0-only", "authors": [ @@ -14,7 +14,8 @@ ], "autoload": { "psr-4": { - "Jspeedz\\PhpConsistentHashing\\": "src/" + "Jspeedz\\PhpConsistentHashing\\": "src/", + "Jspeedz\\PhpConsistentHashing\\Tests\\": "tests/" } }, "scripts": { @@ -30,7 +31,7 @@ "phpstanpro": "Runs PHPStan in PRO mode!" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "config": { "process-timeout": 0 diff --git a/phpstan.neon b/phpstan.neon index 6e2961a..f9de542 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,9 +1,9 @@ parameters: - level: 9 - paths: - - src - - tests - ignoreErrors: - - - message: '#Variable \$oneHundredAndTwentyNodes in isset\(\) always exists and is always null#' - path: tests/MultiProbeConsistentHashTest.php \ No newline at end of file + level: 9 + paths: + - src + - tests + ignoreErrors: + - + message: '#Variable \$oneHundredAndTwentyNodes in isset\(\) always exists and is always null#' + path: tests/MultiProbeConsistentHashTest.php \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index f530f1f..f1ac872 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -24,7 +24,7 @@ + disableCodeCoverageIgnore="false"> diff --git a/src/Benchmark.php b/src/Benchmark.php index 2ca5745..b811f93 100644 --- a/src/Benchmark.php +++ b/src/Benchmark.php @@ -67,8 +67,14 @@ public function fetchKeys(): array { $keys, ); shuffle($keys); + + return array_map(function(mixed $key): string { + if(is_scalar($key) || (is_object($key) && method_exists($key, '__toString'))) { + return (string) $key; + } - return $keys; + throw new Exception('Key cannot be cast to string'); + }, $keys); } /** @@ -199,4 +205,4 @@ public function printResults(array $results): string { return $string; } -} \ No newline at end of file +} diff --git a/src/HashFunctions/Accurate.php b/src/HashFunctions/Accurate.php index fdc75e2..4b4bb00 100644 --- a/src/HashFunctions/Accurate.php +++ b/src/HashFunctions/Accurate.php @@ -31,4 +31,4 @@ function(string $key): float|int { }, ]; } -} \ No newline at end of file +} diff --git a/src/HashFunctions/Standard.php b/src/HashFunctions/Standard.php index ddd8165..abfbb22 100644 --- a/src/HashFunctions/Standard.php +++ b/src/HashFunctions/Standard.php @@ -24,4 +24,4 @@ function(string $key): int|float { }, ]; } -} \ No newline at end of file +} diff --git a/src/MultiProbeConsistentHash.php b/src/MultiProbeConsistentHash.php index 814743c..9a4903f 100644 --- a/src/MultiProbeConsistentHash.php +++ b/src/MultiProbeConsistentHash.php @@ -68,4 +68,4 @@ public function getNode(string $key): ?string { return $targetNode; } -} \ No newline at end of file +} diff --git a/tests/BenchmarkTest.php b/tests/BenchmarkTest.php index 2cd0d38..649f90d 100644 --- a/tests/BenchmarkTest.php +++ b/tests/BenchmarkTest.php @@ -4,33 +4,39 @@ use Jspeedz\PhpConsistentHashing\Benchmark; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use ReflectionClass; #[CoversClass(Benchmark::class)] class BenchmarkTest extends TestCase { - public function testGetAvailableHashCallbacks(): void { + #[Test] + public function getAvailableHashCallbacks(): void { $benchmark = new Benchmark(); $results = $benchmark->getAvailableHashCallbacks(); - $this->assertIsArray($results); $this->assertNotEmpty($results); foreach($results as $result) { + // Ignore this one, as there is no guarantee that the element is actually callable. + // @phpstan-ignore method.alreadyNarrowedType $this->assertIsCallable($result); } $results = $benchmark->getAvailableHashCallbacks([ - 'someHashAlgorithm', + 'md5', ]); - $this->assertIsArray($results); $this->assertCount(1, $results); foreach($results as $result) { + // Ignore this one, as there is no guarantee that the element is actually callable. + // @phpstan-ignore method.alreadyNarrowedType $this->assertIsCallable($result); + $this->assertSame(3895525021, $result('someValue')); } } - public function testGetCombinations(): void { + #[Test] + public function getCombinations(): void { $benchmark = new Benchmark(); // Test case 1: Normal case @@ -74,7 +80,8 @@ public function testGetCombinations(): void { $this->assertEquals($expected, $benchmark->getCombinations($array, $length)); } - public function testSortByTwoColumns(): void { + #[Test] + public function sortByTwoColumns(): void { $benchmark = new Benchmark(); // Test case 1: Normal case with ascending sort @@ -132,7 +139,8 @@ public function testSortByTwoColumns(): void { $this->assertEquals($expected, $array); } - public function testFormatNumber(): void { + #[Test] + public function formatNumber(): void { $class = new ReflectionClass(Benchmark::class); $method = $class->getMethod('formatNumber'); $method->setAccessible(true); @@ -230,7 +238,8 @@ public function testFormatNumber(): void { ); } - public function testPrintResults(): void { + #[Test] + public function printResults(): void { $mock = $this->getMockBuilder(Benchmark::class) ->onlyMethods(['formatNumber']) ->getMock(); @@ -294,4 +303,4 @@ function(int|float $number): string { $this->assertEquals($expected, $mock->printResults($results)); } -} \ No newline at end of file +} diff --git a/tests/HashFunctions/AccurateTest.php b/tests/HashFunctions/AccurateTest.php index 19ee6a1..9e4224c 100644 --- a/tests/HashFunctions/AccurateTest.php +++ b/tests/HashFunctions/AccurateTest.php @@ -4,21 +4,17 @@ use Jspeedz\PhpConsistentHashing\HashFunctions\Accurate; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[CoversClass(Accurate::class)] class AccurateTest extends TestCase { - public function testStandardCallbacks(): void { + #[Test] + public function standardCallbacks(): void { $callbacks = (new Accurate())(); $this->assertCount(5, $callbacks); - $this->assertIsCallable($callbacks[0]); - $this->assertIsCallable($callbacks[1]); - $this->assertIsCallable($callbacks[2]); - $this->assertIsCallable($callbacks[3]); - $this->assertIsCallable($callbacks[4]); - $this->assertSame( crc32('test'), $callbacks[0]('test'), @@ -40,4 +36,4 @@ public function testStandardCallbacks(): void { $callbacks[4]('test'), ); } -} \ No newline at end of file +} diff --git a/tests/HashFunctions/StandardTest.php b/tests/HashFunctions/StandardTest.php index 042a5d6..128f02e 100644 --- a/tests/HashFunctions/StandardTest.php +++ b/tests/HashFunctions/StandardTest.php @@ -4,19 +4,17 @@ use Jspeedz\PhpConsistentHashing\HashFunctions\Standard; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[CoversClass(Standard::class)] class StandardTest extends TestCase { - public function testStandardCallbacks(): void { + #[Test] + public function standardCallbacks(): void { $callbacks = (new Standard())(); $this->assertCount(3, $callbacks); - $this->assertIsCallable($callbacks[0]); - $this->assertIsCallable($callbacks[1]); - $this->assertIsCallable($callbacks[2]); - $this->assertSame( crc32('test'), $callbacks[0]('test'), @@ -30,4 +28,4 @@ public function testStandardCallbacks(): void { $callbacks[2]('test'), ); } -} \ No newline at end of file +} diff --git a/tests/MultiProbeConsistentHashTest.php b/tests/MultiProbeConsistentHashTest.php index 2c64814..73f236b 100644 --- a/tests/MultiProbeConsistentHashTest.php +++ b/tests/MultiProbeConsistentHashTest.php @@ -8,6 +8,7 @@ use Jspeedz\PhpConsistentHashing\HashFunctions\Standard; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Jspeedz\PhpConsistentHashing\MultiProbeConsistentHash; @@ -16,7 +17,8 @@ #[UsesClass(Standard::class)] #[UsesClass(Accurate::class)] class MultiProbeConsistentHashTest extends TestCase { - public function testSetHashFunctions(): void { + #[Test] + public function setHashFunctions(): void { $hash = new MultiProbeConsistentHash(); $hashFunctions = [ function($key) { return crc32($key); }, @@ -32,7 +34,8 @@ function($key) { return crc32(strrev($key)); } $this->assertSame($hashFunctions, $hashFunctionsProperty->getValue($hash)); } - public function testAddNodes(): void { + #[Test] + public function addNodes(): void { $hash = $this->getMockBuilder(MultiProbeConsistentHash::class) ->disableOriginalConstructor() ->onlyMethods([ @@ -67,7 +70,8 @@ public function testAddNodes(): void { ]); } - public function testAddNode(): void { + #[Test] + public function addNode(): void { $hash = new MultiProbeConsistentHash(); $hash->addNode('node1', 1.5); @@ -87,7 +91,8 @@ public function testAddNode(): void { $this->assertEquals(1.5, $totalWeight); } - public function testRemoveNode(): void { + #[Test] + public function removeNode(): void { $hash = new MultiProbeConsistentHash(); $hash->addNode('node1', 1.5); $hash->removeNode('node1'); @@ -107,7 +112,8 @@ public function testRemoveNode(): void { $this->assertEquals(0, $totalWeight); } - public function testGetNode(): void { + #[Test] + public function getNode(): void { $hash = new MultiProbeConsistentHash(); $hashFunctions = [ @@ -124,7 +130,8 @@ function($key) { return crc32(strrev($key)); } $this->assertContains($node, ['node1', 'node2']); } - public function testGetNodeWithNoNodes(): void { + #[Test] + public function getNodeWithNoNodes(): void { $hash = new MultiProbeConsistentHash(); $hashFunctions = [ @@ -145,8 +152,9 @@ function($key) { return crc32(strrev($key)); } * @param array $nodes * @param array $keys */ + #[Test] #[DataProvider('distributionDataProvider')] - public function testDistribution( + public function distribution( float $maximumAllowedDeviationPercentage, array $hashFunctions, array $nodes, @@ -155,6 +163,9 @@ public function testDistribution( $hash = new MultiProbeConsistentHash(); $hash->setHashFunctions($hashFunctions); + /** + * @var array> $distribution + */ $distribution = []; foreach($nodes as $node => $weight) { $hash->addNode($node, $weight); @@ -166,6 +177,10 @@ public function testDistribution( foreach($keys as $key) { $pickedNode = $hash->getNode($key); + if($pickedNode === null) { + $this->fail('Could not find a node'); + } + $distribution[$pickedNode] ??= []; $distribution[$pickedNode][$key] ??= 0; $distribution[$pickedNode][$key] += 1; @@ -183,10 +198,12 @@ public function testDistribution( $this->assertCount(count($nodes), $distribution, 'Did not pick all nodes'); // Count the number of keys assigned to each node - foreach($distribution as &$keys) { + $distributionSums = []; + foreach($distribution as $k => $keys) { $sum = array_sum($keys); $keys = count($keys); + $distributionSums[$k] = $keys; // Make sure the actual counts match up $this->assertSame($runCount * $keys, $sum); } @@ -198,22 +215,24 @@ public function testDistribution( $weight = $weight / $totalWeight * 100; } - $total = array_sum($distribution); - foreach($distribution as &$count) { + $total = array_sum($distributionSums); + foreach($distributionSums as &$count) { $count = $count / $total * 100; } // Compare the expected distribution with the actual distribution $deviations = []; - foreach($nodes as $node => $expectedDistributionPercentage) { - // Unfortunately PHPStan doesn't get what is going on here (Or I don't) - // @phpstan-ignore binaryOp.invalid - $deviation = $expectedDistributionPercentage - $distribution[$node]; + $deviation = $expectedDistributionPercentage - $distributionSums[$node]; $deviation = abs($deviation); $deviations[$node] = $deviation; } + + if(empty($deviations)) { + $this->fail('No deviations found'); + } + foreach($deviations as $deviation) { $this->assertThat( $deviation, @@ -822,7 +841,8 @@ public static function distributionDataProvider(): Generator { } } - public function testStickynessOnNodeDeletions(): void { + #[Test] + public function stickynessOnNodeDeletions(): void { $hash = new MultiProbeConsistentHash(); $hash->setHashFunctions([ diff --git a/tests/data/generatedata.php b/tests/data/generatedata.php index e26efa8..74901c1 100644 --- a/tests/data/generatedata.php +++ b/tests/data/generatedata.php @@ -17,8 +17,9 @@ } $generatorCallbacks = [ - 'random_ip_addresses.json' => function(): false|string { - return long2ip(rand(0, PHP_INT_MAX)); + 'random_ip_addresses.json' => function(): string|false { + $result = long2ip(rand(0, PHP_INT_MAX)); + return empty($result) ? false : $result; }, 'random_strings.json' => function(): string { return bin2hex(random_bytes(10)); @@ -72,4 +73,4 @@ function generateItems(int $count, callable $callback): array { } while(!$valid); file_put_contents($targetDir . $fileName, json_encode($keys)); -} \ No newline at end of file +}