Skip to content

Commit debec32

Browse files
authored
Merge pull request #226 from atomic-ehr/perf/test-suite-package-cache
API/Build: speed up test run
2 parents fe66fb9 + 9e2b1f0 commit debec32

12 files changed

Lines changed: 61 additions & 25 deletions

File tree

‎Makefile‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
AIDBOX_LICENSE_ID ?=
22

3+
# Unit test files run in separate bun processes. Keep this at 1 unless the
4+
# package cache is already warm: on a cold cache several processes install the
5+
# same FHIR package set into the same directory at once and corrupt it. The
6+
# speedup is within noise anyway — the heavy tests are IO-bound and throttle
7+
# each other, so concurrent jobs do not shorten the critical path.
8+
TEST_JOBS ?= 1
9+
# bun ignores the `timeout` key in bunfig.toml, so pass it on the command line:
10+
# generation-heavy tests exceed the 5s default once jobs compete for CPU.
11+
TEST_TIMEOUT ?= 30000
12+
313
TYPECHECK = bunx tsc --noEmit
414

515
VERSION = $(shell cat package.json | grep version | sed -E 's/ *"version": "//' | sed -E 's/",.*//')
@@ -34,7 +44,7 @@ typecheck:
3444

3545
test: typecheck
3646
@find test -name "*.test.ts" -not -path "*/multi-package/*" | sort | \
37-
xargs -P 1 -I{} sh -c 'echo "==> {}" && bun test {} || exit 255'
47+
xargs -P $(TEST_JOBS) -I{} sh -c 'out=$$(bun test --timeout $(TEST_TIMEOUT) "$$1" 2>&1); st=$$?; printf "==> %s\n%s\n" "$$1" "$$out"; [ $$st -eq 0 ] || exit 255' _ {}
3848

3949
test-multi-package: typecheck
4050
bun test test/api/write-generator/multi-package/cda.test.ts

‎src/api/builder.ts‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,13 @@ export type CanonicalManagerOptions = {
4848
/** How a package's shipped `.index.json` is treated: trust it (`"use"`, default), heal a
4949
* broken one with a directory scan (`"recover"`), or rebuild it unconditionally (`"regenerate"`). */
5050
packageIndex?: PackageIndexMode;
51-
/** Drop the CanonicalManager cache before loading packages. */
51+
/** Drop the CanonicalManager cache before loading packages. Note that this wipes the whole
52+
* working directory, every cached package set included — pair it with a dedicated
53+
* `workingDir` so unrelated callers keep their downloads. */
5254
dropCache?: boolean;
55+
/** Directory holding the downloaded packages and their processed cache
56+
* (default: `.codegen-cache/canonical-manager-cache`). */
57+
workingDir?: string;
5358
/** Per-phase patch handlers (package-defect fixes; helpers on the `@atomic-ehr/fhir-canonical-manager/patch` subpath). */
5459
patches?: Partial<Patches>;
5560
};
@@ -306,7 +311,7 @@ export class APIBuilder {
306311
injectedManager ??
307312
CanonicalManager({
308313
packages: [],
309-
workingDir: ".codegen-cache/canonical-manager-cache",
314+
workingDir: cm.workingDir ?? ".codegen-cache/canonical-manager-cache",
310315
registry: cm.registry,
311316
dropCache: cm.dropCache,
312317
patches: {
@@ -556,7 +561,10 @@ export class APIBuilder {
556561

557562
this.logger.debug(`Starting generation with ${this.generators.length} generators`);
558563
try {
559-
if (this.options.cleanOutput) await cleanup(this.options, this.logger);
564+
// An all-in-memory run writes nothing, so wiping the output directory would only
565+
// destroy a previous run's files (and, for concurrent runs, each other's).
566+
const writesToDisk = this.generators.some((gen) => !gen.writer.opts.inMemoryOnly);
567+
if (this.options.cleanOutput && writesToDisk) await cleanup(this.options, this.logger);
560568

561569
let register: Register;
562570
if (this.prebuiltRegister) {

‎src/api/writer-generator/mustache.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,22 @@ export class MustacheGenerator extends FileSystemWriter<MustacheGeneratorOptions
165165
.forEach(this._renderResource.bind(this));
166166

167167
this._renderUtility(modelFactory.createUtility());
168-
this.copyStaticFiles();
169168

170169
if (this.opts.shouldRunHooks) {
171170
await this._runHooks(this.opts.hooks.afterGenerate);
172171
}
173172
return;
174173
}
175174

175+
/** Static files bypass the write buffer, so they are copied once generation is done and
176+
* `opts.inMemoryOnly` reflects the caller again rather than `generateAsync`'s buffering. */
177+
override async generateAsync(tsIndex: TypeSchemaIndex): Promise<void> {
178+
await super.generateAsync(tsIndex);
179+
this.copyStaticFiles();
180+
}
181+
176182
copyStaticFiles() {
183+
if (this.opts.inMemoryOnly) return;
177184
const staticDir = Path.resolve(this.opts.sources.staticSource);
178185
if (!staticDir) {
179186
throw new Error("staticDir must be set in subclass.");

‎src/api/writer-generator/writer.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export abstract class FileSystemWriter<T extends FileSystemWriterOptions = FileS
121121
absPath: Path.resolve(destination),
122122
tokens: [content],
123123
};
124-
fs.cpSync(source, destination);
124+
if (!this.opts.inMemoryOnly) fs.cpSync(source, destination);
125125
}
126126

127127
cp(source: string, destination: string) {
@@ -134,7 +134,7 @@ export abstract class FileSystemWriter<T extends FileSystemWriterOptions = FileS
134134
absPath: Path.resolve(destination),
135135
tokens: [content],
136136
};
137-
fs.cpSync(source, destination);
137+
if (!this.opts.inMemoryOnly) fs.cpSync(source, destination);
138138
}
139139

140140
abstract generate(_tsIndex: TypeSchemaIndex): Promise<void>;
@@ -148,6 +148,7 @@ export abstract class FileSystemWriter<T extends FileSystemWriterOptions = FileS
148148
}
149149

150150
async flushAsync(): Promise<void> {
151+
if (this.opts.inMemoryOnly) return;
151152
const files = this.writtenFiles();
152153
const dirs = new Set<string>();
153154

‎test/api/mustache.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { APIBuilder } from "@root/api/builder";
33
import { mkErrorLogger, r4Manager } from "@typeschema-test/utils";
44

55
describe("Mustache Template Based Generation", async () => {
6-
const report = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() })
6+
const report = await new APIBuilder({ register: await r4Manager(), logger: mkErrorLogger() })
77
.mustache("./examples/mustache/java", {
88
debug: "COMPACT",
99
inMemoryOnly: true,

‎test/api/write-generator/csharp.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { APIBuilder } from "@root/api/builder";
33
import { mkErrorLogger, r4Manager } from "@typeschema-test/utils";
44

55
describe("C# Writer Generator", async () => {
6-
const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() })
6+
const result = await new APIBuilder({ register: await r4Manager(), logger: mkErrorLogger() })
77
.csharp({
88
inMemoryOnly: true,
99
})

‎test/api/write-generator/introspection.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { APIBuilder } from "@root/api/builder";
33
import { mkErrorLogger, r4Manager } from "@typeschema-test/utils";
44

55
describe("IntrospectionWriter - Fhir Schema Output", async () => {
6-
const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() })
6+
const result = await new APIBuilder({ register: await r4Manager(), logger: mkErrorLogger() })
77
.introspection({ fhirSchemas: "introspection" })
88
.introspection({ fhirSchemas: "introspection.ndjson" })
99
.generate();
@@ -30,7 +30,7 @@ describe("IntrospectionWriter - Fhir Schema Output", async () => {
3030
});
3131

3232
describe("IntrospectionWriter - TypeSchema output", async () => {
33-
const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() })
33+
const result = await new APIBuilder({ register: await r4Manager(), logger: mkErrorLogger() })
3434
.typeSchema({
3535
treeShake: {
3636
"hl7.fhir.r4.core": {
@@ -91,7 +91,7 @@ describe("IntrospectionWriter - TypeSchema output", async () => {
9191
});
9292

9393
describe("IntrospectionWriter - flat profile output", async () => {
94-
const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() })
94+
const result = await new APIBuilder({ register: await r4Manager(), logger: mkErrorLogger() })
9595
.typeSchema({
9696
treeShake: {
9797
"hl7.fhir.r4.core": {
@@ -135,7 +135,7 @@ describe("IntrospectionWriter - flat profile output", async () => {
135135
});
136136

137137
describe("IntrospectionWriter - typeTree", async () => {
138-
const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() })
138+
const result = await new APIBuilder({ register: await r4Manager(), logger: mkErrorLogger() })
139139
.typeSchema({
140140
treeShake: {
141141
"hl7.fhir.r4.core": {
@@ -164,7 +164,7 @@ describe("IntrospectionWriter - typeTree", async () => {
164164
});
165165

166166
describe("IntrospectionWriter - StructureDefinition output", async () => {
167-
const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() })
167+
const result = await new APIBuilder({ register: await r4Manager(), logger: mkErrorLogger() })
168168
.typeSchema({
169169
treeShake: {
170170
"hl7.fhir.r4.core": {

‎test/api/write-generator/python.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { APIBuilder } from "@root/api/builder";
33
import { mkErrorLogger, r4Manager } from "@typeschema-test/utils";
44

55
describe("Python Writer Generator", async () => {
6-
const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() })
6+
const result = await new APIBuilder({ register: await r4Manager(), logger: mkErrorLogger() })
77
.python({
88
inMemoryOnly: true,
99
client: "none",
@@ -112,7 +112,7 @@ describe("Python Writer Generator", async () => {
112112

113113
describe("Python R4 Example (with generateProfile)", async () => {
114114
const logger = mkErrorLogger();
115-
const result = await new APIBuilder({ register: r4Manager, logger })
115+
const result = await new APIBuilder({ register: await r4Manager(), logger })
116116
.python({
117117
inMemoryOnly: true,
118118
generateProfile: true,
@@ -191,7 +191,7 @@ describe("Python US Core Example", async () => {
191191

192192
describe("Python client option", async () => {
193193
const gen = async (opts: { client?: "fhirpy" | "none"; fhirpyClient?: boolean }) => {
194-
const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() })
194+
const result = await new APIBuilder({ register: await r4Manager(), logger: mkErrorLogger() })
195195
.python({ inMemoryOnly: true, ...opts })
196196
.generate();
197197
return result.filesGenerated.python!;

‎test/api/write-generator/typescript.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const appendValueSet = (register: Register, pkg: PackageMeta, valueSet: PVS) =>
2020
};
2121

2222
describe("TypeScript Writer Generator", async () => {
23-
const result = await new APIBuilder({ register: r4Manager, logger: mkErrorLogger() })
23+
const result = await new APIBuilder({ register: await r4Manager(), logger: mkErrorLogger() })
2424
.typescript({
2525
inMemoryOnly: true,
2626
})
@@ -181,7 +181,7 @@ describe("TypeScript profile fixed CodeableConcept semantics", async () => {
181181
});
182182

183183
describe("TypeScript CDA with Logical Model Promotion to Resource", async () => {
184-
const result = await new APIBuilder({ register: ccdaManager, logger: mkErrorLogger() })
184+
const result = await new APIBuilder({ register: await ccdaManager(), logger: mkErrorLogger() })
185185
.typeSchema({
186186
promoteLogical: {
187187
"hl7.cda.uv.core": ["http://hl7.org/cda/stds/core/StructureDefinition/Material" as CanonicalUrl],
@@ -209,7 +209,7 @@ describe("TypeScript CDA with Logical Model Promotion to Resource", async () =>
209209

210210
describe("TypeScript R4 Example (with generateProfile)", async () => {
211211
const logger = mkErrorLogger();
212-
const result = await new APIBuilder({ register: r4Manager, logger })
212+
const result = await new APIBuilder({ register: await r4Manager(), logger })
213213
.typescript({
214214
inMemoryOnly: true,
215215
withDebugComment: false,

‎test/unit/api/builtin-patches.test.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ import { mkErrorLogger } from "@typeschema-test/utils";
55
// Real-closure test (downloads hl7.fhir.r5.core once, then served from the CM tarball cache):
66
// the shipped builtin patches drop R5's known-broken CodeSystem profiles at the package index,
77
// and builtinPatches: false is the explicit opt-out. Index patches apply at scan time, so each
8-
// run drops the processed cache to make the configuration observable.
8+
// run drops the processed cache to make the configuration observable. That drop wipes the
9+
// whole working directory, so this test keeps its own — otherwise it would evict every other
10+
// test's and example's downloaded packages on each run.
911
const generate = async (opts: object) => {
10-
const result = await new APIBuilder({ ...opts, logger: mkErrorLogger(), canonicalManager: { dropCache: true } })
12+
const result = await new APIBuilder({
13+
...opts,
14+
logger: mkErrorLogger(),
15+
canonicalManager: { dropCache: true, workingDir: ".codegen-cache/builtin-patches-cache" },
16+
})
1117
.fromPackage("hl7.fhir.r5.core", "5.0.0")
1218
.introspection({ typeSchemas: "type-schemas", inMemoryOnly: true })
1319
.generate();

0 commit comments

Comments
 (0)