test(r): pin the documented export-pattern and discovery caps by literal

The caps in r-provider.md were asserted through the constants themselves, so changing a
cap, or the number the document gives, failed nothing. Pin each cap, check that the
document states the same number, and test the state cap, the pattern length cap, the
hidden-package scan budget and a spent matcher at their exact boundaries. Also pin the
backslash-u fallback to the letter u when fewer than four hex digits follow.
This commit is contained in:
Mick Cooney 2026-10-17 18:40:47 +01:00
parent e7fd13d364
commit 7581ae0697
2 changed files with 177 additions and 0 deletions

View file

@ -0,0 +1,162 @@
/**
* The numeric caps docs/languages/r-provider.md publishes, pinned by literal.
*
* Other tests assert against the exported constants (`toBeLessThan(MAX_SHARED_WORK)`), which
* stays green when a cap changes. These pin the values themselves, and check that the document
* states the same numbers, so that neither can drift from the other unnoticed. Changing a cap
* means changing it here and in the document.
*/
import { describe, expect, it, beforeAll, afterAll } from 'vitest';
import fs from 'node:fs';
import fsp from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
compileLinearRegexDetailed,
MAX_NESTING,
MAX_PACKAGE_STATES,
MAX_PACKAGE_WORK,
MAX_PATTERN_LENGTH,
MAX_SHARED_STATES,
MAX_SHARED_WORK,
MAX_STATES,
MAX_TOTAL_WORK,
SharedWorkBudget,
} from '../../src/core/ingestion/languages/r/linear-regex.js';
import {
hidesUndiscoveredPackage,
MAX_LISTED_DROPPED_PATTERNS,
} from '../../src/core/ingestion/languages/r/package-config.js';
import { compileRExportPattern } from '../../src/core/ingestion/languages/r/export-pattern.js';
import { cleanupTempDirSync } from '../helpers/test-db.js';
const DOC = fs.readFileSync(
fileURLToPath(new URL('../../../docs/languages/r-provider.md', import.meta.url)),
'utf-8',
);
/** The number the document gives right after a backticked constant name, commas removed. */
function documented(pattern: RegExp): number {
const match = pattern.exec(DOC);
if (match === null) throw new Error(`r-provider.md no longer states ${String(pattern)}`);
return Number(match[1].replace(/,/g, ''));
}
describe('the documented export-pattern caps', () => {
it('have the values the document publishes', () => {
expect({
MAX_PATTERN_LENGTH,
MAX_STATES,
MAX_NESTING,
MAX_TOTAL_WORK,
MAX_SHARED_WORK,
MAX_PACKAGE_WORK,
MAX_SHARED_STATES,
MAX_PACKAGE_STATES,
MAX_LISTED_DROPPED_PATTERNS,
}).toEqual({
MAX_PATTERN_LENGTH: 16_384,
MAX_STATES: 16_384,
MAX_NESTING: 256,
MAX_TOTAL_WORK: 40_000_000,
MAX_SHARED_WORK: 100_000_000,
MAX_PACKAGE_WORK: 30_000_000,
MAX_SHARED_STATES: 500_000,
MAX_PACKAGE_STATES: 100_000,
MAX_LISTED_DROPPED_PATTERNS: 50,
});
});
it.each([
['MAX_TOTAL_WORK', /`MAX_TOTAL_WORK` \(([\d,]+) /, MAX_TOTAL_WORK],
['MAX_SHARED_WORK', /`MAX_SHARED_WORK` \(([\d,]+) /, MAX_SHARED_WORK],
['MAX_PACKAGE_WORK', /`MAX_PACKAGE_WORK` \(([\d,]+) /, MAX_PACKAGE_WORK],
['MAX_SHARED_STATES', /`MAX_SHARED_STATES` \(([\d,]+) /, MAX_SHARED_STATES],
['MAX_PACKAGE_STATES', /`MAX_PACKAGE_STATES` \(([\d,]+) /, MAX_PACKAGE_STATES],
['MAX_PATTERN_LENGTH', /`MAX_PATTERN_LENGTH` \(([\d,]+) characters\)/, MAX_PATTERN_LENGTH],
['MAX_STATES', /`MAX_STATES` \(([\d,]+)\)/, MAX_STATES],
['MAX_NESTING', /deeper than ([\d,]+) \(`MAX_NESTING`\)/, MAX_NESTING],
[
'MAX_LISTED_DROPPED_PATTERNS',
/at most ([\d,]+) patterns are named per package \(`MAX_LISTED_DROPPED_PATTERNS`\)/,
MAX_LISTED_DROPPED_PATTERNS,
],
])('%s is the number the document states', (_name, pattern, value) => {
expect(documented(pattern)).toBe(value);
});
it('keep the per-pattern state cap and the pattern length cap equal to the documented 16,384', () => {
// The two are separate limits that the document gives the same number.
expect(MAX_PATTERN_LENGTH).toBe(MAX_STATES);
});
});
describe('the per-pattern state cap sits exactly at MAX_STATES', () => {
it('compiles a pattern that fills the cap', () => {
const result = compileLinearRegexDetailed('^a{16381}$');
expect(result.regex?.stateCount).toBe(MAX_STATES);
});
it('refuses a pattern one state over the cap, and says why', () => {
const result = compileLinearRegexDetailed('^a{16382}$');
expect(result.regex).toBeNull();
expect(result.regex === null ? result.reason : '').toMatch(/state cap/);
});
});
describe('the pattern length cap sits exactly at MAX_PATTERN_LENGTH', () => {
it('accepts a pattern of exactly that many characters', () => {
const pattern = `^${'a'.repeat(MAX_PATTERN_LENGTH - 2)}$`;
expect(pattern.length).toBe(MAX_PATTERN_LENGTH);
// Longer than the state cap would allow if it were expanded, so look at the reason only
// when it is refused: the length rule must not be the one that refuses it.
const result = compileLinearRegexDetailed(pattern);
expect(result.regex === null ? result.reason : '').not.toMatch(/longer than/);
});
it('refuses a pattern one character longer', () => {
const result = compileLinearRegexDetailed(`^${'a'.repeat(MAX_PATTERN_LENGTH - 1)}$`);
expect(result.regex).toBeNull();
expect(result.regex === null ? result.reason : '').toMatch(/longer than 16384 characters/);
});
});
describe('a matcher whose budget is spent answers a strict false', () => {
it('returns false, not undefined, for a name it has not decided', () => {
// Compiled before the budget is spent: a spent budget refuses to compile at all.
const fresh = new SharedWorkBudget(1_000);
const matcher = compileRExportPattern('^foo', fresh);
fresh.charge(2_000);
expect(fresh.isSpent).toBe(true);
expect(matcher?.test('foo_new')).toBe(false);
expect(matcher?.exhausted).toBe(true);
});
});
describe('the hidden-package scan budget of 5,000 directories', () => {
let tmp: string;
beforeAll(() => {
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'r-hidden-scan-'));
});
afterAll(() => cleanupTempDirSync(tmp));
async function treeOf(children: number): Promise<string> {
const root = path.join(tmp, `tree${String(children)}`);
await fsp.mkdir(root, { recursive: true });
await Promise.all(
Array.from({ length: children }, (_, i) => fsp.mkdir(path.join(root, `d${String(i)}`))),
);
return root;
}
it('reads a root and 4,999 directories (5,000 in all) without giving up', async () => {
const root = await treeOf(4_999);
expect(await hidesUndiscoveredPackage([root], new Set())).toBe(false);
}, 30_000);
it('gives up on the 5,001st directory', async () => {
const root = await treeOf(5_000);
expect(await hidesUndiscoveredPackage([root], new Set())).toBe(true);
}, 30_000);
});

View file

@ -727,6 +727,21 @@ describe('compileRExportPattern agrees with RegExp on generated patterns', () =>
});
});
// A `\\u` without four hex digits is the letter `u`, as in JavaScript (`/\\u12/` matches "u12").
// The translator keeps that reading instead of refusing the pattern.
describe('compileRExportPattern: \\u without four hex digits', () => {
it.each([
['\\u12', 'u12', true],
['\\u12', '\u0012', false],
['^\\u$', 'u', true],
['^\\u00zz$', 'u00zz', true],
['^\\u0062$', 'b', true],
])('%s against %s is %s, as RegExp says', (source, name, expected) => {
expect(new RegExp(source).test(name)).toBe(expected);
expect(compileRExportPattern(source)?.test(name)).toBe(expected);
});
});
// docs/languages/r-provider.md: "Character classes use ASCII semantics." R's TRE engine would
// match a locale's letters; the translator deliberately does not, so this is pinned.
describe('compileRExportPattern: POSIX classes are ASCII-only', () => {