Skip to content

Commit 0c7ba55

Browse files
feature #61807 [Uid] Add MockUuidFactory for deterministic UUID generation in tests (momito69)
This PR was squashed before being merged into the 7.4 branch. Discussion ---------- [Uid] Add `MockUuidFactory` for deterministic UUID generation in tests | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | Fix #60978 | License | MIT Similar to the Clock component's ClockInterface and MockClock, I've made a MockUuidFactory and implemented an UuidFactoryInterface to have a way to mock UUID generation for testing purposes. cc `@OskarStark` , `@alexandre`-daubois Commits ------- adb84af [Uid] Add `MockUuidFactory` for deterministic UUID generation in tests
2 parents e96b2e5 + adb84af commit 0c7ba55

File tree

3 files changed

+452
-0
lines changed

3 files changed

+452
-0
lines changed

src/Symfony/Component/Uid/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add microsecond precision to UUIDv7
88
* Default to `UuidV7` when using `UuidFactory`
9+
* Add `MockUuidFactory` to allow deterministic and mockable UUID generation for testing purposes
910

1011
7.3
1112
---
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Uid\Factory;
13+
14+
use Symfony\Component\Uid\Exception\InvalidArgumentException;
15+
use Symfony\Component\Uid\Exception\LogicException;
16+
use Symfony\Component\Uid\TimeBasedUidInterface;
17+
use Symfony\Component\Uid\Uuid;
18+
use Symfony\Component\Uid\UuidV1;
19+
use Symfony\Component\Uid\UuidV3;
20+
use Symfony\Component\Uid\UuidV4;
21+
use Symfony\Component\Uid\UuidV5;
22+
use Symfony\Component\Uid\UuidV6;
23+
24+
class MockUuidFactory extends UuidFactory
25+
{
26+
private \Iterator $sequence;
27+
28+
/**
29+
* @param iterable<string|Uuid> $uuids
30+
*/
31+
public function __construct(
32+
iterable $uuids,
33+
private Uuid|string|null $timeBasedNode = null,
34+
private Uuid|string|null $nameBasedNamespace = null,
35+
) {
36+
$this->sequence = match (true) {
37+
\is_array($uuids) => new \ArrayIterator($uuids),
38+
$uuids instanceof \Iterator => $uuids,
39+
$uuids instanceof \Traversable => new \IteratorIterator($uuids),
40+
};
41+
}
42+
43+
public function create(): Uuid
44+
{
45+
if (!$this->sequence->valid()) {
46+
throw new LogicException('No more UUIDs in sequence.');
47+
}
48+
$uuid = $this->sequence->current();
49+
$this->sequence->next();
50+
51+
return match (true) {
52+
$uuid instanceof Uuid => $uuid,
53+
\is_string($uuid) => Uuid::fromString($uuid),
54+
default => throw new InvalidArgumentException(\sprintf('Next UUID in sequence is not a valid UUID string or object: "%s" given.', get_debug_type($uuid))),
55+
};
56+
}
57+
58+
public function randomBased(): RandomBasedUuidFactory
59+
{
60+
return new class($this->create(...)) extends RandomBasedUuidFactory {
61+
public function __construct(
62+
private \Closure $create,
63+
) {
64+
}
65+
66+
public function create(): UuidV4
67+
{
68+
if (!($uuid = ($this->create)()) instanceof UuidV4) {
69+
throw new InvalidArgumentException(\sprintf('Next UUID in sequence is not a UuidV4: "%s" given.', get_debug_type($uuid)));
70+
}
71+
72+
return $uuid;
73+
}
74+
};
75+
}
76+
77+
public function timeBased(Uuid|string|null $node = null): TimeBasedUuidFactory
78+
{
79+
if (\is_string($node ??= $this->timeBasedNode)) {
80+
$node = Uuid::fromString($node);
81+
}
82+
83+
return new class($this->create(...), $node) extends TimeBasedUuidFactory {
84+
public function __construct(
85+
private \Closure $create,
86+
private ?Uuid $node = null,
87+
) {
88+
}
89+
90+
public function create(?\DateTimeInterface $time = null): Uuid&TimeBasedUidInterface
91+
{
92+
$uuid = ($this->create)();
93+
94+
if (!($uuid instanceof Uuid && $uuid instanceof TimeBasedUidInterface)) {
95+
throw new InvalidArgumentException(\sprintf('Next UUID in sequence is not a Uuid and TimeBasedUidInterface: "%s" given.', get_debug_type($uuid)));
96+
}
97+
98+
if (null !== $time && $uuid->getDateTime() !== $time) {
99+
throw new InvalidArgumentException(\sprintf('Next UUID in sequence does not match the expected time: "%s" != "%s".', $uuid->getDateTime()->format('@U.uT'), $time->format('@U.uT')));
100+
}
101+
102+
if (null !== $this->node && ($uuid instanceof UuidV1 || $uuid instanceof UuidV6) && $uuid->getNode() !== substr($this->node->toRfc4122(), -12)) {
103+
throw new InvalidArgumentException(\sprintf('Next UUID in sequence does not match the expected node: "%s" != "%s".', $uuid->getNode(), substr($this->node->toRfc4122(), -12)));
104+
}
105+
106+
return $uuid;
107+
}
108+
};
109+
}
110+
111+
public function nameBased(Uuid|string|null $namespace = null): NameBasedUuidFactory
112+
{
113+
if (null === $namespace ??= $this->nameBasedNamespace) {
114+
throw new LogicException(\sprintf('A namespace should be defined when using "%s()".', __METHOD__));
115+
}
116+
117+
return new class($this->create(...), $namespace) extends NameBasedUuidFactory {
118+
public function __construct(
119+
private \Closure $create,
120+
private Uuid|string $namespace,
121+
) {
122+
}
123+
124+
public function create(string $name): UuidV5|UuidV3
125+
{
126+
if (!($uuid = ($this->create)()) instanceof UuidV5 && !$uuid instanceof UuidV3) {
127+
throw new InvalidArgumentException(\sprintf('Next UUID in sequence is not a UuidV5 or UuidV3: "%s".', get_debug_type($uuid)));
128+
}
129+
130+
$factory = new UuidFactory(nameBasedClass: $uuid::class, nameBasedNamespace: $this->namespace);
131+
132+
if ($uuid->toRfc4122() !== $expectedUuid = $factory->nameBased()->create($name)->toRfc4122()) {
133+
throw new InvalidArgumentException(\sprintf('Next UUID in sequence does not match the expected named UUID: "%s" != "%s".', $uuid->toRfc4122(), $expectedUuid));
134+
}
135+
136+
return $uuid;
137+
}
138+
};
139+
}
140+
}

0 commit comments

Comments
 (0)