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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { afterEach, expect, test } from "vitest";
import { describe } from "vitest";
import {
makeTempLegacyStackProject,
overrideStackPorts,
requireCliSuccess,
runSupabase,
} from "../../../../../tests/helpers/cli.ts";
Expand Down Expand Up @@ -53,6 +54,7 @@ describe("supabase db diff (e2e, pg-delta declarative privileges)", () => {
exitTimeoutMs: CLI_COMMAND_TIMEOUT_MS,
});
requireCliSuccess(init, "init setup");
await overrideStackPorts(projectDir);

// Exclude the heaviest, least relevant services — `db diff` only needs the
// local Postgres container reachable, same rationale as stop/status.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { afterAll, beforeAll, expect, test } from "vitest";
import { describe } from "vitest";
import {
makeTempLegacyStackProject,
overrideStackPorts,
requireCliSuccess,
runSupabase,
} from "../../../../../../../tests/helpers/cli.ts";
Expand Down Expand Up @@ -57,6 +58,7 @@ describe("db schema declarative sync (e2e)", () => {
exitTimeoutMs: CLI_COMMAND_TIMEOUT_MS,
});
requireCliSuccess(init, "init setup");
await overrideStackPorts(projectDir);

const configPath = path.join(projectDir, "supabase", "config.toml");
const config = readFileSync(configPath, "utf8");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import path from "node:path";
import { promisify } from "node:util";
import { afterEach, describe, expect, test } from "vitest";

import { requireCliSuccess, runSupabase } from "../../../../tests/helpers/cli.ts";
import {
overrideStackPorts,
requireCliSuccess,
runSupabase,
} from "../../../../tests/helpers/cli.ts";
import {
legacySanitizeProjectId,
legacyServiceContainerName,
Expand Down Expand Up @@ -100,6 +104,7 @@ describe("supabase start (e2e)", () => {
exitTimeoutMs: SHORT_E2E_TIMEOUT_MS,
});
requireCliSuccess(init, "init setup");
await overrideStackPorts(projectDir);

const start = await runSupabase(startArgs, {
entrypoint: "legacy",
Expand Down Expand Up @@ -229,6 +234,7 @@ describe("supabase start (e2e)", () => {
if (address === null || typeof address === "string") {
throw new Error("Failed to allocate a proxy port");
}
await overrideStackPorts(projectDir);

const excludeArgs = LEGACY_SERVICE_CATALOG.flatMap((entry) =>
entry.excludeKey === undefined ||
Expand Down Expand Up @@ -288,6 +294,7 @@ describe("supabase start (e2e)", () => {
exitTimeoutMs: SHORT_E2E_TIMEOUT_MS,
});
requireCliSuccess(init, "init setup");
await overrideStackPorts(projectDir);

// A `scratch` image whose entrypoint is not an executable binary — the
// kernel refuses it with exactly the "exec format error" this diagnoses.
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/legacy/commands/status/status.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { afterEach, expect, test } from "vitest";
import { describe } from "vitest";
import {
makeTempLegacyStackProject,
overrideStackPorts,
requireCliSuccess,
runSupabase,
} from "../../../../tests/helpers/cli.ts";
Expand Down Expand Up @@ -44,6 +45,7 @@ describe("supabase status (e2e)", () => {
exitTimeoutMs: CLI_COMMAND_TIMEOUT_MS,
});
requireCliSuccess(init, "init setup");
await overrideStackPorts(projectDir);

const start = await runSupabase(
["start", "--exclude", "studio", "--exclude", "logflare", "--exclude", "vector"],
Expand Down
3 changes: 3 additions & 0 deletions apps/cli/src/legacy/commands/stop/stop.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { afterEach, describe, expect, test } from "vitest";

import {
makeTempLegacyStackProject,
overrideStackPorts,
requireCliSuccess,
runSupabase,
} from "../../../../tests/helpers/cli.ts";
Expand Down Expand Up @@ -59,6 +60,7 @@ describe("supabase stop (e2e)", () => {
exitTimeoutMs: CLI_COMMAND_TIMEOUT_MS,
});
requireCliSuccess(init, "init setup");
await overrideStackPorts(projectDir);

// Exclude the heaviest, least relevant services (Next.js Studio build, the
// logging pipeline) — `stop`'s Docker label-filtering logic doesn't care
Expand Down Expand Up @@ -122,6 +124,7 @@ describe("supabase stop (e2e)", () => {
exitTimeoutMs: CLI_COMMAND_TIMEOUT_MS,
});
requireCliSuccess(init, "init setup");
await overrideStackPorts(projectDir);

const start = await runSupabase(
["start", "--exclude", "studio", "--exclude", "logflare", "--exclude", "vector"],
Expand Down
33 changes: 23 additions & 10 deletions apps/cli/src/next/commands/functions/dev/dev.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdir, writeFile } from "node:fs/promises";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";

Expand All @@ -11,6 +11,7 @@ import {
import { cleanupRegisteredStackProjects } from "../../../../../tests/helpers/stack-e2e-cleanup.ts";

const FUNCTIONS_DEV_STARTUP_TIMEOUT_MS = 60_000;
const FUNCTIONS_URL_PATTERN = /Functions URL:\s+(https?:\/\/[^\s/]+\/functions\/v1)/;
const FUNCTIONS_DEV_STEP_TIMEOUT_MS = 30_000;
const FUNCTIONS_DEV_CLEANUP_TIMEOUT_MS = 30_000;
const FUNCTIONS_DEV_TEST_TIMEOUT_MS =
Expand Down Expand Up @@ -93,9 +94,8 @@ describe("supabase functions dev (e2e)", () => {
/Edge Functions dev server is running\./,
FUNCTIONS_DEV_STARTUP_TIMEOUT_MS,
);
const functionUrlMatch = `${devProc.stdout()}\n${devProc.stderr()}`.match(
/Functions URL:\s+(https?:\/\/[^\s/]+\/functions\/v1)/,
);
await devProc.waitForOutput(FUNCTIONS_URL_PATTERN, FUNCTIONS_DEV_STARTUP_TIMEOUT_MS);
const functionUrlMatch = devProc.stdout().match(FUNCTIONS_URL_PATTERN);
if (functionUrlMatch?.[1] === undefined) {
throw new Error(
`Functions dev output did not include a URL.\nstdout:\n${devProc.stdout()}\nstderr:\n${devProc.stderr()}`,
Expand All @@ -104,18 +104,31 @@ describe("supabase functions dev (e2e)", () => {
const functionUrl = `${functionUrlMatch[1]}/hello-world`;

const functionOffset = devProc.stdout().length;
const functionRestart = devProc.waitForOutput(
FUNCTION_FILES_RESTART_PATTERN,
FUNCTIONS_DEV_STEP_TIMEOUT_MS,
functionOffset,
);
const newResult = await runSupabase(["functions", "new", "hello-world"], {
cwd: project.dir,
home: home.dir,
exitTimeoutMs: FUNCTIONS_DEV_STEP_TIMEOUT_MS,
});
expect(newResult.exitCode).toBe(0);
await functionRestart;
// The dev server prints its startup lines before the file watcher has
// subscribed and exposes no watcher-ready signal, so a write that lands
// in that window is silently lost. Re-touch the created function until
// the dev server reports it, bounded by the step deadline.
const functionRestartDeadline = Date.now() + FUNCTIONS_DEV_STEP_TIMEOUT_MS;
for (;;) {
try {
await devProc.waitForOutput(FUNCTION_FILES_RESTART_PATTERN, 5_000, functionOffset);
Comment thread
7ttp marked this conversation as resolved.
break;
} catch (error) {
if (
Date.now() >= functionRestartDeadline ||
(error instanceof Error && error.message.startsWith("Process exited"))
) {
throw error;
}
await writeFile(functionPath, await readFile(functionPath, "utf8"));
}
}

await assertFunctionResponse(functionUrl, {}, (response, body) => {
expect(response.status).toBe(401);
Expand Down
29 changes: 28 additions & 1 deletion apps/cli/tests/helpers/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spawn } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createServer } from "node:net";
import { homedir, tmpdir } from "node:os";
import path from "node:path";
Expand Down Expand Up @@ -131,6 +131,33 @@ function pickFreePort(): Promise<number> {
});
}

/**
* Rewrites every active port assignment in an `init`-generated
* `supabase/config.toml` with a freshly allocated free port, so stacks started
* from default configs cannot collide on host ports with other e2e stacks on
* the same runner. Commented-out port lines are left untouched.
*/
export async function overrideStackPorts(projectDir: string) {
const configPath = path.join(projectDir, "supabase", "config.toml");
const config = await readFile(configPath, "utf8");
const assigned = new Set<number>();
const lines: string[] = [];
for (const line of config.split("\n")) {
const match = /^(\s*(?:port|smtp_port|pop3_port|inspector_port|shadow_port) = )\d+$/.exec(line);
if (match === null) {
lines.push(line);
continue;
}
let port = await pickFreePort();
while (assigned.has(port)) {
port = await pickFreePort();
Comment thread
7ttp marked this conversation as resolved.
}
assigned.add(port);
lines.push(`${match[1]}${port}`);
}
await writeFile(configPath, lines.join("\n"));
}

async function makeTempProject(prefix = "supabase-project-e2e-") {
const projectDir = await mkdtemp(path.join(tmpdir(), prefix));

Expand Down
Loading