diff --git a/vscode/extension/tests/no_lsp.spec.ts b/vscode/extension/tests/no_lsp.spec.ts new file mode 100644 index 0000000000..b0aa6323b3 --- /dev/null +++ b/vscode/extension/tests/no_lsp.spec.ts @@ -0,0 +1,74 @@ +import { expect, test } from '@playwright/test' +import fs from 'fs-extra' +import os from 'os' +import path from 'path' +import { + createVirtualEnvironment, + pipInstall, + REPO_ROOT, + startVSCode, + SUSHI_SOURCE_PATH, +} from './utils' + +test('missing LSP dependencies shows install prompt', async ({}, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + const pythonEnvDir = path.join(tempDir, '.venv') + const pythonDetails = await createVirtualEnvironment(pythonEnvDir) + const custom_materializations = path.join( + REPO_ROOT, + 'examples', + 'custom_materializations', + ) + const sqlmeshWithExtras = `${REPO_ROOT}[bigquery]` + await pipInstall(pythonDetails, [sqlmeshWithExtras, custom_materializations]) + + try { + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonDetails.pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, + } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson( + path.join(tempDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) + + // Start VS Code + const { window, close } = await startVSCode(tempDir) + + // Open a SQL file to trigger SQLMesh activation + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Wait for the message to show that LSP extras need to be installed + await window.waitForSelector('text=LSP dependencies missing') + expect(await window.locator('text=Install').count()).toBeGreaterThanOrEqual( + 1, + ) + + await close() + } finally { + // Clean up + await fs.remove(tempDir) + } +}) diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts index 3a99288546..d047a9dcd8 100644 --- a/vscode/extension/tests/tcloud.spec.ts +++ b/vscode/extension/tests/tcloud.spec.ts @@ -2,48 +2,35 @@ import { expect, test } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { exec } from 'child_process' -import { promisify } from 'util' -import { REPO_ROOT, startVSCode, SUSHI_SOURCE_PATH } from './utils' - -const execAsync = promisify(exec) +import { + createVirtualEnvironment, + pipInstall, + REPO_ROOT, + startVSCode, + SUSHI_SOURCE_PATH, +} from './utils' /** * Helper function to create and set up a Python virtual environment */ async function setupPythonEnvironment(envDir: string): Promise { // Create virtual environment - const pythonCmd = process.platform === 'win32' ? 'python' : 'python3' - const { stderr } = await execAsync(`${pythonCmd} -m venv "${envDir}"`) - if (stderr && !stderr.includes('WARNING')) { - throw new Error(`Failed to create venv: ${stderr}`) - } - - // Get paths - const isWindows = process.platform === 'win32' - const binDir = path.join(envDir, isWindows ? 'Scripts' : 'bin') - const pythonPath = path.join(binDir, isWindows ? 'python.exe' : 'python') - const pipPath = path.join(binDir, isWindows ? 'pip.exe' : 'pip') + const pythonDetails = await createVirtualEnvironment(envDir) // Install the mock tcloud package const mockTcloudPath = path.join(__dirname, 'tcloud') - const { stderr: pipErr1 } = await execAsync( - `"${pipPath}" install -e "${mockTcloudPath}"`, - ) - if (pipErr1 && !pipErr1.includes('WARNING') && !pipErr1.includes('notice')) { - throw new Error(`Failed to install mock tcloud: ${pipErr1}`) - } + await pipInstall(pythonDetails, [mockTcloudPath]) // Install sqlmesh from the local repository with LSP support - const sqlmeshRepoPath = path.join(__dirname, '..', '..', '..') // Navigate to repo root from tests dir - const { stderr: pipErr2 } = await execAsync( - `"${pipPath}" install -e "${sqlmeshRepoPath}[lsp,bigquery]" "${REPO_ROOT}/examples/custom_materializations"`, + const customMaterializations = path.join( + REPO_ROOT, + 'examples', + 'custom_materializations', ) - if (pipErr2 && !pipErr2.includes('WARNING') && !pipErr2.includes('notice')) { - throw new Error(`Failed to install sqlmesh: ${pipErr2}`) - } + const sqlmeshWithExtras = `${REPO_ROOT}[lsp,bigquery]` + await pipInstall(pythonDetails, [sqlmeshWithExtras, customMaterializations]) - return pythonPath + return pythonDetails.pythonPath } /** diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index ff68c7291f..8150c43c59 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -2,6 +2,8 @@ import path from 'path' import fs from 'fs-extra' import os from 'os' import { _electron as electron, Page } from '@playwright/test' +import { exec } from 'child_process' +import { promisify } from 'util' // Absolute path to the VS Code executable you downloaded in step 1. export const VS_CODE_EXE = fs.readJsonSync( @@ -87,3 +89,51 @@ export const clickExplorerTab = async (page: Page): Promise => { await page.locator("text='Explorer'").waitFor({ state: 'visible' }) } } + +const execAsync = promisify(exec) + +export interface PythonEnvironment { + pythonPath: string + pipPath: string +} + +/** + * Create a virtual environment in the given directory. + * @param venvDir The directory to create the virtual environment in. + */ +export const createVirtualEnvironment = async ( + venvDir: string, +): Promise => { + const pythonCmd = process.platform === 'win32' ? 'python' : 'python3' + const { stderr } = await execAsync(`${pythonCmd} -m venv "${venvDir}"`) + if (stderr && !stderr.includes('WARNING')) { + throw new Error(`Failed to create venv: ${stderr}`) + } + // Get paths + const isWindows = process.platform === 'win32' + const binDir = path.join(venvDir, isWindows ? 'Scripts' : 'bin') + const pythonPath = path.join(binDir, isWindows ? 'python.exe' : 'python') + const pipPath = path.join(binDir, isWindows ? 'pip.exe' : 'pip') + + return { + pythonPath, + pipPath, + } +} + +/** + * Install packages in the given virtual environment. + * @param pythonDetails The Python environment to use. + * @param packagePaths The paths to the packages to install (string[]). + */ +export const pipInstall = async ( + pythonDetails: PythonEnvironment, + packagePaths: string[], +): Promise => { + const { pipPath } = pythonDetails + const execString = `"${pipPath}" install -e "${packagePaths.join('" -e "')}"` + const { stderr } = await execAsync(execString) + if (stderr && !stderr.includes('WARNING') && !stderr.includes('notice')) { + throw new Error(`Failed to install package: ${stderr}`) + } +}