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