ascii.test.js (3286B)
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 // The rule is about code we write, not about a build output or a 17 // dependency. This walks the filesystem, so .gitignore does not apply. 18 const SKIP_DIRS = new Set(['.git', 'node_modules', 'dist']); 19 const CHECK_EXT = new Set([ 20 '.js', '.mjs', '.cjs', '.json', '.md', '.sql', '.yml', '.yaml', 21 '.html', '.css', '.sh', '.txt', 22 ]); 23 const CHECK_NAMES = new Set(['.editorconfig', '.gitignore', '.dockerignore', 'post-receive']); 24 25 // Dockerfile, Dockerfile.worker, and anything else in that family. 26 const CHECK_PREFIXES = ['Dockerfile']; 27 28 function shouldCheck(file) { 29 const base = path.basename(file); 30 if (CHECK_EXT.has(path.extname(file))) return true; 31 if (CHECK_NAMES.has(base)) return true; 32 return CHECK_PREFIXES.some((prefix) => base.startsWith(prefix)); 33 } 34 35 async function* walk(dir) { 36 for (const entry of await fs.readdir(dir, { withFileTypes: true })) { 37 if (entry.isDirectory()) { 38 if (SKIP_DIRS.has(entry.name)) continue; 39 yield* walk(path.join(dir, entry.name)); 40 } else if (entry.isFile()) { 41 yield path.join(dir, entry.name); 42 } 43 } 44 } 45 46 function describeChar(ch) { 47 const code = ch.codePointAt(0); 48 return `U+${code.toString(16).toUpperCase().padStart(4, '0')} ${JSON.stringify(ch)}`; 49 } 50 51 test('every tracked text file is pure ASCII', async () => { 52 const offences = []; 53 54 for await (const file of walk(ROOT)) { 55 if (!shouldCheck(file)) continue; 56 57 const text = await fs.readFile(file, 'utf8'); 58 const lines = text.split('\n'); 59 for (let i = 0; i < lines.length; i += 1) { 60 for (const ch of lines[i]) { 61 const code = ch.codePointAt(0); 62 // Tab, and the printable range. Newlines are already stripped. 63 if (code === 9 || (code >= 32 && code <= 126)) continue; 64 offences.push(`${path.relative(ROOT, file)}:${i + 1}: ${describeChar(ch)}`); 65 break; 66 } 67 } 68 } 69 70 assert.deepEqual(offences, [], `non-ASCII characters found:\n${offences.join('\n')}`); 71 }); 72 73 test('no file uses CRLF line endings or trailing whitespace', async () => { 74 const offences = []; 75 76 for await (const file of walk(ROOT)) { 77 if (!shouldCheck(file)) continue; 78 79 const text = await fs.readFile(file, 'utf8'); 80 const rel = path.relative(ROOT, file); 81 if (text.includes('\r')) offences.push(`${rel}: contains a carriage return`); 82 const lines = text.split('\n'); 83 for (let i = 0; i < lines.length; i += 1) { 84 if (/[ \t]+$/.test(lines[i])) offences.push(`${rel}:${i + 1}: trailing whitespace`); 85 } 86 if (text.length > 0 && !text.endsWith('\n')) offences.push(`${rel}: missing final newline`); 87 } 88 89 assert.deepEqual(offences, [], `formatting problems found:\n${offences.join('\n')}`); 90 });