2026-04-12 19:31:12 +02:00
|
|
|
const fs = require('node:fs/promises');
|
|
|
|
|
const path = require('node:path');
|
|
|
|
|
const { spawn } = require('node:child_process');
|
|
|
|
|
|
|
|
|
|
const ROOT_DIR = path.resolve(__dirname, '..', '..');
|
|
|
|
|
const WORKSPACE_ROOT = path.resolve(ROOT_DIR, '..');
|
|
|
|
|
const LOG_ROOT = path.join(ROOT_DIR, 'logs');
|
|
|
|
|
const SEARCH_LOG_ROOT = path.join(LOG_ROOT, 'searches');
|
|
|
|
|
const FACEAI_BASE_URL = process.env.FACEAI_E2E_BASE_URL || 'http://127.0.0.1:3001';
|
2026-04-22 18:41:37 +02:00
|
|
|
const SIMULATOR_URL = process.env.FACEAI_E2E_SIMULATOR_URL || 'http://127.0.0.1:8080/Foto2.abl?id_gara=1018557&pageRow=96&pageNumber=1';
|
2026-04-12 19:31:12 +02:00
|
|
|
const LEGACY_BASE_URL = process.env.FACEAI_E2E_LEGACY_BASE_URL || 'http://127.0.0.1:8080';
|
|
|
|
|
const LEGACY_HOME_URL = process.env.FACEAI_E2E_LEGACY_HOME_URL || `${LEGACY_BASE_URL}/index.jsp`;
|
|
|
|
|
const SELFIE_NAME = process.env.FACEAI_E2E_SELFIE || 'DSC_1960.JPG';
|
|
|
|
|
const EXPECTED_MATCH_COUNT = Number(process.env.FACEAI_E2E_EXPECTED_MATCH_COUNT || '6');
|
2026-04-22 18:41:37 +02:00
|
|
|
const LEGACY_RACE_ID = process.env.FACEAI_E2E_RACE_ID || '1018557';
|
2026-04-12 19:31:12 +02:00
|
|
|
|
|
|
|
|
function quoteShellArg(value) {
|
|
|
|
|
if (!/[\s"]/u.test(value)) {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `"${value.replace(/"/g, '\\"')}"`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sleep(ms) {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
setTimeout(resolve, ms);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function runCommand(command, args, options = {}) {
|
|
|
|
|
const { cwd = ROOT_DIR, allowFailure = false } = options;
|
|
|
|
|
const executable = process.platform === 'win32' && command === 'npm' ? 'npm.cmd' : command;
|
|
|
|
|
const useShell = process.platform === 'win32';
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const child = useShell
|
|
|
|
|
? spawn([executable, ...args].map(quoteShellArg).join(' '), {
|
|
|
|
|
cwd,
|
|
|
|
|
env: process.env,
|
|
|
|
|
shell: true,
|
|
|
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
|
|
|
})
|
|
|
|
|
: spawn(executable, args, {
|
|
|
|
|
cwd,
|
|
|
|
|
env: process.env,
|
|
|
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let stdout = '';
|
|
|
|
|
let stderr = '';
|
|
|
|
|
|
|
|
|
|
child.stdout.on('data', (chunk) => {
|
|
|
|
|
stdout += chunk.toString();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
child.stderr.on('data', (chunk) => {
|
|
|
|
|
stderr += chunk.toString();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
child.on('error', reject);
|
|
|
|
|
child.on('close', (code) => {
|
|
|
|
|
const result = { code, stdout, stderr };
|
|
|
|
|
if (code === 0 || allowFailure) {
|
|
|
|
|
resolve(result);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const error = new Error(`Command failed: ${executable} ${args.join(' ')}`);
|
|
|
|
|
error.result = result;
|
|
|
|
|
reject(error);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dockerCompose(args, options) {
|
|
|
|
|
return runCommand('docker', ['compose', ...args], options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function prepareHostState() {
|
|
|
|
|
await fs.rm(LOG_ROOT, { recursive: true, force: true });
|
|
|
|
|
await fs.mkdir(LOG_ROOT, { recursive: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function waitForHttp(url, validate, timeoutMs = 3 * 60 * 1000) {
|
|
|
|
|
const deadline = Date.now() + timeoutMs;
|
|
|
|
|
let lastError = null;
|
|
|
|
|
|
|
|
|
|
while (Date.now() < deadline) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
const bodyText = await response.text();
|
|
|
|
|
let parsedBody = null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
parsedBody = JSON.parse(bodyText);
|
|
|
|
|
} catch {
|
|
|
|
|
parsedBody = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (validate({ response, bodyText, parsedBody })) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastError = new Error(`Readiness check did not pass for ${url}.`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
lastError = error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await sleep(1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw lastError || new Error(`Timed out waiting for ${url}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSelfiePath(fileName = SELFIE_NAME) {
|
|
|
|
|
return path.join(WORKSPACE_ROOT, 'test_pkl', 'test_images', fileName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildSimulatorUrl({
|
2026-04-22 18:41:37 +02:00
|
|
|
raceId = LEGACY_RACE_ID,
|
2026-04-12 19:31:12 +02:00
|
|
|
lang = 'it',
|
2026-04-22 18:41:37 +02:00
|
|
|
raceSlug = 'livorno',
|
|
|
|
|
raceName = 'Livorno',
|
2026-04-12 19:31:12 +02:00
|
|
|
raceYear = '2026',
|
|
|
|
|
raceMonthFolder = '04.APRILE',
|
2026-04-22 18:41:37 +02:00
|
|
|
raceFolder = 'LIVORNO',
|
|
|
|
|
pageRow = '96',
|
|
|
|
|
pageNumber = '1'
|
2026-04-12 19:31:12 +02:00
|
|
|
} = {}) {
|
2026-04-22 18:41:37 +02:00
|
|
|
const url = new URL('/Foto2.abl', LEGACY_BASE_URL);
|
|
|
|
|
url.searchParams.set('id_gara', raceId);
|
|
|
|
|
url.searchParams.set('pageRow', pageRow);
|
|
|
|
|
url.searchParams.set('pageNumber', pageNumber);
|
2026-04-12 19:31:12 +02:00
|
|
|
url.searchParams.set('lang', lang);
|
|
|
|
|
return url.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildHandoffUrl({
|
2026-04-22 18:41:37 +02:00
|
|
|
raceId = LEGACY_RACE_ID,
|
2026-04-12 19:31:12 +02:00
|
|
|
lang = 'it',
|
2026-04-22 18:41:37 +02:00
|
|
|
raceSlug = 'livorno',
|
|
|
|
|
raceName = 'Livorno',
|
2026-04-12 19:31:12 +02:00
|
|
|
raceYear = '2026',
|
|
|
|
|
raceMonthFolder = '04.APRILE',
|
2026-04-22 18:41:37 +02:00
|
|
|
raceFolder = 'LIVORNO',
|
2026-04-12 19:31:12 +02:00
|
|
|
userId = '1',
|
|
|
|
|
displayName = `Local Test User ${userId}`,
|
|
|
|
|
email = `local-test-${userId}@example.invalid`,
|
|
|
|
|
membershipStatus = 'active',
|
|
|
|
|
returnUrl = buildSimulatorUrl({ raceId, lang, raceSlug, raceName, raceYear, raceMonthFolder, raceFolder })
|
|
|
|
|
} = {}) {
|
2026-04-22 18:41:37 +02:00
|
|
|
const url = new URL('/dev/legacy/launch', FACEAI_BASE_URL);
|
2026-04-12 19:31:12 +02:00
|
|
|
url.searchParams.set('raceId', raceId);
|
|
|
|
|
url.searchParams.set('raceSlug', raceSlug);
|
|
|
|
|
url.searchParams.set('raceName', raceName);
|
|
|
|
|
url.searchParams.set('raceYear', raceYear);
|
|
|
|
|
url.searchParams.set('raceMonthFolder', raceMonthFolder);
|
|
|
|
|
url.searchParams.set('raceFolder', raceFolder);
|
|
|
|
|
url.searchParams.set('lang', lang);
|
|
|
|
|
url.searchParams.set('returnUrl', returnUrl);
|
|
|
|
|
url.searchParams.set('devUserId', userId);
|
|
|
|
|
url.searchParams.set('devDisplayName', displayName);
|
|
|
|
|
url.searchParams.set('devEmail', email);
|
|
|
|
|
url.searchParams.set('devMembershipStatus', membershipStatus);
|
|
|
|
|
return url.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSearchArtifacts(searchId) {
|
|
|
|
|
const searchRoot = path.join(SEARCH_LOG_ROOT, searchId);
|
|
|
|
|
return {
|
|
|
|
|
searchRoot,
|
|
|
|
|
backendLogPath: path.join(LOG_ROOT, 'backend.log'),
|
|
|
|
|
processorLogPath: path.join(LOG_ROOT, 'processor.log'),
|
|
|
|
|
workerLogPath: path.join(searchRoot, 'worker.log'),
|
|
|
|
|
matcherLogPath: path.join(searchRoot, 'matcher.log')
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function readUtf8(filePath) {
|
|
|
|
|
return fs.readFile(filePath, 'utf8');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
ROOT_DIR,
|
|
|
|
|
LOG_ROOT,
|
|
|
|
|
SEARCH_LOG_ROOT,
|
|
|
|
|
FACEAI_BASE_URL,
|
|
|
|
|
LEGACY_BASE_URL,
|
|
|
|
|
LEGACY_HOME_URL,
|
|
|
|
|
SIMULATOR_URL,
|
|
|
|
|
SELFIE_NAME,
|
|
|
|
|
EXPECTED_MATCH_COUNT,
|
2026-04-22 18:41:37 +02:00
|
|
|
LEGACY_RACE_ID,
|
2026-04-12 19:31:12 +02:00
|
|
|
buildHandoffUrl,
|
|
|
|
|
buildSimulatorUrl,
|
|
|
|
|
dockerCompose,
|
|
|
|
|
getSearchArtifacts,
|
|
|
|
|
getSelfiePath,
|
|
|
|
|
prepareHostState,
|
|
|
|
|
readUtf8,
|
|
|
|
|
runCommand,
|
|
|
|
|
waitForHttp
|
|
|
|
|
};
|