diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index c6be73e403..bc6f4f7ed1 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -2,15 +2,8 @@ import { test, expect } from './fixtures' import fs from 'fs-extra' import os from 'os' import path from 'path' -import { - openLineageView, - runCommand, - saveFile, - SUSHI_SOURCE_PATH, -} from './utils' +import { openLineageView, saveFile, SUSHI_SOURCE_PATH } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -import { execAsync } from '../src/utilities/exec' -import yaml from 'yaml' test('bad project, double model', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp( @@ -260,181 +253,3 @@ test('bad project, double model, check lineage', async ({ await page.waitForTimeout(500) }) - -const setup = async (tempDir: string) => { - // Run the sqlmesh CLI from the root of the repo using the local path - const sqlmeshCliPath = path.resolve(__dirname, '../../../.venv/bin/sqlmesh') - const result = await execAsync(sqlmeshCliPath, ['init', 'duckdb'], { - cwd: tempDir, - }) - expect(result.exitCode).toBe(0) -} - -test.describe('Bad config.py/config.yaml file issues', () => { - test('sqlmesh init, then corrupted config.yaml, bad yaml', async ({ - page, - sharedCodeServer, - }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) - await setup(tempDir) - await createPythonInterpreterSettingsSpecifier(tempDir) - - const configYamlPath = path.join(tempDir, 'config.yaml') - // Write an invalid YAML to config.yaml - await fs.writeFile(configYamlPath, 'invalid_yaml; asdfasudfy') - - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') - - // Open full_model.sql model - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await page - .getByRole('treeitem', { name: 'full_model.sql', exact: true }) - .locator('a') - .click() - - // Wait for the error to appear - await page.waitForSelector('text=Error creating context') - - // Open the problems view - await runCommand(page, 'View: Focus Problems') - - // Asser that the error is present in the problems view - await page - .getByText('Invalid YAML configuration:') - .first() - .isVisible({ timeout: 5_000 }) - }) - - test('sqlmesh init, then corrupted config.yaml, bad parameters', async ({ - page, - sharedCodeServer, - }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) - await setup(tempDir) - await createPythonInterpreterSettingsSpecifier(tempDir) - - const configYamlPath = path.join(tempDir, 'config.yaml') - // Write an invalid YAML to config.yaml - const config = { - gateway: 'test', - } - // Write config to the yaml file - await fs.writeFile(configYamlPath, yaml.stringify(config)) - - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') - - // Open full_model.sql model - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await page - .getByRole('treeitem', { name: 'full_model.sql', exact: true }) - .locator('a') - .click() - - // Wait for the error to appear - await page.waitForSelector('text=Error creating context') - - // Open the problems view - await runCommand(page, 'View: Focus Problems') - - // Asser that the error is present in the problems view - await page - .getByText('Invalid project config:', { exact: true }) - .first() - .isVisible({ timeout: 5_000 }) - }) - - test('sushi example, correct python, bad config', async ({ - page, - sharedCodeServer, - }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - await createPythonInterpreterSettingsSpecifier(tempDir) - - const configPyPath = path.join(tempDir, 'config.py') - // Write an invalid Python to config.py - await fs.writeFile(configPyPath, 'config = {}') - - await page.goto( - `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') - - // Open customers.sql model - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - // Expect the error to appear - await page.waitForSelector('text=Error creating context') - - // Open the problems view - await runCommand(page, 'View: Focus Problems') - - // Assert that the error is present in the problems view - const errorElement = page - .getByText('Config needs to be a valid object of type') - .first() - await expect(errorElement).toBeVisible({ timeout: 5000 }) - }) - - test('sushi example, bad config.py', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - await createPythonInterpreterSettingsSpecifier(tempDir) - - const configPyPath = path.join(tempDir, 'config.py') - // Write an invalid Python to config.py - await fs.writeFile(configPyPath, 'invalid_python_code = [1, 2, 3') - - await page.goto( - `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') - - // Open customers.sql model - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - // Expect the error to appear - await page.waitForSelector('text=Error creating context') - - // Open the problems view - await runCommand(page, 'View: Focus Problems') - - // Assert that the error is present in the problems view - const errorElement = page.getByText('Failed to load config file:').first() - await expect(errorElement).toBeVisible({ timeout: 5000 }) - }) -}) diff --git a/vscode/extension/tests/diagnostics.spec.ts b/vscode/extension/tests/diagnostics.spec.ts index 3c3aa4d0e0..be6511a603 100644 --- a/vscode/extension/tests/diagnostics.spec.ts +++ b/vscode/extension/tests/diagnostics.spec.ts @@ -1,9 +1,11 @@ -import { test } from './fixtures' +import { expect, test } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' import { runCommand, SUSHI_SOURCE_PATH } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' +import { execAsync } from '../src/utilities/exec' +import yaml from 'yaml' test('Workspace diagnostics show up in the diagnostics panel', async ({ page, @@ -43,3 +45,270 @@ test('Workspace diagnostics show up in the diagnostics panel', async ({ await page.waitForSelector('text=problems') await page.waitForSelector('text=All models should have an owner') }) + +test.describe('Bad config.py/config.yaml file issues', () => { + const setup = async (tempDir: string) => { + // Run the sqlmesh CLI from the root of the repo using the local path + const sqlmeshCliPath = path.resolve(__dirname, '../../../.venv/bin/sqlmesh') + const result = await execAsync(sqlmeshCliPath, ['init', 'duckdb'], { + cwd: tempDir, + }) + expect(result.exitCode).toBe(0) + } + + test('sqlmesh init, then corrupted config.yaml, bad yaml', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await setup(tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + const configYamlPath = path.join(tempDir, 'config.yaml') + // Write an invalid YAML to config.yaml + await fs.writeFile(configYamlPath, 'invalid_yaml; asdfasudfy') + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open full_model.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'full_model.sql', exact: true }) + .locator('a') + .click() + + // Wait for the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Asser that the error is present in the problems view + await page + .getByText('Invalid YAML configuration:') + .first() + .isVisible({ timeout: 5_000 }) + }) + + test('sqlmesh init, then corrupted config.yaml, bad parameters', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await setup(tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + const configYamlPath = path.join(tempDir, 'config.yaml') + // Write an invalid YAML to config.yaml + const config = { + gateway: 'test', + } + // Write config to the yaml file + await fs.writeFile(configYamlPath, yaml.stringify(config)) + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open full_model.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'full_model.sql', exact: true }) + .locator('a') + .click() + + // Wait for the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Asser that the error is present in the problems view + await page + .getByText('Invalid project config:', { exact: true }) + .first() + .isVisible({ timeout: 5_000 }) + }) + + test('sushi example, correct python, bad config', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + const configPyPath = path.join(tempDir, 'config.py') + // Write an invalid Python to config.py + await fs.writeFile(configPyPath, 'config = {}') + + await page.goto( + `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open customers.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Expect the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Assert that the error is present in the problems view + const errorElement = page + .getByText('Config needs to be a valid object of type') + .first() + await expect(errorElement).toBeVisible({ timeout: 5000 }) + }) + + test('sushi example, bad config.py', async ({ page, sharedCodeServer }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + const configPyPath = path.join(tempDir, 'config.py') + // Write an invalid Python to config.py + await fs.writeFile(configPyPath, 'invalid_python_code = [1, 2, 3') + + await page.goto( + `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open customers.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Expect the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Assert that the error is present in the problems view + const errorElement = page.getByText('Failed to load config file:').first() + await expect(errorElement).toBeVisible({ timeout: 5000 }) + }) +}) + +test.describe('Diagnostics for bad SQLMesh models', () => { + test('duplicate model names', async ({ page, sharedCodeServer }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + // Copy over the sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + // Duplicate the customers.sql model + const customersSqlPath = path.join(tempDir, 'models', 'customers.sql') + const duplicatedCustomersSqlPath = path.join( + tempDir, + 'models', + 'customers_duplicated.sql', + ) + await fs.copy(customersSqlPath, duplicatedCustomersSqlPath) + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open full_model.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Wait for the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Asser that the error is present in the problems view + await page + .getByText('Duplicate SQLMesh model name') + .first() + .isVisible({ timeout: 5_000 }) + }) + + test('bad model block', async ({ page, sharedCodeServer }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + // Copy over the sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + // Add a model with a bad model block + const customersSqlPath = path.join(tempDir, 'models', 'bad_model.sql') + const contents = + 'MODEL ( name sushi.bad_block, test); SELECT * FROM sushi.customers' + await fs.writeFile(customersSqlPath, contents) + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open the customers.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Wait for the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Assert error is present in the problems view + const errorElement = page + .getByText("Required keyword: 'value' missing for") + .first() + await expect(errorElement).toBeVisible({ timeout: 5000 }) + }) +})