Repository: pda/flexihash Branch: master Commit: 8403c28a5453 Files: 19 Total size: 33.5 KB Directory structure: gitextract_0n0hhiec/ ├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENCE ├── README.md ├── ROADMAP.md ├── composer.json ├── phpcs.xml ├── phpunit.xml.dist ├── src/ │ ├── Exception.php │ ├── Flexihash.php │ └── Hasher/ │ ├── Crc32Hasher.php │ ├── HasherInterface.php │ └── Md5Hasher.php └── tests/ ├── BenchmarkTest.php ├── FlexihashTest.php └── Hasher/ ├── HasherTest.php └── MockHasher.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveralls.yml ================================================ coverage_clover: clover.xml json_path: coveralls-upload.json ================================================ FILE: .gitignore ================================================ /vendor/ ================================================ FILE: .travis.yml ================================================ language: php env: global: - COVERALLS=0 - PHPCS=0 matrix: include: - php: 7.2 - php: 7.3 - php: 7.4 env: COVERALLS=1 PHPCS=1 - php: hhvm - php: nightly allow_failures: - php: hhvm - php: nightly fast_finish: true before_script: - composer install --no-interaction --prefer-source --dev script: - sh -c "if [ '$COVERALLS' = '1' ]; then ./vendor/bin/phpunit --coverage-clover clover.xml ; else ./vendor/bin/phpunit ; fi" - sh -c "if [ '$PHPCS' = '1' ]; then vendor/bin/phpcs -p --extensions=php --standard=./phpcs.xml ./src ./tests ; fi" after_script: - sh -c "if [ '$COVERALLS' = '1' ]; then vendor/bin/coveralls ; fi" notifications: on_success: never on_failure: always ================================================ FILE: CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file which adheres to the guidelines at http://keepachangelog.com/. This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ## [2.0.2] - 2016-04-22 ### Changed - Pinned symfony component version to pass tests on 5.4.x. - Updated coveralls config for new version. - Tweaked README.md to recommend install version 2. - Sorted phpcs errors. ## [2.0.1] - 2016-04-22 ### Changed - Make MD5 hasher return an integer to prevent incorrect remapping due to PHP treating numeric string array keys as integers. ## [2.0.0] - 2015-10-08 ### Added - This CHANGELOG.md file. - A ROADMAP.md file. - PSR-4 autoloading. - Introduce namespacing. - Full PSR-2 support. ### Changed - Reorganisation of files. - Updated readme to reflect composer installation recommendation. ### Removed - PHP<5.4 support ## [1.0.0] - 2015-10-16 ### Added - Setup automatic testing with Travis. - Monitor code coverage with Coveralls. - Get as close to PSR-2 as possible without changing class names. ### Changed - Migrate tests to PHPUnit. ### Removed - Legacy autoloader. ## [0.1.0] - 2012-04-04 Posterity release [Unreleased]: https://github.com/pda/flexihash/compare/v2.0.2...master [2.0.2]: https://github.com/pda/flexihash/compare/v2.0.1...v2.0.2 [2.0.1]: https://github.com/pda/flexihash/compare/v2.0.0...v2.0.1 [2.0.0]: https://github.com/pda/flexihash/compare/v1.0.0...v2.0.0 [1.0.0]: https://github.com/pda/flexihash/compare/v0.1.0...v1.0.0 [0.1.0]: https://github.com/pda/flexihash/tree/v0.1.0 ================================================ FILE: LICENCE ================================================ The MIT License Copyright (c) 2008 Paul Annesley Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Flexihash [![Build Status](https://travis-ci.org/pda/flexihash.svg?branch=master)](https://travis-ci.org/pda/flexihash) [![Coverage Status](https://coveralls.io/repos/github/pda/flexihash/badge.svg?branch=master)](https://coveralls.io/github/pda/flexihash?branch=master) Flexihash is a small PHP library which implements [consistent hashing](http://en.wikipedia.org/wiki/Consistent_hashing), which is most useful in distributed caching. It requires PHP5 and uses [PHPUnit](http://simpletest.org/) for unit testing. ## Installation [Composer](https://getcomposer.org/) is the recommended installation technique. You can find flexihash on [Packagist](https://packagist.org/packages/flexihash/flexihash) so installation is as easy as ``` composer require flexihash/flexihash ``` or in your `composer.json` ```json { "require": { "flexihash/flexihash": "^3.0.0" } } ``` ## Usage ```php $hash = new Flexihash(); // bulk add $hash->addTargets(['cache-1', 'cache-2', 'cache-3']); // simple lookup $hash->lookup('object-a'); // "cache-1" $hash->lookup('object-b'); // "cache-2" // add and remove $hash ->addTarget('cache-4') ->removeTarget('cache-1'); // lookup with next-best fallback (for redundant writes) $hash->lookupList('object', 2); // ["cache-2", "cache-4"] // remove cache-2, expect object to hash to cache-4 $hash->removeTarget('cache-2'); $hash->lookup('object'); // "cache-4" ``` ## Tests ### Unit Test ``` % vendor/bin/phpunit ``` ### Benchmark Test ``` % vendor/bin/phpunit tests/BenchmarkTest.php ``` ## Further Reading * http://www.spiteful.com/2008/03/17/programmers-toolbox-part-3-consistent-hashing/ * http://weblogs.java.net/blog/tomwhite/archive/2007/11/consistent_hash.html ================================================ FILE: ROADMAP.md ================================================ #Roadmap ## v1.0.0 This maintains the historical API but allows for composer autoloading. - [x] Composer support. - [x] PSR2 bar class names. - [x] Migrate tests to PHPUnit. ## v2.0.0 The historical API will be broken by classname changes. - [x] Introduce namespacing. - [x] PSR4 autoloading. - [x] Automated testing. - [x] PHP 5.4 minimum. ## v3.0.0 - [x] PHP 7.2 minimum. - [x] PHPUnit 8. - [x] Enable strict typing mode for all PHP files. ## v4.0.0 - [ ] PHP 7.3 minimum. - [ ] PHPUnit 9. ================================================ FILE: composer.json ================================================ { "name": "flexihash/flexihash", "type": "library", "description": "Flexihash is a small PHP library which implements consistent hashing", "homepage": "https://github.com/pda/flexihash", "license": "MIT", "authors": [ { "name": "Paul Annesley", "email": "paul@annesley.cc", "homepage": "http://paul.annesley.cc" }, { "name": "Dom Morgan", "email": "dom@d3r.com", "homepage": "https://d3r.com" } ], "require": { "php": ">=7.2.0" }, "require-dev": { "phpunit/phpunit": "^8", "squizlabs/php_codesniffer": "3.*", "php-coveralls/php-coveralls": "^2.2", "symfony/config": "^5.1.3", "symfony/console": "^5.1.3", "symfony/filesystem": "^5.1.3", "symfony/stopwatch": "^5.1.3", "symfony/yaml": "^5.1.3" }, "autoload": { "psr-4": { "Flexihash\\": "src/" } }, "autoload-dev": { "psr-4": { "Flexihash\\Tests\\": "tests/" } } } ================================================ FILE: phpcs.xml ================================================ Flexihash Coding Standard ================================================ FILE: phpunit.xml.dist ================================================ tests tests/BenchmarkTest.php ================================================ FILE: src/Exception.php ================================================ target, ... } */ private $positionToTarget = []; /** * Internal map of targets to lists of positions that target is hashed to. * @var array { target => [ position, position, ... ], ... } */ private $targetToPositions = []; /** * Whether the internal map of positions to targets is already sorted. * @var bool */ private $positionToTargetSorted = false; /** * Sorted positions. * * @var array */ private $sortedPositions = []; /** * Internal counter for current number of positions. * * @var integer */ private $positionCount = 0; /** * Constructor. * @param \Flexihash\Hasher\HasherInterface $hasher * @param int $replicas Amount of positions to hash each target to. */ public function __construct(HasherInterface $hasher = null, $replicas = null) { $this->hasher = $hasher ? $hasher : new Crc32Hasher(); if (!empty($replicas)) { $this->replicas = $replicas; } } /** * Add a target. * @param string $target * @param float $weight * @chainable */ public function addTarget($target, $weight = 1) { if (isset($this->targetToPositions[$target])) { throw new Exception("Target '$target' already exists."); } $this->targetToPositions[$target] = []; // hash the target into multiple positions for ($i = 0; $i < round($this->replicas * $weight); ++$i) { $position = $this->hasher->hash($target.$i); $this->positionToTarget[$position] = $target; // lookup $this->targetToPositions[$target] [] = $position; // target removal } $this->positionToTargetSorted = false; ++$this->targetCount; return $this; } /** * Add a list of targets. * * @param array $targets * @param float $weight * @return self fluent */ public function addTargets($targets, $weight = 1) { foreach ($targets as $target) { $this->addTarget($target, $weight); } return $this; } /** * Remove a target. * * @param string $target * @return self fluent * @throws \Flexihash\Exception when target does not exist */ public function removeTarget($target) { if (!isset($this->targetToPositions[$target])) { throw new Exception("Target '$target' does not exist."); } foreach ($this->targetToPositions[$target] as $position) { unset($this->positionToTarget[$position]); } unset($this->targetToPositions[$target]); $this->positionToTargetSorted = false; --$this->targetCount; return $this; } /** * A list of all potential targets. * @return array */ public function getAllTargets(): array { return array_keys($this->targetToPositions); } /** * Looks up the target for the given resource. * @param string $resource * @return string * @throws \Flexihash\Exception when no targets defined */ public function lookup($resource): string { $targets = $this->lookupList($resource, 1); if (empty($targets)) { throw new Exception('No targets exist'); } return $targets[0]; } /** * Get a list of targets for the resource, in order of precedence. * Up to $requestedCount targets are returned, less if there are fewer in total. * * @param string $resource * @param int $requestedCount The length of the list to return * @return array List of targets * @throws \Flexihash\Exception when count is invalid */ public function lookupList($resource, $requestedCount): array { if (!$requestedCount) { throw new Exception('Invalid count requested'); } // handle no targets if (empty($this->positionToTarget)) { return []; } // optimize single target if ($this->targetCount == 1) { return array_unique(array_values($this->positionToTarget)); } // hash resource to a position $resourcePosition = $this->hasher->hash($resource); $results = []; $this->sortPositionTargets(); $positions = $this->sortedPositions; $low = 0; $high = $this->positionCount - 1; $notfound = false; // binary search of the first position greater than resource position while ($high >= $low || $notfound = true) { $probe = (int) floor(($high + $low) / 2); if ($notfound === false && $positions[$probe] <= $resourcePosition) { $low = $probe + 1; } elseif ($probe === 0 || $resourcePosition > $positions[$probe - 1] || $notfound === true) { if ($notfound) { // if not found is true, it means binary search failed to find any position greater // than ressource position, in this case, the last position is the bigest lower // position and first position is the next one after cycle $probe = 0; } $results[] = $this->positionToTarget[$positions[$probe]]; if ($requestedCount > 1) { for ($i = $requestedCount - 1; $i > 0; --$i) { if (++$probe > $this->positionCount - 1) { $probe = 0; // cycle } $results[] = $this->positionToTarget[$positions[$probe]]; } } break; } else { $high = $probe - 1; } } return array_unique($results); } public function __toString(): string { return sprintf( '%s{targets:[%s]}', get_class($this), implode(',', $this->getAllTargets()) ); } // ---------------------------------------- // private methods /** * Sorts the internal mapping (positions to targets) by position. */ private function sortPositionTargets() { // sort by key (position) if not already if (!$this->positionToTargetSorted) { ksort($this->positionToTarget, SORT_REGULAR); $this->positionToTargetSorted = true; $this->sortedPositions = array_keys($this->positionToTarget); $this->positionCount = count($this->sortedPositions); } } } ================================================ FILE: src/Hasher/Crc32Hasher.php ================================================ */ public function hash($string) { return hexdec(substr(md5($string), 0, 8)); } } ================================================ FILE: tests/BenchmarkTest.php ================================================ lookups) as $i) { $results1[$i] = $this->basicHash("t$i", 10); } $results2 = []; foreach (range(1, $this->lookups) as $i) { $results2[$i] = $this->basicHash("t$i", 11); } $differences = 0; foreach (range(1, $this->lookups) as $i) { if ($results1[$i] !== $results2[$i]) { ++$differences; } } $percent = round($differences / $this->lookups * 100); $this->dump("NonConsistentHash: {$percent}% of lookups changed ". "after adding a target to the existing {$this->targets}"); } public function testRemoveTargetWithNonConsistentHash(): void { $results1 = []; foreach (range(1, $this->lookups) as $i) { $results1[$i] = $this->basicHash("t$i", 10); } $results2 = []; foreach (range(1, $this->lookups) as $i) { $results2[$i] = $this->basicHash("t$i", 9); } $differences = 0; foreach (range(1, $this->lookups) as $i) { if ($results1[$i] !== $results2[$i]) { ++$differences; } } $percent = round($differences / $this->lookups * 100); $this->dump("NonConsistentHash: {$percent}% of lookups changed ". "after removing 1 of {$this->targets} targets"); } public function testHopeAddingTargetDoesNotChangeMuchWithCrc32Hasher(): void { $hashSpace = new Flexihash( new Crc32Hasher() ); foreach (range(1, $this->targets) as $i) { $hashSpace->addTarget("target$i"); } $results1 = []; foreach (range(1, $this->lookups) as $i) { $results1[$i] = $hashSpace->lookup("t$i"); } $hashSpace->addTarget('target-new'); $results2 = []; foreach (range(1, $this->lookups) as $i) { $results2[$i] = $hashSpace->lookup("t$i"); } $differences = 0; foreach (range(1, $this->lookups) as $i) { if ($results1[$i] !== $results2[$i]) { ++$differences; } } $percent = round($differences / $this->lookups * 100); $this->dump("ConsistentHash: {$percent}% of lookups changed ". "after adding a target to the existing {$this->targets}"); } public function testHopeRemovingTargetDoesNotChangeMuchWithCrc32Hasher(): void { $hashSpace = new Flexihash( new Crc32Hasher() ); foreach (range(1, $this->targets) as $i) { $hashSpace->addTarget("target$i"); } $results1 = []; foreach (range(1, $this->lookups) as $i) { $results1[$i] = $hashSpace->lookup("t$i"); } $hashSpace->removeTarget('target1'); $results2 = []; foreach (range(1, $this->lookups) as $i) { $results2[$i] = $hashSpace->lookup("t$i"); } $differences = 0; foreach (range(1, $this->lookups) as $i) { if ($results1[$i] !== $results2[$i]) { ++$differences; } } $percent = round($differences / $this->lookups * 100); $this->dump("ConsistentHash: {$percent}% of lookups changed ". "after removing 1 of {$this->targets} targets"); } public function testHashDistributionWithCrc32Hasher(): void { $hashSpace = new Flexihash( new Crc32Hasher() ); foreach (range(1, $this->targets) as $i) { $hashSpace->addTarget("target$i"); } $results = []; foreach (range(1, $this->lookups) as $i) { $results[$i] = $hashSpace->lookup("t$i"); } $distribution = []; foreach ($hashSpace->getAllTargets() as $target) { $distribution[$target] = count(array_keys($results, $target)); } $this->dump(sprintf( 'Distribution of %d lookups per target (min/max/median/avg): %d/%d/%d/%d', $this->lookups / $this->targets, min($distribution), max($distribution), round($this->median($distribution)), round(array_sum($distribution) / count($distribution)) )); } public function testHasherSpeed(): void { $hashCount = 100000; $md5Hasher = new Md5Hasher(); $crc32Hasher = new Crc32Hasher(); $start = microtime(true); for ($i = 0; $i < $hashCount; ++$i) { $md5Hasher->hash("test$i"); } $timeMd5 = microtime(true) - $start; $start = microtime(true); for ($i = 0; $i < $hashCount; ++$i) { $crc32Hasher->hash("test$i"); } $timeCrc32 = microtime(true) - $start; $this->dump(sprintf( 'Hashers timed over %d hashes (MD5 / CRC32): %f / %f', $hashCount, $timeMd5, $timeCrc32 )); } // ---------------------------------------- private function basicHash($value, $targets):int { return abs(crc32($value) % $targets); } /** * @param array $array list of numeric values * @return numeric */ private function median($values):int { $values = array_values($values); sort($values); $count = count($values); $middleFloor = floor($count / 2); if ($count % 2 == 1) { return $values[$middleFloor]; } else { return ($values[$middleFloor] + $values[$middleFloor + 1]) / 2; } } } ================================================ FILE: tests/FlexihashTest.php ================================================ assertEquals($hashSpace->getAllTargets(), []); } public function testAddTargetThrowsExceptionOnDuplicateTarget(): void { $hashSpace = new Flexihash(); $hashSpace->addTarget('t-a'); $this->expectException('Flexihash\Exception'); $hashSpace->addTarget('t-a'); } public function testAddTargetAndGetAllTargets(): void { $hashSpace = new Flexihash(); $hashSpace ->addTarget('t-a') ->addTarget('t-b') ->addTarget('t-c') ; $this->assertEquals($hashSpace->getAllTargets(), ['t-a', 't-b', 't-c']); } public function testAddTargetsAndGetAllTargets(): void { $targets = ['t-a', 't-b', 't-c']; $hashSpace = new Flexihash(); $hashSpace->addTargets($targets); $this->assertEquals($hashSpace->getAllTargets(), $targets); } public function testRemoveTarget(): void { $hashSpace = new Flexihash(); $hashSpace ->addTarget('t-a') ->addTarget('t-b') ->addTarget('t-c') ->removeTarget('t-b') ; $this->assertEquals($hashSpace->getAllTargets(), ['t-a', 't-c']); } public function testRemoveTargetFailsOnMissingTarget(): void { $hashSpace = new Flexihash(); $this->expectException('Flexihash\Exception'); $hashSpace->removeTarget('not-there'); } public function testHashSpaceRepeatableLookups(): void { $hashSpace = new Flexihash(); foreach (range(1, 10) as $i) { $hashSpace->addTarget("target$i"); } $this->assertEquals($hashSpace->lookup('t1'), $hashSpace->lookup('t1')); $this->assertEquals($hashSpace->lookup('t2'), $hashSpace->lookup('t2')); } public function testHashSpaceLookupListEmpty(): void { $hashSpace = new Flexihash(); $this->assertEmpty($hashSpace->lookupList('t1', 2)); } public function testHashSpaceLookupListNoTargets(): void { $this->expectException('Flexihash\Exception'); $this->expectExceptionMessage('No targets exist'); $hashSpace = new Flexihash(); $hashSpace->lookup('t1'); } public function testHashSpaceLookupListNo(): void { $this->expectException('Flexihash\Exception'); $this->expectExceptionMessage('Invalid count requested'); $hashSpace = new Flexihash(); $hashSpace->lookupList('t1', 0); } public function testHashSpaceLookupsAreValidTargets(): void { $targets = []; foreach (range(1, 10) as $i) { $targets [] = "target$i"; } $hashSpace = new Flexihash(); $hashSpace->addTargets($targets); foreach (range(1, 10) as $i) { $this->assertTrue( in_array($hashSpace->lookup("r$i"), $targets), 'target must be in list of targets' ); } } public function testHashSpaceConsistentLookupsAfterAddingAndRemoving(): void { $hashSpace = new Flexihash(); foreach (range(1, 10) as $i) { $hashSpace->addTarget("target$i"); } $results1 = []; foreach (range(1, 100) as $i) { $results1 [] = $hashSpace->lookup("t$i"); } $hashSpace ->addTarget('new-target') ->removeTarget('new-target') ->addTarget('new-target') ->removeTarget('new-target') ; $results2 = []; foreach (range(1, 100) as $i) { $results2 [] = $hashSpace->lookup("t$i"); } // This is probably optimistic, as adding/removing a target may // clobber existing targets and is not expected to restore them. $this->assertEquals($results1, $results2); } public function testHashSpaceConsistentLookupsWithNewInstance(): void { $hashSpace1 = new Flexihash(); foreach (range(1, 10) as $i) { $hashSpace1->addTarget("target$i"); } $results1 = []; foreach (range(1, 100) as $i) { $results1 [] = $hashSpace1->lookup("t$i"); } $hashSpace2 = new Flexihash(); foreach (range(1, 10) as $i) { $hashSpace2->addTarget("target$i"); } $results2 = []; foreach (range(1, 100) as $i) { $results2 [] = $hashSpace2->lookup("t$i"); } $this->assertEquals($results1, $results2); } public function testGetMultipleTargets(): void { $hashSpace = new Flexihash(); foreach (range(1, 10) as $i) { $hashSpace->addTarget("target$i"); } $targets = $hashSpace->lookupList('resource', 2); $this->assertIsArray($targets); $this->assertEquals(count($targets), 2); $this->assertNotEquals($targets[0], $targets[1]); } public function testGetMultipleTargetsWithOnlyOneTarget(): void { $hashSpace = new Flexihash(); $hashSpace->addTarget('single-target'); $targets = $hashSpace->lookupList('resource', 2); $this->assertIsArray($targets); $this->assertEquals(count($targets), 1); $this->assertEquals($targets[0], 'single-target'); } public function testGetMoreTargetsThanExist(): void { $hashSpace = new Flexihash(); $hashSpace->addTarget('target1'); $hashSpace->addTarget('target2'); $targets = $hashSpace->lookupList('resource', 4); $this->assertIsArray($targets); $this->assertEquals(count($targets), 2); $this->assertNotEquals($targets[0], $targets[1]); } public function testGetMultipleTargetsNeedingToLoopToStart(): void { $mockHasher = new MockHasher(); $hashSpace = new Flexihash($mockHasher, 1); $mockHasher->setHashValue(10); $hashSpace->addTarget('t1'); $mockHasher->setHashValue(20); $hashSpace->addTarget('t2'); $mockHasher->setHashValue(30); $hashSpace->addTarget('t3'); $mockHasher->setHashValue(40); $hashSpace->addTarget('t4'); $mockHasher->setHashValue(50); $hashSpace->addTarget('t5'); $mockHasher->setHashValue(35); $targets = $hashSpace->lookupList('resource', 4); $this->assertEquals($targets, ['t4', 't5', 't1', 't2']); } public function testGetMultipleTargetsWithoutGettingAnyBeforeLoopToStart(): void { $mockHasher = new MockHasher(); $hashSpace = new Flexihash($mockHasher, 1); $mockHasher->setHashValue(10); $hashSpace->addTarget('t1'); $mockHasher->setHashValue(20); $hashSpace->addTarget('t2'); $mockHasher->setHashValue(30); $hashSpace->addTarget('t3'); $mockHasher->setHashValue(100); $targets = $hashSpace->lookupList('resource', 2); $this->assertEquals($targets, ['t1', 't2']); } public function testGetMultipleTargetsWithoutNeedingToLoopToStart(): void { $mockHasher = new MockHasher(); $hashSpace = new Flexihash($mockHasher, 1); $mockHasher->setHashValue(10); $hashSpace->addTarget('t1'); $mockHasher->setHashValue(20); $hashSpace->addTarget('t2'); $mockHasher->setHashValue(30); $hashSpace->addTarget('t3'); $mockHasher->setHashValue(15); $targets = $hashSpace->lookupList('resource', 2); $this->assertEquals($targets, ['t2', 't3']); } public function testFallbackPrecedenceWhenServerRemoved(): void { $mockHasher = new MockHasher(); $hashSpace = new Flexihash($mockHasher, 1); $mockHasher->setHashValue(10); $hashSpace->addTarget('t1'); $mockHasher->setHashValue(20); $hashSpace->addTarget('t2'); $mockHasher->setHashValue(30); $hashSpace->addTarget('t3'); $mockHasher->setHashValue(15); $this->assertEquals($hashSpace->lookup('resource'), 't2'); $this->assertEquals( $hashSpace->lookupList('resource', 3), ['t2', 't3', 't1'] ); $hashSpace->removeTarget('t2'); $this->assertEquals($hashSpace->lookup('resource'), 't3'); $this->assertEquals( $hashSpace->lookupList('resource', 3), ['t3', 't1'] ); $hashSpace->removeTarget('t3'); $this->assertEquals($hashSpace->lookup('resource'), 't1'); $this->assertEquals( $hashSpace->lookupList('resource', 3), ['t1'] ); } /** * Does the __toString method behave as we expect. * * @author Dom Morgan */ public function testHashSpaceToString(): void { $mockHasher = new MockHasher(); $hashSpace = new Flexihash($mockHasher, 1); $hashSpace->addTarget('t1'); $hashSpace->addTarget('t2'); $this->assertSame( $hashSpace->__toString(), 'Flexihash\Flexihash{targets:[t1,t2]}' ); } } ================================================ FILE: tests/Hasher/HasherTest.php ================================================ hash('test'); $result2 = $hasher->hash('test'); $result3 = $hasher->hash('different'); $this->assertEquals($result1, $result2); $this->assertNotEquals($result1, $result3); // fragile but worthwhile } public function testMd5Hash(): void { $hasher = new Md5Hasher(); $result1 = $hasher->hash('test'); $result2 = $hasher->hash('test'); $result3 = $hasher->hash('different'); $this->assertEquals($result1, $result2); $this->assertNotEquals($result1, $result3); // fragile but worthwhile } } ================================================ FILE: tests/Hasher/MockHasher.php ================================================ hashValue = $hash; } public function hash($value) { return $this->hashValue; } }