Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions apps/dokploy/__test__/deploy/application.real.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { execSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import type { ApplicationNested } from "@dokploy/server";
import { paths } from "@dokploy/server/constants";
Expand Down Expand Up @@ -198,6 +198,17 @@ async function cleanupFiles(appName: string) {
}
}

const printDeploymentLog = (logPath: string | undefined) => {
if (!logPath || !existsSync(logPath)) {
console.log("📄 The failed test left no deployment log");
return;
}
const lines = readFileSync(logPath, "utf8").trimEnd().split("\n");
console.log(
`📄 Last lines of the deployment log (${lines.length} in total):\n${lines.slice(-80).join("\n")}`,
);
};

// A deploy ends in a swarm service, so a running daemon alone is not enough.
const hasSwarm = () => {
try {
Expand All @@ -213,8 +224,12 @@ const hasSwarm = () => {
};

// CI never skips: a runner that lost Docker has to fail, not go quiet.
// These tests clone from GitHub and resolve base images at a registry, so one
// failed attempt is retried: it may be the network, and its build log is
// printed either way.
describe.skipIf(!process.env.CI && !hasSwarm())(
"deployApplication - REAL Execution Tests",
{ timeout: REAL_TEST_TIMEOUT, retry: 1 },
() => {
let currentAppName: string;
let currentDeployment: any;
Expand Down Expand Up @@ -251,7 +266,13 @@ describe.skipIf(!process.env.CI && !hasSwarm())(
);
});

afterEach(async () => {
afterEach(async ({ task }) => {
// The cleanup below removes the build log, the only place where a
// failed clone or build says why.
if (task.result?.state === "fail") {
printDeploymentLog(currentDeployment?.logPath);
}

// ALWAYS cleanup, even if test failed or passed
console.log(`\n🧹 Cleaning up test: ${currentAppName}`);

Expand Down Expand Up @@ -551,5 +572,4 @@ describe.skipIf(!process.env.CI && !hasSwarm())(
REAL_TEST_TIMEOUT,
);
},
REAL_TEST_TIMEOUT,
);
43 changes: 39 additions & 4 deletions apps/dokploy/__test__/domain-overview/acme-delete-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
buildAcmeDeleteScript,
parseDeletionStatus,
} from "@dokploy/server/utils/domain-overview/acme-delete";
import { afterEach, beforeEach, expect, it } from "vitest";
import { afterAll, afterEach, beforeEach, expect, it } from "vitest";

const found = new Map<string, string | null>();

Expand Down Expand Up @@ -245,10 +245,45 @@ export let markerFile: string;
export let backupDir: string;
export let logDir: string;

// macOS checks a new executable file on its first run. That takes a quarter of
// a second on an idle machine and longer on a busy one, and a copy of the same
// script is a new file again. Written per test, the shims paid for that check
// several times in every test, and it could land inside a docker call with a
// time limit. So each shim exists once per test file, is run once here,
// outside any limit, and is only linked afterwards.
const SHEBANG = "#!/bin/sh\n";
const WARM_UP = "ACME_DELETE_TEST_WARM_UP";
const shimDir = mkdtempSync(path.join(tmpdir(), "acme-delete-shims-"));
const shims = new Map<string, string>();

const shimFor = (content: string) => {
const known = shims.get(content);
if (known) {
return known;
}
const file = path.join(shimDir, String(shims.size));
const warmable = content.startsWith(SHEBANG);
writeFileSync(
file,
warmable
? `${SHEBANG}[ -z "$${WARM_UP}" ] || exit 0\n${content.slice(SHEBANG.length)}`
: content,
);
chmodSync(file, 0o755);
if (warmable) {
spawnSync(file, { env: { ...process.env, [WARM_UP]: "1" } });
}
shims.set(content, file);
return file;
};

afterAll(() => {
rmSync(shimDir, { recursive: true, force: true });
});

export const writeExecutable = (file: string, content: string) => {
rmSync(file, { force: true });
writeFileSync(file, content);
chmodSync(file, 0o755);
symlinkSync(shimFor(content), file);
};

export const TIMEOUT_SHIM = `#!/bin/sh
Expand Down Expand Up @@ -312,7 +347,7 @@ export const setupBin = (options: {
writeFileSync(path.join(binDir, "setsid.py"), SETSID_SHIM);
writeExecutable(
path.join(binDir, "setsid"),
`#!/bin/sh\nexec ${which("python3")} ${path.join(binDir, "setsid.py")} "$@"\n`,
`#!/bin/sh\nexec ${which("python3")} "$(dirname "$0")/setsid.py" "$@"\n`,
);
}
rmSync(path.join(binDir, "ls"), { force: true });
Expand Down
16 changes: 12 additions & 4 deletions apps/dokploy/__test__/domain-overview/acme-delete-signals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,18 @@ describe.skipIf(!REAL_JQ).each(PRIMARY)(
timeout: 80_000,
});

// Where only "inspect" hangs, stop and start answer at once, so a wide
// limit for them costs no time. A narrow one turns a start that stalls
// on a busy runner into traefik_not_restarted.
const wideStopLimit = { stopSeconds: 30 };

it("counts the wait for the stop in time, not in rounds", () => {
const result = exec({ waitSeconds: 1 }, { FAKE_STATE_HANG: "1" });
const result = exec(
{ waitSeconds: 1, ...wideStopLimit },
{ FAKE_STATE_HANG: "1" },
);

expect(resultOf(result.stdout)).toBe("stop_failed");
expect(resultOf(result.stdout), result.stdout).toBe("stop_failed");
const calls = callsOf();
const stopAt = calls.findIndex((c) => c.startsWith("stop "));
const startAt = calls.findIndex((c) => c.startsWith("start "));
Expand All @@ -206,11 +214,11 @@ describe.skipIf(!REAL_JQ).each(PRIMARY)(
const started = Date.now();

const result = exec(
{ waitSeconds: 60, deadlineSeconds: 2 },
{ waitSeconds: 60, deadlineSeconds: 2, ...wideStopLimit },
{ FAKE_STATE_HANG: "1" },
);

expect(["stop_failed", "deadline_exceeded"]).toContain(
expect(["stop_failed", "deadline_exceeded"], result.stdout).toContain(
resultOf(result.stdout),
);
expect(Date.now() - started).toBeLessThan(20_000);
Expand Down