From 28e8134a98a4660adf24831aaeb5bc781cf4717e Mon Sep 17 00:00:00 2001 From: Ben King <9087625+benfdking@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:57:59 +0100 Subject: [PATCH] chore(vscode): better handling no sqlmesh --- vscode/extension/src/utilities/errors.ts | 11 +++++ .../src/utilities/sqlmesh/sqlmesh.ts | 41 +++++++++++++++++-- .../{no_lsp.spec.ts => bad_setup.spec.ts} | 39 ++++++++++++++++++ vscode/extension/tests/lineage.spec.ts | 9 +--- vscode/extension/tests/utils.ts | 22 ++++++++++ 5 files changed, 111 insertions(+), 11 deletions(-) rename vscode/extension/tests/{no_lsp.spec.ts => bad_setup.spec.ts} (67%) diff --git a/vscode/extension/src/utilities/errors.ts b/vscode/extension/src/utilities/errors.ts index 7a0796a449..62d0d016b9 100644 --- a/vscode/extension/src/utilities/errors.ts +++ b/vscode/extension/src/utilities/errors.ts @@ -9,6 +9,7 @@ import { traceInfo } from './common/log' export type ErrorType = | ErrorTypeGeneric | { type: 'not_signed_in' } + | { type: 'sqlmesh_not_found' } | { type: 'sqlmesh_lsp_not_found' } // tcloud_bin_not_found is used when the tcloud executable is not found. This is likely to happen if the user // opens a project that has a `tcloud.yaml` file but doesn't have tcloud installed. @@ -85,6 +86,8 @@ export async function handleError( return case 'not_signed_in': return handleNotSignedInError(authProvider) + case 'sqlmesh_not_found': + return handleSqlmeshNotFoundError() case 'sqlmesh_lsp_not_found': return handleSqlmeshLspNotFoundError() case 'sqlmesh_lsp_dependencies_missing': @@ -118,6 +121,14 @@ const handleNotSignedInError = async ( } } +/** + * Handles the case where the sqlmesh executable is not found. + */ +const handleSqlmeshNotFoundError = async (): Promise => { + traceInfo('handleSqlmeshNotFoundError') + await window.showErrorMessage('SQLMesh not found, please check installation') +} + /** * Handles the case where the sqlmesh_lsp is not found. */ diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 45d9cfbd4c..be60f3be2a 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -287,6 +287,12 @@ export const sqlmeshExec = async (): Promise< args: [], }) } else { + const exists = await doesExecutableExist(sqlmesh) + if (!exists) { + return err({ + type: 'sqlmesh_not_found', + }) + } return ok({ bin: sqlmesh, workspacePath, @@ -384,15 +390,17 @@ export const sqlmeshLspExec = async (): Promise< type: 'not_signed_in', }) } + const exists = await doesExecutableExist(sqlmeshLSP) + if (!exists) { + return err({ + type: 'sqlmesh_lsp_not_found', + }) + } const ensured = await ensureSqlmeshEnterpriseInstalled() if (isErr(ensured)) { return ensured } } - const ensuredDependencies = await ensureSqlmeshLspDependenciesInstalled() - if (isErr(ensuredDependencies)) { - return ensuredDependencies - } const binPath = path.join(interpreterDetails.binPath!, sqlmeshLSP) traceLog(`Bin path: ${binPath}`) if (!fs.existsSync(binPath)) { @@ -400,6 +408,10 @@ export const sqlmeshLspExec = async (): Promise< type: 'sqlmesh_lsp_not_found', }) } + const ensuredDependencies = await ensureSqlmeshLspDependenciesInstalled() + if (isErr(ensuredDependencies)) { + return ensuredDependencies + } return ok({ bin: binPath, workspacePath, @@ -411,6 +423,12 @@ export const sqlmeshLspExec = async (): Promise< args: [], }) } else { + const exists = await doesExecutableExist(sqlmeshLSP) + if (!exists) { + return err({ + type: 'sqlmesh_lsp_not_found', + }) + } return ok({ bin: sqlmeshLSP, workspacePath, @@ -419,3 +437,18 @@ export const sqlmeshLspExec = async (): Promise< }) } } + +async function doesExecutableExist(executable: string): Promise { + const command = process.platform === 'win32' ? 'where.exe' : 'which' + traceLog(`Checking if ${executable} exists with ${command}`) + try { + const result = await execAsync(command, [executable]) + traceLog(`Checked if ${executable} exists with ${command}, with result ${result.exitCode}`) + const exists = result.exitCode === 0 + traceLog(`Checked if ${executable} exists with ${command}, with result ${exists}`) + return exists + } catch { + traceLog(`Checked if ${executable} exists with ${command}, errored, returning false`) + return false + } +} \ No newline at end of file diff --git a/vscode/extension/tests/no_lsp.spec.ts b/vscode/extension/tests/bad_setup.spec.ts similarity index 67% rename from vscode/extension/tests/no_lsp.spec.ts rename to vscode/extension/tests/bad_setup.spec.ts index b0aa6323b3..2c92e89764 100644 --- a/vscode/extension/tests/no_lsp.spec.ts +++ b/vscode/extension/tests/bad_setup.spec.ts @@ -4,6 +4,7 @@ import os from 'os' import path from 'path' import { createVirtualEnvironment, + openLineageView, pipInstall, REPO_ROOT, startVSCode, @@ -72,3 +73,41 @@ test('missing LSP dependencies shows install prompt', async ({}, testInfo) => { await fs.remove(tempDir) } }) + +test('lineage, no sqlmesh found', async ({}) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + const pythonEnvDir = path.join(tempDir, '.venv') + const pythonDetails = await createVirtualEnvironment(pythonEnvDir) + + 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 }, + ) + + const { window, close } = await startVSCode(tempDir) + + // Open lineage view + await openLineageView(window) + + // Assert shows that sqlmesh is not installed + await window.waitForSelector('text=SQLMesh LSP not found') + + await close() + } finally { + // Clean up + await fs.remove(tempDir) + } +}) diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index e2a049a9f0..a75407802f 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -2,19 +2,14 @@ import { test, expect, Page } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { startVSCode, SUSHI_SOURCE_PATH } from './utils' +import { openLineageView, startVSCode, SUSHI_SOURCE_PATH } from './utils' import { writeFileSync } from 'fs' /** * Helper function to launch VS Code and test lineage with given project path config */ async function testLineageWithProjectPath(window: Page): Promise { - // Trigger lineage command - await window.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await window.keyboard.type('Lineage: Focus On View') - await window.keyboard.press('Enter') + await openLineageView(window) // Wait for "Loaded SQLMesh context" text to appear const loadedContextText = window.locator('text=Loaded SQLMesh context') diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index 8150c43c59..7b722c9e2f 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -137,3 +137,25 @@ export const pipInstall = async ( throw new Error(`Failed to install package: ${stderr}`) } } + +/** + * Open the lineage view in the given window. + */ +export const openLineageView = async (window: Page): Promise => { + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('Lineage: Focus On View') + await window.keyboard.press('Enter') +} + +/** + * Restart the SQLMesh servers + */ +export const restartSqlmeshServers = async (window: Page): Promise => { + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('Restart SQLMesh servers') + await window.keyboard.press('Enter') +}