packages/requirements/tests/Unit/Config/Markdown/Profile/SectionBlocksTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Config\Markdown\Profile;
6
7use League\CommonMark\Environment\Environment;
8use League\CommonMark\Exception\CommonMarkException;
9use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
10use League\CommonMark\Parser\MarkdownParser;
11use PHPUnit\Framework\Attributes\CoversClass;
12use PHPUnit\Framework\Attributes\DataProvider;
13use PHPUnit\Framework\Attributes\Small;
14use PHPUnit\Framework\Attributes\UsesClass;
15use PHPUnit\Framework\TestCase;
16use Requirements\Config\Markdown\Nodes;
17use Requirements\Config\Markdown\Profile\BlockKind;
18use Requirements\Config\Markdown\Profile\Occurrences;
19use Requirements\Config\Markdown\Profile\SectionBlocks;
20use Requirements\Config\Markdown\Profile\TextConstraint;
21use Requirements\Input\Fields;
22use Requirements\Input\InvalidInputException;
23
24#[CoversClass(SectionBlocks::class)]
25#[UsesClass(BlockKind::class)]
26#[UsesClass(Fields::class)]
27#[UsesClass(Nodes::class)]
28#[UsesClass(Occurrences::class)]
29#[UsesClass(TextConstraint::class)]
30#[Small]
31final class SectionBlocksTest extends TestCase
32{
33 /**
34 * @param array<string, mixed> $schema
35 * @throws CommonMarkException
36 */
37 #[DataProvider('providerAccepted')]
38 public function testCheckAcceptsMatchingBlocks(string $markdown, array $schema): void
39 {
40 $environment = new Environment();
41 $environment->addExtension(new CommonMarkCoreExtension());
42 $nodes = array_values([...(new MarkdownParser($environment))->parse($markdown)->children()]);
43 SectionBlocks::check($nodes, $schema, 'doc.md');
44 $this->addToAssertionCount(1);
45 }
46
47 /**
48 * @return array<string, array{string, array<string, mixed>}>
49 */
50 public static function providerAccepted(): array
51 {
52 return [
53 'bundled section' => ["The reader shall emit a tree.\n\n> Quoted.\n\n- one\n", ['blocks' => [['type' => ['paragraph', 'bullet-list', 'quote'], 'minContains' => 1]], 'additionalBlocks' => false]],
54 'type as a string' => ["> Quoted.\n", ['blocks' => [['type' => 'quote']]]],
55 'entries in order' => ["A.\n\n- b\n", ['blocks' => [['type' => 'paragraph', 'maxContains' => 1], ['type' => 'bullet-list', 'minContains' => 0]], 'additionalBlocks' => false]],
56 'entry repeats' => ["A.\n\nB.\n\nC.\n", ['blocks' => [['type' => 'paragraph', 'minContains' => 3]], 'additionalBlocks' => false]],
57 'first matching entry only' => ["A.\n", ['blocks' => [['type' => 'paragraph'], ['type' => 'paragraph', 'minContains' => 0, 'maxContains' => 0]], 'additionalBlocks' => false]],
58 'text constraint selects entry' => ["A.\n\nB.\n", ['blocks' => [['type' => 'paragraph', 'text' => ['pattern' => '^A'], 'maxContains' => 1], ['type' => 'paragraph', 'maxContains' => 1]], 'additionalBlocks' => false]],
59 'text constraint sees nothing in a list' => ["- a\n", ['blocks' => [['type' => 'bullet-list', 'text' => ['const' => '']]], 'additionalBlocks' => false]],
60 'text constraint reads paragraph text' => ["A *b*.\n", ['blocks' => [['type' => 'paragraph', 'text' => ['const' => 'A b.']]], 'additionalBlocks' => false]],
61 'additional blocks allowed by default' => ["A.\n\n```\ncode\n```\n", ['blocks' => [['type' => 'paragraph']]]],
62 'additional blocks allowed explicitly' => ["```\ncode\n```\n\nA.\n", ['blocks' => [['type' => 'paragraph']], 'additionalBlocks' => true]],
63 'no blocks and no entries' => ['', ['blocks' => []]],
64 'no blocks with optional entry' => ['', ['blocks' => [['type' => 'paragraph', 'minContains' => 0]]]],
65 ];
66 }
67
68 /**
69 * @throws CommonMarkException
70 */
71 #[DataProvider('providerUnexpected')]
72 public function testCheckRejectsUnexpectedBlock(string $markdown): void
73 {
74 $environment = new Environment();
75 $environment->addExtension(new CommonMarkCoreExtension());
76 $nodes = array_values([...(new MarkdownParser($environment))->parse($markdown)->children()]);
77 $this->expectException(InvalidInputException::class);
78 $this->expectExceptionMessage('doc.md: unexpected document-schema block.');
79 SectionBlocks::check($nodes, ['blocks' => [['type' => 'paragraph', 'maxContains' => 1], ['type' => 'bullet-list', 'minContains' => 0, 'text' => ['pattern' => 'a']]], 'additionalBlocks' => false], 'doc.md');
80 }
81
82 /**
83 * @return array<string, array{string}>
84 */
85 public static function providerUnexpected(): array
86 {
87 return [
88 'kind matching no entry' => ["A.\n\n```\ncode\n```\n"],
89 'earlier entry after a later one' => ["A.\n\n- b\n\nC.\n"],
90 'text constraint on a list' => ["A.\n\n- a\n"],
91 ];
92 }
93
94 /**
95 * @throws CommonMarkException
96 */
97 #[DataProvider('providerOccurrences')]
98 public function testCheckRejectsEntryOccurrences(string $markdown, string $message): void
99 {
100 $environment = new Environment();
101 $environment->addExtension(new CommonMarkCoreExtension());
102 $nodes = array_values([...(new MarkdownParser($environment))->parse($markdown)->children()]);
103 $this->expectException(InvalidInputException::class);
104 $this->expectExceptionMessage($message);
105 SectionBlocks::check($nodes, ['blocks' => [['type' => 'paragraph', 'text' => ['pattern' => '^A'], 'maxContains' => 1], ['type' => ['paragraph', 'quote'], 'maxContains' => 2]]], 'doc.md');
106 }
107
108 /**
109 * @return array<string, array{string, string}>
110 */
111 public static function providerOccurrences(): array
112 {
113 return [
114 'missing section' => ['', 'doc.md: section block 1: document-schema occurrence constraint failed.'],
115 'first entry skipped' => ["B.\n\nA.\n", 'doc.md: section block 1: document-schema occurrence constraint failed.'],
116 'first entry repeated' => ["A.\n\nA2.\n\nB.\n", 'doc.md: section block 1: document-schema occurrence constraint failed.'],
117 'second entry missing' => ["A.\n", 'doc.md: section block 2: document-schema occurrence constraint failed.'],
118 'second entry too often' => ["A.\n\nB.\n\n> C.\n\nD.\n", 'doc.md: section block 2: document-schema occurrence constraint failed.'],
119 ];
120 }
121
122 /**
123 * @param array<string, mixed> $schema
124 * @throws CommonMarkException
125 */
126 #[DataProvider('providerMalformedSchema')]
127 public function testCheckRejectsMalformedSchema(array $schema, string $message): void
128 {
129 $environment = new Environment();
130 $environment->addExtension(new CommonMarkCoreExtension());
131 $nodes = array_values([...(new MarkdownParser($environment))->parse("A.\n")->children()]);
132 $this->expectException(InvalidInputException::class);
133 $this->expectExceptionMessage($message);
134 SectionBlocks::check($nodes, $schema, 'doc.md');
135 }
136
137 /**
138 * @return array<string, array{array<string, mixed>, string}>
139 */
140 public static function providerMalformedSchema(): array
141 {
142 return [
143 'blocks not a list' => [['blocks' => ['first' => ['type' => 'paragraph']]], 'blocks must be a list.'],
144 'entry not a mapping' => [['blocks' => ['paragraph']], 'block must be a mapping.'],
145 'unknown entry key' => [['blocks' => [['type' => 'paragraph', 'header' => []]]], "block: unknown field 'header'."],
146 'type neither string nor list' => [['blocks' => [['type' => 1]]], 'block.type must be a list.'],
147 'text not a mapping' => [['blocks' => [['type' => 'paragraph', 'text' => 'A.']]], 'block.text must be a mapping.'],
148 ];
149 }
150}
151