attempt to fix e2e

This commit is contained in:
kakkokari-gtyih 2026-04-19 02:21:47 +09:00
commit fb0008c85a
5 changed files with 238 additions and 55 deletions

View file

@ -17,7 +17,7 @@
"compile-config": "node ./scripts/compile_config.js",
"build": "rolldown -c",
"build:unit": "rolldown -c --sourcemap",
"build:e2e": "swc src -d src-js -D --strip-leading-paths && swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
"build:e2e": "rolldown -c --e2e",
"watch:swc": "swc src -d built -D -w --strip-leading-paths",
"build:tsc": "tsgo -p tsconfig.json && tsc-alias -p tsconfig.json",
"watch": "pnpm compile-config && node ./scripts/watch.mjs",

View file

@ -53,6 +53,7 @@ function backendDevServerPlugin(): Plugin {
export default defineConfig((args) => {
const isWatchMode = args.watch != null && args.watch !== 'false';
const isE2E = args.e2e != null && args.e2e !== 'false';
// 通常のビルド時にexternalとするモジュール
const externalModules: ExternalOption = [
@ -75,33 +76,52 @@ export default defineConfig((args) => {
'oauth2orize',
];
return {
input: [
'./src/boot/entry.ts',
'./src/boot/cli.ts',
'./src/config.ts',
'./src/postgres.ts',
'./src/server/api/openapi/gen-spec.ts',
],
platform: 'node',
tsconfig: true,
plugins: [
esmShim(),
(isWatchMode ? backendDevServerPlugin() : undefined),
],
output: {
keepNames: true,
minify: !isWatchMode,
sourcemap: isWatchMode,
dir: './built',
cleanDir: !isWatchMode,
format: 'esm',
},
watch: {
include: ['src/**/*.{ts,js,mjs,cjs,tsx,json}'],
clearScreen: false,
},
// ビルドの高速化のために、watchモードのときは外部モジュールは全てバンドルしないようにする
external: isWatchMode ? /^(?!@\/)[^.\/](?!:[\/\\])/ : externalModules,
};
if (isE2E) {
return {
input: './test-server/entry.ts',
platform: 'node',
tsconfig: './test-server/tsconfig.json',
plugins: [
esmShim(),
],
output: {
keepNames: true,
sourcemap: true,
dir: './built-test',
cleanDir: true,
format: 'esm',
},
external: externalModules,
};
} else {
return {
input: [
'./src/boot/entry.ts',
'./src/boot/cli.ts',
'./src/config.ts',
'./src/postgres.ts',
'./src/server/api/openapi/gen-spec.ts',
],
platform: 'node',
tsconfig: true,
plugins: [
esmShim(),
(isWatchMode ? backendDevServerPlugin() : undefined),
],
output: {
keepNames: true,
minify: !isWatchMode,
sourcemap: isWatchMode,
dir: './built',
cleanDir: !isWatchMode,
format: 'esm',
},
watch: {
include: ['src/**/*.{ts,js,mjs,cjs,tsx,json}'],
clearScreen: false,
},
// ビルドの高速化のために、watchモードのときは外部モジュールは全てバンドルしないようにする
external: isWatchMode ? /^(?!@\/)[^.\/](?!:[\/\\])/ : externalModules,
};
}
});

View file

@ -1,6 +1,10 @@
import { setTimeout as delay } from 'node:timers/promises';
import { fileURLToPath } from 'node:url';
import { takeCoverage } from 'node:v8';
import { portToPid } from 'pid-port';
import fkill from 'fkill';
import Fastify from 'fastify';
import Fastify, { type FastifyInstance } from 'fastify';
import { execaNode, type ResultPromise } from 'execa';
import { NestFactory } from '@nestjs/core';
import { MainModule } from '@/MainModule.js';
import { ServerService } from '@/server/ServerService.js';
@ -10,18 +14,27 @@ import { INestApplicationContext } from '@nestjs/common';
const config = loadConfig();
const originEnv = JSON.stringify(process.env);
const entryFilePath = fileURLToPath(import.meta.url);
const controllerPort = config.port + 1000;
const isExecutedDirectly = process.argv[1] != null && entryFilePath === process.argv[1];
process.env.NODE_ENV = 'test';
let app: INestApplicationContext;
let serverService: ServerService;
let controllerServer: FastifyInstance | null = null;
let shutdownPromise: Promise<void> | null = null;
async function flushCoverage() {
if (process.env.NODE_V8_COVERAGE) {
takeCoverage();
}
}
/**
*
*
*/
async function launch() {
await killTestServer();
async function launchApplication() {
console.log('starting application...');
app = await NestFactory.createApplicationContext(MainModule, {
@ -30,21 +43,39 @@ async function launch() {
serverService = app.get(ServerService);
await serverService.launch();
await startControllerEndpoints();
// ジョブキューは必要な時にテストコード側で起動する
// ジョブキューが動くとテスト結果の確認に支障が出ることがあるので意図的に動かさないでいる
console.log('application initialized.');
}
async function disposeApplication() {
await flushCoverage();
if (serverService) {
await serverService.dispose();
}
if (app) {
await app.close();
}
// @ts-expect-error cleanup for relaunch in the same process
app = undefined;
// @ts-expect-error cleanup for relaunch in the same process
serverService = undefined;
}
async function relaunchApplication() {
await disposeApplication();
await launchApplication();
}
/**
* killする
*/
async function killTestServer() {
//
async function killServerAtPort(port: number) {
try {
const pid = await portToPid(config.port);
const pid = await portToPid(port);
if (pid) {
await fkill(pid, { force: true });
}
@ -53,15 +84,44 @@ async function killTestServer() {
}
}
async function killTestServers() {
await Promise.all([
killServerAtPort(config.port),
killServerAtPort(controllerPort),
]);
}
async function shutdownChildProcess() {
if (shutdownPromise) {
return shutdownPromise;
}
shutdownPromise = (async () => {
if (controllerServer) {
await controllerServer.close();
controllerServer = null;
}
await disposeApplication();
})().finally(() => {
shutdownPromise = null;
});
return shutdownPromise;
}
/**
*
* @param port
*/
async function startControllerEndpoints(port = config.port + 1000) {
async function startControllerEndpoints(port = controllerPort) {
const fastify = Fastify();
fastify.get('/healthz', async () => {
return { ok: true };
});
fastify.post<{ Body: { key?: string, value?: string } }>('/env', async (req, res) => {
console.log(req.body);
const key = req.body['key'];
if (!key) {
res.code(400).send({ success: false });
@ -75,24 +135,126 @@ async function startControllerEndpoints(port = config.port + 1000) {
fastify.post<{ Body: { key?: string, value?: string } }>('/env-reset', async (req, res) => {
process.env = JSON.parse(originEnv);
await serverService.dispose();
await app.close();
await killTestServer();
console.log('starting application...');
app = await NestFactory.createApplicationContext(MainModule, {
logger: new NestLogger(),
});
serverService = app.get(ServerService);
await serverService.launch();
await relaunchApplication();
res.code(200).send({ success: true });
});
fastify.post('/shutdown', async (_req, res) => {
res.code(200).send({ success: true });
setImmediate(() => {
void shutdownChildProcess().finally(() => {
process.exit(0);
});
});
});
await fastify.listen({ port: port, host: 'localhost' });
controllerServer = fastify;
}
export default launch;
async function runServerProcess() {
await killTestServers();
const terminate = async (signal: NodeJS.Signals) => {
console.log(`received ${signal}, shutting down test server...`);
await shutdownChildProcess();
process.exit(0);
};
process.on('SIGINT', () => {
void terminate('SIGINT');
});
process.on('SIGTERM', () => {
void terminate('SIGTERM');
});
await launchApplication();
await startControllerEndpoints();
}
async function waitForControllerReady() {
for (let attempt = 0; attempt < 120; attempt++) {
try {
const response = await fetch(`http://127.0.0.1:${controllerPort}/healthz`);
if (response.ok) {
return;
}
} catch {
// NOP
}
await delay(500);
}
throw new Error('test server did not become ready in time');
}
async function requestChildShutdown() {
const response = await fetch(`http://127.0.0.1:${controllerPort}/shutdown`, {
method: 'POST',
body: JSON.stringify({}),
});
if (!response.ok) {
throw new Error('failed to shut down test server');
}
}
async function waitForChildExit(child: ResultPromise) {
await child.catch(() => {
// NOP
});
}
function terminateChild(child: ResultPromise, signal: NodeJS.Signals = 'SIGTERM') {
child.kill(signal);
const timeout = setTimeout(() => {
if (!child.killed) {
child.kill('SIGKILL');
}
}, 5000);
void child.finally(() => {
clearTimeout(timeout);
});
}
export default async function globalSetup() {
await killTestServers();
const child = execaNode(entryFilePath, [], {
stdout: process.stdout,
stderr: process.stderr,
env: {
...process.env,
NODE_ENV: 'test',
},
});
try {
await waitForControllerReady();
} catch (error) {
terminateChild(child);
throw error;
}
return async () => {
try {
await requestChildShutdown();
} catch {
terminateChild(child);
}
await waitForChildExit(child);
};
}
if (isExecutedDirectly) {
void runServerProcess().catch((error: unknown) => {
console.error(error);
process.exit(1);
});
}

View file

@ -25,7 +25,6 @@
"isolatedModules": true,
"jsx": "react-jsx",
"jsxImportSource": "@kitajs/html",
"rootDir": "../src",
"paths": {
"@/*": ["../src/*"]
},

View file

@ -12,6 +12,8 @@ export const baseConfig = defineConfig({
},
restoreMocks: true,
testTimeout: 60000,
hookTimeout: 60000,
teardownTimeout: 60000,
maxWorkers: 1,
logHeapUsage: true,
vmMemoryLimit: 1024,