/** * lib/test.mjs — 작은 테스트 러너(의존성 없음). 형제 제품(AriMail)과 같은 방식: 파일을 node 로 직접 실행하면 순서대로 돈다. * import { test, eq, ok, rejects, tmpDir, done } from '../lib/test.mjs'; * await test('이름', async () => { ... }); 실패하면 즉시 요약을 내고 종료 코드 1. * done(); 마지막에 요약 출력. */ import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; let passed = 0; let failed = 0; const dirs = []; export async function test(name, fn) { const t0 = Date.now(); try { await fn(); passed++; console.log(` ✔ ${name} (${Date.now() - t0}ms)`); } catch (e) { failed++; console.error(` ✘ ${name}\n ${e && e.stack ? e.stack.split('\n').slice(0, 6).join('\n ') : e}`); process.exitCode = 1; } } export function eq(a, b, msg = '') { const sa = JSON.stringify(a); const sb = JSON.stringify(b); if (sa !== sb) throw new Error(`${msg ? msg + ': ' : ''}기대 ${sb} 실제 ${sa}`); } export function ok(v, msg = '') { if (!v) throw new Error(`${msg || '참이어야 합니다'} (실제 ${JSON.stringify(v)})`); } export async function rejects(fn, codeOrRe, msg = '') { try { await fn(); } catch (e) { if (codeOrRe == null) return e; const s = `${e.code || ''} ${e.message || ''}`; if (codeOrRe instanceof RegExp ? codeOrRe.test(s) : (e.code === codeOrRe || String(e.message).includes(codeOrRe))) return e; throw new Error(`${msg || '다른 오류'}: 기대 ${codeOrRe} 실제 ${s}`); } throw new Error(`${msg || '예외가 나야 합니다'}`); } /** 테스트용 임시 디렉터리(종료 시 삭제) */ export function tmpDir(prefix = 'aw-test-') { const d = mkdtempSync(join(tmpdir(), prefix)); dirs.push(d); return d; } export function done() { for (const d of dirs) { try { rmSync(d, { recursive: true, force: true }); } catch (_) {} } console.log(`${failed ? '✘' : '✔'} 통과 ${passed} · 실패 ${failed}`); if (failed) process.exit(1); } export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));