189 lines
5.3 KiB
TypeScript
189 lines
5.3 KiB
TypeScript
import fs from "fs";
|
|
import waitOn from "wait-on";
|
|
import axios from "axios";
|
|
import { spawn, execSync, ChildProcess } from "child_process";
|
|
import killPort from "kill-port";
|
|
import os from "os";
|
|
import path from "path";
|
|
import { Transform } from "stream";
|
|
import { test } from "@playwright/test";
|
|
|
|
const BINARY = "./evcc";
|
|
const IS_CI = !!process.env["GITHUB_ACTIONS"];
|
|
const LOG_ENABLED = !IS_CI;
|
|
|
|
// sometimes evcc startup fails due to infra issues in runner ususally fixed by retry. allowing some fails to avoid github annotations clutter
|
|
let allowedStartupFails = IS_CI ? 2 : 0;
|
|
|
|
function workerPort() {
|
|
const index = Number(process.env["TEST_WORKER_INDEX"] ?? 0);
|
|
return 11000 + index;
|
|
}
|
|
|
|
function ocppPort() {
|
|
const index = Number(process.env["TEST_WORKER_INDEX"] ?? 0);
|
|
return 12000 + index;
|
|
}
|
|
|
|
function logPrefix() {
|
|
return `[worker:${process.env["TEST_WORKER_INDEX"]}]`;
|
|
}
|
|
|
|
function createSteamLog() {
|
|
return new Transform({
|
|
transform(chunk: Buffer, _, callback) {
|
|
const lines = chunk.toString().split("\n");
|
|
lines.forEach((line: string) => {
|
|
if (line.trim()) log(line);
|
|
});
|
|
callback();
|
|
},
|
|
});
|
|
}
|
|
|
|
function log(...args: any[]) {
|
|
if (LOG_ENABLED) {
|
|
console.log(logPrefix(), ...args);
|
|
}
|
|
}
|
|
|
|
export function baseUrl() {
|
|
return `http://localhost:${workerPort()}`;
|
|
}
|
|
|
|
function dbPath() {
|
|
const file = `evcc-${workerPort()}.db`;
|
|
return path.join(os.tmpdir(), file);
|
|
}
|
|
|
|
export async function start(
|
|
config?: string,
|
|
sqlDumps?: string | null,
|
|
flags: string | string[] = "--disable-auth"
|
|
) {
|
|
await _clean();
|
|
if (sqlDumps) {
|
|
await _restoreDatabase(sqlDumps);
|
|
}
|
|
return await _start(config, flags);
|
|
}
|
|
|
|
export async function stop(instance?: ChildProcess) {
|
|
await _stop(instance);
|
|
await _clean();
|
|
}
|
|
|
|
export async function restart(
|
|
config?: string,
|
|
flags: string | string[] = "--disable-auth",
|
|
alreadyStopped = false
|
|
) {
|
|
if (!alreadyStopped) {
|
|
await _stop();
|
|
}
|
|
await _start(config, flags);
|
|
}
|
|
|
|
export async function cleanRestart(config: string, sqlDumps: string) {
|
|
await _stop();
|
|
await _clean();
|
|
if (sqlDumps) {
|
|
await _restoreDatabase(sqlDumps);
|
|
}
|
|
await _start(config);
|
|
}
|
|
|
|
async function _restoreDatabase(sqlDumps: string) {
|
|
const dumps = Array.isArray(sqlDumps) ? sqlDumps : [sqlDumps];
|
|
for (const dump of dumps) {
|
|
log("loading database", dbPath(), dump);
|
|
execSync(`sqlite3 ${dbPath()} < tests/${dump}`);
|
|
}
|
|
}
|
|
|
|
async function _start(config?: string, flags: string | string[] = []) {
|
|
const configArgs = config ? ["--config", config.includes("/") ? config : `tests/${config}`] : [];
|
|
const port = workerPort();
|
|
const ocpp = ocppPort();
|
|
log(`wait until port ${port} is available`);
|
|
// wait for port to be available
|
|
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: LOG_ENABLED });
|
|
log(`starting evcc on ports ${port} (HTTP) and ${ocpp} (OCPP)`);
|
|
const additionalFlags = typeof flags === "string" ? [flags] : flags;
|
|
additionalFlags.push("--log", "debug,httpd:trace");
|
|
log("starting evcc", { config, port, ocpp, additionalFlags });
|
|
const instance = spawn(BINARY, [...configArgs, ...additionalFlags], {
|
|
env: {
|
|
EVCC_NETWORK_PORT: port.toString(),
|
|
EVCC_OCPP_PORT: ocpp.toString(),
|
|
EVCC_DATABASE_DSN: dbPath(),
|
|
},
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
const steamLog = createSteamLog();
|
|
instance.stdout.pipe(steamLog);
|
|
instance.stderr.pipe(steamLog);
|
|
instance.on("exit", (code) => {
|
|
log("evcc terminated", { code, port, config });
|
|
steamLog.end();
|
|
});
|
|
try {
|
|
await waitOn({ resources: [baseUrl()], log: LOG_ENABLED, timeout: 50000 });
|
|
} catch (error) {
|
|
instance.kill("SIGKILL");
|
|
console.error(logPrefix(), `evcc startup failed: ${error}`);
|
|
|
|
if (allowedStartupFails > 0) {
|
|
allowedStartupFails--;
|
|
test.skip(true, `evcc startup timeout (${allowedStartupFails} skips remaining)`);
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
async function _stop(instance?: ChildProcess) {
|
|
const port = workerPort();
|
|
if (instance) {
|
|
log("shutting down evcc hard", { port });
|
|
instance.kill("SIGKILL");
|
|
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: LOG_ENABLED, timeout: 5000 });
|
|
log("evcc is down", { port });
|
|
return;
|
|
}
|
|
let cookie;
|
|
try {
|
|
// check if auth is required
|
|
const res = await axios.get(`${baseUrl()}/api/auth/status`);
|
|
log("auth status", res.status, res.statusText, res.data);
|
|
// login required
|
|
if (!res.data) {
|
|
const res = await axios.post(`${baseUrl()}/api/auth/login`, { password: "secret" });
|
|
log("login", res.status, res.statusText);
|
|
cookie = res.headers["set-cookie"];
|
|
}
|
|
log("shutting down evcc", { port });
|
|
await axios.post(`${baseUrl()}/api/system/shutdown`, {}, { headers: { cookie } });
|
|
} catch (error) {
|
|
const port = workerPort();
|
|
log(`shutdown failed, last resort: kill by port`, port, error);
|
|
try {
|
|
await killPort(port);
|
|
log(`killed process on port ${port}`);
|
|
} catch (killError) {
|
|
log(`no process found on port ${port} or kill failed:`, killError);
|
|
}
|
|
}
|
|
log(`wait until port ${port} is closed`);
|
|
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: LOG_ENABLED, timeout: 5000 });
|
|
log("evcc is down", { port });
|
|
}
|
|
|
|
async function _clean() {
|
|
const db = dbPath();
|
|
if (fs.existsSync(db)) {
|
|
log("delete database", db);
|
|
fs.unlinkSync(db);
|
|
}
|
|
}
|