packages/requirements/tests/Unit/Source/TextSourceTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Source;
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\Model\Source;
13use Requirements\Source\LocalFile;
14use Requirements\Source\ResourceLoader;
15use Requirements\Source\ResourceLocation;
16use Requirements\Source\TextSource;
17use Requirements\Source\Unit;
18use RuntimeException;
19use Tests\Fake\ProjectDirectory;
20use Tests\Fake\SourceDocuments;
21
22#[CoversClass(TextSource::class)]
23#[UsesClass(LocalFile::class)]
24#[UsesClass(ResourceLoader::class)]
25#[UsesClass(ResourceLocation::class)]
26#[UsesClass(Source::class)]
27#[UsesClass(Unit::class)]
28#[Small]
29final class TextSourceTest extends TestCase
30{
31 public function testSelectStableDistinctUnits(): void
32 {
33 $project = new ProjectDirectory();
34 $project->put('source.md', SourceDocuments::MARKDOWN);
35 $source = new Source('test', 'source.md', 'text', 'lines:1-5');
36 $extension = new TextSource();
37 $units = $extension->select($source, 'lines:1-5', $project->directory, false);
38
39 self::assertCount(3, $units);
40 self::assertCount(3, array_unique(array_map(static fn (Unit $unit): string => $unit->location, $units)));
41 self::assertSame($units[0]->location, $extension->select($source, 'lines:1-5', $project->directory, false)[0]->location);
42 }
43
44 /**
45 * @param list<Unit> $expected
46 */
47 #[DataProvider('providerSelectUnits')]
48 public function testSelectLocatesNonblankLines(string $document, string $selector, array $expected): void
49 {
50 $project = new ProjectDirectory();
51 $project->put('source.txt', $document);
52
53 self::assertEquals($expected, (new TextSource())->select(new Source('test', 'source.txt', 'text', $selector), $selector, $project->directory, false));
54 }
55
56 /**
57 * @return array<string, array{string, string, list<Unit>}>
58 */
59 public static function providerSelectUnits(): array
60 {
61 return [
62 'range' => [SourceDocuments::MARKDOWN, 'lines:1-5', [
63 new Unit('line:1', '# Rules'),
64 new Unit('line:3', 'First **rule**.'),
65 new Unit('line:5', 'Second rule.'),
66 ]],
67 'single line' => [SourceDocuments::MARKDOWN, 'lines:3', [new Unit('line:3', 'First **rule**.')]],
68 'one-line range' => [SourceDocuments::MARKDOWN, 'lines:3-3', [new Unit('line:3', 'First **rule**.')]],
69 'blank line' => [SourceDocuments::MARKDOWN, 'lines:2', []],
70 'range ending at the last line' => [SourceDocuments::MARKDOWN, 'lines:5-6', [new Unit('line:5', 'Second rule.')]],
71 'normalized' => [" First\t rule. \n", 'lines:1', [new Unit('line:1', 'First rule.')]],
72 'line endings' => ["First.\r\nSecond.\rThird.\nFourth.", 'lines:1-4', [
73 new Unit('line:1', 'First.'),
74 new Unit('line:2', 'Second.'),
75 new Unit('line:3', 'Third.'),
76 new Unit('line:4', 'Fourth.'),
77 ]],
78 'double-digit lines' => [str_repeat("Rule.\n", 11), 'lines:10-11', [new Unit('line:10', 'Rule.'), new Unit('line:11', 'Rule.')]],
79 ];
80 }
81
82 #[DataProvider('providerSelectMalformed')]
83 public function testSelectRejectsMalformedSelectors(string $selector): void
84 {
85 $project = new ProjectDirectory();
86
87 $this->expectException(RuntimeException::class);
88 $this->expectExceptionMessage('Text selectors use lines:START-END or lines:NUMBER.');
89 (new TextSource())->select(new Source('test', 'missing.txt', 'text', $selector), $selector, $project->directory, false);
90 }
91
92 /**
93 * @return array<string, array{string}>
94 */
95 public static function providerSelectMalformed(): array
96 {
97 return [
98 'zero' => ['lines:0'],
99 'leading zero' => ['lines:01'],
100 'open range' => ['lines:1-'],
101 'zero end' => ['lines:1-0'],
102 'singular' => ['line:1'],
103 'prefix' => ['all lines:1'],
104 'suffix' => ['lines:1-2x'],
105 'trailing newline' => ["lines:1\n"],
106 'css' => ['main p'],
107 ];
108 }
109
110 #[DataProvider('providerSelectOutside')]
111 public function testSelectRejectsRangesOutsideTheSource(string $selector): void
112 {
113 $project = new ProjectDirectory();
114 $project->put('source.md', SourceDocuments::MARKDOWN);
115
116 $this->expectException(RuntimeException::class);
117 $this->expectExceptionMessage('Text line range is outside the source.');
118 (new TextSource())->select(new Source('test', 'source.md', 'text', $selector), $selector, $project->directory, false);
119 }
120
121 /**
122 * @return array<string, array{string}>
123 */
124 public static function providerSelectOutside(): array
125 {
126 return [
127 'reversed' => ['lines:3-2'],
128 'past the end' => ['lines:1-7'],
129 'single line past the end' => ['lines:7'],
130 ];
131 }
132
133 public function testSelectReadsThroughTheGivenLoader(): void
134 {
135 $project = new ProjectDirectory();
136 $project->put('source.txt', 'Original.');
137 $loader = new ResourceLoader();
138 $loader->fetch($project->path('source.txt'));
139 $project->put('source.txt', 'Changed.');
140
141 self::assertEquals([new Unit('line:1', 'Original.')], (new TextSource($loader))->select(new Source('test', 'source.txt', 'text', 'lines:1'), 'lines:1', $project->directory, false));
142 }
143}
144