Skip to content

Commit 5f4ff58

Browse files
authored
fix: use pre-built binary instead of go run in e2e tests (coder#16236)
Using `go run` inside of a test is fragile, because it means we have to wait for `go` to compile the binary while also constrained on resources by the fact that Playwright and coderd are already running. We should instead compile a coder binary for the current platform before the tests and use it directly.
1 parent 84081e9 commit 5f4ff58

13 files changed

+75
-72
lines changed

.github/workflows/ci.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,9 @@ jobs:
704704
- run: make gen/mark-fresh
705705
name: make gen
706706

707+
- run: make site/e2e/bin/coder
708+
name: make coder
709+
707710
- run: pnpm build
708711
env:
709712
NODE_OPTIONS: ${{ github.repository_owner == 'coder' && '--max_old_space_size=8192' || '' }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ site/.swc
3636
.gen-golden
3737

3838
# Build
39+
bin/
3940
build/
4041
dist/
4142
out/

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -944,7 +944,12 @@ test-clean:
944944
go clean -testcache
945945
.PHONY: test-clean
946946

947-
test-e2e: site/node_modules/.installed site/out/index.html
947+
site/e2e/bin/coder: go.mod go.sum $(GO_SRC_FILES)
948+
go build -o $@ \
949+
-tags ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube \
950+
./enterprise/cmd/coder
951+
952+
test-e2e: site/e2e/bin/coder site/node_modules/.installed site/out/index.html
948953
cd site/
949954
ifdef CI
950955
DEBUG=pw:api pnpm playwright:test --forbid-only --workers 1

site/e2e/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as path from "node:path";
22

3-
export const coderMain = path.join(__dirname, "../../enterprise/cmd/coder");
3+
export const coderBinary = path.join(__dirname, "./bin/coder");
44

55
// Default port from the server
66
export const coderPort = process.env.CODER_E2E_PORT

site/e2e/helpers.ts

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import * as ssh from "ssh2";
1515
import { TarWriter } from "utils/tar";
1616
import {
1717
agentPProfPort,
18-
coderMain,
18+
coderBinary,
1919
coderPort,
2020
defaultOrganizationName,
2121
defaultPassword,
@@ -311,12 +311,9 @@ export const createGroup = async (page: Page): Promise<string> => {
311311
export const sshIntoWorkspace = async (
312312
page: Page,
313313
workspace: string,
314-
binaryPath = "go",
314+
binaryPath = coderBinary,
315315
binaryArgs: string[] = [],
316316
): Promise<ssh.Client> => {
317-
if (binaryPath === "go") {
318-
binaryArgs = ["run", coderMain];
319-
}
320317
const sessionToken = await findSessionToken(page);
321318
return new Promise<ssh.Client>((resolve, reject) => {
322319
const cp = spawn(binaryPath, [...binaryArgs, "ssh", "--stdio", workspace], {
@@ -398,7 +395,7 @@ export const startAgent = async (
398395
page: Page,
399396
token: string,
400397
): Promise<ChildProcess> => {
401-
return startAgentWithCommand(page, token, "go", "run", coderMain);
398+
return startAgentWithCommand(page, token, coderBinary);
402399
};
403400

404401
/**
@@ -479,27 +476,21 @@ export const startAgentWithCommand = async (
479476
},
480477
});
481478
cp.stdout.on("data", (data: Buffer) => {
482-
console.info(
483-
`[agent] [stdout] [onData] ${data.toString().replace(/\n$/g, "")}`,
484-
);
479+
console.info(`[agent][stdout] ${data.toString().replace(/\n$/g, "")}`);
485480
});
486481
cp.stderr.on("data", (data: Buffer) => {
487-
console.info(
488-
`[agent] [stderr] [onData] ${data.toString().replace(/\n$/g, "")}`,
489-
);
482+
console.info(`[agent][stderr] ${data.toString().replace(/\n$/g, "")}`);
490483
});
491484

492485
await page
493486
.getByTestId("agent-status-ready")
494-
.waitFor({ state: "visible", timeout: 45_000 });
487+
.waitFor({ state: "visible", timeout: 15_000 });
495488
return cp;
496489
};
497490

498-
export const stopAgent = async (cp: ChildProcess, goRun = true) => {
499-
// When the web server is started with `go run`, it spawns a child process with coder server.
500-
// `pkill -P` terminates child processes belonging the same group as `go run`.
501-
// The command `kill` is used to terminate a web server started as a standalone binary.
502-
exec(goRun ? `pkill -P ${cp.pid}` : `kill ${cp.pid}`, (error) => {
491+
export const stopAgent = async (cp: ChildProcess) => {
492+
// The command `kill` is used to terminate an agent started as a standalone binary.
493+
exec(`kill ${cp.pid}`, (error) => {
503494
if (error) {
504495
throw new Error(`exec error: ${JSON.stringify(error)}`);
505496
}
@@ -922,10 +913,8 @@ export const updateTemplate = async (
922913

923914
const sessionToken = await findSessionToken(page);
924915
const child = spawn(
925-
"go",
916+
coderBinary,
926917
[
927-
"run",
928-
coderMain,
929918
"templates",
930919
"push",
931920
"--test.provisioner",

site/e2e/playwright.config.ts

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import { execSync } from "node:child_process";
21
import * as path from "node:path";
32
import { defineConfig } from "@playwright/test";
43
import {
5-
coderMain,
64
coderPort,
75
coderdPProfPort,
86
e2eFakeExperiment1,
@@ -13,45 +11,12 @@ import {
1311

1412
export const wsEndpoint = process.env.CODER_E2E_WS_ENDPOINT;
1513

16-
// If running terraform tests, verify the requirements exist in the
17-
// environment.
18-
//
19-
// These execs will throw an error if the status code is non-zero.
20-
// So if both these work, then we can launch terraform provisioners.
21-
let hasTerraform = false;
22-
let hasDocker = false;
23-
try {
24-
execSync("terraform --version");
25-
hasTerraform = true;
26-
} catch {
27-
/* empty */
28-
}
29-
30-
try {
31-
execSync("docker --version");
32-
hasDocker = true;
33-
} catch {
34-
/* empty */
35-
}
36-
37-
if (!hasTerraform || !hasDocker) {
38-
const msg = `Terraform provisioners require docker & terraform binaries to function. \n${
39-
hasTerraform
40-
? ""
41-
: "\tThe `terraform` executable is not present in the runtime environment.\n"
42-
}${
43-
hasDocker
44-
? ""
45-
: "\tThe `docker` executable is not present in the runtime environment.\n"
46-
}`;
47-
throw new Error(msg);
48-
}
49-
5014
const localURL = (port: number, path: string): string => {
5115
return `http://localhost:${port}${path}`;
5216
};
5317

5418
export default defineConfig({
19+
globalSetup: require.resolve("./setup/preflight"),
5520
projects: [
5621
{
5722
name: "testsSetup",
@@ -84,7 +49,8 @@ export default defineConfig({
8449
webServer: {
8550
url: `http://localhost:${coderPort}/api/v2/deployment/config`,
8651
command: [
87-
`go run -tags embed ${coderMain} server`,
52+
`go run -tags embed ${path.join(__dirname, "../../enterprise/cmd/coder")}`,
53+
"server",
8854
"--global-config $(mktemp -d -t e2e-XXXXXXXXXX)",
8955
`--access-url=http://localhost:${coderPort}`,
9056
`--http-address=0.0.0.0:${coderPort}`,

site/e2e/proxy.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { type ChildProcess, exec, spawn } from "node:child_process";
2-
import { coderMain, coderPort, workspaceProxyPort } from "./constants";
2+
import { coderBinary, coderPort, workspaceProxyPort } from "./constants";
33
import { waitUntilUrlIsNotResponding } from "./helpers";
44

55
export const startWorkspaceProxy = async (
66
token: string,
77
): Promise<ChildProcess> => {
8-
const cp = spawn("go", ["run", coderMain, "wsproxy", "server"], {
8+
const cp = spawn(coderBinary, ["wsproxy", "server"], {
99
env: {
1010
...process.env,
1111
CODER_PRIMARY_ACCESS_URL: `http://127.0.0.1:${coderPort}`,
@@ -26,8 +26,8 @@ export const startWorkspaceProxy = async (
2626
return cp;
2727
};
2828

29-
export const stopWorkspaceProxy = async (cp: ChildProcess, goRun = true) => {
30-
exec(goRun ? `pkill -P ${cp.pid}` : `kill ${cp.pid}`, (error) => {
29+
export const stopWorkspaceProxy = async (cp: ChildProcess) => {
30+
exec(`kill ${cp.pid}`, (error) => {
3131
if (error) {
3232
throw new Error(`exec error: ${JSON.stringify(error)}`);
3333
}

site/e2e/setup/preflight.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { execSync } from "node:child_process";
2+
import * as path from "node:path";
3+
4+
export default function () {
5+
// If running terraform tests, verify the requirements exist in the
6+
// environment.
7+
//
8+
// These execs will throw an error if the status code is non-zero.
9+
// So if both these work, then we can launch terraform provisioners.
10+
let hasTerraform = false;
11+
let hasDocker = false;
12+
try {
13+
execSync("terraform --version");
14+
hasTerraform = true;
15+
} catch {
16+
/* empty */
17+
}
18+
19+
try {
20+
execSync("docker --version");
21+
hasDocker = true;
22+
} catch {
23+
/* empty */
24+
}
25+
26+
if (!hasTerraform || !hasDocker) {
27+
const msg = `Terraform provisioners require docker & terraform binaries to function. \n${
28+
hasTerraform
29+
? ""
30+
: "\tThe `terraform` executable is not present in the runtime environment.\n"
31+
}${
32+
hasDocker
33+
? ""
34+
: "\tThe `docker` executable is not present in the runtime environment.\n"
35+
}`;
36+
throw new Error(msg);
37+
}
38+
39+
if (!process.env.CI) {
40+
console.info("==> make site/e2e/bin/coder");
41+
execSync("make site/e2e/bin/coder", {
42+
cwd: path.join(__dirname, "../../../"),
43+
});
44+
}
45+
}

site/e2e/tests/app.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ test.beforeEach(async ({ page }) => {
1818
});
1919

2020
test("app", async ({ context, page }) => {
21-
test.setTimeout(75_000);
22-
2321
const appContent = "Hello World";
2422
const token = randomUUID();
2523
const srv = http
@@ -64,7 +62,7 @@ test("app", async ({ context, page }) => {
6462

6563
// Wait for the web terminal to open in a new tab
6664
const pagePromise = context.waitForEvent("page");
67-
await page.getByText(appName).click({ timeout: 60_000 });
65+
await page.getByText(appName).click({ timeout: 10_000 });
6866
const app = await pagePromise;
6967
await app.waitForLoadState("domcontentloaded");
7068
await app.getByText(appContent).isVisible();

site/e2e/tests/outdatedAgent.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,5 @@ test(`ssh with agent ${agentVersion}`, async ({ page }) => {
6464
});
6565

6666
await stopWorkspace(page, workspaceName);
67-
await stopAgent(agent, false);
67+
await stopAgent(agent);
6868
});

site/e2e/tests/outdatedCLI.spec.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ test.beforeEach(async ({ page }) => {
2121
});
2222

2323
test(`ssh with client ${clientVersion}`, async ({ page }) => {
24-
test.setTimeout(60_000);
25-
2624
const token = randomUUID();
2725
const template = await createTemplate(page, {
2826
apply: [

site/e2e/tests/webTerminal.spec.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ test.beforeEach(async ({ page }) => {
1616
});
1717

1818
test("web terminal", async ({ context, page }) => {
19-
test.setTimeout(75_000);
20-
2119
const token = randomUUID();
2220
const template = await createTemplate(page, {
2321
apply: [

0 commit comments

Comments
 (0)