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.
This commit is contained in:
parent
c4d87580ef
commit
b61a8addcf
@ -70,8 +70,8 @@ export const test = base.extend<TestFixtures>({
|
|||||||
// made too). In these cases just accept.
|
// made too). In these cases just accept.
|
||||||
page.on("dialog", (d) => d.accept())
|
page.on("dialog", (d) => d.accept())
|
||||||
|
|
||||||
const codeServerPage = new CodeServerPage(codeServer, page)
|
const codeServerPage = new CodeServerPage(codeServer, page, authenticated)
|
||||||
await codeServerPage.setup(authenticated)
|
await codeServerPage.navigate()
|
||||||
await use(codeServerPage)
|
await use(codeServerPage)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -1,8 +1,45 @@
|
|||||||
|
import * as cp from "child_process"
|
||||||
import { promises as fs } from "fs"
|
import { promises as fs } from "fs"
|
||||||
|
import * as os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
|
import * as util from "util"
|
||||||
import { describe, test, expect } from "./baseFixture"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
|
import { CodeServer } from "./models/CodeServer"
|
||||||
|
|
||||||
|
describe("code-server", true, [], {}, () => {
|
||||||
|
// TODO@asher: Generalize this? Could be nice if we were to ever need
|
||||||
|
// multiple migration tests in other suites.
|
||||||
|
const instances = new Map<string, CodeServer>()
|
||||||
|
test.afterAll(async () => {
|
||||||
|
const procs = Array.from(instances.values())
|
||||||
|
instances.clear()
|
||||||
|
await Promise.all(procs.map((cs) => cs.close()))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn a specific version of code-server using the install script.
|
||||||
|
*/
|
||||||
|
const spawn = async (version: string, dir?: string): Promise<CodeServer> => {
|
||||||
|
let instance = instances.get(version)
|
||||||
|
if (!instance) {
|
||||||
|
await util.promisify(cp.exec)(`./install.sh --method standalone --version ${version}`, {
|
||||||
|
cwd: path.join(__dirname, "../.."),
|
||||||
|
})
|
||||||
|
|
||||||
|
instance = new CodeServer(
|
||||||
|
"code-server@" + version,
|
||||||
|
["--auth=none"],
|
||||||
|
{ VSCODE_DEV: "" },
|
||||||
|
dir,
|
||||||
|
`${os.homedir()}/.local/lib/code-server-${version}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
instances.set(version, instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
describe("CodeServer", true, [], {}, () => {
|
|
||||||
test("should navigate to home page", async ({ codeServerPage }) => {
|
test("should navigate to home page", async ({ codeServerPage }) => {
|
||||||
// We navigate codeServer before each test
|
// We navigate codeServer before each test
|
||||||
// and we start the test with a storage state
|
// and we start the test with a storage state
|
||||||
@ -34,24 +71,50 @@ describe("CodeServer", true, [], {}, () => {
|
|||||||
await codeServerPage.openFile(file)
|
await codeServerPage.openFile(file)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should not share state with other paths", async ({ codeServerPage }) => {
|
test("should migrate state to avoid collisions", async ({ codeServerPage }) => {
|
||||||
|
// This can take a very long time in development because of how long pages
|
||||||
|
// take to load and we are doing a lot of that here.
|
||||||
|
test.slow()
|
||||||
|
|
||||||
const dir = await codeServerPage.workspaceDir
|
const dir = await codeServerPage.workspaceDir
|
||||||
const file = path.join(dir, "foo")
|
const files = [path.join(dir, "foo"), path.join(dir, "bar")]
|
||||||
await fs.writeFile(file, "bar")
|
await Promise.all(
|
||||||
|
files.map((file) => {
|
||||||
|
return fs.writeFile(file, path.basename(file))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
await codeServerPage.openFile(file)
|
// Open a file in the latest instance.
|
||||||
|
await codeServerPage.openFile(files[0])
|
||||||
|
await codeServerPage.stateFlush()
|
||||||
|
|
||||||
// If we reload now VS Code will be unable to save the state changes so wait
|
// Open a file in an older version of code-server. It should not see the
|
||||||
// until those have been written to the database. It flushes every five
|
// file opened in the new instance since the database has a different
|
||||||
// seconds so we need to wait at least that long.
|
// name. This must be accessed through the proxy so it shares the same
|
||||||
await codeServerPage.page.waitForTimeout(5500)
|
// domain and can write to the same database.
|
||||||
|
const cs = await spawn("4.0.2", dir)
|
||||||
|
const address = new URL(await cs.address())
|
||||||
|
await codeServerPage.navigate("/proxy/" + address.port + "/")
|
||||||
|
await codeServerPage.openFile(files[1])
|
||||||
|
expect(await codeServerPage.tabIsVisible(files[0])).toBe(false)
|
||||||
|
await codeServerPage.stateFlush()
|
||||||
|
|
||||||
// The tab should re-open on refresh.
|
// Move back to latest code-server. We should see the file we previously
|
||||||
await codeServerPage.page.reload()
|
// opened with it but not the old code-server file because the new instance
|
||||||
await codeServerPage.waitForTab(file)
|
// already created its own database on this path and will avoid migrating.
|
||||||
|
await codeServerPage.navigate()
|
||||||
|
await codeServerPage.waitForTab(files[0])
|
||||||
|
expect(await codeServerPage.tabIsVisible(files[1])).toBe(false)
|
||||||
|
|
||||||
// The tab should not re-open on a different path.
|
// Open a new path in latest code-server. This one should migrate the
|
||||||
await codeServerPage.setup(true, "/vscode")
|
// database from old code-server but see nothing from the new database
|
||||||
expect(await codeServerPage.tabIsVisible(file)).toBe(false)
|
// created on the root.
|
||||||
|
await codeServerPage.navigate("/vscode")
|
||||||
|
await codeServerPage.waitForTab(files[1])
|
||||||
|
expect(await codeServerPage.tabIsVisible(files[0])).toBe(false)
|
||||||
|
// Should still be open after a reload.
|
||||||
|
await codeServerPage.navigate("/vscode")
|
||||||
|
await codeServerPage.waitForTab(files[1])
|
||||||
|
expect(await codeServerPage.tabIsVisible(files[0])).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -3,7 +3,7 @@ import * as cp from "child_process"
|
|||||||
import { promises as fs } from "fs"
|
import { promises as fs } from "fs"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { Page } from "playwright"
|
import { Page } from "playwright"
|
||||||
import util from "util"
|
import * as util from "util"
|
||||||
import { logError, plural } from "../../../src/common/util"
|
import { logError, plural } from "../../../src/common/util"
|
||||||
import { onLine } from "../../../src/node/util"
|
import { onLine } from "../../../src/node/util"
|
||||||
import { PASSWORD, workspaceDir } from "../../utils/constants"
|
import { PASSWORD, workspaceDir } from "../../utils/constants"
|
||||||
@ -38,12 +38,13 @@ export class CodeServer {
|
|||||||
private process: Promise<CodeServerProcess> | undefined
|
private process: Promise<CodeServerProcess> | undefined
|
||||||
public readonly logger: Logger
|
public readonly logger: Logger
|
||||||
private closed = false
|
private closed = false
|
||||||
private _workspaceDir: Promise<string> | undefined
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
name: string,
|
name: string,
|
||||||
private readonly codeServerArgs: string[],
|
private readonly args: string[],
|
||||||
private readonly codeServerEnv: NodeJS.ProcessEnv,
|
private readonly env: NodeJS.ProcessEnv,
|
||||||
|
private readonly _workspaceDir: Promise<string> | string | undefined,
|
||||||
|
private readonly entry = process.env.CODE_SERVER_TEST_ENTRY || ".",
|
||||||
) {
|
) {
|
||||||
this.logger = logger.named(name)
|
this.logger = logger.named(name)
|
||||||
}
|
}
|
||||||
@ -75,7 +76,7 @@ export class CodeServer {
|
|||||||
*/
|
*/
|
||||||
private async createWorkspace(): Promise<string> {
|
private async createWorkspace(): Promise<string> {
|
||||||
const dir = await this.workspaceDir
|
const dir = await this.workspaceDir
|
||||||
await fs.mkdir(path.join(dir, "User"))
|
await fs.mkdir(path.join(dir, "User"), { recursive: true })
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(dir, "User/settings.json"),
|
path.join(dir, "User/settings.json"),
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@ -96,36 +97,33 @@ export class CodeServer {
|
|||||||
const dir = await this.createWorkspace()
|
const dir = await this.createWorkspace()
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.logger.debug("spawning")
|
const args = [
|
||||||
const proc = cp.spawn(
|
this.entry,
|
||||||
"node",
|
"--extensions-dir",
|
||||||
[
|
path.join(dir, "extensions"),
|
||||||
process.env.CODE_SERVER_TEST_ENTRY || ".",
|
...this.args,
|
||||||
"--extensions-dir",
|
// Using port zero will spawn on a random port.
|
||||||
path.join(dir, "extensions"),
|
"--bind-addr",
|
||||||
...this.codeServerArgs,
|
"127.0.0.1:0",
|
||||||
// Using port zero will spawn on a random port.
|
// Setting the XDG variables would be easier and more thorough but the
|
||||||
"--bind-addr",
|
// modules we import ignores those variables for non-Linux operating
|
||||||
"127.0.0.1:0",
|
// systems so use these flags instead.
|
||||||
// Setting the XDG variables would be easier and more thorough but the
|
"--config",
|
||||||
// modules we import ignores those variables for non-Linux operating
|
path.join(dir, "config.yaml"),
|
||||||
// systems so use these flags instead.
|
"--user-data-dir",
|
||||||
"--config",
|
dir,
|
||||||
path.join(dir, "config.yaml"),
|
// The last argument is the workspace to open.
|
||||||
"--user-data-dir",
|
dir,
|
||||||
dir,
|
]
|
||||||
// The last argument is the workspace to open.
|
this.logger.debug("spawning `node " + args.join(" ") + "`")
|
||||||
dir,
|
const proc = cp.spawn("node", args, {
|
||||||
],
|
cwd: path.join(__dirname, "../../.."),
|
||||||
{
|
env: {
|
||||||
cwd: path.join(__dirname, "../../.."),
|
...process.env,
|
||||||
env: {
|
...this.env,
|
||||||
...process.env,
|
PASSWORD,
|
||||||
...this.codeServerEnv,
|
|
||||||
PASSWORD,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)
|
})
|
||||||
|
|
||||||
const timer = idleTimer("Failed to extract address; did the format change?", reject)
|
const timer = idleTimer("Failed to extract address; did the format change?", reject)
|
||||||
|
|
||||||
@ -136,7 +134,7 @@ export class CodeServer {
|
|||||||
})
|
})
|
||||||
|
|
||||||
proc.on("close", (code) => {
|
proc.on("close", (code) => {
|
||||||
const error = new Error("closed unexpectedly")
|
const error = new Error("code-server closed unexpectedly")
|
||||||
if (!this.closed) {
|
if (!this.closed) {
|
||||||
this.logger.error(error.message, field("code", code))
|
this.logger.error(error.message, field("code", code))
|
||||||
}
|
}
|
||||||
@ -153,7 +151,7 @@ export class CodeServer {
|
|||||||
timer.reset()
|
timer.reset()
|
||||||
|
|
||||||
// Log the line without the timestamp.
|
// Log the line without the timestamp.
|
||||||
this.logger.trace(line.replace(/\[.+\]/, ""))
|
this.logger.debug(line.replace(/\[.+\]/, ""))
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -194,7 +192,11 @@ export class CodeServer {
|
|||||||
export class CodeServerPage {
|
export class CodeServerPage {
|
||||||
private readonly editorSelector = "div.monaco-workbench"
|
private readonly editorSelector = "div.monaco-workbench"
|
||||||
|
|
||||||
constructor(private readonly codeServer: CodeServer, public readonly page: Page) {
|
constructor(
|
||||||
|
private readonly codeServer: CodeServer,
|
||||||
|
public readonly page: Page,
|
||||||
|
private readonly authenticated: boolean,
|
||||||
|
) {
|
||||||
this.page.on("console", (message) => {
|
this.page.on("console", (message) => {
|
||||||
this.codeServer.logger.debug(message)
|
this.codeServer.logger.debug(message)
|
||||||
})
|
})
|
||||||
@ -215,11 +217,18 @@ export class CodeServerPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to a code-server endpoint. By default go to the root.
|
* Navigate to a code-server endpoint (root by default). Then wait for the
|
||||||
|
* editor to become available.
|
||||||
*/
|
*/
|
||||||
async navigate(endpoint = "/") {
|
async navigate(endpoint = "/") {
|
||||||
const to = new URL(endpoint, await this.codeServer.address())
|
const to = new URL(endpoint, await this.codeServer.address())
|
||||||
await this.page.goto(to.toString(), { waitUntil: "networkidle" })
|
await this.page.goto(to.toString(), { waitUntil: "networkidle" })
|
||||||
|
|
||||||
|
// Only reload editor if authenticated. Otherwise we'll get stuck
|
||||||
|
// reloading the login page.
|
||||||
|
if (this.authenticated) {
|
||||||
|
await this.reloadUntilEditorIsReady()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -456,21 +465,7 @@ export class CodeServerPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to code-server then reloads until the editor is ready.
|
* Execute a command in the root of the instance's workspace directory.
|
||||||
*
|
|
||||||
* It is recommended to run setup before using this model in any tests.
|
|
||||||
*/
|
|
||||||
async setup(authenticated: boolean, endpoint = "/") {
|
|
||||||
await this.navigate(endpoint)
|
|
||||||
// If we aren't authenticated we'll see a login page so we can't wait until
|
|
||||||
// the editor is ready.
|
|
||||||
if (authenticated) {
|
|
||||||
await this.reloadUntilEditorIsReady()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a command in t root of the instance's workspace directory.
|
|
||||||
*/
|
*/
|
||||||
async exec(command: string): Promise<void> {
|
async exec(command: string): Promise<void> {
|
||||||
await util.promisify(cp.exec)(command, {
|
await util.promisify(cp.exec)(command, {
|
||||||
@ -488,4 +483,15 @@ export class CodeServerPage {
|
|||||||
cwd: path.join(__dirname, "../../.."),
|
cwd: path.join(__dirname, "../../.."),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for state to be flushed to the database.
|
||||||
|
*/
|
||||||
|
async stateFlush(): Promise<void> {
|
||||||
|
// If we reload too quickly VS Code will be unable to save the state changes
|
||||||
|
// so wait until those have been written to the database. It flushes every
|
||||||
|
// five seconds so we need to wait at least that long.
|
||||||
|
// TODO@asher: There must be a better way.
|
||||||
|
await this.page.waitForTimeout(5500)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
2
vendor/package.json
vendored
2
vendor/package.json
vendored
@ -7,6 +7,6 @@
|
|||||||
"postinstall": "./postinstall.sh"
|
"postinstall": "./postinstall.sh"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"code-oss-dev": "coder/vscode#bd734e3d9f21b1bce4dabab2514177e90c090ee6"
|
"code-oss-dev": "coder/vscode#94384412221f432c15bb679315c49964925090be"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
vendor/yarn.lock
vendored
4
vendor/yarn.lock
vendored
@ -274,9 +274,9 @@ clone-response@^1.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-response "^1.0.0"
|
mimic-response "^1.0.0"
|
||||||
|
|
||||||
code-oss-dev@coder/vscode#bd734e3d9f21b1bce4dabab2514177e90c090ee6:
|
code-oss-dev@coder/vscode#94384412221f432c15bb679315c49964925090be:
|
||||||
version "1.63.0"
|
version "1.63.0"
|
||||||
resolved "https://codeload.github.com/coder/vscode/tar.gz/bd734e3d9f21b1bce4dabab2514177e90c090ee6"
|
resolved "https://codeload.github.com/coder/vscode/tar.gz/94384412221f432c15bb679315c49964925090be"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@microsoft/applicationinsights-web" "^2.6.4"
|
"@microsoft/applicationinsights-web" "^2.6.4"
|
||||||
"@parcel/watcher" "2.0.3"
|
"@parcel/watcher" "2.0.3"
|
||||||
|
Reference in New Issue
Block a user