Passed
Branch main (7c3c91)
by Eric
15:48
created

ConsistentHashTest   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 289
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 148
dl 0
loc 289
rs 10
c 1
b 0
f 0
wmc 30
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of Esi\ConsistentHash.
7
 *
8
 * (c) Eric Sizemore <[email protected]>
9
 * (c) Paul Annesley <[email protected]>
10
 *
11
 * This source file is subject to the MIT license. For the full copyright and
12
 * license information, please view the LICENSE file that was distributed with
13
 * this source code.
14
 */
15
16
namespace Esi\ConsistentHash\Tests;
17
18
use Esi\ConsistentHash\ConsistentHash;
19
use Esi\ConsistentHash\Exception\TargetException;
20
use Esi\ConsistentHash\Hasher\Crc32Hasher;
21
use Esi\ConsistentHash\Tests\Hasher\MockHasher;
22
use PHPUnit\Framework\Attributes\CoversClass;
23
use PHPUnit\Framework\Attributes\Group;
24
use PHPUnit\Framework\Attributes\UsesClass;
25
use PHPUnit\Framework\TestCase;
26
27
use function range;
28
29
/**
30
 * @internal
31
 */
32
#[CoversClass(ConsistentHash::class)]
33
#[CoversClass(TargetException::class)]
34
#[UsesClass(Crc32Hasher::class)]
35
#[Group('default')]
36
class ConsistentHashTest extends TestCase
37
{
38
    public function testAddTargetAndGetAllTargets(): void
39
    {
40
        $hashSpace = new ConsistentHash();
41
        $hashSpace->addTarget('t-a');
42
        $hashSpace->addTarget('t-b');
43
        $hashSpace->addTarget('t-c');
44
45
        self::assertEquals(['t-a', 't-b', 't-c'], $hashSpace->getAllTargets());
46
    }
47
48
    public function testAddTargetsAndGetAllTargets(): void
49
    {
50
        $targets = ['t-a', 't-b', 't-c'];
51
52
        $hashSpace = new ConsistentHash();
53
        $hashSpace->addTargets($targets);
54
        self::assertEquals($hashSpace->getAllTargets(), $targets);
55
    }
56
57
    public function testAddTargetThrowsExceptionOnDuplicateTarget(): void
58
    {
59
        $hashSpace = new ConsistentHash();
60
        $hashSpace->addTarget('t-a');
61
        $this->expectException(TargetException::class);
62
        $hashSpace->addTarget('t-a');
63
    }
64
65
    public function testFallbackPrecedenceWhenServerRemoved(): void
66
    {
67
        $mockHasher = new MockHasher(0);
68
        $hashSpace  = new ConsistentHash($mockHasher, 1);
69
70
        $mockHasher->setHashValue(10);
71
        $hashSpace->addTarget('t1');
72
73
        $mockHasher->setHashValue(20);
74
        $hashSpace->addTarget('t2');
75
76
        $mockHasher->setHashValue(30);
77
        $hashSpace->addTarget('t3');
78
79
        $mockHasher->setHashValue(15);
80
81
        self::assertEquals('t2', $hashSpace->lookup('resource'));
82
        self::assertEquals(
83
            ['t2', 't3', 't1'],
84
            $hashSpace->lookupList('resource', 3),
85
        );
86
87
        $hashSpace->removeTarget('t2');
88
89
        self::assertEquals('t3', $hashSpace->lookup('resource'));
90
        self::assertEquals(
91
            ['t3', 't1'],
92
            $hashSpace->lookupList('resource', 3),
93
        );
94
95
        $hashSpace->removeTarget('t3');
96
97
        self::assertEquals('t1', $hashSpace->lookup('resource'));
98
        self::assertEquals(
99
            ['t1'],
100
            $hashSpace->lookupList('resource', 3),
101
        );
102
    }
103
    public function testGetAllTargetsEmpty(): void
104
    {
105
        $hashSpace = new ConsistentHash();
106
        self::assertEquals([], $hashSpace->getAllTargets());
107
    }
108
109
    public function testGetMoreTargetsThanExist(): void
110
    {
111
        $hashSpace = new ConsistentHash();
112
        $hashSpace->addTarget('target1');
113
        $hashSpace->addTarget('target2');
114
115
        $targets = $hashSpace->lookupList('resource', 4);
116
117
        self::assertCount(2, $targets);
118
        self::assertNotEquals($targets[0], $targets[1]);
119
    }
120
121
    public function testGetMultipleTargets(): void
122
    {
123
        $hashSpace = new ConsistentHash();
124
        foreach (range(1, 10) as $i) {
125
            $hashSpace->addTarget(\sprintf('target%s', $i));
126
        }
127
128
        $targets = $hashSpace->lookupList('resource', 2);
129
130
        self::assertEquals(2, \count($targets));
131
        self::assertNotEquals($targets[0], $targets[1]);
132
    }
133
134
    public function testGetMultipleTargetsNeedingToLoopToStart(): void
135
    {
136
        $mockHasher = new MockHasher(0);
137
        $hashSpace  = new ConsistentHash($mockHasher, 1);
138
139
        $mockHasher->setHashValue(10);
140
        $hashSpace->addTarget('t1');
141
142
        $mockHasher->setHashValue(20);
143
        $hashSpace->addTarget('t2');
144
145
        $mockHasher->setHashValue(30);
146
        $hashSpace->addTarget('t3');
147
148
        $mockHasher->setHashValue(40);
149
        $hashSpace->addTarget('t4');
150
151
        $mockHasher->setHashValue(50);
152
        $hashSpace->addTarget('t5');
153
154
        $mockHasher->setHashValue(35);
155
        $targets = $hashSpace->lookupList('resource', 4);
156
157
        self::assertEquals(['t4', 't5', 't1', 't2'], $targets);
158
    }
159
160
    public function testGetMultipleTargetsWithOnlyOneTarget(): void
161
    {
162
        $hashSpace = new ConsistentHash();
163
        $hashSpace->addTarget('single-target');
164
165
        $targets = $hashSpace->lookupList('resource', 2);
166
167
        self::assertCount(1, $targets);
168
        self::assertEquals('single-target', $targets[0]);
169
    }
170
171
    public function testGetMultipleTargetsWithoutGettingAnyBeforeLoopToStart(): void
172
    {
173
        $mockHasher = new MockHasher(0);
174
        $hashSpace  = new ConsistentHash($mockHasher, 1);
175
176
        $mockHasher->setHashValue(10);
177
        $hashSpace->addTarget('t1');
178
179
        $mockHasher->setHashValue(20);
180
        $hashSpace->addTarget('t2');
181
182
        $mockHasher->setHashValue(30);
183
        $hashSpace->addTarget('t3');
184
185
        $mockHasher->setHashValue(100);
186
        $targets = $hashSpace->lookupList('resource', 2);
187
188
        self::assertEquals(['t1', 't2'], $targets);
189
    }
190
191
    public function testGetMultipleTargetsWithoutNeedingToLoopToStart(): void
192
    {
193
        $mockHasher = new MockHasher(0);
194
        $hashSpace  = new ConsistentHash($mockHasher, 1);
195
196
        $mockHasher->setHashValue(10);
197
        $hashSpace->addTarget('t1');
198
199
        $mockHasher->setHashValue(20);
200
        $hashSpace->addTarget('t2');
201
202
        $mockHasher->setHashValue(30);
203
        $hashSpace->addTarget('t3');
204
205
        $mockHasher->setHashValue(15);
206
        $targets = $hashSpace->lookupList('resource', 2);
207
208
        self::assertEquals(['t2', 't3'], $targets);
209
    }
210
211
    public function testHashSpaceConsistentLookupsAfterAddingAndRemoving(): void
212
    {
213
        $hashSpace = new ConsistentHash();
214
        foreach (range(1, 10) as $i) {
215
            $hashSpace->addTarget(\sprintf('target%s', $i));
216
        }
217
218
        $results1 = [];
219
        foreach (range(1, 100) as $i) {
220
            $results1[] = $hashSpace->lookup(\sprintf('t%s', $i));
221
        }
222
223
        $hashSpace->addTarget('new-target');
224
        $hashSpace->removeTarget('new-target');
225
        $hashSpace->addTarget('new-target');
226
        $hashSpace->removeTarget('new-target');
227
228
        $results2 = [];
229
        foreach (range(1, 100) as $i) {
230
            $results2[] = $hashSpace->lookup(\sprintf('t%s', $i));
231
        }
232
233
        // This is probably optimistic, as adding/removing a target may
234
        // clobber existing targets and is not expected to restore them.
235
        self::assertEquals($results1, $results2);
236
    }
237
238
    public function testHashSpaceConsistentLookupsWithNewInstance(): void
239
    {
240
        $hashSpace1 = new ConsistentHash();
241
        foreach (range(1, 10) as $i) {
242
            $hashSpace1->addTarget(\sprintf('target%s', $i));
243
        }
244
245
        $results1 = [];
246
        foreach (range(1, 100) as $i) {
247
            $results1[] = $hashSpace1->lookup(\sprintf('t%s', $i));
248
        }
249
250
        $hashSpace2 = new ConsistentHash();
251
        foreach (range(1, 10) as $i) {
252
            $hashSpace2->addTarget(\sprintf('target%s', $i));
253
        }
254
255
        $results2 = [];
256
        foreach (range(1, 100) as $i) {
257
            $results2[] = $hashSpace2->lookup(\sprintf('t%s', $i));
258
        }
259
260
        self::assertEquals($results1, $results2);
261
    }
262
263
    public function testHashSpaceLookupListEmpty(): void
264
    {
265
        $hashSpace = new ConsistentHash();
266
        self::assertEmpty($hashSpace->lookupList('t1', 2));
267
    }
268
269
    public function testHashSpaceLookupListNoTargets(): void
270
    {
271
        $this->expectException(TargetException::class);
272
        $this->expectExceptionMessage('No targets exist');
273
        $hashSpace = new ConsistentHash();
274
        $hashSpace->lookup('t1');
275
    }
276
277
    public function testHashSpaceLookupsAreValidTargets(): void
278
    {
279
        $targets = [];
280
        foreach (range(1, 10) as $i) {
281
            $targets[] = \sprintf('target%s', $i);
282
        }
283
284
        $hashSpace = new ConsistentHash();
285
        $hashSpace->addTargets($targets);
286
287
        foreach (range(1, 10) as $i) {
288
            self::assertTrue(
289
                \in_array($hashSpace->lookup(\sprintf('r%s', $i)), $targets, true),
290
                'target must be in list of targets',
291
            );
292
        }
293
    }
294
295
    public function testHashSpaceRepeatableLookups(): void
296
    {
297
        $hashSpace = new ConsistentHash();
298
        foreach (range(1, 10) as $i) {
299
            $hashSpace->addTarget(\sprintf('target%s', $i));
300
        }
301
302
        self::assertEquals($hashSpace->lookup('t1'), $hashSpace->lookup('t1'));
303
        self::assertEquals($hashSpace->lookup('t2'), $hashSpace->lookup('t2'));
304
    }
305
306
    public function testRemoveTarget(): void
307
    {
308
        $hashSpace = new ConsistentHash();
309
        $hashSpace->addTarget('t-a');
310
        $hashSpace->addTarget('t-b');
311
        $hashSpace->addTarget('t-c');
312
        $hashSpace->removeTarget('t-b');
313
        self::assertEquals(['t-a', 't-c'], $hashSpace->getAllTargets());
314
    }
315
316
    public function testRemoveTargetFailsOnMissingTarget(): void
317
    {
318
        $hashSpace = new ConsistentHash();
319
        $this->expectException(TargetException::class);
320
        $hashSpace->removeTarget('not-there');
321
    }
322
}
323