338 lines
11 KiB
TypeScript
338 lines
11 KiB
TypeScript
import { alwaysExternal } from "@trigger.dev/core/v3/build";
|
|
import type { BuildManifest, WorkerManifest } from "@trigger.dev/core/v3/schemas";
|
|
import * as fs from "node:fs";
|
|
import { mkdir, rename, rm } from "node:fs/promises";
|
|
import * as path from "node:path";
|
|
import { rimraf } from "rimraf";
|
|
import { buildWorker, rewriteBuildManifestPaths } from "../src/build/buildWorker.js";
|
|
import { loadConfig } from "../src/config.js";
|
|
import { indexWorkerManifest } from "../src/indexing/indexWorkerManifest.js";
|
|
import { writeJSONFile } from "../src/utilities/fileSystem.js";
|
|
import { logger } from "../src/utilities/logger.js";
|
|
import { normalizeImportPath } from "../src/utilities/normalizeImportPath.js";
|
|
import { getTmpDir } from "../src/utilities/tempDirectories.js";
|
|
import type { TestCase } from "./fixtures.js";
|
|
import { fixturesConfig } from "./fixtures.js";
|
|
import type { E2EOptions } from "./schemas.js";
|
|
import { E2EOptionsSchema } from "./schemas.js";
|
|
import type { PackageManager } from "./utils.js";
|
|
import {
|
|
executeTestCaseRun,
|
|
installFixtureDeps,
|
|
LOCKFILES,
|
|
parsePackageManager,
|
|
runTsc,
|
|
} from "./utils.js";
|
|
|
|
const TIMEOUT = 120_000;
|
|
|
|
interface E2EFixtureTest extends TestCase {
|
|
fixtureDir: string;
|
|
packageManager: PackageManager;
|
|
tempDir: string;
|
|
workspaceDir: string;
|
|
}
|
|
|
|
const testCases: TestCase[] = process.env.MOD
|
|
? fixturesConfig.filter(({ id }) => process.env.MOD === id)
|
|
: fixturesConfig;
|
|
|
|
let options: E2EOptions;
|
|
|
|
try {
|
|
options = E2EOptionsSchema.parse({
|
|
logLevel: process.env.LOG,
|
|
packageManager: process.env.PM,
|
|
});
|
|
} catch (_e) {
|
|
options = {
|
|
logLevel: "log",
|
|
};
|
|
}
|
|
|
|
logger.loggerLevel = options.logLevel;
|
|
|
|
if (testCases.length === 0) {
|
|
if (process.env.MOD) {
|
|
throw new Error(`No test case found for ${process.env.MOD}`);
|
|
} else {
|
|
throw new Error("Nothing to test");
|
|
}
|
|
}
|
|
|
|
describe("buildWorker", async () => {
|
|
beforeEach<E2EFixtureTest>(async ({ fixtureDir, skip, packageManager, workspaceDir }) => {
|
|
await rimraf(path.join(workspaceDir, "**/node_modules"), {
|
|
glob: true,
|
|
});
|
|
|
|
await rimraf(path.join(workspaceDir, ".yarn"), { glob: true });
|
|
|
|
if (
|
|
packageManager === "npm" &&
|
|
(fs.existsSync(path.resolve(path.join(workspaceDir, "yarn.lock"))) ||
|
|
fs.existsSync(path.resolve(path.join(workspaceDir, "yarn.lock.copy"))))
|
|
) {
|
|
// `npm ci` & `npm install` will update an existing yarn.lock
|
|
try {
|
|
await rename(
|
|
path.resolve(path.join(workspaceDir, "yarn.lock")),
|
|
path.resolve(path.join(workspaceDir, "yarn.lock.copy"))
|
|
);
|
|
} catch (_e) {
|
|
await rename(
|
|
path.resolve(path.join(workspaceDir, "yarn.lock.copy")),
|
|
path.resolve(path.join(workspaceDir, "yarn.lock"))
|
|
);
|
|
}
|
|
}
|
|
|
|
if (
|
|
options.packageManager &&
|
|
!fs.existsSync(path.resolve(fixtureDir, LOCKFILES[options.packageManager]))
|
|
) {
|
|
skip();
|
|
}
|
|
|
|
await installFixtureDeps({ fixtureDir, packageManager, workspaceDir });
|
|
}, TIMEOUT);
|
|
|
|
afterEach<E2EFixtureTest>(async ({ packageManager, workspaceDir }) => {
|
|
if (packageManager === "npm") {
|
|
try {
|
|
await rename(
|
|
path.resolve(path.join(workspaceDir, "yarn.lock.copy")),
|
|
path.resolve(path.join(workspaceDir, "yarn.lock"))
|
|
);
|
|
} catch {}
|
|
}
|
|
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
for (let testCase of testCases) {
|
|
test.extend<E2EFixtureTest>({
|
|
// Seed `workspaceRelativeDir` before the spread so the key always exists.
|
|
// vitest 4 resolves fixture-to-fixture dependencies strictly at
|
|
// `test.extend()` time: the `workspaceDir` fixture below destructures
|
|
// `workspaceRelativeDir`, so it must be a defined fixture even for test
|
|
// cases that don't set it (it's an optional `TestCase` field). The spread
|
|
// overrides this default when the case provides its own value.
|
|
workspaceRelativeDir: "",
|
|
...testCase,
|
|
fixtureDir: async ({ id }, use) =>
|
|
await use(path.resolve(path.join(process.cwd(), "e2e/fixtures", id))),
|
|
workspaceDir: async ({ fixtureDir, workspaceRelativeDir }, use) =>
|
|
await use(path.resolve(path.join(fixtureDir, workspaceRelativeDir))),
|
|
packageManager: async ({ workspaceDir }, use) =>
|
|
await use(await parsePackageManager(options.packageManager, workspaceDir)),
|
|
tempDir: async ({ workspaceDir }, use) => {
|
|
const existingTempDir = path.resolve(path.join(workspaceDir, ".trigger"));
|
|
|
|
if (fs.existsSync(existingTempDir)) {
|
|
await rm(existingTempDir, { force: true, recursive: true });
|
|
}
|
|
await use(
|
|
(await mkdir(path.join(workspaceDir, ".trigger"), { recursive: true })) as string
|
|
);
|
|
},
|
|
})(
|
|
`fixture ${testCase.id}`,
|
|
{ timeout: TIMEOUT },
|
|
async ({
|
|
id,
|
|
tempDir,
|
|
tsconfig,
|
|
packageManager,
|
|
fixtureDir,
|
|
workspaceDir,
|
|
wantConfigInvalidError,
|
|
wantConfigNotFoundError,
|
|
wantBuildWorkerError,
|
|
wantIndexingError,
|
|
buildManifestMatcher,
|
|
workerManifestMatcher,
|
|
runs,
|
|
}) => {
|
|
let resolvedConfig: Awaited<ReturnType<typeof loadConfig>>;
|
|
|
|
const configExpect = expect(
|
|
(async () => {
|
|
resolvedConfig = await loadConfig({
|
|
cwd: workspaceDir,
|
|
});
|
|
})(),
|
|
wantConfigNotFoundError || wantConfigInvalidError
|
|
? "does not resolve config"
|
|
: "resolves config"
|
|
);
|
|
|
|
if (wantConfigNotFoundError) {
|
|
await configExpect.rejects.toThrowError();
|
|
return;
|
|
}
|
|
|
|
await configExpect.resolves.not.toThrowError();
|
|
|
|
if (wantConfigInvalidError) {
|
|
expect(resolvedConfig!).toBeUndefined();
|
|
return;
|
|
}
|
|
|
|
expect(resolvedConfig!).toBeTruthy();
|
|
|
|
if (tsconfig) {
|
|
const tscResult = await runTsc(
|
|
workspaceDir,
|
|
tsconfig,
|
|
packageManager === "yarn" ? fixtureDir : undefined
|
|
);
|
|
|
|
expect(tscResult.success).toBe(true);
|
|
}
|
|
|
|
const destination = getTmpDir(workspaceDir, "build");
|
|
|
|
let buildManifest: BuildManifest;
|
|
|
|
const buildExpect = expect(
|
|
(async () => {
|
|
buildManifest = await buildWorker({
|
|
target: "deploy",
|
|
environment: "test",
|
|
destination: destination.path,
|
|
resolvedConfig: resolvedConfig!,
|
|
rewritePaths: false,
|
|
forcedExternals: alwaysExternal,
|
|
});
|
|
})(),
|
|
wantBuildWorkerError ? "does not build" : "builds"
|
|
);
|
|
|
|
if (wantBuildWorkerError) {
|
|
await buildExpect.rejects.toThrowError();
|
|
return;
|
|
}
|
|
|
|
await buildExpect.resolves.not.toThrowError();
|
|
|
|
if (buildManifestMatcher) {
|
|
for (const external of buildManifestMatcher.externals ?? []) {
|
|
expect(buildManifest!.externals).toContainEqual(external);
|
|
}
|
|
|
|
for (const file of buildManifestMatcher.files ?? []) {
|
|
const found = (buildManifestMatcher.files ?? []).find((f) => f?.entry === file?.entry);
|
|
expect(found).toBeTruthy();
|
|
}
|
|
} else {
|
|
expect(buildManifest!).toBeTruthy();
|
|
}
|
|
|
|
logger.debug("Build manifest", buildManifest!);
|
|
|
|
const rewrittenManifest = rewriteBuildManifestPaths(buildManifest!, destination.path);
|
|
|
|
if (resolvedConfig!.instrumentedPackageNames?.length ?? 0 > 0) {
|
|
expect(rewrittenManifest.loaderEntryPoint).toBe("/app/src/entryPoints/loader.mjs");
|
|
} else {
|
|
expect(rewrittenManifest.loaderEntryPoint).toBeUndefined();
|
|
}
|
|
|
|
expect(rewrittenManifest.indexWorkerEntryPoint).toBe(
|
|
"/app/src/entryPoints/managed-index-worker.mjs"
|
|
);
|
|
|
|
const stdout: string[] = [];
|
|
const stderr: string[] = [];
|
|
|
|
let workerManifest: WorkerManifest;
|
|
|
|
const indexExpect = expect(
|
|
(async () => {
|
|
workerManifest = await indexWorkerManifest({
|
|
runtime: buildManifest!.runtime,
|
|
indexWorkerPath: buildManifest!.indexWorkerEntryPoint,
|
|
buildManifestPath: path.join(destination.path, "build.json"),
|
|
nodeOptions: buildManifest!.loaderEntryPoint
|
|
? `--import=${normalizeImportPath(buildManifest!.loaderEntryPoint)}`
|
|
: undefined,
|
|
env: testCase.envVars ?? {},
|
|
otelHookExclude: buildManifest!.otelImportHook?.exclude,
|
|
otelHookInclude: buildManifest!.otelImportHook?.include,
|
|
handleStdout(data) {
|
|
stdout.push(data);
|
|
logger.debug("indexWorkerManifest handleStdout");
|
|
logger.debug(data);
|
|
},
|
|
handleStderr(data) {
|
|
if (!data.includes("DeprecationWarning")) {
|
|
stderr.push(data);
|
|
logger.debug("indexWorkerManifest handleStderr");
|
|
logger.debug(data);
|
|
}
|
|
},
|
|
});
|
|
})(),
|
|
wantIndexingError ? "does not index" : "indexes"
|
|
);
|
|
|
|
if (wantIndexingError) {
|
|
await indexExpect.rejects.toThrowError();
|
|
return;
|
|
}
|
|
|
|
await indexExpect.resolves.not.toThrowError();
|
|
|
|
if (workerManifestMatcher) {
|
|
expect(workerManifest!).toMatchObject(workerManifestMatcher);
|
|
} else {
|
|
expect(workerManifest!).toBeTruthy();
|
|
}
|
|
|
|
logger.debug("Worker manifest", workerManifest!);
|
|
|
|
if (runs && runs.length > 0) {
|
|
await writeJSONFile(path.join(destination.path, "index.json"), workerManifest!);
|
|
}
|
|
|
|
for (const taskRun of runs || []) {
|
|
const { result, totalDurationMs, spans } = await executeTestCaseRun({
|
|
run: taskRun,
|
|
testCase,
|
|
destination: destination.path,
|
|
workerManifest: workerManifest!,
|
|
contentHash: buildManifest!.contentHash,
|
|
});
|
|
|
|
logger.debug("Task run result", result);
|
|
|
|
expect(result.ok).toBe(taskRun.result.ok);
|
|
|
|
if (result.ok) {
|
|
if (taskRun.result.durationMs) {
|
|
expect(totalDurationMs).toBeGreaterThanOrEqual(taskRun.result.durationMs);
|
|
}
|
|
|
|
if (taskRun.result.output) {
|
|
expect(result.output).toEqual(taskRun.result.output);
|
|
}
|
|
|
|
if (taskRun.result.outputType) {
|
|
expect(result.outputType).toEqual(taskRun.result.outputType);
|
|
}
|
|
|
|
if (taskRun.result.spans) {
|
|
for (const spanName of taskRun.result.spans) {
|
|
const foundSpan = spans.find((span) => span.name === spanName);
|
|
|
|
expect(foundSpan).toBeTruthy();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
});
|