Skip to content

Commit 8e41fd7

Browse files
committed
test: add test for error codes doc and impl
This adds several sanity checks for error codes. It scans: * all natives (js sources), * doc/api/*.md documentation * src/node_errors.h (errors definition from the C++ side). There is also a whitelist of manually created errors from JS side, currently consisting of ERR_HTTP2_ERROR and ERR_UNKNOWN_BUILTIN_MODULE. Alsom all ERR_NAPI_ codes are whitelisted, as those are created directly on the cpp side, without declaring them first. The performed checks: 1. All errors used from JS should be defined in `internal/errors` and present in its .codes object. Whitelist (mentioned above) applies. 2. All errors instantiated from JS without arguments should support 0-arguments version. 3. All errors mentioned in doc should defined either in JS, C++, or in the whitelist. 4. All errors mentioned anywhere should be documented. 5. Documentation of error codes should be sorted, have no repeats, and include exactly one entry for every error code mentioned in the documentation, formatted as `/\n### (ERR_[A-Z0-9_]+)\n`. 6. All doc entries for error codes should have appropriate anchors. There is also a --report flag, which prints all the current issues and exits without asserting, for manual inspection. Individual fixes for those issues are landed in separate commits. Refs: #21421 Refs: #21440 Refs: #21483 Refs: #21484 Refs: #21485 Refs: #21487 PR-URL: #21470
1 parent 9f06a05 commit 8e41fd7

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed
+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
'use strict';
2+
3+
// Flags: --expose-internals
4+
5+
// Note: this script reads Node.js sources and docs, and expects to be run on
6+
// the very same Node.js version as the source and doc tree is.
7+
8+
require('../common');
9+
const assert = require('assert');
10+
const errors = require('internal/errors').codes;
11+
const fs = require('fs');
12+
const path = require('path');
13+
const natives = process.binding('natives');
14+
15+
// --report prints only the list of failed checks and does not assert
16+
const report = process.argv[2] === '--report';
17+
const results = [];
18+
function check(ok, type, name, reason) {
19+
const label = `${type}: ${name} - ${reason}`;
20+
if (report) {
21+
if (!ok) results.push(label);
22+
} else {
23+
assert.ok(ok, label);
24+
}
25+
}
26+
27+
const set = {
28+
all: new Set(),
29+
js: new Set(), // Error codes found in natives.
30+
doc: new Set(), // Error codes mentioned in `doc/api/*.md`.
31+
cpp: new Set(), // Error codes found in `src/node_errors.h`.
32+
noargs: new Set(), // Subset of `js` which is constructed without arguments.
33+
// `set.manual` contains errors manually created from js and not defined in
34+
// internals/errors. That is not scanned now, update this list if required.
35+
manual: new Set(['ERR_HTTP2_ERROR', 'ERR_UNKNOWN_BUILTIN_MODULE']),
36+
// `set.examples` contains errors mentioned in documentation purely for
37+
// demonstration purposes. These errors are not required to be present.
38+
examples: new Set(['ERR_ERROR_1'])
39+
};
40+
41+
const root = path.join(__dirname, '..', '..');
42+
43+
// File containing error definitions
44+
const jsdata = natives['internal/errors'];
45+
// File containing error documentation
46+
let docdata = fs.readFileSync(path.join(root, 'doc/api/errors.md'), 'utf-8')
47+
docdata = docdata.replace(/## Legacy [\s\S]*/, '');
48+
// File containing cpp-side errors
49+
const cppdata = fs.readFileSync(path.join(root, 'src/node_errors.h'), 'utf-8');
50+
// Directory containing documentation
51+
const docdir = path.join(root, 'doc/api/');
52+
53+
function addSource(source, type) {
54+
// eslint-disable-next-line node-core/no-unescaped-regexp-dot
55+
const re = /(.)?(ERR_[A-Z0-9_]+)(..)?/g;
56+
let match;
57+
while (match = re.exec(source)) {
58+
if (match[1] === '_') continue; // does not start with ERR_
59+
const name = match[2];
60+
if (type === 'doc' && set.examples.has(name)) continue; // is an example
61+
set.all.add(name);
62+
set[type].add(name);
63+
if (type === 'js' && match[3] === '()') {
64+
// Is directly called without arguments from js, we will try that
65+
set.noargs.add(name);
66+
}
67+
}
68+
}
69+
70+
// Add all errors from JS natives
71+
for (const file of Object.keys(natives)) addSource(natives[file], 'js');
72+
// Add all errors from src/node_errors.h
73+
addSource(cppdata, 'cpp');
74+
// Add all errors from doc/api/*.md files
75+
for (const file of fs.readdirSync(docdir)) {
76+
if (!file.endsWith('.md')) continue;
77+
let data = fs.readFileSync(path.join(docdir, file), 'utf-8');
78+
if (file === 'errors.md') data = data.replace(/## Legacy [\s\S]*/, '');
79+
addSource(data, 'doc');
80+
}
81+
82+
// Check that we have all js errors
83+
for (const name of set.js) {
84+
if (set.manual.has(name)) continue;
85+
const defined = jsdata.includes(`E('${name}',`);
86+
check(defined, 'js', name, 'missing JS implementation (source)');
87+
if (defined) {
88+
check(errors[name], 'js', name, 'missing JS implementation (runtime)');
89+
}
90+
}
91+
92+
// Check that we can initialize errors called without args
93+
for (const name of set.noargs) {
94+
if (!errors[name]) continue; // Already catched that above
95+
let ok = true;
96+
try {
97+
new errors[name]();
98+
} catch (e) {
99+
ok = false;
100+
}
101+
check(ok, 'init', name, 'failed init without args, but is called with "()"');
102+
}
103+
104+
// Check that we have implementation for all errors mentioned in docs.
105+
// C++ does not need that, and JS is already checked above.
106+
for (const name of set.doc) {
107+
const ok = set.manual.has(name) ||
108+
name.startsWith('ERR_NAPI_') || // napi errors are created directly
109+
jsdata.includes(`E('${name}',`) ||
110+
cppdata.includes(`V(${name}, `);
111+
const reason = docdata.includes(`### ${name}\n`) ?
112+
'documented' : 'mentioned in doc/api/';
113+
check(ok, 'impl', name, `missing implementation, ${reason}`);
114+
}
115+
116+
// Check that we have documentation for all errors
117+
for (const name of set.all) {
118+
const ok = docdata.includes(`### ${name}\n`);
119+
check(ok, 'doc', name, 'missing documentation');
120+
// Check that documentation references are correctly formatted
121+
if (ok) {
122+
const anchor = docdata.includes(`\n\n<a id="${name}"></a>\n### ${name}\n`);
123+
check(anchor, 'doc', name, 'missing anchor or not properly formatted');
124+
}
125+
}
126+
127+
// Check that documentation is sorted, formatted, and does not contain dupes
128+
{
129+
const compare = (a, b) => {
130+
// HTTP2_ should come after HTTP_
131+
if (a.startsWith('ERR_HTTP_') && b.startsWith('ERR_HTTP2_')) return -1;
132+
if (a.startsWith('ERR_HTTP2_') && b.startsWith('ERR_HTTP_')) return 1;
133+
if (a < b) return -1;
134+
if (a > b) return 1;
135+
return 0;
136+
};
137+
const re = /\n### (ERR_[A-Z0-9_]+)\n/g;
138+
let match;
139+
let last;
140+
const documented = new Set();
141+
while (match = re.exec(docdata)) {
142+
const name = match[1];
143+
if (documented.has(name)) {
144+
check(false, 'doc', name, 'duplicate documentation entry');
145+
} else {
146+
const sorted = !last || compare(name, last) === 1;
147+
check(sorted, 'doc', name, 'is out of order');
148+
documented.add(name);
149+
}
150+
last = name;
151+
}
152+
}
153+
154+
if (report) {
155+
console.log('Report mode');
156+
console.log(results.join('\n'));
157+
console.log(`There were ${results.length} problems found`);
158+
}

0 commit comments

Comments
 (0)