2121use function is_dir ;
2222use function is_file ;
2323use function json_decode ;
24+ use function mb_strpos ;
2425use function mkdir ;
2526use function random_bytes ;
2627use function rmdir ;
@@ -40,22 +41,35 @@ class PackageGeneratorTest extends UnitTest
4041 */
4142 private string $ dataFile ;
4243
44+ /**
45+ * Lives alongside `dataFile` in the same throwaway directory.
46+ */
47+ private string $ markdownFile ;
48+
4349 protected function setUp (): void
4450 {
4551 parent ::setUp ();
4652
47- $ this -> dataFile = sprintf (
48- '%s%slight-packages-%s%sdotkernel-packages.json ' ,
53+ $ directory = sprintf (
54+ '%s%slight-packages-%s ' ,
4955 sys_get_temp_dir (),
5056 DIRECTORY_SEPARATOR ,
5157 bin2hex (random_bytes (8 )),
52- DIRECTORY_SEPARATOR
5358 );
59+
60+ $ this ->dataFile = $ directory . DIRECTORY_SEPARATOR . 'dotkernel-packages.json ' ;
61+ $ this ->markdownFile = $ directory . DIRECTORY_SEPARATOR . 'dotkernel-packages-oss-lifecycle.md ' ;
5462 }
5563
5664 protected function tearDown (): void
5765 {
58- foreach ([$ this ->dataFile , $ this ->dataFile . '.tmp ' ] as $ file ) {
66+ $ files = [
67+ $ this ->dataFile ,
68+ $ this ->dataFile . '.tmp ' ,
69+ $ this ->markdownFile ,
70+ $ this ->markdownFile . '.tmp ' ,
71+ ];
72+ foreach ($ files as $ file ) {
5973 if (is_file ($ file )) {
6074 unlink ($ file );
6175 }
@@ -76,6 +90,20 @@ public function testGetDataFileReturnsTheConfiguredPath(): void
7690 $ this ->assertSame ($ this ->dataFile , $ generator ->getDataFile ());
7791 }
7892
93+ public function testGetMarkdownFileReturnsNullWhenNoneIsConfigured (): void
94+ {
95+ $ generator = $ this ->createGenerator ([], []);
96+
97+ $ this ->assertNull ($ generator ->getMarkdownFile ());
98+ }
99+
100+ public function testGetMarkdownFileReturnsTheConfiguredPath (): void
101+ {
102+ $ generator = $ this ->createGenerator ([], [], markdownFile: $ this ->markdownFile );
103+
104+ $ this ->assertSame ($ this ->markdownFile , $ generator ->getMarkdownFile ());
105+ }
106+
79107 /**
80108 * @throws Exception
81109 * @throws JsonException
@@ -155,6 +183,132 @@ public function testWriteLeavesNoTemporaryFileBehind(): void
155183 $ this ->assertFileDoesNotExist ($ this ->dataFile . '.tmp ' );
156184 }
157185
186+ /**
187+ * No `markdownFile` configured is the default, and must not be treated as an error.
188+ *
189+ * @throws Exception
190+ */
191+ public function testWriteDoesNotCreateAMarkdownFileWhenNoneIsConfigured (): void
192+ {
193+ $ this ->createGenerator (
194+ [['name ' => 'dot-cache ' , 'archived ' => false ]],
195+ [$ this ->metadataPath ('dot-cache ' ) => 'osslifecycle=active ' ]
196+ )->write ();
197+
198+ $ this ->assertFileDoesNotExist ($ this ->markdownFile );
199+ }
200+
201+ /**
202+ * @throws Exception
203+ */
204+ public function testWriteCreatesTheMarkdownFileWithFrontMatterAndPackagesGroupedByLifecycle (): void
205+ {
206+ $ generator = $ this ->createGenerator (
207+ [
208+ [
209+ 'name ' => 'dot-cache ' ,
210+ 'description ' => 'Dotkernel cache component ' ,
211+ 'archived ' => false ,
212+ ],
213+ [
214+ 'name ' => 'dot-console ' ,
215+ 'archived ' => true ,
216+ ],
217+ [
218+ 'name ' => 'dot-mail ' ,
219+ 'description ' => 'Mail component ' ,
220+ 'archived ' => false ,
221+ ],
222+ ],
223+ [
224+ $ this ->metadataPath ('dot-cache ' ) => 'osslifecycle=active ' ,
225+ $ this ->composerPath ('dot-cache ' ) => '{"require":{"php":"~8.3.0 || ~8.4.0"}} ' ,
226+ $ this ->metadataPath ('dot-console ' ) => 'osslifecycle=archived ' ,
227+ $ this ->metadataPath ('dot-mail ' ) => 'osslifecycle=maintenance ' ,
228+ ],
229+ markdownFile: $ this ->markdownFile ,
230+ baseUrl: 'https://example.test ' ,
231+ );
232+
233+ $ generator ->write ();
234+
235+ $ markdown = $ this ->markdownContents ();
236+
237+ $ this ->assertStringStartsWith (
238+ "--- \n"
239+ . "title: \"Dotkernel Packages OSS Lifecycle | Support status for every dot-* package \"\n" ,
240+ $ markdown
241+ );
242+ $ this ->assertStringContainsString (
243+ 'canonical_url: "https://example.test/dotkernel-packages-oss-lifecycle/" ' ,
244+ $ markdown
245+ );
246+ $ this ->assertStringContainsString ("language: \"en \"\n--- \n" , $ markdown );
247+ $ this ->assertStringContainsString ('# Dotkernel Packages OSS Lifecycle ' , $ markdown );
248+
249+ // Grouped under a heading per lifecycle, in the same order as the JSON listing.
250+ $ activeAt = mb_strpos ($ markdown , '## Active ' );
251+ $ archivedAt = mb_strpos ($ markdown , '## Archived ' );
252+ $ maintenanceAt = mb_strpos ($ markdown , '## Maintenance ' );
253+ $ cacheAt = mb_strpos ($ markdown , '[dot-cache] ' );
254+ $ consoleAt = mb_strpos ($ markdown , '[dot-console] ' );
255+ $ mailAt = mb_strpos ($ markdown , '[dot-mail] ' );
256+
257+ $ this ->assertNotFalse ($ activeAt );
258+ $ this ->assertNotFalse ($ archivedAt );
259+ $ this ->assertNotFalse ($ maintenanceAt );
260+ $ this ->assertLessThan ($ cacheAt , $ activeAt );
261+ $ this ->assertLessThan ($ mailAt , $ maintenanceAt );
262+ $ this ->assertLessThan ($ consoleAt , $ archivedAt );
263+
264+ $ this ->assertStringContainsString (
265+ '- [dot-cache](https://github.com/dotkernel/dot-cache) (PHP ~8.3.0 || ~8.4.0): '
266+ . 'Dotkernel cache component ' ,
267+ $ markdown
268+ );
269+ $ this ->assertStringContainsString (
270+ '- [dot-console](https://github.com/dotkernel/dot-console) - archived on GitHub ' ,
271+ $ markdown
272+ );
273+ $ this ->assertStringContainsString (
274+ '- [dot-mail](https://github.com/dotkernel/dot-mail): Mail component ' ,
275+ $ markdown
276+ );
277+ }
278+
279+ /**
280+ * `lifecycleLabel()` falls back to a capitalised version of the raw value for anything not in
281+ * `LIFECYCLE_LABELS`.
282+ *
283+ * @throws Exception
284+ */
285+ public function testWriteMarkdownFileFallsBackToACapitalisedLabelForAnUnrecognisedLifecycle (): void
286+ {
287+ $ this ->createGenerator (
288+ [['name ' => 'dot-experimental ' , 'archived ' => false ]],
289+ [$ this ->metadataPath ('dot-experimental ' ) => 'osslifecycle=experimental ' ],
290+ markdownFile: $ this ->markdownFile ,
291+ )->write ();
292+
293+ $ this ->assertStringContainsString ('## Experimental ' , $ this ->markdownContents ());
294+ }
295+
296+ /**
297+ * Same crash-safety guarantee as the JSON listing.
298+ *
299+ * @throws Exception
300+ */
301+ public function testWriteLeavesNoTemporaryMarkdownFileBehind (): void
302+ {
303+ $ this ->createGenerator (
304+ [['name ' => 'dot-cache ' , 'archived ' => false ]],
305+ [$ this ->metadataPath ('dot-cache ' ) => 'osslifecycle=active ' ],
306+ markdownFile: $ this ->markdownFile ,
307+ )->write ();
308+
309+ $ this ->assertFileDoesNotExist ($ this ->markdownFile . '.tmp ' );
310+ }
311+
158312 /**
159313 * @throws Exception
160314 * @throws JsonException
@@ -640,6 +794,8 @@ private function createGenerator(
640794 array $ files ,
641795 array $ ignoreRepos = [],
642796 bool $ includeArchived = true ,
797+ ?string $ markdownFile = null ,
798+ string $ baseUrl = '' ,
643799 ): PackageGenerator {
644800 $ client = $ this ->createMock (GitHubClientInterface::class);
645801
@@ -672,7 +828,9 @@ function (string $path, string $accept = GitHubClientInterface::ACCEPT_JSON) use
672828 $ this ->dataFile ,
673829 self ::ORG ,
674830 $ ignoreRepos ,
675- $ includeArchived
831+ $ includeArchived ,
832+ $ markdownFile ,
833+ $ baseUrl ,
676834 );
677835 }
678836
@@ -725,4 +883,14 @@ private function decodeDataFile(): array
725883
726884 return $ decoded ;
727885 }
886+
887+ private function markdownContents (): string
888+ {
889+ $ this ->assertFileExists ($ this ->markdownFile );
890+
891+ $ contents = file_get_contents ($ this ->markdownFile );
892+ $ this ->assertIsString ($ contents );
893+
894+ return $ contents ;
895+ }
728896}
0 commit comments