mirror of
https://github.com/C4illin/ConvertX.git
synced 2026-10-12 23:26:34 +08:00
test: run tests against docker image (#664)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
cubic-dev-ai[bot]
autofix-ci[bot]
parent
7fa8c9819e
commit
36cee01dde
@@ -74,6 +74,21 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and load for testing
|
||||
if: matrix.platform == 'linux/amd64'
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
load: true
|
||||
tags: convertx:test
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||
|
||||
- name: Run integration tests
|
||||
if: matrix.platform == 'linux/amd64'
|
||||
run: docker run --rm -v ./tests:/app/tests -v ./src:/app/src -v ./package.json:/app/package.json convertx:test bun test tests/integration
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v7
|
||||
|
||||
@@ -29,5 +29,5 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Run tests
|
||||
run: bun test
|
||||
- name: Run unit tests
|
||||
run: bun run test
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
services:
|
||||
convertx:
|
||||
image: convertx:test
|
||||
pull_policy: build
|
||||
build:
|
||||
context: .
|
||||
# dockerfile: Debian.Dockerfile
|
||||
|
||||
+4
-1
@@ -8,13 +8,16 @@
|
||||
"format:eslint": "eslint --fix .",
|
||||
"format:prettier": "prettier --write .",
|
||||
"build:js": "tsc",
|
||||
"build:docker": "docker compose build",
|
||||
"build": "bun x @tailwindcss/cli -i ./src/main.css -o ./public/generated.css && bun run build:js",
|
||||
"lint": "npm-run-all 'lint:*'",
|
||||
"lint:tsc": "tsc --noEmit",
|
||||
"lint:knip": "knip",
|
||||
"lint:eslint": "eslint .",
|
||||
"lint:prettier": "prettier --check .",
|
||||
"lint:xss": "xss-scan"
|
||||
"lint:xss": "xss-scan",
|
||||
"test": "bun test --path-ignore-patterns=tests/integration",
|
||||
"test:integration": "docker run --rm -v ./tests:/app/tests -v ./src:/app/src -v ./package.json:/app/package.json convertx:test bun test tests/integration"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/html": "^1.4.2",
|
||||
|
||||
@@ -23,7 +23,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
console.log(`pandoc v${stdout.match(/pandoc ([\d.]+)/)?.[1] || "unknown"}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
console.log(`FFmpeg v${stdout.match(/version ([\w+.-]+)/)?.[1] || "unknown"}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
console.log(`Vips v${stdout.match(/vips-([\d.]+)/)?.[1] || "unknown"}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]?.replace("Version: ", ""));
|
||||
console.log(`ImageMagick v${stdout.match(/ImageMagick ([\d.-]+)/)?.[1] || "unknown"}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
console.log(`GraphicsMagick v${stdout.match(/GraphicsMagick ([\d.]+)/)?.[1] || "unknown"}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
console.log(`Inkscape v${stdout.match(/Inkscape ([\d.]+)/)?.[1] || "unknown"}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
console.log(`djxl v${stdout.match(/(?:v)?([\d.]+)/)?.[1] || "unknown"}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -100,7 +100,8 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`dasel ${stdout.split("\n")[0]}`);
|
||||
const ver = stdout.replace("dasel", "").trim().split(/\s+/)[0] || "unknown";
|
||||
console.log(`dasel ${ver}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -111,7 +112,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
console.log(`XeTeX v${stdout.match(/XeTeX ([\d.-]+)/)?.[1] || "unknown"}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -122,7 +123,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`resvg v${stdout.split("\n")[0]}`);
|
||||
console.log(`resvg v${(stdout.split("\n")[0] ?? "").trim().replace(/^v/, "")}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -133,8 +134,8 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
const firstLines = stdout.split("\n");
|
||||
console.log(`assimp ${firstLines[5] || firstLines[0] || ""}`);
|
||||
const match = stdout.match(/Version ([\d.]+)/i);
|
||||
console.log(`assimp v${match?.[1] || "unknown"}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -145,7 +146,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
console.log(`ebook-convert v${stdout.match(/calibre ([\d.]+)/)?.[1] || "unknown"}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -167,7 +168,8 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
const ver = stdout.match(/potrace ([\d.]+)/)?.[1]?.replace(/\.$/, "") || "unknown";
|
||||
console.log(`potrace v${ver}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -178,7 +180,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
console.log(`LibreOffice v${stdout.match(/LibreOffice ([\d.]+)/)?.[1] || "unknown"}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -204,7 +206,8 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`${stdout.split("\n")[0]}`);
|
||||
const ver = stdout.match(/([\d.]+)/)?.[1] || "unknown";
|
||||
console.log(`markitdown v${ver}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import { afterEach, expect, mock, spyOn, test } from "bun:test";
|
||||
import { exec } from "node:child_process";
|
||||
import { readFile } from "node:fs";
|
||||
|
||||
// mocks have to be defined before importing the module under test,
|
||||
// otherwise the real modules will be loaded first and the mocks won't take effect.
|
||||
mock.module("node:child_process", () => ({
|
||||
// The mock checks the environment variable MOCK_EXEC_ERROR at call-time so
|
||||
// individual tests can trigger error paths by setting that env before
|
||||
// importing the module under test.
|
||||
exec: mock((cmd: string, cb: (error: Error | null, stdout: string) => void) => {
|
||||
const shouldError = (process.env.MOCK_EXEC_ERROR || "")
|
||||
.split(",")
|
||||
.some((p) => p && cmd.includes(p));
|
||||
|
||||
if (shouldError) {
|
||||
cb(new Error(`${cmd} not found`), "");
|
||||
return;
|
||||
}
|
||||
|
||||
// resvg, bun, heif-info, dasel, and the perl one-liner (msgconvert) print just
|
||||
// the bare version number — the source code itself prepends the tool name as a label.
|
||||
if (
|
||||
cmd.startsWith("resvg") ||
|
||||
cmd.startsWith("bun") ||
|
||||
cmd.startsWith("heif-info") ||
|
||||
cmd.startsWith("dasel") ||
|
||||
cmd.startsWith("perl")
|
||||
) {
|
||||
cb(null, "1.0.0\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// assimp's real output is multi-line; the source reads line index 5.
|
||||
if (cmd.startsWith("assimp")) {
|
||||
cb(null, "l1\nl2\nl3\nl4\nl5\nVersion 1.0.0 (GIT commit abc123)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// magick prefixes its version line with "Version: ".
|
||||
if (cmd.startsWith("magick")) {
|
||||
cb(null, "Version: ImageMagick v1.0.0\n");
|
||||
return;
|
||||
}
|
||||
|
||||
cb(null, `${cmd} v1.0.0\n`);
|
||||
}),
|
||||
}));
|
||||
|
||||
mock.module("node:fs", () => ({
|
||||
readFile: mock(
|
||||
(path: string, encoding: string, cb: (error: Error | null, data: string) => void) => {
|
||||
cb(null, 'PRETTY_NAME="Ubuntu 22.04 LTS"\n');
|
||||
},
|
||||
),
|
||||
}));
|
||||
|
||||
mock.module("../../package.json", () => ({
|
||||
version: "1.0.0-test",
|
||||
}));
|
||||
|
||||
// We import the module in each test after the spies and the desired NODE_ENV were set
|
||||
// so that the top level execution is under control of each test
|
||||
|
||||
let consoleLogSpy: ReturnType<typeof spyOn>;
|
||||
let consoleErrorSpy: ReturnType<typeof spyOn>;
|
||||
|
||||
afterEach(() => {
|
||||
consoleLogSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.MOCK_EXEC_ERROR;
|
||||
});
|
||||
|
||||
test("prints only the version and terminates itself immediately when NODE_ENV is not production", async () => {
|
||||
consoleLogSpy = spyOn(console, "log");
|
||||
consoleErrorSpy = spyOn(console, "error");
|
||||
|
||||
// import the module after the spies were set so that the initial console.log within the module
|
||||
// gets recognized by the spy
|
||||
await import("../../src/helpers/printVersions?test=" + Math.random());
|
||||
|
||||
// empty the queue to make sure all callbacks have been processed
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("ConvertX v1.0.0-test");
|
||||
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||
expect(readFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("prints system information and tool versions in production mode", async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
consoleLogSpy = spyOn(console, "log");
|
||||
consoleErrorSpy = spyOn(console, "error");
|
||||
|
||||
// import module under test after setting NODE_ENV and activating the spies
|
||||
await import("../../src/helpers/printVersions?test=" + Math.random());
|
||||
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("ConvertX v1.0.0-test");
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("Ubuntu 22.04 LTS");
|
||||
expect(readFile).toHaveBeenCalledTimes(1);
|
||||
expect(exec).toHaveBeenCalledTimes(18); // Corresponds exactly to the number of exec calls in the source code
|
||||
});
|
||||
|
||||
test("logs error paths when tools are missing", async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
// Trigger a few error paths ("perl" is the msgconvert version lookup)
|
||||
process.env.MOCK_EXEC_ERROR = "pandoc,resvg,bun,perl";
|
||||
consoleLogSpy = spyOn(console, "log");
|
||||
consoleErrorSpy = spyOn(console, "error");
|
||||
|
||||
await import("../../src/helpers/printVersions?test=" + Math.random());
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Pandoc is not installed.");
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("resvg is not installed");
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Bun is not installed. wait what");
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"msgconvert (libemail-outlook-message-perl) is not installed",
|
||||
);
|
||||
});
|
||||
|
||||
test("processes output parsing correctly and logs no errors in the success case", async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
consoleLogSpy = spyOn(console, "log");
|
||||
consoleErrorSpy = spyOn(console, "error");
|
||||
|
||||
// import module under test after setting NODE_ENV and activating the spies
|
||||
await import("../../src/helpers/printVersions?test=" + Math.random());
|
||||
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
// testing some specific outputs
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("pandoc -v v1.0.0");
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("ffmpeg -version v1.0.0");
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("resvg v1.0.0");
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("Bun v1.0.0");
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("dasel 1.0.0");
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("msgconvert v1.0.0");
|
||||
|
||||
// make sure that error paths have not been triggered
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { afterEach, beforeEach, expect, spyOn, test } from "bun:test";
|
||||
|
||||
let consoleLogSpy: ReturnType<typeof spyOn>;
|
||||
let consoleErrorSpy: ReturnType<typeof spyOn>;
|
||||
let originalNodeEnv: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
originalNodeEnv = process.env.NODE_ENV;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (consoleLogSpy) consoleLogSpy.mockRestore();
|
||||
if (consoleErrorSpy) consoleErrorSpy.mockRestore();
|
||||
process.env.NODE_ENV = originalNodeEnv;
|
||||
});
|
||||
|
||||
test("prints system information and tool versions in production mode without errors", async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
consoleLogSpy = spyOn(console, "log");
|
||||
consoleErrorSpy = spyOn(console, "error");
|
||||
|
||||
await import("../../src/helpers/printVersions?test=" + Math.random());
|
||||
|
||||
const printVersionsContent = await Bun.file("src/helpers/printVersions.ts").text();
|
||||
const expectedLogsCount = (printVersionsContent.match(/console\.log/g) || []).length;
|
||||
|
||||
// Poll every 50ms (up to 3 seconds) until all expected versions are logged
|
||||
for (let i = 0; i < 60; i++) {
|
||||
if (consoleLogSpy.mock.calls.length >= expectedLogsCount) break;
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
|
||||
// We expect exactly one log per console.log statement in the file
|
||||
expect(consoleLogSpy.mock.calls.length).toBe(expectedLogsCount);
|
||||
|
||||
// Verify the standard format "ToolName v1.2.3" for all tools (except ConvertX version and OS)
|
||||
// ConvertX vX.X.X is the first call, OS is the second call.
|
||||
// Actually, because callbacks are async, we should just check if it matches the pattern or is OS string
|
||||
for (const call of consoleLogSpy.mock.calls) {
|
||||
const msg = call[0];
|
||||
|
||||
// Ignore the OS string which doesn't follow the pattern
|
||||
if (msg.includes("Linux") || msg.includes("Debian") || msg.includes("Ubuntu")) continue;
|
||||
|
||||
// Dasel on Debian Sid prints "development" instead of a numerical version
|
||||
if (msg === "dasel development") continue;
|
||||
|
||||
// The pattern is "Name vVersion" where Version must start with a digit
|
||||
// e.g., "FFmpeg v8.1.2-2+b3", "potrace v1.16"
|
||||
expect(msg).toMatch(/^[a-zA-Z0-9_-]+\s+v\d+[a-zA-Z0-9+.-]*$/);
|
||||
expect(msg).not.toMatch(/unknown/i);
|
||||
}
|
||||
}, 10000);
|
||||
Reference in New Issue
Block a user