Skip to content

Commit c50e40d

Browse files
authored
Run and fix codegen tests, make codegen caches per repo root (#64395)
1 parent f1f5c28 commit c50e40d

6 files changed

Lines changed: 89 additions & 28 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,17 @@ jobs:
298298
- run: git add .
299299
- run: git diff --staged --exit-code --stat
300300

301+
codegen:
302+
runs-on: ubuntu-latest
303+
steps:
304+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
305+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
306+
with:
307+
node-version: '>=22.18.0'
308+
- uses: ./.github/actions/setup-go
309+
- run: npm ci
310+
- run: npx hereby test:codegen
311+
301312
tidy:
302313
runs-on: ubuntu-slim
303314
steps:
@@ -341,6 +352,7 @@ jobs:
341352
if: ${{ always() }}
342353
needs:
343354
- build
355+
- codegen
344356
- release-build
345357
- extension
346358
- format

‎Herebyfile.mjs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -947,8 +947,11 @@ export const testTools = task({
947947

948948
export const testCodegen = task({
949949
name: "test:codegen",
950-
description: "Runs opt-in incremental codegen tests; excluded from validate and test:all. Because this runs asserts on build codegen, it takes awhile and is somewhat redundant.",
951-
run: () => run("node", ["--test", "./tools/scripts/gen/*.test.mts"]),
950+
description: "Runs incremental codegen tests.",
951+
run: async () => {
952+
await run("go", ["-C", "tsc", "mod", "download"]);
953+
await run("node", ["--test", "--test-concurrency=1", "./tools/scripts/gen/*.test.mts"]);
954+
},
952955
});
953956

954957
export const buildAPI = task({

‎tools/scripts/gen/cache.mts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ export default async function cache({
9090
console.log(`skipped ${commandText}: codegen outputs are already up to date`);
9191
return true;
9292
}
93-
9493
for (const file of previous) file.invalidate();
9594
for (const [command, ...args] of commands) {
9695
await run(command, args, { cwd, env: environment });

‎tools/scripts/gen/generatedFile.mts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ function digest(value: string | Buffer): string {
1919
return createHash("sha256").update(value).digest("hex");
2020
}
2121

22+
export const defaultCacheDirectory = path.join(os.tmpdir(), "typescript-codegen", digest(repoRoot));
23+
2224
function readIfExists(file: string): Buffer | undefined {
2325
try {
2426
return fs.readFileSync(file);
@@ -35,7 +37,7 @@ export class GeneratedFile {
3537
private readonly cacheFile: string;
3638
private readonly key: unknown;
3739

38-
constructor(fileName: string, inputs: readonly string[], cacheDirectory = path.join(os.tmpdir(), "typescript-codegen"), key?: unknown) {
40+
constructor(fileName: string, inputs: readonly string[], cacheDirectory = defaultCacheDirectory, key?: unknown) {
3941
this.fileName = path.resolve(fileName);
4042
this.inputs = [...new Set([...commonInputs, ...inputs].map(file => path.resolve(file)))].sort();
4143
this.key = key;

‎tools/scripts/gen/generatedFile.test.mts‎

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@ import { runInNewContext } from "node:vm";
1010
import { x } from "tinyexec";
1111
import ts from "typescript";
1212
import cache from "./cache.mts";
13-
import { GeneratedFile } from "./generatedFile.mts";
13+
import {
14+
defaultCacheDirectory,
15+
GeneratedFile,
16+
} from "./generatedFile.mts";
1417
import {
1518
enableFileFingerprintCache,
1619
getFileFingerprint,
20+
repoRoot,
1721
run,
1822
} from "./utils.mts";
1923

@@ -212,6 +216,38 @@ test("invocation fingerprints reuse metadata and invalidate writes and commands"
212216
}
213217
});
214218

219+
test("fingerprint cache scopes can overlap", context => {
220+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tsgo-fingerprint-"));
221+
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
222+
const input = path.join(directory, "input.ts");
223+
fs.writeFileSync(input, "input");
224+
const stats = context.mock.method(fs, "statSync");
225+
syncBuiltinESMExports();
226+
context.after(() => {
227+
stats.mock.restore();
228+
syncBuiltinESMExports();
229+
});
230+
const disableFirst = enableFileFingerprintCache();
231+
const disableSecond = enableFileFingerprintCache();
232+
try {
233+
getFileFingerprint(input);
234+
getFileFingerprint(input);
235+
assert.equal(stats.mock.callCount(), 1);
236+
disableFirst();
237+
getFileFingerprint(input);
238+
getFileFingerprint(input);
239+
assert.equal(stats.mock.callCount(), 2);
240+
disableSecond();
241+
getFileFingerprint(input);
242+
getFileFingerprint(input);
243+
assert.equal(stats.mock.callCount(), 4);
244+
}
245+
finally {
246+
disableSecond();
247+
disableFirst();
248+
}
249+
});
250+
215251
test("command cache snapshots and generated files share invocation fingerprints", async context => {
216252
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tsgo-fingerprint-"));
217253
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
@@ -251,11 +287,12 @@ test("changing inputs during generation does not mark stale output current", con
251287
assert.equal(new GeneratedFile(output, [input], cache).isCurrent(), false);
252288
});
253289

254-
test("default metadata has a stable path in the OS temporary directory", context => {
290+
test("default metadata is isolated by repository worktree", context => {
255291
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tsgo-codegen-"));
256292
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
257293
const output = path.join(directory, "output.ts");
258-
const cacheFile = path.join(os.tmpdir(), "typescript-codegen", createHash("sha256").update(output).digest("hex") + ".json");
294+
assert.equal(path.basename(defaultCacheDirectory), createHash("sha256").update(repoRoot).digest("hex"));
295+
const cacheFile = path.join(defaultCacheDirectory, createHash("sha256").update(output).digest("hex") + ".json");
259296
context.after(() => fs.rmSync(cacheFile, { force: true }));
260297
const file = new GeneratedFile(output, []);
261298
file.write("generated");
@@ -319,9 +356,9 @@ test("generate:go runs Go generators directly and shares caches with Go fallback
319356
assert.match(current.stdout, /Unicode tables are up to date/);
320357
assert.deepEqual(files.map(file => fs.statSync(path.join(root, file)).mtimeMs), timestamps);
321358
const fallback = await x("go", ["-C", "./tsc", "generate", "./internal/diagnostics"], { throwOnError: true, nodeOptions: { cwd: root } });
322-
assert.equal(fallback.stdout.match(/Codegen outputs are up to date/g)?.length, 2);
359+
assert.equal(fallback.stdout.match(/codegen outputs are already up to date/g)?.length, 2);
323360
const nested = await x("npx", ["hereby", "generate:compileroptions"], { throwOnError: true, nodeOptions: { cwd: path.join(root, "tsc/internal/core") } });
324-
assert.equal(nested.stdout.match(/Codegen outputs are up to date/g)?.length, 2);
361+
assert.equal(nested.stdout.match(/codegen outputs are already up to date/g)?.length, 2);
325362
});
326363

327364
test("generate includes standalone generators without Go traversal", async () => {
@@ -465,7 +502,7 @@ test("bundled generation skips unchanged library outputs", async () => {
465502
await generate();
466503
const files = ["libs_generated.go", "embed_generated.go"].map(file => path.join(root, "tsc/internal/bundled", file));
467504
const timestamps = files.map(file => fs.statSync(file).mtimeMs);
468-
assert.match((await generate()).stdout, /Codegen outputs are up to date\./);
505+
assert.match((await generate()).stdout, /codegen outputs are already up to date/);
469506
assert.deepEqual(files.map(file => fs.statSync(file).mtimeMs), timestamps);
470507
});
471508

@@ -684,7 +721,7 @@ test("Unicode generation repairs outputs and supports force", async context => {
684721
assert.match((await generate()).stdout, /Generated Unicode tables\./);
685722
assert.deepEqual(fs.readFileSync(file), originals[index]);
686723
}
687-
const cacheFile = path.join(os.tmpdir(), "typescript-codegen", createHash("sha256").update(files[0]).digest("hex") + ".json");
724+
const cacheFile = path.join(defaultCacheDirectory, createHash("sha256").update(files[0]).digest("hex") + ".json");
688725
context.after(() => fs.rmSync(cacheFile, { force: true }));
689726
fs.rmSync(cacheFile);
690727
assert.match((await generate()).stdout, /Generated Unicode tables\./);
@@ -719,7 +756,7 @@ test("enum generation skips unchanged outputs and Go verification", async () =>
719756
assert.match(forced.stdout, /All generated values match Go\./);
720757
assert.match((await generate()).stdout, /Enums are up to date\./);
721758
const verifier = path.join(root, "tsc/internal/api/enum_values_generated.go");
722-
const cacheFile = path.join(os.tmpdir(), "typescript-codegen", createHash("sha256").update(verifier).digest("hex") + ".json");
759+
const cacheFile = path.join(defaultCacheDirectory, createHash("sha256").update(verifier).digest("hex") + ".json");
723760
fs.rmSync(cacheFile);
724761
const regenerated = await generate();
725762
assert.match(regenerated.stdout, /All generated values match Go\./);
@@ -730,17 +767,19 @@ test("AST generation forwards force to schema generators and the kind stringer",
730767
const root = path.resolve(import.meta.dirname, "../../..");
731768
const generate = (force = false) => x("npx", ["hereby", "generate:ast", ...(force ? ["--force"] : [])], { throwOnError: true, nodeOptions: { cwd: root } });
732769
await generate();
770+
const output = path.join(root, "tsc/internal/ast/kind_stringer_generated.go");
771+
const oldTime = new Date("2000-01-01T00:00:00Z");
772+
fs.utimesSync(output, oldTime, oldTime);
733773
const forced = await generate(true);
734774
assert.match(forced.stdout, /Wrote .*encoder_generated\.go/);
735775
assert.match(forced.stdout, /Wrote .*ast_generated\.go/);
736776
assert.match(forced.stdout, /Generated .*ast\.generated\.ts/);
737-
assert.match(forced.stdout, /Generated codegen outputs\./);
738-
const output = path.join(root, "tsc/internal/ast/kind_stringer_generated.go");
777+
assert.notEqual(fs.statSync(output).mtimeMs, oldTime.getTime());
739778
const timestamp = fs.statSync(output).mtimeMs;
740779
assert.doesNotMatch(forced.stdout, /\$ node .*tools\/scripts\/tsc\/generate\.ts/);
741780
const current = await generate();
742781
assert.doesNotMatch(current.stdout, /(?:Wrote|Generated) /);
743-
assert.match(current.stdout, /Codegen outputs are up to date\./);
782+
assert.match(current.stdout, /codegen outputs are already up to date/);
744783
assert.equal(fs.statSync(output).mtimeMs, timestamp);
745784
});
746785

@@ -764,14 +803,14 @@ test("API protocol generation caches formatted output and supports force", async
764803
await generate();
765804
const timestamp = fs.statSync(output).mtimeMs;
766805
const current = await generate();
767-
assert.match(current.stdout, /Codegen outputs are up to date\./);
806+
assert.match(current.stdout, /codegen outputs are already up to date/);
768807
assert.doesNotMatch(current.stdout, /\$ node .*cache\.mts/);
769808
assert.equal(fs.statSync(output).mtimeMs, timestamp);
770809
const unrelated = path.join(root, "tsc/internal/parser/codegen_cache_probe.go");
771810
assert.equal(fs.existsSync(unrelated), false);
772811
context.after(() => fs.rmSync(unrelated, { force: true }));
773812
fs.writeFileSync(unrelated, "package parser\n");
774-
assert.match((await generate()).stdout, /Codegen outputs are up to date\./);
813+
assert.match((await generate()).stdout, /codegen outputs are already up to date/);
775814
fs.rmSync(unrelated);
776815
const schema = path.join(root, "tsc/internal/api/requestfilesystem/codegen_cache_probe.go");
777816
assert.equal(fs.existsSync(schema), false);
@@ -783,17 +822,21 @@ test("API protocol generation caches formatted output and supports force", async
783822
}
784823
});
785824
fs.writeFileSync(schema, 'package requestfilesystem\n\nconst KindCodegenCacheProbe Kind = "codegen-cache-probe"\n');
786-
assert.match((await generate()).stdout, /Generated codegen outputs\./);
825+
await generate();
787826
assert.match(fs.readFileSync(output, "utf8"), /"codegen-cache-probe"/);
788827
fs.rmSync(schema);
789-
assert.match((await generate()).stdout, /Generated codegen outputs\./);
828+
await generate();
790829
assert.equal(fs.readFileSync(output, "utf8"), original);
791-
assert.match((await generate(true)).stdout, /Generated codegen outputs\./);
792-
assert.match((await generate()).stdout, /Codegen outputs are up to date\./);
830+
const oldTime = new Date("2000-01-01T00:00:00Z");
831+
fs.utimesSync(output, oldTime, oldTime);
832+
await generate(true);
833+
assert.notEqual(fs.statSync(output).mtimeMs, oldTime.getTime());
834+
assert.match((await generate()).stdout, /codegen outputs are already up to date/);
835+
fs.utimesSync(output, oldTime, oldTime);
793836
const nested = await x("go", ["-C", "./tsc", "generate", "./internal/api"], {
794837
throwOnError: true,
795838
nodeOptions: { cwd: root, env: { ...process.env, TSGO_HEREBY_FORCE: "1" } },
796839
});
797-
assert.match(nested.stdout, /Generated codegen outputs\./);
798-
assert.match((await generate()).stdout, /Codegen outputs are up to date\./);
840+
assert.notEqual(fs.statSync(output).mtimeMs, oldTime.getTime());
841+
assert.match((await generate()).stdout, /codegen outputs are already up to date/);
799842
});

‎tools/scripts/gen/utils.mts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ import {
1313
export const repoRoot = path.resolve(import.meta.dirname, "../../..");
1414

1515
const fileFingerprints = new Map<string, { stats: fs.BigIntStats; hash: string; }>();
16-
let reuseFileFingerprints = false;
16+
let fileFingerprintCacheScopes = 0;
1717
let activeFingerprintWriters = 0;
1818

1919
export function enableFileFingerprintCache(): () => void {
20-
const previous = reuseFileFingerprints;
21-
reuseFileFingerprints = true;
20+
fileFingerprintCacheScopes++;
2221
fileFingerprints.clear();
22+
let enabled = true;
2323
return () => {
24-
reuseFileFingerprints = previous;
24+
if (!enabled) return;
25+
enabled = false;
26+
fileFingerprintCacheScopes--;
2527
fileFingerprints.clear();
2628
};
2729
}
@@ -44,7 +46,7 @@ export function getFileFingerprint(file: string, fresh = false): string {
4446
file = path.resolve(file);
4547
fresh ||= activeFingerprintWriters > 0;
4648
const previous = fileFingerprints.get(file);
47-
if (!fresh && reuseFileFingerprints && previous) return previous.hash;
49+
if (!fresh && fileFingerprintCacheScopes > 0 && previous) return previous.hash;
4850
const stats = fs.statSync(file, { bigint: true });
4951
if (
5052
!fresh && previous

0 commit comments

Comments
 (0)