mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
283 lines
8.3 KiB
TypeScript
283 lines
8.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { EventEmitter } from "node:events";
|
|
import test from "node:test";
|
|
|
|
import { AppRuntimeConfig } from "../main/config/runtime-config";
|
|
import { createNodecgProcessManager } from "../main/nodecg/process-manager";
|
|
|
|
class MockChildProcess extends EventEmitter {
|
|
pid: number | undefined;
|
|
killed = false;
|
|
exitCode: number | null = null;
|
|
signalCode: NodeJS.Signals | null = null;
|
|
|
|
constructor(pid: number) {
|
|
super();
|
|
this.pid = pid;
|
|
}
|
|
}
|
|
|
|
function getBaseConfig(): AppRuntimeConfig {
|
|
return {
|
|
title: "Scoreko",
|
|
userModelId: "com.scoreko.desktop",
|
|
userDataDirectoryName: "scoreko",
|
|
nodecgPort: "9090",
|
|
bundleName: "scoreko-dev",
|
|
mainDashboardRoute: "dashboard/scoreko-dev/main.html?standalone=true",
|
|
loadingDashboardRoute: "dashboard/loading/main.html?standalone=true",
|
|
loadDelayMs: 10000,
|
|
startupTimeoutMs: 100,
|
|
nodecgKillTimeoutMs: 10,
|
|
};
|
|
}
|
|
|
|
test("startNodeCG validates NodeCG installation before starting", async () => {
|
|
const manager = createNodecgProcessManager({
|
|
isDev: true,
|
|
nodecgRootPath: "/fake/scoreko-dev",
|
|
nodecgBaseUrl: "http://127.0.0.1:9090",
|
|
appConfig: getBaseConfig(),
|
|
log: () => undefined,
|
|
deps: {
|
|
pathExists: () => false,
|
|
spawnProcess: () => {
|
|
throw new Error("it must not try to start if validation fails");
|
|
},
|
|
},
|
|
});
|
|
|
|
await assert.rejects(async () => {
|
|
await manager.startNodecgProcess();
|
|
}, /Scoreko app folder does not exist/);
|
|
});
|
|
|
|
test("startNodeCG fails when there are no read/write permissions", async () => {
|
|
const manager = createNodecgProcessManager({
|
|
isDev: true,
|
|
nodecgRootPath: "/fake/scoreko-dev",
|
|
nodecgBaseUrl: "http://127.0.0.1:9090",
|
|
appConfig: getBaseConfig(),
|
|
log: () => undefined,
|
|
deps: {
|
|
pathExists: () => true,
|
|
hasReadWriteAccess: () => false,
|
|
},
|
|
});
|
|
|
|
await assert.rejects(async () => {
|
|
await manager.startNodecgProcess();
|
|
}, /No read\/write permissions on scoreko app folder/);
|
|
});
|
|
|
|
test("waitForNodeCGReady resolves when endpoint returns 404", async () => {
|
|
const child = new MockChildProcess(4321);
|
|
const manager = createNodecgProcessManager({
|
|
isDev: true,
|
|
nodecgRootPath: "/fake/scoreko-dev",
|
|
nodecgBaseUrl: "http://127.0.0.1:9090",
|
|
appConfig: getBaseConfig(),
|
|
log: () => undefined,
|
|
deps: {
|
|
platform: "linux",
|
|
pathExists: () => true,
|
|
spawnProcess: () => child as unknown as import("node:child_process").ChildProcess,
|
|
fetchUrl: async () => ({ ok: false, status: 404 }) as Response,
|
|
setTimer: (handler: (...args: unknown[]) => void, _timeoutMs: number) => {
|
|
handler();
|
|
return 0 as never;
|
|
},
|
|
stdoutWrite: () => undefined,
|
|
stderrWrite: () => undefined,
|
|
probePortAvailable: async () => true,
|
|
hasReadWriteAccess: () => true,
|
|
},
|
|
});
|
|
|
|
await manager.startNodecgProcess();
|
|
await assert.doesNotReject(async () => {
|
|
await manager.waitForNodecgReady(Date.now());
|
|
});
|
|
});
|
|
|
|
test("stopNodeCG sends SIGTERM and then SIGKILL if the process does not exit", async () => {
|
|
const child = new MockChildProcess(9999);
|
|
const timers: Array<() => void> = [];
|
|
const killSignals: Array<{ pid: number; signal: NodeJS.Signals }> = [];
|
|
|
|
const manager = createNodecgProcessManager({
|
|
isDev: true,
|
|
nodecgRootPath: "/fake/scoreko-dev",
|
|
nodecgBaseUrl: "http://127.0.0.1:9090",
|
|
appConfig: getBaseConfig(),
|
|
log: () => undefined,
|
|
deps: {
|
|
platform: "linux",
|
|
pathExists: () => true,
|
|
spawnProcess: () => child as unknown as import("node:child_process").ChildProcess,
|
|
fetchUrl: async () => ({ ok: false, status: 404 }) as Response,
|
|
killProcess: (pid, signal) => {
|
|
killSignals.push({ pid, signal });
|
|
},
|
|
setTimer: (handler: (...args: unknown[]) => void, _timeoutMs: number) => {
|
|
timers.push(() => handler());
|
|
return 0 as never;
|
|
},
|
|
stdoutWrite: () => undefined,
|
|
stderrWrite: () => undefined,
|
|
probePortAvailable: async () => true,
|
|
hasReadWriteAccess: () => true,
|
|
},
|
|
});
|
|
|
|
await manager.startNodecgProcess();
|
|
const stopPromise = manager.stopNodecgProcessGracefully();
|
|
|
|
assert.deepEqual(killSignals, [{ pid: -9999, signal: "SIGTERM" }]);
|
|
|
|
timers.forEach((runTimer) => {
|
|
runTimer();
|
|
});
|
|
|
|
assert.deepEqual(killSignals, [
|
|
{ pid: -9999, signal: "SIGTERM" },
|
|
{ pid: -9999, signal: "SIGKILL" },
|
|
]);
|
|
|
|
child.emit("exit", 0, null);
|
|
await stopPromise;
|
|
});
|
|
|
|
test("stopNodeCG reuses the same promise when invoked in parallel", async () => {
|
|
const child = new MockChildProcess(5555);
|
|
|
|
const manager = createNodecgProcessManager({
|
|
isDev: true,
|
|
nodecgRootPath: "/fake/scoreko-dev",
|
|
nodecgBaseUrl: "http://127.0.0.1:9090",
|
|
appConfig: getBaseConfig(),
|
|
log: () => undefined,
|
|
deps: {
|
|
pathExists: () => true,
|
|
spawnProcess: () => child as unknown as import("node:child_process").ChildProcess,
|
|
fetchUrl: async () => ({ ok: false, status: 404 }) as Response,
|
|
killProcess: () => undefined,
|
|
setTimer: () => 0,
|
|
stdoutWrite: () => undefined,
|
|
stderrWrite: () => undefined,
|
|
probePortAvailable: async () => true,
|
|
hasReadWriteAccess: () => true,
|
|
},
|
|
});
|
|
|
|
await manager.startNodecgProcess();
|
|
const firstStop = manager.stopNodecgProcessGracefully();
|
|
const secondStop = manager.stopNodecgProcessGracefully();
|
|
|
|
assert.equal(firstStop, secondStop);
|
|
|
|
child.emit("exit", 0, null);
|
|
await firstStop;
|
|
});
|
|
|
|
test("stopNodeCG normalizes negative timeout to zero", async () => {
|
|
const child = new MockChildProcess(7777);
|
|
const timeouts: number[] = [];
|
|
|
|
const manager = createNodecgProcessManager({
|
|
isDev: true,
|
|
nodecgRootPath: "/fake/scoreko-dev",
|
|
nodecgBaseUrl: "http://127.0.0.1:9090",
|
|
appConfig: {
|
|
...getBaseConfig(),
|
|
nodecgKillTimeoutMs: -10,
|
|
},
|
|
log: () => undefined,
|
|
deps: {
|
|
pathExists: () => true,
|
|
spawnProcess: () => child as unknown as import("node:child_process").ChildProcess,
|
|
fetchUrl: async () => ({ ok: false, status: 404 }) as Response,
|
|
killProcess: () => undefined,
|
|
setTimer: (handler, timeoutMs) => {
|
|
timeouts.push(timeoutMs);
|
|
handler();
|
|
return 0;
|
|
},
|
|
stdoutWrite: () => undefined,
|
|
stderrWrite: () => undefined,
|
|
probePortAvailable: async () => true,
|
|
hasReadWriteAccess: () => true,
|
|
},
|
|
});
|
|
|
|
await manager.startNodecgProcess();
|
|
const stopPromise = manager.stopNodecgProcessGracefully();
|
|
|
|
assert.ok(timeouts.includes(0));
|
|
|
|
child.emit("exit", 0, null);
|
|
await stopPromise;
|
|
});
|
|
|
|
test("startNodeCG fails if the port is already in use", async () => {
|
|
const manager = createNodecgProcessManager({
|
|
isDev: true,
|
|
nodecgRootPath: "/fake/scoreko-dev",
|
|
nodecgBaseUrl: "http://127.0.0.1:9090",
|
|
appConfig: getBaseConfig(),
|
|
log: () => undefined,
|
|
deps: {
|
|
pathExists: () => true,
|
|
hasReadWriteAccess: () => true,
|
|
probePortAvailable: async () => false,
|
|
},
|
|
});
|
|
|
|
await assert.rejects(async () => {
|
|
await manager.startNodecgProcess();
|
|
}, /is already in use/);
|
|
});
|
|
|
|
test("waitForNodeCGReady exposes diagnostics when NodeCG exits before readiness", async () => {
|
|
const child = new MockChildProcess(4242);
|
|
const manager = createNodecgProcessManager({
|
|
isDev: true,
|
|
nodecgRootPath: "/fake/scoreko-dev",
|
|
nodecgBaseUrl: "http://127.0.0.1:9090",
|
|
appConfig: getBaseConfig(),
|
|
log: () => undefined,
|
|
deps: {
|
|
pathExists: () => true,
|
|
platform: "linux",
|
|
spawnProcess: () => child as unknown as import("node:child_process").ChildProcess,
|
|
fetchUrl: async () => {
|
|
child.emit("exit", 1, null);
|
|
throw new Error("still starting");
|
|
},
|
|
setTimer: (handler) => {
|
|
handler();
|
|
return 0;
|
|
},
|
|
stdoutWrite: () => undefined,
|
|
stderrWrite: () => undefined,
|
|
probePortAvailable: async () => true,
|
|
hasReadWriteAccess: () => true,
|
|
},
|
|
});
|
|
|
|
await manager.startNodecgProcess();
|
|
|
|
await assert.rejects(
|
|
async () => {
|
|
await manager.waitForNodecgReady(Date.now());
|
|
},
|
|
(error: unknown) => {
|
|
assert.ok(error instanceof Error);
|
|
assert.match(error.message, /NodeCG exited before becoming ready/);
|
|
assert.match(error.message, /Last recorded exit/);
|
|
assert.match(error.message, /NodeCG path: \/fake\/scoreko-dev/);
|
|
return true;
|
|
},
|
|
);
|
|
});
|