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