ascii.test.js (3376B)
1 // test/ascii.test.js - enforces the repository wide ASCII rule 2 // 3 // Every source and documentation file must be plain ASCII. This is a hard 4 // project requirement rather than a style preference, so it is tested rather 5 // than left to review. Smart quotes, dashes and arrows pasted in from a 6 // browser are the usual cause of a failure here. 7 8 import test from 'node:test'; 9 import assert from 'node:assert/strict'; 10 import fs from 'node:fs/promises'; 11 import path from 'node:path'; 12 import { fileURLToPath } from 'node:url'; 13 14 const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); 15 16 // Vendored third party files are excluded. The rule is about code we 17 // write; rewriting someone else's minified build or their licence text to 18 // satisfy our own house style would be both pointless and wrong. 19 const SKIP_DIRS = new Set(['.git', 'node_modules', 'data', 'tmp', 'vendor']); 20 const CHECK_EXT = new Set([ 21 '.js', '.mjs', '.cjs', '.json', '.md', '.sql', '.yml', '.yaml', 22 '.html', '.css', '.sh', '.txt', 23 ]); 24 const CHECK_NAMES = new Set(['.editorconfig', '.gitignore', '.dockerignore', 'post-receive']); 25 26 // Dockerfile, Dockerfile.worker, and anything else in that family. 27 const CHECK_PREFIXES = ['Dockerfile']; 28 29 function shouldCheck(file) { 30 const base = path.basename(file); 31 if (CHECK_EXT.has(path.extname(file))) return true; 32 if (CHECK_NAMES.has(base)) return true; 33 return CHECK_PREFIXES.some((prefix) => base.startsWith(prefix)); 34 } 35 36 async function* walk(dir) { 37 for (const entry of await fs.readdir(dir, { withFileTypes: true })) { 38 if (entry.isDirectory()) { 39 if (SKIP_DIRS.has(entry.name)) continue; 40 yield* walk(path.join(dir, entry.name)); 41 } else if (entry.isFile()) { 42 yield path.join(dir, entry.name); 43 } 44 } 45 } 46 47 function describeChar(ch) { 48 const code = ch.codePointAt(0); 49 return `U+${code.toString(16).toUpperCase().padStart(4, '0')} ${JSON.stringify(ch)}`; 50 } 51 52 test('every tracked text file is pure ASCII', async () => { 53 const offences = []; 54 55 for await (const file of walk(ROOT)) { 56 if (!shouldCheck(file)) continue; 57 58 const text = await fs.readFile(file, 'utf8'); 59 const lines = text.split('\n'); 60 for (let i = 0; i < lines.length; i += 1) { 61 for (const ch of lines[i]) { 62 const code = ch.codePointAt(0); 63 // Tab, and the printable range. Newlines are already stripped. 64 if (code === 9 || (code >= 32 && code <= 126)) continue; 65 offences.push(`${path.relative(ROOT, file)}:${i + 1}: ${describeChar(ch)}`); 66 break; 67 } 68 } 69 } 70 71 assert.deepEqual(offences, [], `non-ASCII characters found:\n${offences.join('\n')}`); 72 }); 73 74 test('no file uses CRLF line endings or trailing whitespace', async () => { 75 const offences = []; 76 77 for await (const file of walk(ROOT)) { 78 if (!shouldCheck(file)) continue; 79 80 const text = await fs.readFile(file, 'utf8'); 81 const rel = path.relative(ROOT, file); 82 if (text.includes('\r')) offences.push(`${rel}: contains a carriage return`); 83 const lines = text.split('\n'); 84 for (let i = 0; i < lines.length; i += 1) { 85 if (/[ \t]+$/.test(lines[i])) offences.push(`${rel}:${i + 1}: trailing whitespace`); 86 } 87 if (text.length > 0 && !text.endsWith('\n')) offences.push(`${rel}: missing final newline`); 88 } 89 90 assert.deepEqual(offences, [], `formatting problems found:\n${offences.join('\n')}`); 91 });