Skip to content

Commit 83fc759

Browse files
committed
ci: test extension through code-server and in ci
1 parent ca58880 commit 83fc759

File tree

9 files changed

+260
-32
lines changed

9 files changed

+260
-32
lines changed

.github/workflows/pr.yaml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,31 @@ jobs:
1717
- uses: actions/checkout@v4
1818
- uses: actions/setup-node@v4
1919
with:
20-
node-version: '20'
20+
node-version: '22'
2121
- uses: pnpm/action-setup@v4
2222
with:
2323
version: latest
2424
- name: Install dependencies
2525
run: pnpm install
2626
- name: Run CI
2727
run: pnpm run ci
28+
test-vscode-e2e:
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@v4
32+
- uses: actions/setup-node@v4
33+
with:
34+
node-version: '22'
35+
- uses: pnpm/action-setup@v4
36+
with:
37+
version: latest
38+
- name: Install dependencies
39+
run: pnpm install
40+
- name: Install code-server
41+
run: curl -fsSL https://code-server.dev/install.sh | sh
42+
- name: Install Playwright browsers
43+
working-directory: ./vscode/extension
44+
run: pnpm exec playwright install
45+
- name: Run e2e tests
46+
working-directory: ./vscode/extension
47+
run: pnpm run test:e2e tests/stop.spec.ts

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
22

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
{
2+
"engines": {
3+
"node": ">=22.0.0",
4+
"pnpm": ">=10.12.1"
5+
},
26
"scripts": {
37
"ci": "pnpm run lint && pnpm run -r ci",
48
"fmt": "prettier --write .",

vscode/extension/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,10 @@
120120
"lint": "eslint src",
121121
"lint:fix": "eslint src --fix",
122122
"test:unit": "vitest run",
123-
"test:e2e": "playwright test",
124-
"test:e2e:ui": "playwright test --ui",
125-
"test:e2e:headed": "playwright test --headed",
123+
"code-server": "code-server",
124+
"test:e2e": "pnpm run vscode:package && playwright test",
125+
"test:e2e:ui": "pnpm run vscode:package && playwright test --ui",
126+
"test:e2e:headed": "pnpm run vscode:package && playwright test --headed",
126127
"fetch-vscode": "tsx scripts/fetch-vscode.ts",
127128
"compile": "pnpm run check-types && node esbuild.js",
128129
"check-types": "tsc --noEmit -p ./tsconfig.build.json",

vscode/extension/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default defineConfig({
1111
use: {
1212
// ⭢ we'll launch Electron ourselves – no browser needed
1313
browserName: 'chromium',
14-
headless: false, // headed makes screenshots deterministic
14+
headless: true, // headless mode for tests
1515
launchOptions: {
1616
slowMo: process.env.CI ? 0 : 100,
1717
},
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { test, expect } from '@playwright/test'
2+
import path from 'path'
3+
import fs from 'fs-extra'
4+
import os from 'os'
5+
import { startCodeServer, stopCodeServer } from './utils_code_server'
6+
7+
test('code-server starts successfully', async ({ page }) => {
8+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'code-server-test-'))
9+
const context = await startCodeServer(tempDir)
10+
11+
try {
12+
// Navigate to code-server instance
13+
await page.goto(`http://127.0.0.1:${context.codeServerPort}`)
14+
15+
// Wait for code-server to load
16+
await page.waitForLoadState('networkidle')
17+
18+
// Check that code-server is running
19+
await expect(page.locator('body')).toBeVisible()
20+
21+
// Look for VS Code interface elements
22+
await expect(page.locator('[role="application"]')).toBeVisible({
23+
timeout: 10000,
24+
})
25+
} finally {
26+
await stopCodeServer(context)
27+
}
28+
})
Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,65 @@
11
import path from 'path'
2-
import { startVSCode, SUSHI_SOURCE_PATH } from './utils'
2+
import { SUSHI_SOURCE_PATH } from './utils'
33
import os from 'os'
44
import { test } from '@playwright/test'
55
import fs from 'fs-extra'
6+
import { startCodeServer, stopCodeServer } from './utils_code_server'
67

7-
test('Stop server works', async () => {
8+
test('Stop server works', async ({ page }) => {
9+
test.setTimeout(360000) // Increase timeout to 6 minutes
10+
11+
console.log('Starting test: Stop server works')
812
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-'))
913
await fs.copy(SUSHI_SOURCE_PATH, tempDir)
1014

15+
const context = await startCodeServer(tempDir, true)
16+
1117
try {
12-
const { window, close } = await startVSCode(tempDir)
18+
// Navigate to code-server instance
19+
await page.goto(`http://127.0.0.1:${context.codeServerPort}`)
20+
21+
// Wait for code-server to load
22+
await page.waitForLoadState('networkidle')
23+
await page.waitForSelector('[role="application"]', { timeout: 10000 })
1324

14-
// Wait for the models folder to be visible
15-
await window.waitForSelector('text=models')
25+
// Wait for the models folder to be visible in the file explorer
26+
await page.waitForSelector('text=models')
1627

1728
// Click on the models folder, excluding external_models
18-
await window
29+
await page
1930
.getByRole('treeitem', { name: 'models', exact: true })
2031
.locator('a')
2132
.click()
2233

23-
// Open the customer_revenue_lifetime model
24-
await window
34+
// Open the customers.sql model
35+
await page
2536
.getByRole('treeitem', { name: 'customers.sql', exact: true })
2637
.locator('a')
2738
.click()
2839

29-
await window.waitForSelector('text=grain')
30-
await window.waitForSelector('text=Loaded SQLMesh Context')
40+
await page.waitForSelector('text=grain')
41+
await page.waitForSelector('text=Loaded SQLMesh Context')
42+
43+
console.log('Stopping server...')
3144

3245
// Stop the server
33-
await window.keyboard.press(
34-
process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P',
35-
)
36-
await window.keyboard.type('SQLMesh: Stop Server')
37-
await window.keyboard.press('Enter')
46+
await page.keyboard.press('Meta+Shift+P')
47+
await page.keyboard.type('SQLMesh: Stop Server')
48+
await page.keyboard.press('Enter')
3849

3950
// Await LSP server stopped message
40-
await window.waitForSelector('text=LSP server stopped')
51+
await page.waitForSelector('text=LSP server stopped')
4152

4253
// Render the model
43-
await window.keyboard.press(
44-
process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P',
45-
)
46-
await window.keyboard.type('Render Model')
47-
await window.keyboard.press('Enter')
54+
await page.keyboard.press('Meta+Shift+P')
55+
await page.keyboard.type('Render Model')
56+
await page.keyboard.press('Enter')
4857

4958
// Await error message
50-
await window.waitForSelector(
59+
await page.waitForSelector(
5160
'text="Failed to render model: LSP client not ready."',
5261
)
53-
await close()
5462
} finally {
55-
await fs.remove(tempDir)
63+
await stopCodeServer(context)
5664
}
5765
})

vscode/extension/tests/utils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import { exec } from 'child_process'
66
import { promisify } from 'util'
77

88
// Absolute path to the VS Code executable you downloaded in step 1.
9-
export const VS_CODE_EXE = fs.readJsonSync(
10-
path.join(__dirname, '..', '.vscode-test', 'paths.json'),
11-
).executablePath
9+
export const VS_CODE_EXE = 'nothing'
10+
// fs.readJsonSync(
11+
// path.join(__dirname, '..', '.vscode-test', 'paths.json'),
12+
// ).executablePath
1213
// Where your extension lives on disk
1314
export const EXT_PATH = path.resolve(__dirname, '..')
1415
// Where the sushi project lives which we copy from
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { spawn, ChildProcess, execSync } from 'child_process'
2+
import path from 'path'
3+
import fs from 'fs-extra'
4+
5+
export interface CodeServerContext {
6+
codeServerProcess: ChildProcess
7+
codeServerPort: number
8+
tempDir: string
9+
}
10+
11+
/**
12+
* @param tempDir - The temporary directory to use for the code-server instance
13+
* @param placeFileWithPythonInterpreter - Whether to place a vscode/settings.json file in the temp directory that points to the python interpreter of the environmen the test is running in.
14+
* @returns The code-server context
15+
*/
16+
export async function startCodeServer(
17+
tempDir: string,
18+
placeFileWithPythonInterpreter: boolean = false,
19+
): Promise<CodeServerContext> {
20+
// Find an available port
21+
const codeServerPort = Math.floor(Math.random() * 10000) + 50000
22+
23+
// Create .vscode/settings.json with Python interpreter if requested
24+
if (placeFileWithPythonInterpreter) {
25+
const vscodeDir = path.join(tempDir, '.vscode')
26+
await fs.ensureDir(vscodeDir)
27+
28+
// Get the current Python interpreter path
29+
const pythonPath = execSync('which python', {
30+
encoding: 'utf-8',
31+
}).trim()
32+
33+
const settings = {
34+
'python.defaultInterpreterPath': path.join(
35+
__dirname,
36+
'..',
37+
'..',
38+
'..',
39+
'.venv',
40+
'bin',
41+
'python',
42+
),
43+
}
44+
45+
await fs.writeJson(path.join(vscodeDir, 'settings.json'), settings, {
46+
spaces: 2,
47+
})
48+
console.log(
49+
`Created .vscode/settings.json with Python interpreter: ${pythonPath}`,
50+
)
51+
}
52+
53+
// Get the extension version from package.json
54+
const extensionDir = path.join(__dirname, '..')
55+
const packageJson = JSON.parse(
56+
fs.readFileSync(path.join(extensionDir, 'package.json'), 'utf-8'),
57+
)
58+
const version = packageJson.version
59+
const extensionName = packageJson.name || 'sqlmesh'
60+
61+
// Look for the specific version .vsix file
62+
const vsixFileName = `${extensionName}-${version}.vsix`
63+
const vsixPath = path.join(extensionDir, vsixFileName)
64+
65+
if (!fs.existsSync(vsixPath)) {
66+
throw new Error(
67+
`Extension file ${vsixFileName} not found. Run "pnpm run vscode:package" first.`,
68+
)
69+
}
70+
71+
console.log(`Using extension: ${vsixFileName}`)
72+
73+
// Install the extension first
74+
const extensionsDir = path.join(tempDir, 'extensions')
75+
console.log('Installing extension...')
76+
execSync(
77+
`pnpm run code-server --user-data-dir "${tempDir}" --extensions-dir "${extensionsDir}" --install-extension "${vsixPath}"`,
78+
{ stdio: 'inherit' },
79+
)
80+
81+
// Start code-server instance
82+
const codeServerProcess = spawn(
83+
'pnpm',
84+
[
85+
'run',
86+
'code-server',
87+
'--bind-addr',
88+
`127.0.0.1:${codeServerPort}`,
89+
'--auth',
90+
'none',
91+
'--disable-telemetry',
92+
'--disable-update-check',
93+
'--disable-workspace-trust',
94+
'--user-data-dir',
95+
tempDir,
96+
'--extensions-dir',
97+
extensionsDir,
98+
tempDir,
99+
],
100+
{
101+
stdio: 'pipe',
102+
cwd: path.join(__dirname, '..'),
103+
},
104+
)
105+
106+
// Wait for code-server to be ready
107+
await new Promise<void>((resolve, reject) => {
108+
let output = ''
109+
const timeout = setTimeout(() => {
110+
reject(new Error('Code-server failed to start within timeout'))
111+
}, 30000)
112+
113+
codeServerProcess.stdout?.on('data', data => {
114+
output += data.toString()
115+
if (output.includes('HTTP server listening on')) {
116+
clearTimeout(timeout)
117+
resolve()
118+
}
119+
})
120+
121+
codeServerProcess.stderr?.on('data', data => {
122+
console.error('Code-server stderr:', data.toString())
123+
})
124+
125+
codeServerProcess.on('error', error => {
126+
clearTimeout(timeout)
127+
reject(error)
128+
})
129+
130+
codeServerProcess.on('exit', code => {
131+
if (code !== 0) {
132+
clearTimeout(timeout)
133+
reject(new Error(`Code-server exited with code ${code}`))
134+
}
135+
})
136+
})
137+
138+
return { codeServerProcess, codeServerPort, tempDir }
139+
}
140+
141+
export async function stopCodeServer(
142+
context: CodeServerContext,
143+
): Promise<void> {
144+
const { codeServerProcess, tempDir } = context
145+
146+
// Clean up code-server process
147+
codeServerProcess.kill('SIGTERM')
148+
149+
// Wait for process to exit
150+
await new Promise<void>(resolve => {
151+
codeServerProcess.on('exit', () => {
152+
resolve()
153+
})
154+
// Force kill after 5 seconds
155+
setTimeout(() => {
156+
if (!codeServerProcess.killed) {
157+
codeServerProcess.kill('SIGKILL')
158+
}
159+
resolve()
160+
}, 5000)
161+
})
162+
163+
// Clean up temporary directory
164+
await fs.remove(tempDir)
165+
}

0 commit comments

Comments
 (0)