1193 lines
40 KiB
JavaScript
1193 lines
40 KiB
JavaScript
/**
|
|
* Integration tests: media-img REST (`serve`) and line IPC (`ipc`) — TCP on all platforms;
|
|
* Unix domain socket on non-Windows (same JSON line protocol as TCP).
|
|
*
|
|
* Run (from packages/media/cpp, after build:release):
|
|
* npm run test:media
|
|
*
|
|
* npm run test:media -- --rest-only
|
|
* npm run test:media -- --ipc-only
|
|
* npm run test:media -- --templates-only
|
|
* npm run test:media -- --glob-batch-only
|
|
* npm run test:media:glob:raw (ARW under tests/assets/raw; skips if folder missing or empty)
|
|
* npm run test:media:url (HTTP URL inputs — needs network)
|
|
* npm run test:media:multipart (multipart upload → image body only)
|
|
*
|
|
* Fixtures: tests/assets (run `node tests/assets/build-fixtures.mjs` if missing).
|
|
*
|
|
* Each run writes **tests/test-report-last.md** (markdown: summary, per-suite timings, step ms).
|
|
*
|
|
* Env:
|
|
* MEDIA_IMG_TEST_UNIX — Unix socket path for IPC UDS test (default /tmp/media-img-test.sock)
|
|
*/
|
|
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
rmSync,
|
|
unlinkSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import net from 'node:net';
|
|
import { tmpdir } from 'node:os';
|
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import {
|
|
mediaExePath,
|
|
defaultAssetsDir,
|
|
timeouts,
|
|
ipcUnixPath,
|
|
platform,
|
|
} from './media-presets.js';
|
|
import { requestLineJson, connectTcp, connectUnix } from './media-line-ipc.js';
|
|
import { probeTcpPort, createAssert, pipeWorkerStderr } from './test-commons.js';
|
|
import {
|
|
buildMetricsBundle,
|
|
createMetricsCollector,
|
|
describePngFile,
|
|
fileByteSize,
|
|
pngDimensionsFromBuffer,
|
|
renderMarkdownReport,
|
|
} from './reports.js';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
/** Last run markdown report (overwritten each invocation). */
|
|
const TEST_REPORT_PATH = join(__dirname, '..', 'tests', 'test-report-last.md');
|
|
|
|
const EXE = mediaExePath(__dirname);
|
|
const stats = createAssert();
|
|
const { assert } = stats;
|
|
|
|
const restOnly = process.argv.includes('--rest-only');
|
|
const ipcOnly = process.argv.includes('--ipc-only');
|
|
const templatesOnly = process.argv.includes('--templates-only');
|
|
const globBatchOnly = process.argv.includes('--glob-batch-only');
|
|
const globRaw = process.argv.includes('--glob-raw');
|
|
const urlOnly = process.argv.includes('--url-only');
|
|
const multipartOnly = process.argv.includes('--multipart-only');
|
|
|
|
function getFreePort() {
|
|
return new Promise((resolvePort, reject) => {
|
|
const s = net.createServer();
|
|
s.listen(0, '127.0.0.1', () => {
|
|
const p = s.address().port;
|
|
s.close(() => resolvePort(p));
|
|
});
|
|
s.on('error', reject);
|
|
});
|
|
}
|
|
|
|
async function waitListen(host, port, label) {
|
|
for (let i = 0; i < timeouts.connectAttempts; i++) {
|
|
if (await probeTcpPort(host, port, 300)) return;
|
|
await new Promise((r) => setTimeout(r, timeouts.connectRetryMs));
|
|
}
|
|
throw new Error(`${label}: nothing listening on ${host}:${port}`);
|
|
}
|
|
|
|
function createTestReport() {
|
|
return {
|
|
meta: {},
|
|
notes: [],
|
|
suites: [],
|
|
images: [],
|
|
_cur: null,
|
|
note(text) {
|
|
this.notes.push(text);
|
|
},
|
|
addImage(row) {
|
|
this.images.push(row);
|
|
},
|
|
beginSuite(name) {
|
|
if (this._cur) this.endSuite();
|
|
this._cur = { name, steps: [], t0: performance.now() };
|
|
},
|
|
endSuite() {
|
|
if (!this._cur) return;
|
|
this._cur.totalMs = performance.now() - this._cur.t0;
|
|
this.suites.push(this._cur);
|
|
this._cur = null;
|
|
},
|
|
step(label, ms, detail = '') {
|
|
if (!this._cur) return;
|
|
this._cur.steps.push({
|
|
label,
|
|
ms: Math.round(ms * 100) / 100,
|
|
detail: detail || '',
|
|
});
|
|
},
|
|
finalize(stats, wallMs, exe, assetsDir, extra = {}) {
|
|
const prevAbort = this.meta.abortReason;
|
|
const prevErr = this.meta.uncaughtError;
|
|
this.meta = {
|
|
generatedAt: new Date().toISOString(),
|
|
node: process.version,
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
cwd: process.cwd(),
|
|
argv: process.argv.slice(2),
|
|
exe,
|
|
assetsDir: assetsDir || '',
|
|
passed: stats.passed,
|
|
failed: stats.failed,
|
|
wallClockMs: Math.round(wallMs * 100) / 100,
|
|
...(prevAbort ? { abortReason: prevAbort } : {}),
|
|
...(prevErr ? { uncaughtError: prevErr } : {}),
|
|
...extra,
|
|
};
|
|
if (this._cur) this.endSuite();
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Record main fixture PNGs (bytes + pixel size from IHDR). */
|
|
function registerFixtureImages(rep, assetsDir) {
|
|
const entries = [
|
|
['fixture square-64.png', join(assetsDir, 'square-64.png')],
|
|
['fixture checker-128x128.png', join(assetsDir, 'checker-128x128.png')],
|
|
['fixture glob-in/root.png', join(assetsDir, 'glob-in', 'root.png')],
|
|
['fixture glob-in/sub/leaf.png', join(assetsDir, 'glob-in', 'sub', 'leaf.png')],
|
|
];
|
|
for (const [label, p] of entries) {
|
|
if (!existsSync(p)) continue;
|
|
const d = describePngFile(p);
|
|
rep.addImage({
|
|
label,
|
|
inputBytes: d.bytes,
|
|
widthPx: d.widthPx,
|
|
heightPx: d.heightPx,
|
|
note: 'on-disk fixture',
|
|
});
|
|
}
|
|
}
|
|
|
|
function timeSync(rep, label, fn, detailFn) {
|
|
const t0 = performance.now();
|
|
const result = fn();
|
|
const ms = performance.now() - t0;
|
|
rep.step(label, ms, detailFn ? detailFn(result) : '');
|
|
return { result, ms };
|
|
}
|
|
|
|
async function timeAsync(rep, label, fn, detailFn) {
|
|
const t0 = performance.now();
|
|
const result = await fn();
|
|
const ms = performance.now() - t0;
|
|
rep.step(label, ms, detailFn ? detailFn(result) : '');
|
|
return { result, ms };
|
|
}
|
|
|
|
function writeTestReportFile(rep, metricsCollector, startedAtIso) {
|
|
mkdirSync(dirname(TEST_REPORT_PATH), { recursive: true });
|
|
const finishedAtIso = new Date().toISOString();
|
|
const metrics = buildMetricsBundle(metricsCollector, startedAtIso, finishedAtIso);
|
|
const failed = rep.meta.failed ?? 0;
|
|
const ok =
|
|
failed === 0 && !rep.meta.abortReason && !rep.meta.uncaughtError;
|
|
let md = renderMarkdownReport({
|
|
ok,
|
|
passed: rep.meta.passed,
|
|
failed,
|
|
abortReason: rep.meta.abortReason,
|
|
uncaughtError: rep.meta.uncaughtError,
|
|
error: rep.meta.uncaughtError,
|
|
meta: {
|
|
cwd: process.cwd(),
|
|
displayName: 'media-img integration',
|
|
testName: 'media-img-integration',
|
|
writtenAt: finishedAtIso,
|
|
generatedAt: rep.meta.generatedAt,
|
|
exe: rep.meta.exe,
|
|
assetsDir: rep.meta.assetsDir,
|
|
argv: Array.isArray(rep.meta.argv) ? rep.meta.argv.join(' ') : String(rep.meta.argv ?? ''),
|
|
wallClockMs: rep.meta.wallClockMs,
|
|
},
|
|
metrics,
|
|
images: rep.images,
|
|
mediaSuites: rep.suites,
|
|
integrationNotes: rep.notes,
|
|
});
|
|
md += `\n---\n\n*Artifact: \`tests/test-report-last.md\` — overwritten on each test run.*\n`;
|
|
writeFileSync(TEST_REPORT_PATH, md, 'utf8');
|
|
}
|
|
|
|
/** Multipart POST /v1/resize: image bytes in response (not JSON). */
|
|
async function multipartResizeTests(base, inPng, rep) {
|
|
const pngBytes = readFileSync(inPng);
|
|
const srcDim = pngDimensionsFromBuffer(pngBytes);
|
|
const inPngSize = fileByteSize(inPng) ?? pngBytes.length;
|
|
rep.addImage({
|
|
label: 'multipart upload source (PNG body in form)',
|
|
inputBytes: inPngSize,
|
|
widthPx: srcDim?.width ?? null,
|
|
heightPx: srcDim?.height ?? null,
|
|
note: 'fixture bytes attached to each multipart request',
|
|
});
|
|
const blob = () => new Blob([pngBytes], { type: 'image/png' });
|
|
|
|
const form = new FormData();
|
|
form.append('file', blob(), 'square-64.png');
|
|
form.append('max_width', '32');
|
|
form.append('max_height', '32');
|
|
const { result: r1, ms: r1Ms } = await timeAsync(
|
|
rep,
|
|
'multipart POST (file → jpeg)',
|
|
async () => {
|
|
const res = await fetch(`${base}/v1/resize`, {
|
|
method: 'POST',
|
|
body: form,
|
|
signal: AbortSignal.timeout(timeouts.httpMs),
|
|
});
|
|
const ab = await res.arrayBuffer();
|
|
return { res, ab };
|
|
},
|
|
(x) => `HTTP ${x.res.status}, ${x.res.headers.get('content-type') ?? ''}`,
|
|
);
|
|
assert(r1.res.ok, 'multipart: field file');
|
|
assert(r1.res.headers.get('content-type') === 'image/jpeg', 'multipart default format → image/jpeg');
|
|
{
|
|
const ab = r1.ab;
|
|
rep.addImage({
|
|
label: 'multipart HTTP response (→ jpeg)',
|
|
inputBytes: inPngSize,
|
|
outputBytes: ab.byteLength,
|
|
computeMs: r1Ms,
|
|
contentType: r1.res.headers.get('content-type') ?? '',
|
|
note: 'resized output body',
|
|
});
|
|
assert(Buffer.from(ab).length > 0, 'multipart file: non-empty body');
|
|
}
|
|
|
|
const fImage = new FormData();
|
|
fImage.append('image', blob(), 'x.png');
|
|
fImage.append('max_width', '24');
|
|
fImage.append('format', 'webp');
|
|
const { result: r2, ms: r2Ms } = await timeAsync(
|
|
rep,
|
|
'multipart POST (image → webp)',
|
|
async () => {
|
|
const res = await fetch(`${base}/v1/resize`, {
|
|
method: 'POST',
|
|
body: fImage,
|
|
signal: AbortSignal.timeout(timeouts.httpMs),
|
|
});
|
|
const ab = await res.arrayBuffer();
|
|
return { res, ab };
|
|
},
|
|
(x) => `HTTP ${x.res.status}, ${x.res.headers.get('content-type') ?? ''}`,
|
|
);
|
|
assert(r2.res.ok, 'multipart: field image + format webp');
|
|
assert(r2.res.headers.get('content-type') === 'image/webp', 'multipart webp → image/webp');
|
|
{
|
|
const ab = r2.ab;
|
|
rep.addImage({
|
|
label: 'multipart HTTP response (→ webp)',
|
|
inputBytes: inPngSize,
|
|
outputBytes: ab.byteLength,
|
|
computeMs: r2Ms,
|
|
contentType: r2.res.headers.get('content-type') ?? '',
|
|
note: 'resized output body',
|
|
});
|
|
assert(Buffer.from(ab).byteLength > 8, 'multipart webp: RIFF/WebP header size');
|
|
}
|
|
|
|
const fUpload = new FormData();
|
|
fUpload.append('upload', blob(), 'up.png');
|
|
fUpload.append('max_width', '20');
|
|
const { result: r3, ms: r3Ms } = await timeAsync(
|
|
rep,
|
|
'multipart POST (upload alias → jpeg)',
|
|
async () => {
|
|
const res = await fetch(`${base}/v1/resize`, {
|
|
method: 'POST',
|
|
body: fUpload,
|
|
signal: AbortSignal.timeout(timeouts.httpMs),
|
|
});
|
|
const ab = await res.arrayBuffer();
|
|
return { res, ab };
|
|
},
|
|
(x) => `HTTP ${x.res.status}, ${x.res.headers.get('content-type') ?? ''}`,
|
|
);
|
|
assert(r3.res.ok, 'multipart: field upload');
|
|
assert(r3.res.headers.get('content-type') === 'image/jpeg', 'multipart upload alias → jpeg');
|
|
{
|
|
const ab = r3.ab;
|
|
rep.addImage({
|
|
label: 'multipart HTTP response (upload field → jpeg)',
|
|
inputBytes: inPngSize,
|
|
outputBytes: ab.byteLength,
|
|
computeMs: r3Ms,
|
|
contentType: r3.res.headers.get('content-type') ?? '',
|
|
note: 'resized output body',
|
|
});
|
|
assert(Buffer.from(ab).length > 0, 'multipart upload: non-empty body');
|
|
}
|
|
|
|
const fPng = new FormData();
|
|
fPng.append('file', blob(), 'square-64.png');
|
|
fPng.append('max_width', '16');
|
|
fPng.append('format', 'png');
|
|
const { result: r4, ms: r4Ms } = await timeAsync(
|
|
rep,
|
|
'multipart POST (file → png)',
|
|
async () => {
|
|
const res = await fetch(`${base}/v1/resize`, {
|
|
method: 'POST',
|
|
body: fPng,
|
|
signal: AbortSignal.timeout(timeouts.httpMs),
|
|
});
|
|
const ab = await res.arrayBuffer();
|
|
return { res, ab };
|
|
},
|
|
(x) => `HTTP ${x.res.status}, ${x.res.headers.get('content-type') ?? ''}`,
|
|
);
|
|
assert(r4.res.ok, 'multipart: format png');
|
|
assert(r4.res.headers.get('content-type') === 'image/png', 'multipart png → image/png');
|
|
{
|
|
const ab = r4.ab;
|
|
const dim = pngDimensionsFromBuffer(Buffer.from(ab));
|
|
rep.addImage({
|
|
label: 'multipart HTTP response (→ png)',
|
|
inputBytes: inPngSize,
|
|
outputBytes: ab.byteLength,
|
|
computeMs: r4Ms,
|
|
widthPx: dim?.width ?? null,
|
|
heightPx: dim?.height ?? null,
|
|
contentType: r4.res.headers.get('content-type') ?? '',
|
|
note: 'resized output body',
|
|
});
|
|
}
|
|
|
|
const noFile = new FormData();
|
|
noFile.append('max_width', '10');
|
|
const { result: r5, ms: r5Ms } = await timeAsync(
|
|
rep,
|
|
'multipart POST (no file → 400)',
|
|
async () => {
|
|
const res = await fetch(`${base}/v1/resize`, {
|
|
method: 'POST',
|
|
body: noFile,
|
|
signal: AbortSignal.timeout(timeouts.httpMs),
|
|
});
|
|
const errText = await res.text();
|
|
return { res, errText };
|
|
},
|
|
(x) => `HTTP ${x.res.status}`,
|
|
);
|
|
assert(r5.res.status === 400, 'multipart without image → 400');
|
|
rep.addImage({
|
|
label: 'multipart HTTP 400 error body',
|
|
outputBytes: Buffer.byteLength(r5.errText, 'utf8'),
|
|
computeMs: r5Ms,
|
|
contentType: r5.res.headers.get('content-type') ?? '',
|
|
note: 'JSON error',
|
|
});
|
|
const jErr = JSON.parse(r5.errText);
|
|
assert(jErr?.error && typeof jErr.error === 'string', 'multipart 400 JSON error body');
|
|
}
|
|
|
|
async function suiteMultipartOnly(assetsDir, rep) {
|
|
console.log('\n── REST: multipart POST /v1/resize only ──\n');
|
|
|
|
const inPng = resolve(assetsDir, 'square-64.png');
|
|
assert(existsSync(inPng), `fixture ${inPng}`);
|
|
|
|
const port = await getFreePort();
|
|
const proc = spawn(EXE, ['serve', '--host', '127.0.0.1', '--port', String(port)], {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
pipeWorkerStderr(proc, '[media-img:serve:multipart]');
|
|
|
|
rep.beginSuite('REST: multipart POST /v1/resize only');
|
|
try {
|
|
await timeAsync(rep, 'wait until HTTP server accepts', () => waitListen('127.0.0.1', port, 'serve'), () => `127.0.0.1:${port}`);
|
|
const base = `http://127.0.0.1:${port}`;
|
|
const { result: h } = await timeAsync(
|
|
rep,
|
|
'GET /health',
|
|
() => fetch(`${base}/health`, { signal: AbortSignal.timeout(timeouts.httpMs) }),
|
|
(res) => `HTTP ${res.status}`,
|
|
);
|
|
assert(h.ok, 'GET /health ok');
|
|
const hj = await h.json();
|
|
assert(hj?.ok === true && hj?.service === 'media-img', 'GET /health JSON');
|
|
|
|
await multipartResizeTests(base, inPng, rep);
|
|
} finally {
|
|
rep.endSuite();
|
|
proc.kill();
|
|
await new Promise((r) => setTimeout(r, 150));
|
|
}
|
|
}
|
|
|
|
async function suiteRest(assetsDir, rep) {
|
|
console.log('\n── REST (media-img serve) ──\n');
|
|
|
|
const inPng = resolve(assetsDir, 'square-64.png');
|
|
assert(existsSync(inPng), `fixture ${inPng}`);
|
|
|
|
const port = await getFreePort();
|
|
const proc = spawn(EXE, ['serve', '--host', '127.0.0.1', '--port', String(port)], {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
pipeWorkerStderr(proc, '[media-img:serve]');
|
|
|
|
rep.beginSuite('REST (media-img serve)');
|
|
try {
|
|
await timeAsync(rep, 'wait until HTTP server accepts', () => waitListen('127.0.0.1', port, 'serve'), () => `127.0.0.1:${port}`);
|
|
|
|
const base = `http://127.0.0.1:${port}`;
|
|
const { result: h } = await timeAsync(
|
|
rep,
|
|
'GET /health',
|
|
() => fetch(`${base}/health`, { signal: AbortSignal.timeout(timeouts.httpMs) }),
|
|
(res) => `HTTP ${res.status}`,
|
|
);
|
|
assert(h.ok, 'GET /health ok');
|
|
const hj = await h.json();
|
|
assert(hj?.ok === true && hj?.service === 'media-img', 'GET /health JSON');
|
|
|
|
const outDir = mkdtempSync(join(tmpdir(), 'media-rest-'));
|
|
const outPng = join(outDir, 'out-32.png');
|
|
const inPngBytes = fileByteSize(inPng);
|
|
const { result: r1, ms: r1Ms } = await timeAsync(
|
|
rep,
|
|
'POST /v1/resize JSON → PNG on disk',
|
|
async () => {
|
|
const res = await fetch(`${base}/v1/resize`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
input: inPng,
|
|
output: outPng,
|
|
max_width: 32,
|
|
max_height: 32,
|
|
}),
|
|
signal: AbortSignal.timeout(timeouts.httpMs),
|
|
});
|
|
const j = await res.json();
|
|
return { res, j };
|
|
},
|
|
(x) => `HTTP ${x.res.status}`,
|
|
);
|
|
assert(r1.res.ok, 'POST /v1/resize ok');
|
|
const j1 = r1.j;
|
|
assert(j1?.ok === true, 'resize response ok');
|
|
assert(existsSync(outPng), 'output png exists');
|
|
{
|
|
const d = describePngFile(outPng);
|
|
rep.addImage({
|
|
label: 'REST JSON → disk (out-32.png)',
|
|
inputBytes: inPngBytes,
|
|
outputBytes: d.bytes,
|
|
computeMs: r1Ms,
|
|
widthPx: d.widthPx,
|
|
heightPx: d.heightPx,
|
|
note: 'server wrote from JSON resize',
|
|
});
|
|
}
|
|
|
|
const outJpg = join(outDir, 'out.jpg');
|
|
const { result: r2, ms: r2Ms } = await timeAsync(
|
|
rep,
|
|
'POST /v1/resize JSON → JPEG on disk',
|
|
async () => {
|
|
const res = await fetch(`${base}/v1/resize`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
input: inPng,
|
|
output: outJpg,
|
|
max_width: 48,
|
|
format: 'jpeg',
|
|
}),
|
|
signal: AbortSignal.timeout(timeouts.httpMs),
|
|
});
|
|
const j = await res.json();
|
|
return { res, j };
|
|
},
|
|
(x) => `HTTP ${x.res.status}`,
|
|
);
|
|
assert(r2.res.ok, 'POST /v1/resize jpeg');
|
|
assert(existsSync(outJpg), 'output jpg exists');
|
|
{
|
|
const b = fileByteSize(outJpg);
|
|
if (b != null) {
|
|
rep.addImage({
|
|
label: 'REST JSON → disk (out.jpg)',
|
|
inputBytes: inPngBytes,
|
|
outputBytes: b,
|
|
computeMs: r2Ms,
|
|
contentType: 'image/jpeg',
|
|
note: 'server wrote from JSON resize',
|
|
});
|
|
}
|
|
}
|
|
|
|
await multipartResizeTests(base, inPng, rep);
|
|
|
|
const { result: bad } = await timeAsync(
|
|
rep,
|
|
'POST /v1/resize JSON missing input file → 500',
|
|
() =>
|
|
fetch(`${base}/v1/resize`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ input: '/nope/nope.png', output: join(outDir, 'x.png') }),
|
|
signal: AbortSignal.timeout(timeouts.httpMs),
|
|
}),
|
|
(res) => `HTTP ${res.status}`,
|
|
);
|
|
assert(bad.status === 500, 'POST /v1/resize missing file → 500');
|
|
|
|
rmSync(outDir, { recursive: true, force: true });
|
|
} finally {
|
|
rep.endSuite();
|
|
proc.kill();
|
|
await new Promise((r) => setTimeout(r, 150));
|
|
}
|
|
}
|
|
|
|
async function suiteDstTemplateRest(assetsDir, rep) {
|
|
console.log('\n── REST: dst path templates (${SRC_DIR}, ${SRC_NAME}, …) ──\n');
|
|
|
|
const inPng = resolve(assetsDir, 'square-64.png');
|
|
assert(existsSync(inPng), `fixture ${inPng}`);
|
|
|
|
const port = await getFreePort();
|
|
const proc = spawn(EXE, ['serve', '--host', '127.0.0.1', '--port', String(port)], {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
pipeWorkerStderr(proc, '[media-img:serve:tpl]');
|
|
|
|
rep.beginSuite('REST: dst path templates (${SRC_DIR}, ${SRC_NAME}, …)');
|
|
try {
|
|
await timeAsync(rep, 'wait until HTTP server accepts', () => waitListen('127.0.0.1', port, 'serve'), () => `port ${port}`);
|
|
const base = `http://127.0.0.1:${port}`;
|
|
const outDir = mkdtempSync(join(tmpdir(), 'media-dst-rest-'));
|
|
|
|
const outPattern = join(outDir, '${SRC_NAME}_thumb.webp');
|
|
const { result: rt } = await timeAsync(
|
|
rep,
|
|
'POST ${SRC_NAME}_thumb.webp template',
|
|
() =>
|
|
fetch(`${base}/v1/resize`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
input: inPng,
|
|
output: outPattern,
|
|
max_width: 24,
|
|
format: 'webp',
|
|
}),
|
|
signal: AbortSignal.timeout(timeouts.httpMs),
|
|
}),
|
|
(res) => `HTTP ${res.status}`,
|
|
);
|
|
assert(rt.ok, 'POST /v1/resize template ok');
|
|
const jt = await rt.json();
|
|
assert(jt?.ok === true, 'template resize JSON ok');
|
|
const expectedWebp = join(outDir, 'square-64_thumb.webp');
|
|
assert(existsSync(expectedWebp), `expected ${expectedWebp}`);
|
|
|
|
const outAmp = join(outDir, '&{SRC_NAME}_tiny.png');
|
|
const { result: r2 } = await timeAsync(
|
|
rep,
|
|
'POST &{SRC_NAME} + expand_glob false',
|
|
() =>
|
|
fetch(`${base}/v1/resize`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
input: inPng,
|
|
output: outAmp,
|
|
expand_glob: false,
|
|
max_width: 16,
|
|
}),
|
|
signal: AbortSignal.timeout(timeouts.httpMs),
|
|
}),
|
|
(res) => `HTTP ${res.status}`,
|
|
);
|
|
assert(r2.ok, 'template + expand_glob false');
|
|
assert(existsSync(join(outDir, 'square-64_tiny.png')), 'ampersand template output');
|
|
|
|
const subDir = join(assetsDir, 'tpl-sub');
|
|
const outNested = join(subDir, '${SRC_NAME}_nested.png');
|
|
const { result: rn } = await timeAsync(
|
|
rep,
|
|
'POST nested ${SRC_NAME} template',
|
|
() =>
|
|
fetch(`${base}/v1/resize`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
input: inPng,
|
|
output: outNested,
|
|
max_width: 12,
|
|
}),
|
|
signal: AbortSignal.timeout(timeouts.httpMs),
|
|
}),
|
|
(res) => `HTTP ${res.status}`,
|
|
);
|
|
assert(rn.ok, 'POST nested subdir + ${SRC_NAME}');
|
|
const expectedNested = join(subDir, 'square-64_nested.png');
|
|
assert(existsSync(expectedNested), `nested template ${expectedNested}`);
|
|
|
|
const outViaSrcDir = '${SRC_DIR}/tpl-from-srcdir/${SRC_NAME}_sd.webp';
|
|
const { result: rsd } = await timeAsync(
|
|
rep,
|
|
'POST ${SRC_DIR} in output path',
|
|
() =>
|
|
fetch(`${base}/v1/resize`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
input: inPng,
|
|
output: outViaSrcDir,
|
|
max_width: 10,
|
|
format: 'webp',
|
|
}),
|
|
signal: AbortSignal.timeout(timeouts.httpMs),
|
|
}),
|
|
(res) => `HTTP ${res.status}`,
|
|
);
|
|
assert(rsd.ok, 'POST ${SRC_DIR} in output path');
|
|
const fromSrcDir = join(assetsDir, 'tpl-from-srcdir', 'square-64_sd.webp');
|
|
assert(existsSync(fromSrcDir), `expected ${fromSrcDir}`);
|
|
|
|
rmSync(outDir, { recursive: true, force: true });
|
|
rmSync(subDir, { recursive: true, force: true });
|
|
rmSync(join(assetsDir, 'tpl-from-srcdir'), { recursive: true, force: true });
|
|
} finally {
|
|
rep.endSuite();
|
|
proc.kill();
|
|
await new Promise((r) => setTimeout(r, 150));
|
|
}
|
|
}
|
|
|
|
async function suiteIpcTcp(assetsDir, rep) {
|
|
console.log('\n── IPC TCP (media-img ipc --host --port) ──\n');
|
|
|
|
const inPng = resolve(assetsDir, 'square-64.png');
|
|
assert(existsSync(inPng), `fixture ${inPng}`);
|
|
|
|
const port = await getFreePort();
|
|
const proc = spawn(EXE, ['ipc', '--host', '127.0.0.1', '--port', String(port)], {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
pipeWorkerStderr(proc, '[media-img:ipc]');
|
|
|
|
rep.beginSuite('IPC TCP (media-img ipc --host --port)');
|
|
try {
|
|
await timeAsync(rep, 'wait until IPC TCP accepts', () => waitListen('127.0.0.1', port, 'ipc'), () => `port ${port}`);
|
|
|
|
const outDir = mkdtempSync(join(tmpdir(), 'media-ipc-tcp-'));
|
|
const outPng = join(outDir, 'ipc-out.png');
|
|
|
|
const { result: sock } = await timeAsync(rep, 'TCP connect (line 1)', () => connectTcp('127.0.0.1', port), () => '');
|
|
const { result: res, ms: ipcMs } = await timeAsync(
|
|
rep,
|
|
'IPC JSON line (ok resize)',
|
|
() =>
|
|
requestLineJson(
|
|
sock,
|
|
{
|
|
input: inPng,
|
|
output: outPng,
|
|
max_width: 32,
|
|
max_height: 32,
|
|
},
|
|
timeouts.ipcReadMs,
|
|
),
|
|
(r) => (r && typeof r === 'object' ? `ok=${r.ok}` : ''),
|
|
);
|
|
sock.destroy();
|
|
assert(res?.ok === true, 'IPC line JSON ok');
|
|
assert(existsSync(outPng), 'IPC output file exists');
|
|
{
|
|
const d = describePngFile(outPng);
|
|
const inB = fileByteSize(inPng);
|
|
rep.addImage({
|
|
label: 'IPC TCP → disk (ipc-out.png)',
|
|
inputBytes: inB,
|
|
outputBytes: d.bytes,
|
|
computeMs: ipcMs,
|
|
widthPx: d.widthPx,
|
|
heightPx: d.heightPx,
|
|
note: 'line-JSON resize',
|
|
});
|
|
}
|
|
|
|
const { result: sock2 } = await timeAsync(rep, 'TCP connect (line 2)', () => connectTcp('127.0.0.1', port), () => '');
|
|
const { result: res2 } = await timeAsync(
|
|
rep,
|
|
'IPC JSON line (missing input)',
|
|
() =>
|
|
requestLineJson(sock2, { input: '/not/found.png', output: join(outDir, 'bad.png') }, timeouts.ipcReadMs),
|
|
(r) => (r && typeof r === 'object' ? `ok=${r.ok}` : ''),
|
|
);
|
|
sock2.destroy();
|
|
assert(res2?.ok === false, 'IPC error path ok=false');
|
|
|
|
rmSync(outDir, { recursive: true, force: true });
|
|
} finally {
|
|
rep.endSuite();
|
|
proc.kill();
|
|
await new Promise((r) => setTimeout(r, 150));
|
|
}
|
|
}
|
|
|
|
async function suiteDstTemplateIpcTcp(assetsDir, rep) {
|
|
console.log('\n── IPC TCP: dst templates ──\n');
|
|
|
|
const inPng = resolve(assetsDir, 'square-64.png');
|
|
assert(existsSync(inPng), `fixture ${inPng}`);
|
|
|
|
const port = await getFreePort();
|
|
const proc = spawn(EXE, ['ipc', '--host', '127.0.0.1', '--port', String(port)], {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
pipeWorkerStderr(proc, '[media-img:ipc:tpl]');
|
|
|
|
rep.beginSuite('IPC TCP: dst templates');
|
|
try {
|
|
await timeAsync(rep, 'wait until IPC TCP accepts', () => waitListen('127.0.0.1', port, 'ipc'), () => `port ${port}`);
|
|
const outDir = mkdtempSync(join(tmpdir(), 'media-dst-ipc-'));
|
|
const outPattern = join(outDir, '${SRC_NAME}_ipc.webp');
|
|
|
|
const { result: sock } = await timeAsync(rep, 'TCP connect', () => connectTcp('127.0.0.1', port), () => '');
|
|
const { result: res } = await timeAsync(
|
|
rep,
|
|
'IPC JSON line (${SRC_NAME}_ipc.webp)',
|
|
() =>
|
|
requestLineJson(
|
|
sock,
|
|
{
|
|
input: inPng,
|
|
output: outPattern,
|
|
max_width: 20,
|
|
format: 'webp',
|
|
},
|
|
timeouts.ipcReadMs,
|
|
),
|
|
(r) => (r && typeof r === 'object' ? `ok=${r.ok}` : ''),
|
|
);
|
|
sock.destroy();
|
|
assert(res?.ok === true, 'IPC template ok');
|
|
assert(existsSync(join(outDir, 'square-64_ipc.webp')), 'IPC template output file');
|
|
|
|
rmSync(outDir, { recursive: true, force: true });
|
|
} finally {
|
|
rep.endSuite();
|
|
proc.kill();
|
|
await new Promise((r) => setTimeout(r, 150));
|
|
}
|
|
}
|
|
|
|
function suiteDstTemplateCli(assetsDir, rep) {
|
|
console.log('\n── CLI: resize --src / --dst templates ──\n');
|
|
|
|
const inPng = resolve(assetsDir, 'square-64.png');
|
|
assert(existsSync(inPng), `fixture ${inPng}`);
|
|
|
|
const outDir = mkdtempSync(join(tmpdir(), 'media-dst-cli-'));
|
|
const dst = join(outDir, '&{SRC_NAME}_cli.jpg');
|
|
|
|
rep.beginSuite('CLI: resize --src / --dst templates');
|
|
try {
|
|
const { result: r } = timeSync(
|
|
rep,
|
|
'spawnSync resize (template dst)',
|
|
() =>
|
|
spawnSync(EXE, ['resize', '--src', inPng, '--dst', dst, '--max-width', '18', '--format', 'jpeg'], {
|
|
encoding: 'utf8',
|
|
}),
|
|
(x) => `exit ${x.status}`,
|
|
);
|
|
assert(r.status === 0, `CLI template exit 0, stderr: ${r.stderr}`);
|
|
assert(existsSync(join(outDir, 'square-64_cli.jpg')), 'CLI template output');
|
|
|
|
rmSync(outDir, { recursive: true, force: true });
|
|
} finally {
|
|
rep.endSuite();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* HTTPS fetch + default output basename under cwd (picsum → 200.jpg, 1600.jpg).
|
|
* Requires network; can be slow.
|
|
*/
|
|
function suiteUrlResizeCli(rep) {
|
|
console.log('\n── CLI: resize https://picsum.photos (default cwd output) ──\n');
|
|
|
|
const tmp = mkdtempSync(join(tmpdir(), 'media-url-'));
|
|
const cases = [
|
|
{ url: 'https://picsum.photos/200', expect: '200.jpg' },
|
|
{ url: 'https://picsum.photos/1600', expect: '1600.jpg' },
|
|
];
|
|
|
|
rep.beginSuite('CLI: resize HTTPS URL (picsum.photos)');
|
|
try {
|
|
for (const { url, expect } of cases) {
|
|
const outPath = join(tmp, expect);
|
|
if (existsSync(outPath)) {
|
|
try {
|
|
unlinkSync(outPath);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
const { result: r, ms: urlMs } = timeSync(
|
|
rep,
|
|
`spawnSync resize URL → ${expect}`,
|
|
() =>
|
|
spawnSync(
|
|
EXE,
|
|
['resize', url, '--max-width', '64', '--max-height', '64', '--no-cache', '--url-timeout', '45'],
|
|
{
|
|
cwd: tmp,
|
|
encoding: 'utf8',
|
|
timeout: 120_000,
|
|
},
|
|
),
|
|
(x) => `exit ${x.status}, network+libvips`,
|
|
);
|
|
assert(r.status === 0, `resize URL exit 0 (${expect}): ${r.stderr || r.stdout}`);
|
|
assert(existsSync(outPath), `expected output ${outPath}`);
|
|
const outSz = fileByteSize(outPath);
|
|
rep.addImage({
|
|
label: `CLI URL resize → ${expect}`,
|
|
outputBytes: outSz,
|
|
computeMs: urlMs,
|
|
note: 'HTTPS source + resize',
|
|
});
|
|
}
|
|
} finally {
|
|
rep.endSuite();
|
|
rmSync(tmp, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
/** Recursive glob under tests/assets/glob-in; outputs kept for manual inspection (see tests/assets/.gitignore). */
|
|
function suiteGlobBatchCli(assetsDir, rep) {
|
|
console.log('\n── CLI: **/*.png glob + ${SRC_DIR}/out/${SRC_NAME}_medium.jpg ──\n');
|
|
console.log(' (outputs under glob-in/**/out/ are kept for manual verification)\n');
|
|
|
|
const rootPng = join(assetsDir, 'glob-in', 'root.png');
|
|
const leafPng = join(assetsDir, 'glob-in', 'sub', 'leaf.png');
|
|
assert(existsSync(rootPng), `fixture ${rootPng}`);
|
|
assert(existsSync(leafPng), `fixture ${leafPng}`);
|
|
|
|
const dstTmpl = '${SRC_DIR}/out/${SRC_NAME}_medium.jpg';
|
|
const jobs = [
|
|
{ src: rootPng, out: join(assetsDir, 'glob-in', 'out', 'root_medium.jpg') },
|
|
{ src: leafPng, out: join(assetsDir, 'glob-in', 'sub', 'out', 'leaf_medium.jpg') },
|
|
];
|
|
|
|
rep.beginSuite('CLI: recursive glob batch + dst templates');
|
|
try {
|
|
for (const job of jobs) {
|
|
const srcAbs = resolve(job.src).replace(/\\/g, '/');
|
|
const { result: r, ms } = timeSync(
|
|
rep,
|
|
`spawnSync resize (${basename(job.src)} → _medium.jpg)`,
|
|
() =>
|
|
spawnSync(EXE, ['resize', '--src', srcAbs, '--dst', dstTmpl, '--max-width', '40', '--format', 'jpeg'], {
|
|
encoding: 'utf8',
|
|
}),
|
|
(x) => `exit ${x.status}`,
|
|
);
|
|
assert(r.status === 0, `glob batch exit 0, stderr: ${r.stderr}`);
|
|
assert(existsSync(job.out), `expected ${job.out}`);
|
|
rep.addImage({
|
|
label: `CLI glob: ${basename(job.src)} → ${basename(job.out)}`,
|
|
inputBytes: fileByteSize(job.src),
|
|
outputBytes: fileByteSize(job.out),
|
|
computeMs: ms,
|
|
note: '${SRC_DIR}/out/${SRC_NAME}_medium.jpg',
|
|
});
|
|
}
|
|
} finally {
|
|
rep.endSuite();
|
|
}
|
|
}
|
|
|
|
function collectArwPathsRecursive(rawDir) {
|
|
const out = [];
|
|
function walk(d) {
|
|
let entries;
|
|
try {
|
|
entries = readdirSync(d, { withFileTypes: true });
|
|
} catch {
|
|
return;
|
|
}
|
|
for (const ent of entries) {
|
|
const p = join(d, ent.name);
|
|
if (ent.isDirectory()) {
|
|
if (ent.name === 'out') continue;
|
|
walk(p);
|
|
} else if (/\.arw$/i.test(ent.name)) out.push(p);
|
|
}
|
|
}
|
|
walk(rawDir);
|
|
return out;
|
|
}
|
|
|
|
/** Recursive glob under tests/assets/raw for *.arw (user-supplied; gitignored). */
|
|
function suiteGlobBatchRawArw(assetsDir, rep) {
|
|
const rawRoot = join(assetsDir, 'raw');
|
|
console.log('\n── CLI: **/*.arw glob (tests/assets/raw) + ${SRC_DIR}/out/${SRC_NAME}_medium.jpg ──\n');
|
|
console.log(' (outputs under raw/**/out/ are kept for manual verification)\n');
|
|
|
|
if (!existsSync(rawRoot)) {
|
|
console.log(' (skip: tests/assets/raw does not exist — create it and add .arw files)\n');
|
|
rep.note('CLI glob batch (raw ARW): skipped (tests/assets/raw missing)');
|
|
return;
|
|
}
|
|
|
|
const arwFiles = collectArwPathsRecursive(rawRoot);
|
|
if (arwFiles.length === 0) {
|
|
console.log(' (skip: no .arw files under tests/assets/raw)\n');
|
|
rep.note('CLI glob batch (raw ARW): skipped (no .arw files)');
|
|
return;
|
|
}
|
|
|
|
const dstTmpl = '${SRC_DIR}/out/${SRC_NAME}_medium.jpg';
|
|
|
|
rep.beginSuite('CLI: recursive glob batch (raw **/*.arw) + dst templates');
|
|
try {
|
|
for (const arwPath of arwFiles) {
|
|
const srcAbs = resolve(arwPath).replace(/\\/g, '/');
|
|
const stem = basename(arwPath).replace(/\.arw$/i, '');
|
|
const expected = join(dirname(arwPath), 'out', `${stem}_medium.jpg`);
|
|
const { result: r, ms } = timeSync(
|
|
rep,
|
|
`spawnSync resize (${basename(arwPath)} → _medium.jpg)`,
|
|
() =>
|
|
spawnSync(EXE, ['resize', '--src', srcAbs, '--dst', dstTmpl, '--max-width', '40', '--format', 'jpeg'], {
|
|
encoding: 'utf8',
|
|
}),
|
|
(x) => `exit ${x.status}`,
|
|
);
|
|
assert(r.status === 0, `glob batch raw exit 0, stderr: ${r.stderr}`);
|
|
assert(existsSync(expected), `expected ${expected}`);
|
|
rep.addImage({
|
|
label: `CLI raw: ${basename(arwPath)} → ${basename(expected)}`,
|
|
inputBytes: fileByteSize(arwPath),
|
|
outputBytes: fileByteSize(expected),
|
|
computeMs: ms,
|
|
note: 'ARW → JPEG',
|
|
});
|
|
}
|
|
} finally {
|
|
rep.endSuite();
|
|
}
|
|
}
|
|
|
|
async function suiteIpcUnix(assetsDir, rep) {
|
|
console.log('\n── IPC Unix (media-img ipc --unix) ──\n');
|
|
|
|
const path = ipcUnixPath();
|
|
if (existsSync(path)) {
|
|
try {
|
|
unlinkSync(path);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
const inPng = resolve(assetsDir, 'checker-128x128.png');
|
|
assert(existsSync(inPng), `fixture ${inPng}`);
|
|
|
|
const proc = spawn(EXE, ['ipc', '--unix', path], {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
pipeWorkerStderr(proc, '[media-img:ipc:uds]');
|
|
|
|
const outDir = mkdtempSync(join(tmpdir(), 'media-ipc-uds-'));
|
|
const outPng = join(outDir, 'uds-out.png');
|
|
|
|
rep.beginSuite('IPC Unix (media-img ipc --unix)');
|
|
try {
|
|
await timeAsync(rep, 'wait for UDS path', async () => {
|
|
for (let i = 0; i < timeouts.connectAttempts; i++) {
|
|
if (existsSync(path)) return;
|
|
await new Promise((r) => setTimeout(r, timeouts.connectRetryMs));
|
|
}
|
|
throw new Error('unix socket path did not appear');
|
|
}, () => path);
|
|
|
|
const { result: res, ms: udsMs } = await timeAsync(
|
|
rep,
|
|
'Unix connect + IPC JSON line',
|
|
async () => {
|
|
let sock;
|
|
for (let i = 0; i < timeouts.connectAttempts; i++) {
|
|
try {
|
|
sock = await connectUnix(path);
|
|
break;
|
|
} catch {
|
|
if (i === timeouts.connectAttempts - 1) throw new Error('connect unix failed');
|
|
await new Promise((r) => setTimeout(r, timeouts.connectRetryMs));
|
|
}
|
|
}
|
|
const lineRes = await requestLineJson(
|
|
sock,
|
|
{
|
|
input: inPng,
|
|
output: outPng,
|
|
max_width: 64,
|
|
},
|
|
timeouts.ipcReadMs,
|
|
);
|
|
sock.destroy();
|
|
return lineRes;
|
|
},
|
|
(r) => (r && typeof r === 'object' ? `ok=${r.ok}` : ''),
|
|
);
|
|
assert(res?.ok === true, 'UDS line JSON ok');
|
|
assert(existsSync(outPng), 'UDS output file exists');
|
|
{
|
|
const d = describePngFile(outPng);
|
|
const inB = fileByteSize(inPng);
|
|
rep.addImage({
|
|
label: 'IPC Unix → disk (uds-out.png)',
|
|
inputBytes: inB,
|
|
outputBytes: d.bytes,
|
|
computeMs: udsMs,
|
|
widthPx: d.widthPx,
|
|
heightPx: d.heightPx,
|
|
note: 'UDS line-JSON resize',
|
|
});
|
|
}
|
|
} finally {
|
|
rep.endSuite();
|
|
proc.kill();
|
|
try {
|
|
if (existsSync(path)) unlinkSync(path);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
rmSync(outDir, { recursive: true, force: true });
|
|
await new Promise((r) => setTimeout(r, 150));
|
|
}
|
|
}
|
|
|
|
async function run() {
|
|
const wallStart = performance.now();
|
|
const rep = createTestReport();
|
|
const metricsCollector = createMetricsCollector();
|
|
const startedAtIso = new Date().toISOString();
|
|
let exitCode = 1;
|
|
let assetsDir = '';
|
|
|
|
try {
|
|
assetsDir = resolve(defaultAssetsDir(__dirname));
|
|
|
|
if (!existsSync(EXE)) {
|
|
rep.meta.abortReason = `Binary not found: ${EXE}`;
|
|
console.error(rep.meta.abortReason);
|
|
return;
|
|
}
|
|
|
|
if (!(globBatchOnly && globRaw)) {
|
|
const need = ['square-64.png', 'checker-128x128.png', 'glob-in/root.png', 'glob-in/sub/leaf.png'];
|
|
const missing = need.filter((f) => !existsSync(join(assetsDir, f)));
|
|
if (missing.length) {
|
|
rep.meta.abortReason = `Missing fixtures under ${assetsDir}: ${missing.join(', ')} (run: node tests/assets/build-fixtures.mjs)`;
|
|
console.error(`Missing fixtures under ${assetsDir}: ${missing.join(', ')}`);
|
|
console.error('Run: node tests/assets/build-fixtures.mjs');
|
|
return;
|
|
}
|
|
}
|
|
|
|
console.log(`\nmedia-img integration tests\n binary: ${EXE}\n assets: ${assetsDir}\n`);
|
|
|
|
registerFixtureImages(rep, assetsDir);
|
|
|
|
if (templatesOnly) {
|
|
await suiteDstTemplateRest(assetsDir, rep);
|
|
await suiteDstTemplateIpcTcp(assetsDir, rep);
|
|
suiteDstTemplateCli(assetsDir, rep);
|
|
console.log(`\nDone (templates only). Passed: ${stats.passed} Failed: ${stats.failed}\n`);
|
|
exitCode = stats.failed > 0 ? 1 : 0;
|
|
return;
|
|
}
|
|
|
|
if (globBatchOnly) {
|
|
if (globRaw) {
|
|
suiteGlobBatchRawArw(assetsDir, rep);
|
|
console.log(`\nDone (glob batch raw only). Passed: ${stats.passed} Failed: ${stats.failed}\n`);
|
|
} else {
|
|
suiteGlobBatchCli(assetsDir, rep);
|
|
console.log(`\nDone (glob batch only). Passed: ${stats.passed} Failed: ${stats.failed}\n`);
|
|
}
|
|
exitCode = stats.failed > 0 ? 1 : 0;
|
|
return;
|
|
}
|
|
|
|
if (urlOnly) {
|
|
suiteUrlResizeCli(rep);
|
|
console.log(`\nDone (URL only). Passed: ${stats.passed} Failed: ${stats.failed}\n`);
|
|
exitCode = stats.failed > 0 ? 1 : 0;
|
|
return;
|
|
}
|
|
|
|
if (multipartOnly) {
|
|
await suiteMultipartOnly(assetsDir, rep);
|
|
console.log(`\nDone (multipart only). Passed: ${stats.passed} Failed: ${stats.failed}\n`);
|
|
exitCode = stats.failed > 0 ? 1 : 0;
|
|
return;
|
|
}
|
|
|
|
const runRest = !ipcOnly;
|
|
const runIpc = !restOnly;
|
|
|
|
if (runRest) {
|
|
await suiteRest(assetsDir, rep);
|
|
await suiteDstTemplateRest(assetsDir, rep);
|
|
}
|
|
if (runIpc) {
|
|
await suiteIpcTcp(assetsDir, rep);
|
|
await suiteDstTemplateIpcTcp(assetsDir, rep);
|
|
if (!platform.isWin) {
|
|
await suiteIpcUnix(assetsDir, rep);
|
|
} else {
|
|
console.log('\n── IPC Unix (media-img ipc --unix) ──\n');
|
|
console.log(' (skipped on Windows — use TCP IPC or run tests on Linux/macOS)\n');
|
|
rep.note('IPC Unix suite skipped on this platform (Windows).');
|
|
}
|
|
}
|
|
if (!ipcOnly) {
|
|
suiteDstTemplateCli(assetsDir, rep);
|
|
suiteGlobBatchCli(assetsDir, rep);
|
|
}
|
|
|
|
console.log(`\nDone. Passed: ${stats.passed} Failed: ${stats.failed}\n`);
|
|
exitCode = stats.failed > 0 ? 1 : 0;
|
|
} catch (e) {
|
|
rep.meta.uncaughtError = String(e?.stack || e);
|
|
console.error(e);
|
|
exitCode = 1;
|
|
} finally {
|
|
rep.finalize(stats, performance.now() - wallStart, EXE, assetsDir);
|
|
try {
|
|
writeTestReportFile(rep, metricsCollector, startedAtIso);
|
|
console.log(`Test report written: ${TEST_REPORT_PATH}`);
|
|
} catch (e) {
|
|
console.error('Failed to write test report:', e);
|
|
}
|
|
process.exit(exitCode);
|
|
}
|
|
}
|
|
|
|
run().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|