|
1 | 1 | import * as common from '../common/index.mjs'; |
2 | 2 |
|
3 | | -import assert from 'assert'; |
4 | | -import fs from 'fs'; |
5 | | -import path from 'path'; |
| 3 | +import assert from 'node:assert'; |
| 4 | +import fs from 'node:fs'; |
| 5 | +import path from 'node:path'; |
6 | 6 |
|
7 | 7 | if (common.isWindows) { |
8 | 8 | common.skip('`make doc` does not run on Windows'); |
9 | 9 | } |
10 | 10 |
|
11 | | -// This tests that `make doc` generates the documentation properly. |
12 | | -// Note that for this test to pass, `make doc` must be run first. |
| 11 | +// This tests that `make doc` generates the documentation properly: an HTML |
| 12 | +// page and a JSON file for every Markdown file under `doc/api`, and nothing |
| 13 | +// for anything else. Note that for this test to pass, `make doc` must be run |
| 14 | +// first. |
13 | 15 |
|
14 | 16 | const apiURL = new URL('../../out/doc/api/', import.meta.url); |
15 | 17 | const mdURL = new URL('../../doc/api/', import.meta.url); |
16 | | -const allMD = fs.readdirSync(mdURL); |
17 | | -const allDocs = fs.readdirSync(apiURL); |
18 | | -assert.ok(allDocs.includes('index.html')); |
19 | 18 |
|
20 | | -const actualDocs = allDocs.filter( |
21 | | - (name) => { |
22 | | - const extension = path.extname(name); |
23 | | - return extension === '.html' || extension === '.json'; |
24 | | - }, |
25 | | -); |
26 | | - |
27 | | -for (const name of actualDocs) { |
28 | | - if (name.startsWith('all.') || name === 'apilinks.json') continue; |
29 | | - |
30 | | - assert.ok( |
31 | | - allMD.includes(name.replace(/\.\w+$/, '.md')), |
32 | | - `Unexpected output: out/doc/api/${name}, remove and rerun.`, |
33 | | - ); |
| 19 | +// Not all files under `doc/api` are expected in the generated output. |
| 20 | +// Parse out list of excluded files. |
| 21 | +let skippedDocs = []; |
| 22 | +const makefile = await fs.open(new URL('../../Makefile', import.meta.url)); |
| 23 | +try { |
| 24 | + // Find the `skip_apidoc_files = ` line and parse out the list of files. |
| 25 | + for await (const line of makefile.readLines()) { |
| 26 | + if (line.startsWith('skip_apidoc_files')) { |
| 27 | + skippedDocs = line.split('=')[1]?.trim().split(/\s/).map((n) => path.basename(n)); |
| 28 | + } |
| 29 | + } |
| 30 | +} finally { |
| 31 | + makefile.close(); |
34 | 32 | } |
35 | 33 |
|
36 | | -const toc = fs.readFileSync(new URL('./index.html', apiURL), 'utf8'); |
37 | | -const re = /href=("([^/]+\.html)"|([^/]+\.html))/; |
38 | | -const globalRe = new RegExp(re, 'g'); |
39 | | -const links = toc.match(globalRe); |
40 | | -assert.notStrictEqual(links, null); |
| 34 | +const sources = fs.readdirSync(mdURL) |
| 35 | + .filter((name) => path.extname(name) === '.md') |
| 36 | + .map((name) => path.basename(name, '.md')); |
| 37 | +const generated = fs.readdirSync(apiURL); |
41 | 38 |
|
42 | | -// Filter out duplicate links, leave just filenames, add expected JSON files. |
43 | | -const linkedHtmls = [...new Set(links)].map((link) => link.match(re)[1]) |
44 | | - .concat(['index.html']); |
45 | | -const expectedJsons = linkedHtmls |
46 | | - .map((name) => name.replace('.html', '.json')); |
47 | | -const expectedDocs = linkedHtmls.concat(expectedJsons); |
48 | | -const renamedDocs = ['policy.json', 'policy.html']; |
49 | | -const skipedDocs = ['dtls.json', 'dtls.html', 'quic.json', 'quic.html']; |
| 39 | +// These have no Markdown source of their own. |
| 40 | +const expectedExtras = ['index.html', 'all.html', 'all.json']; |
| 41 | +for (const name of expectedExtras) { |
| 42 | + assert.ok(generated.includes(name), `${name} was not generated`); |
| 43 | +} |
50 | 44 |
|
51 | | -// Test that all the relative links in the TOC match to the actual documents. |
52 | | -for (const expectedDoc of expectedDocs) { |
53 | | - if (skipedDocs.includes(expectedDoc)) continue; |
54 | | - assert.ok(actualDocs.includes(expectedDoc), `${expectedDoc} does not exist`); |
| 45 | +for (const name of sources) { |
| 46 | + for (const extension of ['.html', '.json']) { |
| 47 | + const file = `${name}${extension}`; |
| 48 | + if (skippedDocs.includes(name)) { |
| 49 | + assert.ok(!generated.includes(file), `${file} was unexpectedly generated`); |
| 50 | + continue; |
| 51 | + } |
| 52 | + assert.ok(generated.includes(file), `${file} was not generated`); |
| 53 | + assert.notStrictEqual( |
| 54 | + fs.statSync(new URL(`./${file}`, apiURL)).size, |
| 55 | + 0, |
| 56 | + `${file} is empty`, |
| 57 | + ); |
| 58 | + } |
55 | 59 | } |
56 | 60 |
|
57 | | -// Test that all the actual documents match to the relative links in the TOC |
58 | | -// and that they are not empty files. |
59 | | -for (const actualDoc of actualDocs) { |
60 | | - // When renaming the documentation, the old url is lost |
61 | | - // Unless the old file is still available pointing to the correct location |
62 | | - // 301 redirects are not yet automated. So keeping the old URL is a |
63 | | - // reasonable workaround. |
64 | | - if (renamedDocs.includes(actualDoc) || skipedDocs.includes(actualDoc) || |
65 | | - actualDoc === 'apilinks.json') continue; |
| 61 | +// Every generated page must come from a current Markdown file, so a stale |
| 62 | +// `out/doc/api` (e.g. from a renamed document) is noticed. |
| 63 | +const expectedPages = ['404.html', 'apilinks.json', ...expectedExtras]; |
| 64 | +for (const name of generated) { |
| 65 | + const extension = path.extname(name); |
| 66 | + if (extension !== '.html' && extension !== '.json') continue; |
| 67 | + if (expectedPages.includes(name)) continue; |
66 | 68 | assert.ok( |
67 | | - expectedDocs.includes(actualDoc), `${actualDoc} does not match TOC`); |
68 | | - |
69 | | - assert.notStrictEqual( |
70 | | - fs.statSync(new URL(`./${actualDoc}`, apiURL)).size, |
71 | | - 0, |
72 | | - `${actualDoc} is empty`, |
| 69 | + sources.includes(path.basename(name, extension)), |
| 70 | + `Unexpected output: out/doc/api/${name}, remove and rerun.`, |
73 | 71 | ); |
74 | 72 | } |
0 commit comments