packages/requirements/tests/Unit/Report/EvidenceMatcherTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Report;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\Small;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use Requirements\Input\InvalidInputException;
13use Requirements\Model\Excerpt;
14use Requirements\Model\Source;
15use Requirements\Report\EvidenceMatcher;
16use Requirements\Source\DomSource;
17use Requirements\Source\JsonSource;
18use Requirements\Source\LocalFile;
19use Requirements\Source\Registry;
20use Requirements\Source\ResourceLoader;
21use Requirements\Source\ResourceLocation;
22use Requirements\Source\TextFragment;
23use Requirements\Source\TextSource;
24use Requirements\Source\Unit;
25use RuntimeException;
26use Tests\Fake\MemorySource;
27use Tests\Fake\ProjectDirectory;
28
29#[CoversClass(EvidenceMatcher::class)]
30#[UsesClass(Excerpt::class)]
31#[UsesClass(Source::class)]
32#[UsesClass(Registry::class)]
33#[UsesClass(DomSource::class)]
34#[UsesClass(JsonSource::class)]
35#[UsesClass(TextSource::class)]
36#[UsesClass(ResourceLoader::class)]
37#[UsesClass(Unit::class)]
38#[UsesClass(LocalFile::class)]
39#[UsesClass(ResourceLocation::class)]
40#[UsesClass(TextFragment::class)]
41#[Small]
42final class EvidenceMatcherTest extends TestCase
43{
44    #[DataProvider('providerMatchingQuotes')]
45    public function testMatchReturnsTheKeyOfTheQuotedUnit(string $quote): void
46    {
47        $source = new Source('memory', 'memory:message', 'memory', '*');
48        $key = (new Unit('message:1', 'A service message.'))->key($source);
49        $matcher = new EvidenceMatcher(new Registry(['memory' => MemorySource::class]), '/', false);
50        self::assertSame($key, $matcher->match($source, new Excerpt('message', $quote), ['memory' => ['other', $key]]));
51    }
52
53    /**
54     * @return array<string, array{string}>
55     */
56    public static function providerMatchingQuotes(): array
57    {
58        return [
59            'exact' => ['A service message.'],
60            'whitespace differs' => ["  A\n\tservice\u{00a0} message. "],
61        ];
62    }
63
64    public function testMatchRejectsASelectorMatchingNoUnit(): void
65    {
66        $project = new ProjectDirectory();
67        $source = new Source('manual', 'source.html', 'html', 'main p');
68        $matcher = new EvidenceMatcher(new Registry(), $project->directory, false);
69        $this->expectException(RuntimeException::class);
70        $this->expectExceptionMessage('Evidence must select exactly one unit: #missing');
71        $matcher->match($source, new Excerpt('#missing', 'Nothing.'), ['manual' => []]);
72    }
73
74    public function testMatchRejectsASelectorMatchingSeveralUnits(): void
75    {
76        $project = new ProjectDirectory();
77        $source = new Source('manual', 'source.html', 'html', 'main p');
78        $matcher = new EvidenceMatcher(new Registry(), $project->directory, false);
79        $this->expectException(RuntimeException::class);
80        $this->expectExceptionMessage('Evidence must select exactly one unit: main p');
81        $matcher->match($source, new Excerpt('main p', 'Names shall start with a letter.'), ['manual' => []]);
82    }
83
84    public function testMatchRejectsAUnitOutsideTheScope(): void
85    {
86        $source = new Source('memory', 'memory:message', 'memory', '*');
87        $matcher = new EvidenceMatcher(new Registry(['memory' => MemorySource::class]), '/', false);
88        $this->expectException(RuntimeException::class);
89        $this->expectExceptionMessage('Evidence is outside the declared scope: message');
90        $matcher->match($source, new Excerpt('message', 'A service message.'), ['memory' => [], 'other' => [(new Unit('message:1', 'A service message.'))->key($source)]]);
91    }
92
93    public function testMatchRejectsAPartialQuotation(): void
94    {
95        $source = new Source('memory', 'memory:message', 'memory', '*');
96        $matcher = new EvidenceMatcher(new Registry(['memory' => MemorySource::class]), '/', false);
97        $this->expectException(RuntimeException::class);
98        $this->expectExceptionMessage('Quotation differs from the complete source unit: message');
99        $matcher->match($source, new Excerpt('message', 'A service'), ['memory' => [(new Unit('message:1', 'A service message.'))->key($source)]]);
100    }
101
102    public function testMatchRejectsAnUnknownFormat(): void
103    {
104        $source = new Source('manual', 'source.txt', 'unknown', '*');
105        $matcher = new EvidenceMatcher(new Registry(), '/', false);
106        $this->expectException(InvalidInputException::class);
107        $this->expectExceptionMessage('Unknown source extension: unknown');
108        $matcher->match($source, new Excerpt('line:1', 'Text.'), ['manual' => []]);
109    }
110
111    #[DataProvider('providerReadingModes')]
112    public function testMatchReadsTheSnapshotUnlessLive(bool $live, string $quote): void
113    {
114        $project = new ProjectDirectory();
115        $project->put('current.txt', 'New text.');
116        $project->put('snapshot.txt', 'Old text.');
117        $source = new Source('notes', 'current.txt', 'text', 'lines:1', 'snapshot.txt', hash('sha256', 'Old text.'));
118        $key = (new Unit('line:1', $quote))->key($source);
119        $matcher = new EvidenceMatcher(new Registry(), $project->directory, $live);
120        self::assertSame($key, $matcher->match($source, new Excerpt('lines:1', $quote), ['notes' => [$key]]));
121    }
122
123    /**
124     * @return array<string, array{bool, string}>
125     */
126    public static function providerReadingModes(): array
127    {
128        return [
129            'snapshot' => [false, 'Old text.'],
130            'live' => [true, 'New text.'],
131        ];
132    }
133
134    #[DataProvider('providerStaleQuotes')]
135    public function testMatchRejectsTheTextOfTheOtherReadingMode(bool $live, string $quote): void
136    {
137        $project = new ProjectDirectory();
138        $project->put('current.txt', 'New text.');
139        $project->put('snapshot.txt', 'Old text.');
140        $source = new Source('notes', 'current.txt', 'text', 'lines:1', 'snapshot.txt', hash('sha256', 'Old text.'));
141        $matcher = new EvidenceMatcher(new Registry(), $project->directory, $live);
142        $this->expectException(RuntimeException::class);
143        $this->expectExceptionMessage('Quotation differs from the complete source unit: lines:1');
144        $matcher->match($source, new Excerpt('lines:1', $quote), ['notes' => [(new Unit('line:1', $quote))->key($source)]]);
145    }
146
147    /**
148     * @return array<string, array{bool, string}>
149     */
150    public static function providerStaleQuotes(): array
151    {
152        return [
153            'snapshot' => [false, 'New text.'],
154            'live' => [true, 'Old text.'],
155        ];
156    }
157}
158