Skip to content

Commit f92cbb7

Browse files
authored
Merge pull request #3664 from code-asher/parallel-tests
2 parents 4a47ce7 + 2238d73 commit f92cbb7

19 files changed

+468
-269
lines changed

.github/workflows/ci.yaml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -334,8 +334,9 @@ jobs:
334334
needs: package-linux-amd64
335335
runs-on: ubuntu-latest
336336
env:
337-
PASSWORD: e45432jklfdsab
338-
CODE_SERVER_ADDRESS: http://localhost:8080
337+
# Since we build code-server we might as well run tests from the release
338+
# since VS Code will load faster due to the bundling.
339+
CODE_SERVER_TEST_ENTRY: "./release-packages/code-server-linux-amd64"
339340
steps:
340341
- uses: actions/checkout@v2
341342

@@ -362,9 +363,11 @@ jobs:
362363
name: release-packages
363364
path: ./release-packages
364365

365-
- name: Untar code-server file
366+
- name: Untar code-server release
366367
run: |
367-
cd release-packages && tar -xzf code-server*-linux-amd64.tar.gz
368+
cd release-packages
369+
tar -xzf code-server*-linux-amd64.tar.gz
370+
mv code-server*-linux-amd64 code-server-linux-amd64
368371
369372
- name: Install dependencies
370373
if: steps.cache-yarn.outputs.cache-hit != 'true'
@@ -380,9 +383,7 @@ jobs:
380383
yarn install --check-files
381384
382385
- name: Run end-to-end tests
383-
run: |
384-
./release-packages/code-server*-linux-amd64/bin/code-server --log trace &
385-
yarn test:e2e
386+
run: yarn test:e2e
386387

387388
- name: Upload test artifacts
388389
if: always()

ci/dev/test-e2e.sh

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,35 @@ set -euo pipefail
33

44
main() {
55
cd "$(dirname "$0")/../.."
6+
source ./ci/lib.sh
7+
8+
local dir="$PWD"
9+
if [[ ! ${CODE_SERVER_TEST_ENTRY-} ]]; then
10+
echo "Set CODE_SERVER_TEST_ENTRY to test another build of code-server"
11+
else
12+
pushd "$CODE_SERVER_TEST_ENTRY"
13+
dir="$PWD"
14+
popd
15+
fi
16+
17+
echo "Testing build in '$dir'"
18+
19+
# Simple sanity checks to see that we've built. There could still be things
20+
# wrong (native modules version issues, incomplete build, etc).
21+
if [[ ! -d $dir/out ]]; then
22+
echo >&2 "No code-server build detected"
23+
echo >&2 "You can build it with 'yarn build' or 'yarn watch'"
24+
exit 1
25+
fi
26+
27+
if [[ ! -d $dir/lib/vscode/out ]]; then
28+
echo >&2 "No VS Code build detected"
29+
echo >&2 "You can build it with 'yarn build:vscode' or 'yarn watch'"
30+
exit 1
31+
fi
32+
633
cd test
7-
# We set these environment variables because they're used in the e2e tests
8-
# they don't have to be these values, but these are the defaults
9-
PASSWORD=e45432jklfdsab CODE_SERVER_ADDRESS=http://localhost:8080 yarn playwright test "$@"
34+
yarn playwright test "$@"
1035
}
1136

1237
main "$@"

ci/dev/watch.ts

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import browserify from "browserify"
22
import * as cp from "child_process"
33
import * as fs from "fs"
44
import * as path from "path"
5+
import { onLine } from "../../src/node/util"
56

67
async function main(): Promise<void> {
78
try {
@@ -97,38 +98,6 @@ class Watcher {
9798
path.join(this.rootPath, "out/browser/pages/vscode.js"),
9899
]
99100

100-
// From https://github.com/chalk/ansi-regex
101-
const pattern = [
102-
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
103-
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))",
104-
].join("|")
105-
const re = new RegExp(pattern, "g")
106-
107-
/**
108-
* Split stdout on newlines and strip ANSI codes.
109-
*/
110-
const onLine = (proc: cp.ChildProcess, callback: (strippedLine: string, originalLine: string) => void): void => {
111-
let buffer = ""
112-
if (!proc.stdout) {
113-
throw new Error("no stdout")
114-
}
115-
proc.stdout.setEncoding("utf8")
116-
proc.stdout.on("data", (d) => {
117-
const data = buffer + d
118-
const split = data.split("\n")
119-
const last = split.length - 1
120-
121-
for (let i = 0; i < last; ++i) {
122-
callback(split[i].replace(re, ""), split[i])
123-
}
124-
125-
// The last item will either be an empty string (the data ended with a
126-
// newline) or a partial line (did not end with a newline) and we must
127-
// wait to parse it until we get a full line.
128-
buffer = split[last]
129-
})
130-
}
131-
132101
let startingVscode = false
133102
let startedVscode = false
134103
onLine(vscode, (line, original) => {

src/node/util.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,38 @@ export interface Paths {
1717
runtime: string
1818
}
1919

20+
// From https://github.com/chalk/ansi-regex
21+
const pattern = [
22+
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
23+
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))",
24+
].join("|")
25+
const re = new RegExp(pattern, "g")
26+
27+
/**
28+
* Split stdout on newlines and strip ANSI codes.
29+
*/
30+
export const onLine = (proc: cp.ChildProcess, callback: (strippedLine: string, originalLine: string) => void): void => {
31+
let buffer = ""
32+
if (!proc.stdout) {
33+
throw new Error("no stdout")
34+
}
35+
proc.stdout.setEncoding("utf8")
36+
proc.stdout.on("data", (d) => {
37+
const data = buffer + d
38+
const split = data.split("\n")
39+
const last = split.length - 1
40+
41+
for (let i = 0; i < last; ++i) {
42+
callback(split[i].replace(re, ""), split[i])
43+
}
44+
45+
// The last item will either be an empty string (the data ended with a
46+
// newline) or a partial line (did not end with a newline) and we must
47+
// wait to parse it until we get a full line.
48+
buffer = split[last]
49+
})
50+
}
51+
2052
export const paths = getEnvPaths()
2153

2254
/**

test/e2e/baseFixture.ts

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,71 @@
1+
import { field, logger } from "@coder/logger"
12
import { test as base } from "@playwright/test"
2-
import { CodeServer } from "./models/CodeServer"
3+
import { CodeServer, CodeServerPage } from "./models/CodeServer"
34

4-
export const test = base.extend<{ codeServerPage: CodeServer }>({
5-
codeServerPage: async ({ page }, use) => {
6-
const codeServer = new CodeServer(page)
7-
await codeServer.navigate()
8-
await use(codeServer)
5+
/**
6+
* Wraps `test.describe` to create and manage an instance of code-server. If you
7+
* don't use this you will need to create your own code-server instance and pass
8+
* it to `test.use`.
9+
*
10+
* If `includeCredentials` is `true` page requests will be authenticated.
11+
*/
12+
export const describe = (name: string, includeCredentials: boolean, fn: (codeServer: CodeServer) => void) => {
13+
test.describe(name, () => {
14+
// This will spawn on demand so nothing is necessary on before.
15+
const codeServer = new CodeServer(name)
16+
17+
// Kill code-server after the suite has ended. This may happen even without
18+
// doing it explicitly but it seems prudent to be sure.
19+
test.afterAll(async () => {
20+
await codeServer.close()
21+
})
22+
23+
const storageState = JSON.parse(process.env.STORAGE || "{}")
24+
25+
// Sanity check to ensure the cookie is set.
26+
const cookies = storageState?.cookies
27+
if (includeCredentials && (!cookies || cookies.length !== 1 || !!cookies[0].key)) {
28+
logger.error("no cookies", field("storage", JSON.stringify(cookies)))
29+
throw new Error("no credentials to include")
30+
}
31+
32+
test.use({
33+
// Makes `codeServer` and `authenticated` available to the extend call
34+
// below.
35+
codeServer,
36+
authenticated: includeCredentials,
37+
// This provides a cookie that authenticates with code-server.
38+
storageState: includeCredentials ? storageState : {},
39+
})
40+
41+
fn(codeServer)
42+
})
43+
}
44+
45+
interface TestFixtures {
46+
authenticated: boolean
47+
codeServer: CodeServer
48+
codeServerPage: CodeServerPage
49+
}
50+
51+
/**
52+
* Create a test that spawns code-server if necessary and ensures the page is
53+
* ready.
54+
*/
55+
export const test = base.extend<TestFixtures>({
56+
authenticated: false,
57+
codeServer: undefined, // No default; should be provided through `test.use`.
58+
codeServerPage: async ({ authenticated, codeServer, page }, use) => {
59+
// It's possible code-server might prevent navigation because of unsaved
60+
// changes (seems to happen based on timing even if no changes have been
61+
// made too). In these cases just accept.
62+
page.on("dialog", (d) => d.accept())
63+
64+
const codeServerPage = new CodeServerPage(codeServer, page)
65+
await codeServerPage.setup(authenticated)
66+
await use(codeServerPage)
967
},
1068
})
1169

70+
/** Shorthand for test.expect. */
1271
export const expect = test.expect

test/e2e/browser.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { test, expect } from "./baseFixture"
1+
import { describe, test, expect } from "./baseFixture"
22

33
// This is a "gut-check" test to make sure playwright is working as expected
4-
test.describe("browser", () => {
4+
describe("browser", true, () => {
55
test("browser should display correct userAgent", async ({ codeServerPage, browserName }) => {
66
const displayNames = {
77
chromium: "Chrome",

test/e2e/codeServer.test.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
1-
import { CODE_SERVER_ADDRESS, storageState } from "../utils/constants"
2-
import { test, expect } from "./baseFixture"
1+
import { describe, test, expect } from "./baseFixture"
32

4-
test.describe("CodeServer", () => {
5-
test.use({
6-
storageState,
7-
})
8-
9-
test(`should navigate to ${CODE_SERVER_ADDRESS}`, async ({ codeServerPage }) => {
3+
describe("CodeServer", true, () => {
4+
test("should navigate to home page", async ({ codeServerPage }) => {
105
// We navigate codeServer before each test
116
// and we start the test with a storage state
127
// which means we should be logged in
138
// so it should be on the address
149
const url = codeServerPage.page.url()
1510
// We use match because there may be a / at the end
1611
// so we don't want it to fail if we expect http://localhost:8080 to match http://localhost:8080/
17-
expect(url).toMatch(CODE_SERVER_ADDRESS)
12+
expect(url).toMatch(await codeServerPage.address())
1813
})
1914

2015
test("should always see the code-server editor", async ({ codeServerPage }) => {

test/e2e/globalSetup.test.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
import { storageState } from "../utils/constants"
2-
import { test, expect } from "./baseFixture"
1+
import { describe, test, expect } from "./baseFixture"
32

43
// This test is to make sure the globalSetup works as expected
54
// meaning globalSetup ran and stored the storageState
6-
test.describe("globalSetup", () => {
7-
test.use({
8-
storageState,
9-
})
10-
5+
describe("globalSetup", true, () => {
116
test("should keep us logged in using the storageState", async ({ codeServerPage }) => {
127
// Make sure the editor actually loaded
138
expect(await codeServerPage.isEditorVisible()).toBe(true)

test/e2e/login.test.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import { PASSWORD } from "../utils/constants"
2-
import { test, expect } from "./baseFixture"
3-
4-
test.describe("login", () => {
5-
// Reset the browser so no cookies are persisted
6-
// by emptying the storageState
7-
test.use({
8-
storageState: {},
9-
})
2+
import { describe, test, expect } from "./baseFixture"
103

4+
describe("login", false, () => {
115
test("should see the login page", async ({ codeServerPage }) => {
126
// It should send us to the login page
137
expect(await codeServerPage.page.title()).toBe("code-server login")

test/e2e/logout.test.ts

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,7 @@
1-
import { CODE_SERVER_ADDRESS, PASSWORD } from "../utils/constants"
2-
import { test, expect } from "./baseFixture"
3-
4-
test.describe("logout", () => {
5-
// Reset the browser so no cookies are persisted
6-
// by emptying the storageState
7-
test.use({
8-
storageState: {},
9-
})
10-
11-
test("should be able login and logout", async ({ codeServerPage }) => {
12-
// Type in password
13-
await codeServerPage.page.fill(".password", PASSWORD)
14-
// Click the submit button and login
15-
await codeServerPage.page.click(".submit")
16-
await codeServerPage.page.waitForLoadState("networkidle")
17-
// We do this because occassionally code-server doesn't load on Firefox
18-
// but loads if you reload once or twice
19-
await codeServerPage.reloadUntilEditorIsReady()
20-
// Make sure the editor actually loaded
21-
expect(await codeServerPage.isEditorVisible()).toBe(true)
1+
import { describe, test, expect } from "./baseFixture"
222

3+
describe("logout", true, () => {
4+
test("should be able logout", async ({ codeServerPage }) => {
235
// Click the Application menu
246
await codeServerPage.page.click("[aria-label='Application Menu']")
257

@@ -28,17 +10,11 @@ test.describe("logout", () => {
2810
expect(await codeServerPage.page.isVisible(logoutButton)).toBe(true)
2911

3012
await codeServerPage.page.hover(logoutButton)
31-
// TODO(@jsjoeio)
32-
// Look into how we're attaching the handlers for the logout feature
33-
// We need to see how it's done upstream and add logging to the
34-
// handlers themselves.
35-
// They may be attached too slowly, hence why we need this timeout
36-
await codeServerPage.page.waitForTimeout(2000)
3713

3814
// Recommended by Playwright for async navigation
3915
// https://github.com/microsoft/playwright/issues/1987#issuecomment-620182151
4016
await Promise.all([codeServerPage.page.waitForNavigation(), codeServerPage.page.click(logoutButton)])
4117
const currentUrl = codeServerPage.page.url()
42-
expect(currentUrl).toBe(`${CODE_SERVER_ADDRESS}/login`)
18+
expect(currentUrl).toBe(`${await codeServerPage.address()}/login`)
4319
})
4420
})

0 commit comments

Comments
 (0)