Skip to content

Commit b61a8ad

Browse files
authored
feat: migrate state to new database name (#4938)
* Merge setup and navigate functions Whenever we navigate we probably want to make sure the editor is ready so might as well just have one function. * Add customizable entry and workspace directory * Add test for state db migration * Update Code This contains the state migrations.
1 parent c4d8758 commit b61a8ad

File tree

5 files changed

+142
-73
lines changed

5 files changed

+142
-73
lines changed

test/e2e/baseFixture.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ export const test = base.extend<TestFixtures>({
7070
// made too). In these cases just accept.
7171
page.on("dialog", (d) => d.accept())
7272

73-
const codeServerPage = new CodeServerPage(codeServer, page)
74-
await codeServerPage.setup(authenticated)
73+
const codeServerPage = new CodeServerPage(codeServer, page, authenticated)
74+
await codeServerPage.navigate()
7575
await use(codeServerPage)
7676
},
7777
})

test/e2e/codeServer.test.ts

+78-15
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,45 @@
1+
import * as cp from "child_process"
12
import { promises as fs } from "fs"
3+
import * as os from "os"
24
import * as path from "path"
5+
import * as util from "util"
36
import { describe, test, expect } from "./baseFixture"
7+
import { CodeServer } from "./models/CodeServer"
8+
9+
describe("code-server", true, [], {}, () => {
10+
// TODO@asher: Generalize this? Could be nice if we were to ever need
11+
// multiple migration tests in other suites.
12+
const instances = new Map<string, CodeServer>()
13+
test.afterAll(async () => {
14+
const procs = Array.from(instances.values())
15+
instances.clear()
16+
await Promise.all(procs.map((cs) => cs.close()))
17+
})
18+
19+
/**
20+
* Spawn a specific version of code-server using the install script.
21+
*/
22+
const spawn = async (version: string, dir?: string): Promise<CodeServer> => {
23+
let instance = instances.get(version)
24+
if (!instance) {
25+
await util.promisify(cp.exec)(`./install.sh --method standalone --version ${version}`, {
26+
cwd: path.join(__dirname, "../.."),
27+
})
28+
29+
instance = new CodeServer(
30+
"code-server@" + version,
31+
["--auth=none"],
32+
{ VSCODE_DEV: "" },
33+
dir,
34+
`${os.homedir()}/.local/lib/code-server-${version}`,
35+
)
36+
37+
instances.set(version, instance)
38+
}
39+
40+
return instance
41+
}
442

5-
describe("CodeServer", true, [], {}, () => {
643
test("should navigate to home page", async ({ codeServerPage }) => {
744
// We navigate codeServer before each test
845
// and we start the test with a storage state
@@ -34,24 +71,50 @@ describe("CodeServer", true, [], {}, () => {
3471
await codeServerPage.openFile(file)
3572
})
3673

37-
test("should not share state with other paths", async ({ codeServerPage }) => {
74+
test("should migrate state to avoid collisions", async ({ codeServerPage }) => {
75+
// This can take a very long time in development because of how long pages
76+
// take to load and we are doing a lot of that here.
77+
test.slow()
78+
3879
const dir = await codeServerPage.workspaceDir
39-
const file = path.join(dir, "foo")
40-
await fs.writeFile(file, "bar")
80+
const files = [path.join(dir, "foo"), path.join(dir, "bar")]
81+
await Promise.all(
82+
files.map((file) => {
83+
return fs.writeFile(file, path.basename(file))
84+
}),
85+
)
4186

42-
await codeServerPage.openFile(file)
87+
// Open a file in the latest instance.
88+
await codeServerPage.openFile(files[0])
89+
await codeServerPage.stateFlush()
4390

44-
// If we reload now VS Code will be unable to save the state changes so wait
45-
// until those have been written to the database. It flushes every five
46-
// seconds so we need to wait at least that long.
47-
await codeServerPage.page.waitForTimeout(5500)
91+
// Open a file in an older version of code-server. It should not see the
92+
// file opened in the new instance since the database has a different
93+
// name. This must be accessed through the proxy so it shares the same
94+
// domain and can write to the same database.
95+
const cs = await spawn("4.0.2", dir)
96+
const address = new URL(await cs.address())
97+
await codeServerPage.navigate("/proxy/" + address.port + "/")
98+
await codeServerPage.openFile(files[1])
99+
expect(await codeServerPage.tabIsVisible(files[0])).toBe(false)
100+
await codeServerPage.stateFlush()
48101

49-
// The tab should re-open on refresh.
50-
await codeServerPage.page.reload()
51-
await codeServerPage.waitForTab(file)
102+
// Move back to latest code-server. We should see the file we previously
103+
// opened with it but not the old code-server file because the new instance
104+
// already created its own database on this path and will avoid migrating.
105+
await codeServerPage.navigate()
106+
await codeServerPage.waitForTab(files[0])
107+
expect(await codeServerPage.tabIsVisible(files[1])).toBe(false)
52108

53-
// The tab should not re-open on a different path.
54-
await codeServerPage.setup(true, "/vscode")
55-
expect(await codeServerPage.tabIsVisible(file)).toBe(false)
109+
// Open a new path in latest code-server. This one should migrate the
110+
// database from old code-server but see nothing from the new database
111+
// created on the root.
112+
await codeServerPage.navigate("/vscode")
113+
await codeServerPage.waitForTab(files[1])
114+
expect(await codeServerPage.tabIsVisible(files[0])).toBe(false)
115+
// Should still be open after a reload.
116+
await codeServerPage.navigate("/vscode")
117+
await codeServerPage.waitForTab(files[1])
118+
expect(await codeServerPage.tabIsVisible(files[0])).toBe(false)
56119
})
57120
})

test/e2e/models/CodeServer.ts

+59-53
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as cp from "child_process"
33
import { promises as fs } from "fs"
44
import * as path from "path"
55
import { Page } from "playwright"
6-
import util from "util"
6+
import * as util from "util"
77
import { logError, plural } from "../../../src/common/util"
88
import { onLine } from "../../../src/node/util"
99
import { PASSWORD, workspaceDir } from "../../utils/constants"
@@ -38,12 +38,13 @@ export class CodeServer {
3838
private process: Promise<CodeServerProcess> | undefined
3939
public readonly logger: Logger
4040
private closed = false
41-
private _workspaceDir: Promise<string> | undefined
4241

4342
constructor(
4443
name: string,
45-
private readonly codeServerArgs: string[],
46-
private readonly codeServerEnv: NodeJS.ProcessEnv,
44+
private readonly args: string[],
45+
private readonly env: NodeJS.ProcessEnv,
46+
private readonly _workspaceDir: Promise<string> | string | undefined,
47+
private readonly entry = process.env.CODE_SERVER_TEST_ENTRY || ".",
4748
) {
4849
this.logger = logger.named(name)
4950
}
@@ -75,7 +76,7 @@ export class CodeServer {
7576
*/
7677
private async createWorkspace(): Promise<string> {
7778
const dir = await this.workspaceDir
78-
await fs.mkdir(path.join(dir, "User"))
79+
await fs.mkdir(path.join(dir, "User"), { recursive: true })
7980
await fs.writeFile(
8081
path.join(dir, "User/settings.json"),
8182
JSON.stringify({
@@ -96,36 +97,33 @@ export class CodeServer {
9697
const dir = await this.createWorkspace()
9798

9899
return new Promise((resolve, reject) => {
99-
this.logger.debug("spawning")
100-
const proc = cp.spawn(
101-
"node",
102-
[
103-
process.env.CODE_SERVER_TEST_ENTRY || ".",
104-
"--extensions-dir",
105-
path.join(dir, "extensions"),
106-
...this.codeServerArgs,
107-
// Using port zero will spawn on a random port.
108-
"--bind-addr",
109-
"127.0.0.1:0",
110-
// Setting the XDG variables would be easier and more thorough but the
111-
// modules we import ignores those variables for non-Linux operating
112-
// systems so use these flags instead.
113-
"--config",
114-
path.join(dir, "config.yaml"),
115-
"--user-data-dir",
116-
dir,
117-
// The last argument is the workspace to open.
118-
dir,
119-
],
120-
{
121-
cwd: path.join(__dirname, "../../.."),
122-
env: {
123-
...process.env,
124-
...this.codeServerEnv,
125-
PASSWORD,
126-
},
100+
const args = [
101+
this.entry,
102+
"--extensions-dir",
103+
path.join(dir, "extensions"),
104+
...this.args,
105+
// Using port zero will spawn on a random port.
106+
"--bind-addr",
107+
"127.0.0.1:0",
108+
// Setting the XDG variables would be easier and more thorough but the
109+
// modules we import ignores those variables for non-Linux operating
110+
// systems so use these flags instead.
111+
"--config",
112+
path.join(dir, "config.yaml"),
113+
"--user-data-dir",
114+
dir,
115+
// The last argument is the workspace to open.
116+
dir,
117+
]
118+
this.logger.debug("spawning `node " + args.join(" ") + "`")
119+
const proc = cp.spawn("node", args, {
120+
cwd: path.join(__dirname, "../../.."),
121+
env: {
122+
...process.env,
123+
...this.env,
124+
PASSWORD,
127125
},
128-
)
126+
})
129127

130128
const timer = idleTimer("Failed to extract address; did the format change?", reject)
131129

@@ -136,7 +134,7 @@ export class CodeServer {
136134
})
137135

138136
proc.on("close", (code) => {
139-
const error = new Error("closed unexpectedly")
137+
const error = new Error("code-server closed unexpectedly")
140138
if (!this.closed) {
141139
this.logger.error(error.message, field("code", code))
142140
}
@@ -153,7 +151,7 @@ export class CodeServer {
153151
timer.reset()
154152

155153
// Log the line without the timestamp.
156-
this.logger.trace(line.replace(/\[.+\]/, ""))
154+
this.logger.debug(line.replace(/\[.+\]/, ""))
157155
if (resolved) {
158156
return
159157
}
@@ -194,7 +192,11 @@ export class CodeServer {
194192
export class CodeServerPage {
195193
private readonly editorSelector = "div.monaco-workbench"
196194

197-
constructor(private readonly codeServer: CodeServer, public readonly page: Page) {
195+
constructor(
196+
private readonly codeServer: CodeServer,
197+
public readonly page: Page,
198+
private readonly authenticated: boolean,
199+
) {
198200
this.page.on("console", (message) => {
199201
this.codeServer.logger.debug(message)
200202
})
@@ -215,11 +217,18 @@ export class CodeServerPage {
215217
}
216218

217219
/**
218-
* Navigate to a code-server endpoint. By default go to the root.
220+
* Navigate to a code-server endpoint (root by default). Then wait for the
221+
* editor to become available.
219222
*/
220223
async navigate(endpoint = "/") {
221224
const to = new URL(endpoint, await this.codeServer.address())
222225
await this.page.goto(to.toString(), { waitUntil: "networkidle" })
226+
227+
// Only reload editor if authenticated. Otherwise we'll get stuck
228+
// reloading the login page.
229+
if (this.authenticated) {
230+
await this.reloadUntilEditorIsReady()
231+
}
223232
}
224233

225234
/**
@@ -456,21 +465,7 @@ export class CodeServerPage {
456465
}
457466

458467
/**
459-
* Navigates to code-server then reloads until the editor is ready.
460-
*
461-
* It is recommended to run setup before using this model in any tests.
462-
*/
463-
async setup(authenticated: boolean, endpoint = "/") {
464-
await this.navigate(endpoint)
465-
// If we aren't authenticated we'll see a login page so we can't wait until
466-
// the editor is ready.
467-
if (authenticated) {
468-
await this.reloadUntilEditorIsReady()
469-
}
470-
}
471-
472-
/**
473-
* Execute a command in t root of the instance's workspace directory.
468+
* Execute a command in the root of the instance's workspace directory.
474469
*/
475470
async exec(command: string): Promise<void> {
476471
await util.promisify(cp.exec)(command, {
@@ -488,4 +483,15 @@ export class CodeServerPage {
488483
cwd: path.join(__dirname, "../../.."),
489484
})
490485
}
486+
487+
/**
488+
* Wait for state to be flushed to the database.
489+
*/
490+
async stateFlush(): Promise<void> {
491+
// If we reload too quickly VS Code will be unable to save the state changes
492+
// so wait until those have been written to the database. It flushes every
493+
// five seconds so we need to wait at least that long.
494+
// TODO@asher: There must be a better way.
495+
await this.page.waitForTimeout(5500)
496+
}
491497
}

vendor/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
"postinstall": "./postinstall.sh"
88
},
99
"devDependencies": {
10-
"code-oss-dev": "coder/vscode#bd734e3d9f21b1bce4dabab2514177e90c090ee6"
10+
"code-oss-dev": "coder/vscode#94384412221f432c15bb679315c49964925090be"
1111
}
1212
}

vendor/yarn.lock

+2-2
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,9 @@ clone-response@^1.0.2:
274274
dependencies:
275275
mimic-response "^1.0.0"
276276

277-
code-oss-dev@coder/vscode#bd734e3d9f21b1bce4dabab2514177e90c090ee6:
277+
code-oss-dev@coder/vscode#94384412221f432c15bb679315c49964925090be:
278278
version "1.63.0"
279-
resolved "https://codeload.github.com/coder/vscode/tar.gz/bd734e3d9f21b1bce4dabab2514177e90c090ee6"
279+
resolved "https://codeload.github.com/coder/vscode/tar.gz/94384412221f432c15bb679315c49964925090be"
280280
dependencies:
281281
"@microsoft/applicationinsights-web" "^2.6.4"
282282
"@parcel/watcher" "2.0.3"

0 commit comments

Comments
 (0)