Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 8 additions & 63 deletions test/docs/library-examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

import { describe, expect, test } from "bun:test";
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
import { readFileSync, symlinkSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { ROOT } from "../root.js";
import { SETTINGS_SCHEMA_PATH, schemaNotBuilt } from "../settings-schema.js";
import { SETTINGS_SCHEMA_PATH } from "../settings-schema.js";
import { withTempDir } from "../temp-dir.js";

const PAGE = "docs/reference/library.md";
Expand Down Expand Up @@ -55,13 +55,9 @@ export function tsFences(markdown: string): Fence[] {
* The fences as one module: only the two package paths are rewritten, on the
* lines they already occupy, so a program line is a page line by offset alone
* and nothing else about a fence is repaired before tsc sees it. The page
* imports each name once, in the first example that uses it. `schema` is where
* the package's schema subpath lands.
* imports each name once, in the first example that uses it.
*/
export function examplesProgram(
fences: readonly Fence[],
schema: string,
): {
export function examplesProgram(fences: readonly Fence[]): {
text: string;
pageLine: (programLine: number) => number;
} {
Expand All @@ -71,7 +67,7 @@ export function examplesProgram(
for (const fence of fences) {
starts.push(line);
const body = fence.body
.replaceAll(`"${PACKAGE}/settings.schema.json"`, JSON.stringify(schema))
.replaceAll(`"${PACKAGE}/settings.schema.json"`, JSON.stringify(SETTINGS_SCHEMA_PATH))
.replaceAll(`"${PACKAGE}"`, JSON.stringify(ENTRY));
parts.push(body);
line += body.split("\n").length;
Expand All @@ -96,39 +92,15 @@ export function examplesProgram(
/** `tsc --pretty false` prints `<file>(<line>,<col>): error TS<code>: <message>`, continuation lines indented. */
const DIAGNOSTIC = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/;

/**
* Whether the program imports `module` (a static import, a re-export, a dynamic import, each spelled as the page
* spells it: `from`, or `import` and an optional parenthesis, then the quoted path); a string value naming the path
* is not a dependency.
*/
function importsModule(program: string, module: string): boolean {
const quoted = JSON.stringify(module);
for (let at = program.indexOf(quoted); at !== -1; at = program.indexOf(quoted, at + 1)) {
if (/\b(?:from|import)\s*\(?\s*$/.test(program.slice(0, at))) {
return true;
}
}
return false;
}

/**
* Every diagnostic tsc reports for the page's fences, as `<label>:<page line>: TS<code>: <message>`.
* A diagnostic outside the examples file keeps its own path. The compile runs in a temp project
* that symlinks this checkout's node_modules, so `types` and the runtime dependencies resolve
* exactly as they do for src/; the project is removed on every path. A fence that imports the
* schema subpath needs the built schema, so its absence is named before tsc would misreport it as
* a module the page cannot find.
* exactly as they do for src/; the project is removed on every path.
*/
export function compileExamples(
markdown: string,
label: string,
schema = SETTINGS_SCHEMA_PATH,
): Promise<string[]> {
export function compileExamples(markdown: string, label: string): Promise<string[]> {
const fences = tsFences(markdown);
const { text, pageLine } = examplesProgram(fences, schema);
if (importsModule(text, schema) && !existsSync(schema)) {
return Promise.reject(schemaNotBuilt(schema));
}
const { text, pageLine } = examplesProgram(fences);
return withTempDir("gsac-library-examples-", (dir) => {
symlinkSync(join(ROOT, "node_modules"), join(dir, "node_modules"), "dir");
const examples = join(dir, "examples.ts");
Expand Down Expand Up @@ -261,31 +233,4 @@ describe(`${PAGE} examples`, () => {
}
},
);

// The schema subpath resolves to a built file no checkout starts with; a page importing it names the build step,
// and a page that only spells the subpath as a string value compiles without the file.
test("a fence importing the schema subpath on an unbuilt checkout fails naming the build step, not a missing module", () =>
withTempDir("gsac-library-unbuilt-", async (dir) => {
const unbuilt = join(dir, "lib", "settings.schema.json");
const importsSchema = [
"```ts",
'import schema from "@vivswan/github-settings-as-code/settings.schema.json" with { type: "json" };',
"",
"console.log(schema.$schema);",
"```",
"",
].join("\n");
await expect(compileExamples(importsSchema, "page.md", unbuilt)).rejects.toThrow(
/lib\/settings\.schema\.json is not built; run `bun run build:schema`$/,
);
const noSchema = [
"```ts",
'import { SECTION_KEYS } from "@vivswan/github-settings-as-code";',
"",
'console.log(SECTION_KEYS.length, "@vivswan/github-settings-as-code/settings.schema.json");',
"```",
"",
].join("\n");
expect(await compileExamples(noSchema, "page.md", unbuilt)).toEqual([]);
}));
});
11 changes: 2 additions & 9 deletions test/sections/refusal-messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/

import { describe, expect, test } from "bun:test";
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { walk as walkTree } from "estree-walker";
import { parseSync } from "oxc-parser";
Expand Down Expand Up @@ -865,11 +865,6 @@ function read(root: string, path: string): string {
export function refusalSources(root: string): Source[] {
const sources: Source[] = [];
const add = (path: string, sites: Sites) => {
if (!existsSync(join(root, path))) {
throw new Error(
`${path} is a refusal source of this census but does not exist; a moved or renamed source is listed again under its new path`,
);
}
sources.push({ path, text: read(root, path), sites });
};
const sectionDirs = readdirSync(join(root, "src/sections"), { withFileTypes: true })
Expand Down Expand Up @@ -960,9 +955,7 @@ describe("every parse-refusal message a user can read is pinned by a test", () =
withTempDir("refusal-sources-", (root) => {
mkdirSync(join(root, "src/sections/planted"), { recursive: true });
writeFileSync(join(root, "src/sections/planted/schema.ts"), "export const a = 1;\n");
expect(() => refusalSources(root)).toThrow(
/^src\/sections\/planted\/index\.ts is a refusal source of this census but does not exist/,
);
expect(() => refusalSources(root)).toThrow(/ENOENT.*src\/sections\/planted\/index\.ts/);
}));

/** One literal per exclusion position, then a message in every spelling a source can give one. */
Expand Down
16 changes: 2 additions & 14 deletions test/settings-schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { existsSync, readFileSync } from "node:fs";
import { join, relative } from "node:path";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { ROOT } from "./root.js";

/** The built, gitignored schema; `bun run test` and `bun run fuzz` run `bun run build:schema` before loading it. */
Expand All @@ -11,20 +11,8 @@ export interface SettingsSchemaFile {
[keyword: string]: unknown;
}

/**
* The one line a test fails with when the built schema is absent, raised before any step that would misreport the
* absence as a defect of its own (tsc's TS2307 against a docs page, ajv's compile error): the generator only writes
* the file into the tree, so a test cannot build it elsewhere and names the build step instead.
*/
export function schemaNotBuilt(path: string): Error {
return new Error(`${relative(ROOT, path)} is not built; run \`bun run build:schema\``);
}

/** Read at run time rather than imported: `tsc` resolves a JSON import, so an import would fail every typecheck on
* a checkout that has not built the file. */
export function readSettingsSchema(): SettingsSchemaFile {
if (!existsSync(SETTINGS_SCHEMA_PATH)) {
throw schemaNotBuilt(SETTINGS_SCHEMA_PATH);
}
return JSON.parse(readFileSync(SETTINGS_SCHEMA_PATH, "utf8")) as SettingsSchemaFile;
}
Loading