Lines 0.00% 0 / 64
Methods 0.00% 0 / 20
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 createBackend n/a 0 / 0 n/a 0 / 0 0
 advanceTime n/a 0 / 0 n/a 0 / 0 0
 setUp 0.00% 0 / 1 0.00% 0 / 1 2
 testAMissingKeyReadsAsNull 0.00% 0 / 2 0.00% 0 / 1 2
 testAValueComesBackByteForByte 0.00% 0 / 4 0.00% 0 / 1 2
 testAWriteReplacesWhatWasThere 0.00% 0 / 3 0.00% 0 / 1 2
 testRemovingLeavesNothingAndIsSafeToRepeat 0.00% 0 / 4 0.00% 0 / 1 2
 testATtlExpiresTheValue 0.00% 0 / 6 0.00% 0 / 1 2
 testWritingWithoutATtlClearsOneTheKeyHad 0.00% 0 / 4 0.00% 0 / 1 2
 testCountingCreatesTheKeyItIsMissing 0.00% 0 / 4 0.00% 0 / 1 2
 testCountingKeepsAnExpiry 0.00% 0 / 4 0.00% 0 / 1 2
 testCountingWhatIsNotAWholeNumberIsRefused 0.00% 0 / 2 0.00% 0 / 1 2
 testCountingANumberWithANewlineIsRefused 0.00% 0 / 2 0.00% 0 / 1 2
 testCountingAFloatIsRefused 0.00% 0 / 2 0.00% 0 / 1 2
 testAPrefixScanReturnsOnlyThatPrefix 0.00% 0 / 7 0.00% 0 / 1 2
 testAPrefixScanOfAnEmptyScopeIsEmpty 0.00% 0 / 1 0.00% 0 / 1 2
 testRemovingAPrefixLeavesOtherPrefixesAlone 0.00% 0 / 5 0.00% 0 / 1 2
 testProbingSucceedsAgainstAnEngineThatIsThere 0.00% 0 / 2 0.00% 0 / 1 2
 getPrefix 0.00% 0 / 1 0.00% 0 / 1 2
 getOtherPrefix 0.00% 0 / 1 0.00% 0 / 1 2
 buildPrefixedKey 0.00% 0 / 1 0.00% 0 / 1 2
 [Stewart\Testing\Exception\AssertsReason] assertThrowsReason 0.00% 0 / 8 0.00% 0 / 1 12
14abstract class StoreBackendContract extends TestCase
15{
16    use AssertsReason;
17
18    protected StoreBackend $backendUnderTest;
19
20    abstract protected function createBackend(): StoreBackend;
21
22    abstract protected function advanceTime(Duration $span): void;
23
24    protected function setUp(): void
25    {
26        $this->backendUnderTest = $this->createBackend();
27    }
28
29    public function testAMissingKeyReadsAsNull(): void
30    {
31        self::assertNull($this->backendUnderTest->read($this->buildPrefixedKey('absent')));
32        self::assertFalse($this->backendUnderTest->exists($this->buildPrefixedKey('absent')));
33    }
34
35    public function testAValueComesBackByteForByte(): void
36    {
37        $raw = '{"target":21.5,"mode":"héat/0"}';
38
39        $this->backendUnderTest->write($this->buildPrefixedKey('thermostat'), $raw, null);
40
41        self::assertSame($raw, $this->backendUnderTest->read($this->buildPrefixedKey('thermostat')));
42        self::assertTrue($this->backendUnderTest->exists($this->buildPrefixedKey('thermostat')));
43    }
44
45    public function testAWriteReplacesWhatWasThere(): void
46    {
47        $this->backendUnderTest->write($this->buildPrefixedKey('mode'), '"away"', null);
48        $this->backendUnderTest->write($this->buildPrefixedKey('mode'), '"home"', null);
49
50        self::assertSame('"home"', $this->backendUnderTest->read($this->buildPrefixedKey('mode')));
51    }
52
53    public function testRemovingLeavesNothingAndIsSafeToRepeat(): void
54    {
55        $this->backendUnderTest->write($this->buildPrefixedKey('mode'), '"away"', null);
56        $this->backendUnderTest->remove($this->buildPrefixedKey('mode'));
57        $this->backendUnderTest->remove($this->buildPrefixedKey('mode'));
58
59        self::assertNull($this->backendUnderTest->read($this->buildPrefixedKey('mode')));
60    }
61
62    public function testATtlExpiresTheValue(): void
63    {
64        $this->backendUnderTest->write($this->buildPrefixedKey('seen'), 'true', Duration::milliseconds(50));
65
66        self::assertSame('true', $this->backendUnderTest->read($this->buildPrefixedKey('seen')));
67
68        $this->advanceTime(Duration::milliseconds(150));
69
70        self::assertNull($this->backendUnderTest->read($this->buildPrefixedKey('seen')));
71        self::assertFalse($this->backendUnderTest->exists($this->buildPrefixedKey('seen')));
72        self::assertNotContains($this->buildPrefixedKey('seen'), $this->backendUnderTest->keysWithPrefix($this->getPrefix()));
73    }
74
75    public function testWritingWithoutATtlClearsOneTheKeyHad(): void
76    {
77        $this->backendUnderTest->write($this->buildPrefixedKey('seen'), 'true', Duration::milliseconds(50));
78        $this->backendUnderTest->write($this->buildPrefixedKey('seen'), 'false', null);
79
80        $this->advanceTime(Duration::milliseconds(150));
81
82        self::assertSame('false', $this->backendUnderTest->read($this->buildPrefixedKey('seen')));
83    }
84
85    public function testCountingCreatesTheKeyItIsMissing(): void
86    {
87        self::assertSame(1, $this->backendUnderTest->increment($this->buildPrefixedKey('opens'), 1));
88        self::assertSame(4, $this->backendUnderTest->increment($this->buildPrefixedKey('opens'), 3));
89        self::assertSame(3, $this->backendUnderTest->increment($this->buildPrefixedKey('opens'), -1));
90        self::assertSame('3', $this->backendUnderTest->read($this->buildPrefixedKey('opens')));
91    }
92
93    public function testCountingKeepsAnExpiry(): void
94    {
95        $this->backendUnderTest->write($this->buildPrefixedKey('recent'), '1', Duration::milliseconds(50));
96        $this->backendUnderTest->increment($this->buildPrefixedKey('recent'), 1);
97
98        $this->advanceTime(Duration::milliseconds(150));
99
100        self::assertNull($this->backendUnderTest->read($this->buildPrefixedKey('recent')));
101    }
102
103    public function testCountingWhatIsNotAWholeNumberIsRefused(): void
104    {
105        $this->backendUnderTest->write($this->buildPrefixedKey('mode'), '"away"', null);
106
107        $this->assertThrowsReason(StoreError::ValueNotIncrementable, fn() => $this->backendUnderTest->increment($this->buildPrefixedKey('mode'), 1));
108    }
109
110    public function testCountingANumberWithANewlineIsRefused(): void
111    {
112        $this->backendUnderTest->write($this->buildPrefixedKey('visits'), "5\n", null);
113
114        $this->assertThrowsReason(StoreError::ValueNotIncrementable, fn() => $this->backendUnderTest->increment($this->buildPrefixedKey('visits'), 1));
115    }
116
117    public function testCountingAFloatIsRefused(): void
118    {
119        $this->backendUnderTest->write($this->buildPrefixedKey('target'), '21.5', null);
120
121        $this->assertThrowsReason(StoreError::ValueNotIncrementable, fn() => $this->backendUnderTest->increment($this->buildPrefixedKey('target'), 1));
122    }
123
124    public function testAPrefixScanReturnsOnlyThatPrefix(): void
125    {
126        $this->backendUnderTest->write($this->buildPrefixedKey('a'), '1', null);
127        $this->backendUnderTest->write($this->buildPrefixedKey('b'), '2', null);
128        $this->backendUnderTest->write($this->getPrefix() . 'deeper:c', '3', null);
129        $this->backendUnderTest->write($this->getOtherPrefix() . 'a', '4', null);
130
131        $found = $this->backendUnderTest->keysWithPrefix($this->getPrefix());
132        sort($found);
133
134        self::assertSame([$this->buildPrefixedKey('a'), $this->buildPrefixedKey('b'), $this->getPrefix() . 'deeper:c'], $found);
135    }
136
137    public function testAPrefixScanOfAnEmptyScopeIsEmpty(): void
138    {
139        self::assertSame([], $this->backendUnderTest->keysWithPrefix($this->getPrefix()));
140    }
141
142    public function testRemovingAPrefixLeavesOtherPrefixesAlone(): void
143    {
144        $this->backendUnderTest->write($this->buildPrefixedKey('a'), '1', null);
145        $this->backendUnderTest->write($this->getOtherPrefix() . 'a', '2', null);
146
147        $this->backendUnderTest->removeByPrefix($this->getPrefix());
148
149        self::assertSame([], $this->backendUnderTest->keysWithPrefix($this->getPrefix()));
150        self::assertSame('2', $this->backendUnderTest->read($this->getOtherPrefix() . 'a'));
151    }
152
153    public function testProbingSucceedsAgainstAnEngineThatIsThere(): void
154    {
155        $this->backendUnderTest->probe();
156
157        $this->addToAssertionCount(1);
158    }
159
160    protected function getPrefix(): string
161    {
162        return 'stewart:app:heating:';
163    }
164
165    protected function getOtherPrefix(): string
166    {
167        return 'stewart:global:';
168    }
169
170    private function buildPrefixedKey(string $key): string
171    {
172        return $this->getPrefix() . $key;
173    }
174}

From Stewart\Testing\Exception\AssertsReason

13trait AssertsReason
14{
15    /**
16     * @param Closure(): mixed $action
17     * @return StewartException<ExceptionReason>
18     */
19    protected function assertThrowsReason(ExceptionReason $reason, Closure $action): StewartException
20    {
21        $exceptionClass = preg_replace('/Error$/', 'Exception', $reason::class);
22        \assert(\is_string($exceptionClass) && is_subclass_of($exceptionClass, StewartException::class));
23
24        try {
25            $action();
26        } catch (StewartException $e) {
27            self::assertInstanceOf($exceptionClass, $e);
28            self::assertSame($reason, $e->reason, $e->getMessage());
29
30            return $e;
31        }
32
33        self::fail(\sprintf('Expected %s to be thrown with reason %s.', $exceptionClass, $reason->name));
34    }
35}