packages/deriver/tests/Unit/Deriver/Evaluator/Call/StateRegistryTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Call;
6
7use Deriver\Deriver\Evaluator\Call\State\StateSlot;
8use Deriver\Deriver\Evaluator\Call\StateRegistry;
9use Deriver\Deriver\Evaluator\Term;
10use Deriver\Exception\InvalidInputException;
11use PHPUnit\Framework\Attributes\CoversClass;
12use PHPUnit\Framework\Attributes\Small;
13use PHPUnit\Framework\Attributes\UsesClass;
14use PHPUnit\Framework\TestCase;
15
16/**
17 * @covers \Deriver\Deriver\Evaluator\Call\StateRegistry
18 */
19#[CoversClass(StateRegistry::class)]
20#[UsesClass(StateSlot::class)]
21#[UsesClass(\Deriver\Deriver\Evaluator\Scalar\Identity::class)]
22#[UsesClass(\Deriver\Deriver\Evaluator\Domain\Lattice::class)]
23#[UsesClass(Term::class)]
24#[Small]
25final class StateRegistryTest extends TestCase
26{
27 public function testRegisterRejectsDuplicateOrUnqualifiedIdentities(): void
28 {
29 $registry = new StateRegistry([new StateSlot('example.slot')]);
30 $this->expectException(InvalidInputException::class);
31 $registry->register(new StateSlot('example.slot'));
32 }
33 public function testRegisterRejectsAnInitializerOutsideItsType(): void
34 {
35 $this->expectException(InvalidInputException::class);
36 (new StateRegistry([]))->register(new StateSlot('example.slot', 'int', Term::constant('bad')));
37 }
38 public function testRegisterFingerprintsTheLifecycleContract(): void
39 {
40 $copy = new StateRegistry([new StateSlot('example.slot')]);
41 $reset = new StateRegistry([new StateSlot('example.slot', clone: \Deriver\Deriver\Evaluator\Call\State\ClonePolicy::Reset)]);
42 self::assertNotSame($copy->manifest, $reset->manifest);
43 }
44
45 #[\PHPUnit\Framework\Attributes\DataProvider('providerInvalidSlots')]
46 public function testRegisterRejectsMalformedIdentitiesAndLifecycleContracts(StateSlot $slot, string $message): void
47 {
48 $registry = new StateRegistry([]);
49 $this->expectException(InvalidInputException::class);
50 $this->expectExceptionMessage($message);
51 $registry->register($slot);
52 }
53
54 /**
55 * @return iterable<string,array{StateSlot,string}>
56 */
57 public static function providerInvalidSlots(): iterable
58 {
59 foreach (['', 'unqualified', '.name', '1bad.name', 'a.', 'a..b', 'a:b:', 'parameter:reserved', "a.b\n", 'a/b'] as $id) {
60 yield 'identity ' . $id => [new StateSlot($id), 'EVALUATOR_CONFLICT'];
61 }
62 foreach (['', 'resource', 'int|', 'integer', 'int|Box'] as $type) {
63 yield 'type ' . $type => [new StateSlot('example.slot', $type), 'EVALUATOR_CONTRACT_VIOLATION'];
64 }
65 yield 'wrong initial type' => [new StateSlot('example.slot', 'int', Term::constant('value')), 'incompatible state initializer'];
66 }
67
68 public function testRegisterCapturesEveryContractComponentInAStableOrderedManifest(): void
69 {
70 $a = new StateSlot('a.slot', 'int', Term::constant(7));
71 $b = new StateSlot('b:slot', 'string|null', Term::constant(null), \Deriver\Deriver\Evaluator\Call\State\ClonePolicy::Reset, \Deriver\Deriver\Evaluator\Call\State\InvalidationPolicy::Preserve);
72 $first = new StateRegistry([$b, $a]);
73 $second = new StateRegistry([$a, $b]);
74 self::assertSame(['a.slot', 'b:slot'], array_keys($first->slots));
75 self::assertSame(['slot:a.slot', 'slot:b:slot'], array_keys($first->manifest));
76 self::assertSame($a, $first->slots['a.slot']);
77 self::assertSame($b, $first->slots['b:slot']);
78 self::assertSame($second->manifest, $first->manifest);
79 self::assertStringStartsWith('int:copy:havoc:', $first->manifest['slot:a.slot']);
80 self::assertStringStartsWith('string|null:reset:preserve:', $first->manifest['slot:b:slot']);
81 }
82
83 #[\PHPUnit\Framework\Attributes\DataProvider('providerSemanticChanges')]
84 public function testRegisterDistinguishesEverySemanticallyDifferentSlot(StateSlot $before, StateSlot $after): void
85 {
86 self::assertNotSame((new StateRegistry([$before]))->manifest, (new StateRegistry([$after]))->manifest);
87 }
88
89 /**
90 * @return iterable<string,array{StateSlot,StateSlot}>
91 */
92 public static function providerSemanticChanges(): iterable
93 {
94 yield 'type' => [new StateSlot('example.slot', 'int'), new StateSlot('example.slot', 'string')];
95 yield 'clone' => [new StateSlot('example.slot'), new StateSlot('example.slot', clone:\Deriver\Deriver\Evaluator\Call\State\ClonePolicy::Reset)];
96 yield 'invalidation' => [new StateSlot('example.slot'), new StateSlot('example.slot', invalidation:\Deriver\Deriver\Evaluator\Call\State\InvalidationPolicy::Preserve)];
97 yield 'initializer' => [new StateSlot('example.slot', 'int', Term::constant(1)), new StateSlot('example.slot', 'int', Term::constant(2))];
98 yield 'symbolic versus null' => [new StateSlot('example.slot'), new StateSlot('example.slot', initial:Term::constant(null))];
99 yield 'identity' => [new StateSlot('example.one'), new StateSlot('example.two')];
100 }
101
102 #[\PHPUnit\Framework\Attributes\DataProvider('providerValidSlots')]
103 public function testRegisterAcceptsSupportedTypesAndQualifiedIdentities(string $id, string $type): void
104 {
105 $slot = new StateSlot($id, $type);
106 $registry = new StateRegistry([]);
107 $registry->register($slot);
108 self::assertSame([$id => $slot], $registry->slots);
109 self::assertSame([$type . ':copy:havoc:symbolic'], array_values($registry->manifest));
110 }
111
112 /**
113 * @return iterable<string,array{string,string}>
114 */
115 public static function providerValidSlots(): iterable
116 {
117 foreach (['mixed', 'null', 'bool', 'int', 'float', 'string', 'array', 'object', 'int|string|null'] as $type) {
118 yield $type => ['Vendor-1:state_2.value', $type];
119 }
120 }
121}
122