conductor

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

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