From 2493c13084ef5ddfac2e07df8e05b4e70ba24175 Mon Sep 17 00:00:00 2001 From: jspeedz <6095226+jspeedz@users.noreply.github.com> Date: Fri, 16 May 2025 17:21:13 +0200 Subject: [PATCH 1/7] Add missing newlines at end of files Ensure all files end with a newline to adhere to POSIX standards and maintain consistency across the codebase. This change improves compatibility with text editors and version control tools. --- benchmarking/benchmark.php | 2 +- src/Benchmark.php | 2 +- src/HashFunctions/Accurate.php | 2 +- src/HashFunctions/Standard.php | 2 +- src/MultiProbeConsistentHash.php | 2 +- tests/BenchmarkTest.php | 2 +- tests/HashFunctions/AccurateTest.php | 2 +- tests/HashFunctions/StandardTest.php | 2 +- tests/data/generatedata.php | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) 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/src/Benchmark.php b/src/Benchmark.php index 2ca5745..c8158e5 100644 --- a/src/Benchmark.php +++ b/src/Benchmark.php @@ -199,4 +199,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..7118226 100644 --- a/tests/BenchmarkTest.php +++ b/tests/BenchmarkTest.php @@ -294,4 +294,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..ad379d5 100644 --- a/tests/HashFunctions/AccurateTest.php +++ b/tests/HashFunctions/AccurateTest.php @@ -40,4 +40,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..ae3dd3f 100644 --- a/tests/HashFunctions/StandardTest.php +++ b/tests/HashFunctions/StandardTest.php @@ -30,4 +30,4 @@ public function testStandardCallbacks(): void { $callbacks[2]('test'), ); } -} \ No newline at end of file +} diff --git a/tests/data/generatedata.php b/tests/data/generatedata.php index e26efa8..1e6a21e 100644 --- a/tests/data/generatedata.php +++ b/tests/data/generatedata.php @@ -72,4 +72,4 @@ function generateItems(int $count, callable $callback): array { } while(!$valid); file_put_contents($targetDir . $fileName, json_encode($keys)); -} \ No newline at end of file +} From 74a2c3e0ee377606ebf7d771d9bd895bdf7a8edd Mon Sep 17 00:00:00 2001 From: jspeedz <6095226+jspeedz@users.noreply.github.com> Date: Fri, 16 May 2025 17:22:45 +0200 Subject: [PATCH 2/7] Refactor tests with #[Test] attribute and improve type checks Replaced traditional PHPUnit test naming convention with #[Test] attribute for improved clarity. Enhanced type handling in `Benchmark` to map keys to strings with additional checks. Updated composer.json dependencies, and phpunit.xml for better test and coverage management. --- .run/PHPUnit.run.xml | 5 --- composer.json | 5 +-- phpunit.xml | 2 +- src/Benchmark.php | 8 ++++- tests/BenchmarkTest.php | 25 ++++++++----- tests/HashFunctions/AccurateTest.php | 10 ++---- tests/HashFunctions/StandardTest.php | 8 ++--- tests/MultiProbeConsistentHashTest.php | 50 ++++++++++++++++++-------- 8 files changed, 69 insertions(+), 44 deletions(-) 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/composer.json b/composer.json index 1654b19..187903e 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Multi-probe consistent hashing implementation for PHP", "type": "library", "require-dev": { - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "^11.5", "phpstan/phpstan": "^1.11" }, "license": "GPL-3.0-only", @@ -14,7 +14,8 @@ ], "autoload": { "psr-4": { - "Jspeedz\\PhpConsistentHashing\\": "src/" + "Jspeedz\\PhpConsistentHashing\\": "src/", + "Jspeedz\\PhpConsistentHashing\\Tests\\": "tests/" } }, "scripts": { 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 c8158e5..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); } /** diff --git a/tests/BenchmarkTest.php b/tests/BenchmarkTest.php index 7118226..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(); diff --git a/tests/HashFunctions/AccurateTest.php b/tests/HashFunctions/AccurateTest.php index ad379d5..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'), diff --git a/tests/HashFunctions/StandardTest.php b/tests/HashFunctions/StandardTest.php index ae3dd3f..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'), 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([ From 116f3598cbfdf598e97d6cec080f5f8a7a284662 Mon Sep 17 00:00:00 2001 From: jspeedz <6095226+jspeedz@users.noreply.github.com> Date: Fri, 16 May 2025 17:24:22 +0200 Subject: [PATCH 3/7] Add PHP 8.4 support to GitHub Actions build workflow Updated the build configuration to include PHP 8.4 in the test matrix. This ensures compatibility with the latest PHP version and keeps the project up-to-date with ongoing PHP developments. --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d6c82e..ad80c94 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,11 +14,12 @@ jobs: 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 From 8d5decb778c3e7e26028c8ba62bf65ae49e14261 Mon Sep 17 00:00:00 2001 From: jspeedz <6095226+jspeedz@users.noreply.github.com> Date: Fri, 16 May 2025 17:25:51 +0200 Subject: [PATCH 4/7] Fix return type for random_ip_addresses.json callback Updated the callback function to always return a string, ensuring type consistency. This resolves potential issues with unexpected return values during data generation. --- tests/data/generatedata.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/generatedata.php b/tests/data/generatedata.php index 1e6a21e..4691955 100644 --- a/tests/data/generatedata.php +++ b/tests/data/generatedata.php @@ -17,7 +17,7 @@ } $generatorCallbacks = [ - 'random_ip_addresses.json' => function(): false|string { + 'random_ip_addresses.json' => function(): string { return long2ip(rand(0, PHP_INT_MAX)); }, 'random_strings.json' => function(): string { From 0c8c959a902ad8cedee088cacaa8cb76fc3ca6e6 Mon Sep 17 00:00:00 2001 From: jspeedz <6095226+jspeedz@users.noreply.github.com> Date: Fri, 16 May 2025 17:30:21 +0200 Subject: [PATCH 5/7] Update PHPStan dependency to version ^2.1 Upgraded the PHPStan dependency in composer.json to ensure compatibility with the latest features and bug fixes. This change helps maintain development tools' reliability and consistency. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 187903e..2401cc4 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "type": "library", "require-dev": { "phpunit/phpunit": "^11.5", - "phpstan/phpstan": "^1.11" + "phpstan/phpstan": "^2.1" }, "license": "GPL-3.0-only", "authors": [ From 22c94a35d4898cf13176b2dbae6511522329b009 Mon Sep 17 00:00:00 2001 From: jspeedz <6095226+jspeedz@users.noreply.github.com> Date: Fri, 16 May 2025 17:53:08 +0200 Subject: [PATCH 6/7] Update to support both php <8.4 and 8.4 --- phpstan.neon | 16 ++++++++-------- tests/data/generatedata.php | 5 +++-- 2 files changed, 11 insertions(+), 10 deletions(-) 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/tests/data/generatedata.php b/tests/data/generatedata.php index 4691955..74901c1 100644 --- a/tests/data/generatedata.php +++ b/tests/data/generatedata.php @@ -17,8 +17,9 @@ } $generatorCallbacks = [ - 'random_ip_addresses.json' => function(): 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)); From c736db6549afbe10b972154412d2ad8bbfb64a27 Mon Sep 17 00:00:00 2001 From: jspeedz <6095226+jspeedz@users.noreply.github.com> Date: Sat, 17 May 2025 12:45:10 +0200 Subject: [PATCH 7/7] Update to support only php 8.4 this version, as phpstan can't differentiate errors between versions easily --- .github/workflows/build.yml | 2 -- composer.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ad80c94..65d7c94 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,8 +12,6 @@ jobs: strategy: matrix: php: - - "8.2" - - "8.3" - "8.4" # dependencies: # - "lowest" diff --git a/composer.json b/composer.json index 2401cc4..a45640c 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "phpstanpro": "Runs PHPStan in PRO mode!" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "config": { "process-timeout": 0