mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
0903dcd5ab
Prettier splits a long db.prepare('...').all() into db on one line and
.prepare( on the next, which put the call two lines below its marker and
made every parity oracle in the repository tests count as a raw statement.
The marker now also covers the method-chain lines that continue the
statement right below it, up to the first line that does not start with a
dot.
219 lines
9.5 KiB
TypeScript
219 lines
9.5 KiB
TypeScript
/**
|
|
* scripts/test-sql-ratchet.mjs is the CI gate that keeps raw better-sqlite3
|
|
* statements in the server tests from growing back while the suite moves onto
|
|
* the ORM factories (tests/helpers/factories). A gate nobody tests passes
|
|
* forever once it breaks, so this runs the real script as a child process
|
|
* against throwaway server roots (--dir), proves it fails on growth and on
|
|
* stale entries, honours the exemption list and the allow marker, and refuses
|
|
* a broken setup instead of passing silently. The last case runs it on this
|
|
* repository, so the committed baseline is checked by the unit suite too.
|
|
*/
|
|
import { execFileSync } from 'node:child_process';
|
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
|
|
const SCRIPT = path.join(__dirname, '../../../scripts/test-sql-ratchet.mjs');
|
|
const SERVER_ROOT = path.join(__dirname, '../../..');
|
|
|
|
interface ExecError {
|
|
status: number | null;
|
|
stdout: string;
|
|
stderr: string;
|
|
}
|
|
|
|
interface Baseline {
|
|
exempt: Record<string, string>;
|
|
counts: Record<string, number>;
|
|
}
|
|
|
|
// Spelled in pieces so this file holds no raw call of its own for the ratchet to count.
|
|
const CALL = ['.', 'prepare('].join('');
|
|
const stmt = (n: number) => `db${CALL}'SELECT 1').get();\n`.repeat(n);
|
|
|
|
const roots: string[] = [];
|
|
|
|
function serverRoot(files: Record<string, string>, baseline: unknown = { exempt: {}, counts: {} }): string {
|
|
// null writes no baseline file at all.
|
|
const dir = mkdtempSync(path.join(tmpdir(), 'trek-test-sql-'));
|
|
roots.push(dir);
|
|
mkdirSync(path.join(dir, 'tests'), { recursive: true });
|
|
mkdirSync(path.join(dir, 'scripts'), { recursive: true });
|
|
for (const [file, text] of Object.entries(files)) {
|
|
mkdirSync(path.dirname(path.join(dir, file)), { recursive: true });
|
|
writeFileSync(path.join(dir, file), text);
|
|
}
|
|
if (baseline !== null) {
|
|
writeFileSync(
|
|
path.join(dir, 'scripts/test-sql-baseline.json'),
|
|
typeof baseline === 'string' ? baseline : JSON.stringify(baseline),
|
|
);
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
function run(dir: string, ...args: string[]): { status: number | null; out: string } {
|
|
try {
|
|
const stdout = execFileSync(process.execPath, [SCRIPT, `--dir=${dir}`, ...args], {
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
return { status: 0, out: stdout };
|
|
} catch (err) {
|
|
const { status, stdout, stderr } = err as ExecError;
|
|
return { status, out: `${stdout}${stderr}` };
|
|
}
|
|
}
|
|
|
|
function readBaseline(dir: string): Baseline {
|
|
return JSON.parse(readFileSync(path.join(dir, 'scripts/test-sql-baseline.json'), 'utf8')) as Baseline;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of roots.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('test-sql-ratchet.mjs', () => {
|
|
it('TSQL-001: passes when no test file holds a raw statement', () => {
|
|
const dir = serverRoot({ 'tests/a.test.ts': 'await makeUser(orm);\n' });
|
|
const { status, out } = run(dir);
|
|
expect(status).toBe(0);
|
|
expect(out).toContain('test-sql: 0 raw statement(s) in 0 baselined file(s), 0 file(s) exempt');
|
|
});
|
|
|
|
it('TSQL-002: fails a file without a baseline entry that holds one', () => {
|
|
const { status, out } = run(serverRoot({ 'tests/unit/new.test.ts': stmt(1) }));
|
|
expect(status).toBe(1);
|
|
expect(out).toContain(
|
|
'FAIL tests/unit/new.test.ts: 1 raw statement(s), a file without a baseline entry may hold none.',
|
|
);
|
|
});
|
|
|
|
it('TSQL-003: fails a baselined file that grows past its entry, passes at it', () => {
|
|
const baseline = { exempt: {}, counts: { 'tests/a.test.ts': 2 } };
|
|
const grown = run(serverRoot({ 'tests/a.test.ts': stmt(3) }, baseline));
|
|
expect(grown.status).toBe(1);
|
|
expect(grown.out).toContain('FAIL tests/a.test.ts: 3 raw statement(s), its baseline is 2.');
|
|
expect(run(serverRoot({ 'tests/a.test.ts': stmt(2) }, baseline)).status).toBe(0);
|
|
});
|
|
|
|
it('TSQL-004: counts every call on a line, in helpers too, and walks .ts and .mjs alike', () => {
|
|
const dir = serverRoot({
|
|
'tests/helpers/seed.ts': `db${CALL}'a').run(); db${CALL}'b').run();\n`,
|
|
'tests/tools/x.mjs': stmt(1),
|
|
'tests/notes.md': stmt(5),
|
|
});
|
|
const { status, out } = run(dir);
|
|
expect(status).toBe(1);
|
|
expect(out).toContain('tests/helpers/seed.ts: 2 raw');
|
|
expect(out).toContain('tests/tools/x.mjs: 1 raw');
|
|
expect(out).not.toContain('notes.md');
|
|
});
|
|
|
|
it('TSQL-005: an allow marker with a reason on the line or the line above takes the statement out of the count', () => {
|
|
const text =
|
|
`// test-sql-allow: PRAGMA introspection has no ORM equivalent\n${stmt(1)}` +
|
|
`db${CALL}'PRAGMA table_info(x)').all(); // test-sql-allow: same reason\n`;
|
|
expect(run(serverRoot({ 'tests/a.test.ts': text })).status).toBe(0);
|
|
});
|
|
|
|
it('TSQL-005b: a marker covers a statement broken into a method chain, and no further', () => {
|
|
const chained =
|
|
`// test-sql-allow: the raw statement is the legacy oracle\n` +
|
|
`const legacy = testDb\n ${CALL}'SELECT 1')\n .all();\n`;
|
|
expect(run(serverRoot({ 'tests/a.test.ts': chained })).status).toBe(0);
|
|
|
|
// The chain ends at the first line that does not start with a dot.
|
|
const past = `${chained}const other = testDb\n ${CALL}'SELECT 2')\n .all();\n`;
|
|
const { status, out } = run(serverRoot({ 'tests/b.test.ts': past }));
|
|
expect(status).toBe(1);
|
|
expect(out).toContain('tests/b.test.ts: 1 raw statement(s)');
|
|
});
|
|
|
|
it('TSQL-006: a marker without a reason, or two lines up, does not count', () => {
|
|
const bare = run(serverRoot({ 'tests/a.test.ts': `// test-sql-allow:\n${stmt(1)}` }));
|
|
expect(bare.status).toBe(1);
|
|
const far = run(serverRoot({ 'tests/b.test.ts': `// test-sql-allow: reason\n\n${stmt(1)}` }));
|
|
expect(far.status).toBe(1);
|
|
});
|
|
|
|
it('TSQL-007: an exempt file is not counted, and an exemption for a gone file fails', () => {
|
|
const exempt = { 'tests/unit/db/dialect.test.ts': 'pins the SQL each dialect function renders' };
|
|
const ok = run(serverRoot({ 'tests/unit/db/dialect.test.ts': stmt(40) }, { exempt, counts: {} }));
|
|
expect(ok.status).toBe(0);
|
|
expect(ok.out).toContain('1 file(s) exempt');
|
|
|
|
const gone = run(serverRoot({ 'tests/a.test.ts': '' }, { exempt, counts: {} }));
|
|
expect(gone.status).toBe(1);
|
|
expect(gone.out).toContain('FAIL tests/unit/db/dialect.test.ts is exempt in scripts/test-sql-baseline.json, but the file is gone');
|
|
});
|
|
|
|
it('TSQL-008: fails an entry above its file, or for a file with none left, until --update lowers it', () => {
|
|
const shrunk = run(serverRoot({ 'tests/a.test.ts': stmt(1) }, { exempt: {}, counts: { 'tests/a.test.ts': 3 } }));
|
|
expect(shrunk.status).toBe(1);
|
|
expect(shrunk.out).toContain('FAIL tests/a.test.ts is held at 3 in scripts/test-sql-baseline.json, but it has 1 now.');
|
|
expect(shrunk.out).toContain('npm run lint:test-sql -- --update');
|
|
|
|
const converted = run(serverRoot({ 'tests/a.test.ts': '' }, { exempt: {}, counts: { 'tests/a.test.ts': 3 } }));
|
|
expect(converted.status).toBe(1);
|
|
expect(converted.out).toContain('but it has none left (or the file is gone)');
|
|
});
|
|
|
|
it('TSQL-009: --update only lowers and drops counts, never adds one, never touches the exemptions', () => {
|
|
const exempt = { 'tests/raw.test.ts': 'raw by nature' };
|
|
const dir = serverRoot(
|
|
{
|
|
'tests/shrunk.test.ts': stmt(2),
|
|
'tests/converted.test.ts': '',
|
|
'tests/grown.test.ts': stmt(9),
|
|
'tests/new.test.ts': stmt(1),
|
|
'tests/raw.test.ts': stmt(4),
|
|
},
|
|
{
|
|
exempt,
|
|
counts: { 'tests/shrunk.test.ts': 5, 'tests/converted.test.ts': 2, 'tests/grown.test.ts': 6, 'tests/gone.test.ts': 1 },
|
|
},
|
|
);
|
|
const { status } = run(dir, '--update');
|
|
expect(readBaseline(dir)).toEqual({
|
|
exempt,
|
|
counts: { 'tests/grown.test.ts': 6, 'tests/shrunk.test.ts': 2 },
|
|
});
|
|
// The grown file and the new one still fail after the update.
|
|
expect(status).toBe(1);
|
|
});
|
|
|
|
it('TSQL-010: fails closed on a missing, unparsable or malformed baseline', () => {
|
|
const missing = serverRoot({ 'tests/a.test.ts': '' }, null);
|
|
expect(run(missing).status).toBe(1);
|
|
expect(run(missing).out).toContain('test-sql-baseline.json cannot be read');
|
|
|
|
expect(run(serverRoot({}, '{ not json')).status).toBe(1);
|
|
expect(run(serverRoot({}, '[]')).status).toBe(1);
|
|
expect(run(serverRoot({}, { counts: {} })).status).toBe(1);
|
|
// A count that is not a whole number above zero, an exemption without a
|
|
// reason, or a file both exempt and counted is a hand edit gone wrong.
|
|
expect(run(serverRoot({}, { exempt: {}, counts: { 'tests/a.test.ts': 0 } })).status).toBe(1);
|
|
expect(run(serverRoot({}, { exempt: {}, counts: { 'tests/a.test.ts': '2' } })).status).toBe(1);
|
|
expect(run(serverRoot({}, { exempt: { 'tests/a.test.ts': ' ' }, counts: {} })).out).toContain('needs a reason');
|
|
expect(
|
|
run(serverRoot({}, { exempt: { 'tests/a.test.ts': 'why' }, counts: { 'tests/a.test.ts': 1 } })).out,
|
|
).toContain('both exempt and counted');
|
|
});
|
|
|
|
it('TSQL-011: fails closed when tests/ is missing', () => {
|
|
const dir = serverRoot({});
|
|
rmSync(path.join(dir, 'tests'), { recursive: true });
|
|
const { status, out } = run(dir);
|
|
expect(status).toBe(1);
|
|
expect(out).toContain('tests/ does not exist');
|
|
});
|
|
|
|
it('TSQL-012: the committed tree matches the committed baseline', () => {
|
|
const { status, out } = run(SERVER_ROOT);
|
|
expect(out).not.toContain('FAIL');
|
|
expect(status).toBe(0);
|
|
});
|
|
});
|