syntax.test.js (2377B)
1 // test/syntax.test.js - every JavaScript file parses 2 // 3 // Most modules are covered because a test imports them, but entry points 4 // and the dashboard are never imported by anything: the entry points start 5 // listeners, and the dashboard only ever runs in a browser. A syntax error 6 // in either would otherwise reach a deployment. 7 // 8 // node --check parses without executing, which is what makes this safe to 9 // run over files that would start a server on import. 10 11 import test from 'node:test'; 12 import assert from 'node:assert/strict'; 13 import fs from 'node:fs/promises'; 14 import path from 'node:path'; 15 import { execFile } from 'node:child_process'; 16 import { promisify } from 'node:util'; 17 import { fileURLToPath } from 'node:url'; 18 19 const execFileAsync = promisify(execFile); 20 const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); 21 const SKIP = new Set(['.git', 'node_modules', 'data', 'tmp']); 22 23 async function* walk(dir) { 24 for (const entry of await fs.readdir(dir, { withFileTypes: true })) { 25 if (entry.isDirectory()) { 26 if (SKIP.has(entry.name)) continue; 27 yield* walk(path.join(dir, entry.name)); 28 } else if (entry.isFile() && entry.name.endsWith('.js')) { 29 yield path.join(dir, entry.name); 30 } 31 } 32 } 33 34 test('every javascript file parses', async () => { 35 const files = []; 36 for await (const file of walk(ROOT)) files.push(file); 37 assert.ok(files.length > 20, `expected to find source files, found ${files.length}`); 38 39 const failures = []; 40 // Batched, since each check is a process. 41 for (let i = 0; i < files.length; i += 8) { 42 const batch = files.slice(i, i + 8).map(async (file) => { 43 try { 44 await execFileAsync(process.execPath, ['--check', file], { timeout: 20000 }); 45 } catch (e) { 46 failures.push(`${path.relative(ROOT, file)}: ${(e.stderr || e.message).trim().split('\n').slice(0, 3).join(' ')}`); 47 } 48 }); 49 await Promise.all(batch); 50 } 51 52 assert.deepEqual(failures, [], `files failed to parse:\n${failures.join('\n')}`); 53 }); 54 55 test('the shell scripts parse', async () => { 56 const failures = []; 57 for (const script of ['hooks/post-receive']) { 58 try { 59 await execFileAsync('sh', ['-n', path.join(ROOT, script)], { timeout: 20000 }); 60 } catch (e) { 61 failures.push(`${script}: ${(e.stderr || e.message).trim()}`); 62 } 63 } 64 assert.deepEqual(failures, []); 65 });