conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

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 });