Skip to content

Commit fd05818

Browse files
authored
Implement vscode test controller (#5042)
1 parent cfac2fc commit fd05818

File tree

4 files changed

+73
-46
lines changed

4 files changed

+73
-46
lines changed

vscode/extension/src/extension.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { selector, completionProvider } from './completion/completion'
2121
import { LineagePanel } from './webviews/lineagePanel'
2222
import { RenderedModelProvider } from './providers/renderedModelProvider'
2323
import { sleep } from './utilities/sleep'
24+
import { controller as testController, setupTestController } from './tests/tests'
2425

2526
let lspClient: LSPClient | undefined
2627

@@ -128,6 +129,7 @@ export async function activate(context: vscode.ExtensionContext) {
128129
)
129130
}
130131
context.subscriptions.push(lspClient)
132+
setupTestController(lspClient)
131133
} else {
132134
lspClient = new LSPClient()
133135
const result = await lspClient.start()
@@ -140,6 +142,7 @@ export async function activate(context: vscode.ExtensionContext) {
140142
)
141143
} else {
142144
context.subscriptions.push(lspClient)
145+
setupTestController(lspClient)
143146
}
144147
}
145148
}
@@ -175,6 +178,8 @@ export async function activate(context: vscode.ExtensionContext) {
175178
)
176179
} else {
177180
context.subscriptions.push(lspClient)
181+
setupTestController(lspClient)
182+
context.subscriptions.push(testController)
178183
}
179184

180185
if (lspClient && !lspClient.hasCompletionCapability()) {

vscode/extension/src/lsp/custom.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type CustomLSPMethods =
3232
| AllModelsForRenderMethod
3333
| SupportedMethodsMethod
3434
| FormatProjectMethod
35+
| ListWorkspaceTests
3536

3637
interface AllModelsRequest {
3738
textDocument: {

vscode/extension/src/lsp/lsp.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,8 @@ export class LSPClient implements Disposable {
252252

253253
try {
254254
const result = await this.client.sendRequest<Response>(method, request)
255-
if (result.response_error) {
256-
return err(result.response_error)
255+
if ((result as any).response_error) {
256+
return err((result as any).response_error)
257257
}
258258
return ok(result)
259259
} catch (error) {
Lines changed: 65 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,87 @@
11
import * as vscode from 'vscode'
2+
import path from 'path'
23
import { LSPClient } from '../lsp/lsp'
4+
import { isErr } from '@bus/result'
5+
import { sqlmeshExec } from '../utilities/sqlmesh/sqlmesh'
6+
import { execAsync } from '../utilities/exec'
37

48
export const controller = vscode.tests.createTestController(
59
'sqlmeshTests',
610
'SQLMesh Tests',
711
)
812

913
export const setupTestController = (lsp: LSPClient) => {
10-
// First, create the `resolveHandler`. This may initially be called with
11-
// "undefined" to ask for all tests in the workspace to be discovered, usually
12-
// when the user opens the Test Explorer for the first time.
13-
controller.resolveHandler = async test => {
14-
if (!test) {
15-
await discoverAllFilesInWorkspace()
16-
} else {
17-
await parseTestsInFileContents(test)
18-
}
14+
controller.resolveHandler = async () => {
15+
await discoverWorkspaceTests()
1916
}
2017

21-
// When text documents are open, parse tests in them.
22-
vscode.workspace.onDidOpenTextDocument(parseTestsInDocument)
23-
// We could also listen to document changes to re-parse unsaved changes:
24-
vscode.workspace.onDidChangeTextDocument(e =>
25-
parseTestsInDocument(e.document),
18+
controller.createRunProfile(
19+
'Run',
20+
vscode.TestRunProfileKind.Run,
21+
(request, token) => runTests(request, token),
22+
true,
2623
)
2724

28-
// In this function, we'll get the file TestItem if we've already found it,
29-
// otherwise we'll create it with `canResolveChildren = true` to indicate it
30-
// can be passed to the `controller.resolveHandler` to gets its children.
31-
function getOrCreateFile(uri: vscode.Uri) {
32-
const existing = controller.items.get(uri.toString())
33-
if (existing) {
34-
return existing
25+
async function discoverWorkspaceTests() {
26+
const result = await lsp.call_custom_method('sqlmesh/list_workspace_tests', {})
27+
if (isErr(result)) {
28+
vscode.window.showErrorMessage(`Failed to list SQLMesh tests: ${result.error.message}`)
29+
return
30+
}
31+
controller.items.replace([])
32+
const files = new Map<string, vscode.TestItem>()
33+
for (const entry of result.value.tests) {
34+
const uri = vscode.Uri.parse(entry.uri)
35+
let fileItem = files.get(uri.toString())
36+
if (!fileItem) {
37+
fileItem = controller.createTestItem(uri.toString(), path.basename(uri.fsPath), uri)
38+
fileItem.canResolveChildren = true
39+
files.set(uri.toString(), fileItem)
40+
controller.items.add(fileItem)
41+
}
42+
const testId = `${uri.toString()}::${entry.name}`
43+
const testItem = controller.createTestItem(testId, entry.name, uri)
44+
fileItem.children.add(testItem)
3545
}
36-
37-
const file = controller.createTestItem(
38-
uri.toString(),
39-
uri.path.split('/').pop()!,
40-
uri,
41-
)
42-
file.canResolveChildren = true
43-
return file
4446
}
4547

46-
function parseTestsInDocument(e: vscode.TextDocument) {
47-
if (e.uri.scheme === 'file' && e.uri.path.endsWith('.md')) {
48-
parseTestsInFileContents(getOrCreateFile(e.uri), e.getText())
48+
async function runTests(request: vscode.TestRunRequest, token: vscode.CancellationToken) {
49+
const run = controller.createTestRun(request)
50+
const exec = await sqlmeshExec()
51+
if (isErr(exec)) {
52+
vscode.window.showErrorMessage(`Unable to run tests: ${JSON.stringify(exec.error)}`)
53+
run.end()
54+
return
4955
}
50-
}
5156

52-
async function parseTestsInFileContents(
53-
file: vscode.TestItem,
54-
contents?: string,
55-
) {
56-
// If a document is open, VS Code already knows its contents. If this is being
57-
// called from the resolveHandler when a document isn't open, we'll need to
58-
// read them from disk ourselves.
59-
if (contents === undefined) {
60-
const rawContent = await vscode.workspace.fs.readFile(file.uri)
61-
contents = new TextDecoder().decode(rawContent)
57+
const tests: vscode.TestItem[] = []
58+
const collect = (item: vscode.TestItem) => {
59+
if (item.children.size === 0) tests.push(item)
60+
item.children.forEach(collect)
6261
}
6362

64-
// some custom logic to fill in test.children from the contents...
63+
if (request.include) request.include.forEach(collect)
64+
else controller.items.forEach(collect)
65+
66+
for (const t of tests) run.started(t)
67+
68+
const patterns = tests.map(t => {
69+
const [uriStr, name] = t.id.split('::')
70+
const uri = vscode.Uri.parse(uriStr)
71+
return `${uri.fsPath}::${name}`
72+
})
73+
74+
const result = await execAsync(exec.value.bin, [...exec.value.args, 'test', ...patterns], {
75+
cwd: exec.value.workspacePath,
76+
env: exec.value.env,
77+
signal: token as unknown as AbortSignal,
78+
})
79+
80+
const passed = result.exitCode === 0
81+
for (const t of tests) {
82+
if (passed) run.passed(t)
83+
else run.failed(t, new vscode.TestMessage(result.stderr || 'Failed'))
84+
}
85+
run.end()
6586
}
6687
}

0 commit comments

Comments
 (0)