From b79c31e1c3cb7406181eafa7659b8c1b6d937300 Mon Sep 17 00:00:00 2001 From: sezna Date: Tue, 10 Dec 2024 06:04:19 -0800 Subject: [PATCH 01/82] initial work on testing --- vscode/src/testExplorer.ts | 240 +++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 vscode/src/testExplorer.ts diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts new file mode 100644 index 0000000000..0a0f12933b --- /dev/null +++ b/vscode/src/testExplorer.ts @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + + +import * as vscode from 'vscode'; + +export async function initTestExplorer(context: vscode.ExtensionContext) { + const ctrl = vscode.tests.createTestController('mathTestController', 'Markdown Math'); + context.subscriptions.push(ctrl); + + const fileChangedEmitter = new vscode.EventEmitter(); + const watchingTests = new Map(); + fileChangedEmitter.event(uri => { + if (watchingTests.has('ALL')) { + startTestRun(new vscode.TestRunRequest(undefined, undefined, watchingTests.get('ALL'), true)); + return; + } + + const include: vscode.TestItem[] = []; + let profile: vscode.TestRunProfile | undefined; + for (const [item, thisProfile] of watchingTests) { + const cast = item as vscode.TestItem; + if (cast.uri?.toString() == uri.toString()) { + include.push(cast); + profile = thisProfile; + } + } + + if (include.length) { + startTestRun(new vscode.TestRunRequest(include, undefined, profile, true)); + } + }); + + const runHandler = (request: vscode.TestRunRequest, cancellation: vscode.CancellationToken) => { + if (!request.continuous) { + return startTestRun(request); + } + + if (request.include === undefined) { + watchingTests.set('ALL', request.profile); + cancellation.onCancellationRequested(() => watchingTests.delete('ALL')); + } else { + request.include.forEach(item => watchingTests.set(item, request.profile)); + cancellation.onCancellationRequested(() => request.include!.forEach(item => watchingTests.delete(item))); + } + }; + + const startTestRun = (request: vscode.TestRunRequest) => { + const queue: { test: vscode.TestItem; data: TestCase }[] = []; + const run = ctrl.createTestRun(request); + // map of file uris to statements on each line: + const coveredLines = new Map(); + + const discoverTests = async (tests: Iterable) => { + for (const test of tests) { + if (request.exclude?.includes(test)) { + continue; + } + + const data = testData.get(test); + if (data instanceof TestCase) { + run.enqueued(test); + queue.push({ test, data }); + } else { + if (data instanceof TestFile && !data.didResolve) { + await data.updateFromDisk(ctrl, test); + } + + await discoverTests(gatherTestItems(test.children)); + } + + if (test.uri && !coveredLines.has(test.uri.toString()) && request.profile?.kind === vscode.TestRunProfileKind.Coverage) { + try { + const lines = (await getContentFromFilesystem(test.uri)).split('\n'); + coveredLines.set( + test.uri.toString(), + lines.map((lineText, lineNo) => + lineText.trim().length ? new vscode.StatementCoverage(0, new vscode.Position(lineNo, 0)) : undefined + ) + ); + } catch { + // ignored + } + } + } + }; + + const runTestQueue = async () => { + for (const { test, data } of queue) { + run.appendOutput(`Running ${test.id}\r\n`); + if (run.token.isCancellationRequested) { + run.skipped(test); + } else { + run.started(test); + await data.run(test, run); + } + + const lineNo = test.range!.start.line; + const fileCoverage = coveredLines.get(test.uri!.toString()); + const lineInfo = fileCoverage?.[lineNo]; + if (lineInfo) { + (lineInfo.executed as number)++; + } + + run.appendOutput(`Completed ${test.id}\r\n`); + } + + for (const [uri, statements] of coveredLines) { + run.addCoverage(new MarkdownFileCoverage(uri, statements)); + } + + run.end(); + }; + + discoverTests(request.include ?? gatherTestItems(ctrl.items)).then(runTestQueue); + }; + + ctrl.refreshHandler = async () => { + await Promise.all(getWorkspaceTestPatterns().map(({ pattern }) => findInitialFiles(ctrl, pattern))); + }; + + ctrl.createRunProfile('Run Tests', vscode.TestRunProfileKind.Run, runHandler, true, undefined, true); + + const coverageProfile = ctrl.createRunProfile('Run with Coverage', vscode.TestRunProfileKind.Coverage, runHandler, true, undefined, true); + coverageProfile.loadDetailedCoverage = async (_testRun, coverage) => { + if (coverage instanceof MarkdownFileCoverage) { + return coverage.coveredLines.filter((l): l is vscode.StatementCoverage => !!l); + } + + return []; + }; + + ctrl.resolveHandler = async item => { + if (!item) { + context.subscriptions.push(...startWatchingWorkspace(ctrl, fileChangedEmitter)); + return; + } + + const data = testData.get(item); + if (data instanceof TestFile) { + await data.updateFromDisk(ctrl, item); + } + }; + + function updateNodeForDocument(e: vscode.TextDocument) { + if (e.uri.scheme !== 'file') { + return; + } + + if (!e.uri.path.endsWith('.md')) { + return; + } + + const { file, data } = getOrCreateFile(ctrl, e.uri); + data.updateFromContents(ctrl, e.getText(), file); + } + + for (const document of vscode.workspace.textDocuments) { + updateNodeForDocument(document); + } + + context.subscriptions.push( + vscode.workspace.onDidOpenTextDocument(updateNodeForDocument), + vscode.workspace.onDidChangeTextDocument(e => updateNodeForDocument(e.document)), + ); +} + +function getOrCreateFile(controller: vscode.TestController, uri: vscode.Uri) { + const existing = controller.items.get(uri.toString()); + if (existing) { + return { file: existing, data: testData.get(existing) as TestFile }; + } + + const file = controller.createTestItem(uri.toString(), uri.path.split('/').pop()!, uri); + controller.items.add(file); + + const data = new TestFile(); + testData.set(file, data); + + file.canResolveChildren = true; + return { file, data }; +} + +function gatherTestItems(collection: vscode.TestItemCollection) { + const items: vscode.TestItem[] = []; + collection.forEach(item => items.push(item)); + return items; +} + +function getWorkspaceTestPatterns() { + if (!vscode.workspace.workspaceFolders) { + return []; + } + + return vscode.workspace.workspaceFolders.map(workspaceFolder => ({ + workspaceFolder, + pattern: new vscode.RelativePattern(workspaceFolder, '**/*.md'), + })); +} + +async function findInitialFiles(controller: vscode.TestController, pattern: vscode.GlobPattern) { + for (const file of await vscode.workspace.findFiles(pattern)) { + getOrCreateFile(controller, file); + } +} + +function startWatchingWorkspace(controller: vscode.TestController, fileChangedEmitter: vscode.EventEmitter) { + return getWorkspaceTestPatterns().map(({ pattern }) => { + const watcher = vscode.workspace.createFileSystemWatcher(pattern); + + watcher.onDidCreate(uri => { + getOrCreateFile(controller, uri); + fileChangedEmitter.fire(uri); + }); + watcher.onDidChange(async uri => { + const { file, data } = getOrCreateFile(controller, uri); + if (data.didResolve) { + await data.updateFromDisk(controller, file); + } + fileChangedEmitter.fire(uri); + }); + watcher.onDidDelete(uri => controller.items.delete(uri.toString())); + + findInitialFiles(controller, pattern); + + return watcher; + }); +} + +class MarkdownFileCoverage extends vscode.FileCoverage { + constructor(uri: string, public readonly coveredLines: (vscode.StatementCoverage | undefined)[]) { + super(vscode.Uri.parse(uri), new vscode.TestCoverageCount(0, 0)); + for (const line of coveredLines) { + if (line) { + this.statementCoverage.covered += line.executed ? 1 : 0; + this.statementCoverage.total++; + } + } + } +} \ No newline at end of file From 54eb2097daf73c74c7ed2ab62433c75c3fb45c71 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 10 Dec 2024 11:33:17 -0800 Subject: [PATCH 02/82] progress on test explorer --- vscode/src/extension.ts | 2 + vscode/src/testExplorer.ts | 172 +++++++++++-------------------------- 2 files changed, 52 insertions(+), 122 deletions(-) diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 750700f373..adec34f8bc 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -28,6 +28,7 @@ import { initCodegen } from "./qirGeneration.js"; import { activateTargetProfileStatusBarItem } from "./statusbar.js"; import { initTelemetry } from "./telemetry.js"; import { registerWebViewCommands } from "./webviewPanel.js"; +import { initTestExplorer } from "./testExplorer.js"; export async function activate( context: vscode.ExtensionContext, @@ -75,6 +76,7 @@ export async function activate( context.subscriptions.push(...registerQSharpNotebookHandlers()); + initTestExplorer(context); initAzureWorkspaces(context); initCodegen(context); activateDebugger(context); diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 0a0f12933b..126a0e950a 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -3,10 +3,48 @@ import * as vscode from 'vscode'; +import { loadProject } from './projectSystem'; +import { IProjectConfig, log } from "qsharp-lang"; +import { getActiveQSharpDocumentUri } from './programConfig'; export async function initTestExplorer(context: vscode.ExtensionContext) { - const ctrl = vscode.tests.createTestController('mathTestController', 'Markdown Math'); + const ctrl: vscode.TestController = vscode.tests.createTestController('qsharpTestController', 'Q# Tests'); context.subscriptions.push(ctrl); + const item = ctrl.createTestItem("Q# test","test fn"); + ctrl.items.add(item); + + + ctrl.refreshHandler = async () => { + log.info("1"); + if (!vscode.workspace.workspaceFolders) { + log.info("No workspace detected; not starting test explorer") + return; + } + + log.info("2"); + const docUri = getActiveQSharpDocumentUri(); + if (!docUri) { + log.info("No active document detected; not starting test explorer") + return; + } + + + const projectConfig: IProjectConfig = await loadProject(docUri); + if (!projectConfig) { + log.info("No project detected; not starting test explorer") + return; + } + log.info("3"); + + const sources = projectConfig.packageGraphSources.root.sources; + for (const [sourceUrl, sourceContent] of sources) { + const testItem = ctrl.createTestItem(sourceUrl, sourceUrl); + ctrl.items.add(testItem); + } + log.info("4"); + + + }; const fileChangedEmitter = new vscode.EventEmitter(); const watchingTests = new Map(); @@ -46,100 +84,21 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { }; const startTestRun = (request: vscode.TestRunRequest) => { - const queue: { test: vscode.TestItem; data: TestCase }[] = []; + const queue: { test: vscode.TestItem; data: string }[] = []; const run = ctrl.createTestRun(request); // map of file uris to statements on each line: - const coveredLines = new Map(); - - const discoverTests = async (tests: Iterable) => { - for (const test of tests) { - if (request.exclude?.includes(test)) { - continue; - } - - const data = testData.get(test); - if (data instanceof TestCase) { - run.enqueued(test); - queue.push({ test, data }); - } else { - if (data instanceof TestFile && !data.didResolve) { - await data.updateFromDisk(ctrl, test); - } - - await discoverTests(gatherTestItems(test.children)); - } - - if (test.uri && !coveredLines.has(test.uri.toString()) && request.profile?.kind === vscode.TestRunProfileKind.Coverage) { - try { - const lines = (await getContentFromFilesystem(test.uri)).split('\n'); - coveredLines.set( - test.uri.toString(), - lines.map((lineText, lineNo) => - lineText.trim().length ? new vscode.StatementCoverage(0, new vscode.Position(lineNo, 0)) : undefined - ) - ); - } catch { - // ignored - } - } - } - }; - - const runTestQueue = async () => { - for (const { test, data } of queue) { - run.appendOutput(`Running ${test.id}\r\n`); - if (run.token.isCancellationRequested) { - run.skipped(test); - } else { - run.started(test); - await data.run(test, run); - } - - const lineNo = test.range!.start.line; - const fileCoverage = coveredLines.get(test.uri!.toString()); - const lineInfo = fileCoverage?.[lineNo]; - if (lineInfo) { - (lineInfo.executed as number)++; - } - - run.appendOutput(`Completed ${test.id}\r\n`); - } - - for (const [uri, statements] of coveredLines) { - run.addCoverage(new MarkdownFileCoverage(uri, statements)); - } - - run.end(); - }; - - discoverTests(request.include ?? gatherTestItems(ctrl.items)).then(runTestQueue); - }; - - ctrl.refreshHandler = async () => { - await Promise.all(getWorkspaceTestPatterns().map(({ pattern }) => findInitialFiles(ctrl, pattern))); + // const coveredLines = new Map(); + // run the test TODO + vscode.window.showInformationMessage('Running tests...'); }; - ctrl.createRunProfile('Run Tests', vscode.TestRunProfileKind.Run, runHandler, true, undefined, true); - const coverageProfile = ctrl.createRunProfile('Run with Coverage', vscode.TestRunProfileKind.Coverage, runHandler, true, undefined, true); - coverageProfile.loadDetailedCoverage = async (_testRun, coverage) => { - if (coverage instanceof MarkdownFileCoverage) { - return coverage.coveredLines.filter((l): l is vscode.StatementCoverage => !!l); - } - - return []; - }; - ctrl.resolveHandler = async item => { if (!item) { context.subscriptions.push(...startWatchingWorkspace(ctrl, fileChangedEmitter)); return; } - const data = testData.get(item); - if (data instanceof TestFile) { - await data.updateFromDisk(ctrl, item); - } }; function updateNodeForDocument(e: vscode.TextDocument) { @@ -147,12 +106,12 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { return; } - if (!e.uri.path.endsWith('.md')) { + if (!e.uri.path.endsWith('.qs')) { return; } - const { file, data } = getOrCreateFile(ctrl, e.uri); - data.updateFromContents(ctrl, e.getText(), file); + // const { file, data } = getOrCreateFile(ctrl, e.uri); + // data.updateFromContents(ctrl, e.getText(), file); } for (const document of vscode.workspace.textDocuments) { @@ -165,21 +124,6 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { ); } -function getOrCreateFile(controller: vscode.TestController, uri: vscode.Uri) { - const existing = controller.items.get(uri.toString()); - if (existing) { - return { file: existing, data: testData.get(existing) as TestFile }; - } - - const file = controller.createTestItem(uri.toString(), uri.path.split('/').pop()!, uri); - controller.items.add(file); - - const data = new TestFile(); - testData.set(file, data); - - file.canResolveChildren = true; - return { file, data }; -} function gatherTestItems(collection: vscode.TestItemCollection) { const items: vscode.TestItem[] = []; @@ -194,20 +138,15 @@ function getWorkspaceTestPatterns() { return vscode.workspace.workspaceFolders.map(workspaceFolder => ({ workspaceFolder, - pattern: new vscode.RelativePattern(workspaceFolder, '**/*.md'), + pattern: new vscode.RelativePattern(workspaceFolder, '**/*.qs'), })); } -async function findInitialFiles(controller: vscode.TestController, pattern: vscode.GlobPattern) { - for (const file of await vscode.workspace.findFiles(pattern)) { - getOrCreateFile(controller, file); - } -} function startWatchingWorkspace(controller: vscode.TestController, fileChangedEmitter: vscode.EventEmitter) { return getWorkspaceTestPatterns().map(({ pattern }) => { const watcher = vscode.workspace.createFileSystemWatcher(pattern); - +/* watcher.onDidCreate(uri => { getOrCreateFile(controller, uri); fileChangedEmitter.fire(uri); @@ -219,22 +158,11 @@ function startWatchingWorkspace(controller: vscode.TestController, fileChangedEm } fileChangedEmitter.fire(uri); }); + */ watcher.onDidDelete(uri => controller.items.delete(uri.toString())); - findInitialFiles(controller, pattern); + // findInitialFiles(controller, pattern); return watcher; }); -} - -class MarkdownFileCoverage extends vscode.FileCoverage { - constructor(uri: string, public readonly coveredLines: (vscode.StatementCoverage | undefined)[]) { - super(vscode.Uri.parse(uri), new vscode.TestCoverageCount(0, 0)); - for (const line of coveredLines) { - if (line) { - this.statementCoverage.covered += line.executed ? 1 : 0; - this.statementCoverage.total++; - } - } - } } \ No newline at end of file From e6a0941b6920d92c821d299d817e2767dd98b8b2 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 10 Dec 2024 13:27:26 -0800 Subject: [PATCH 03/82] test collection works --- compiler/qsc_frontend/src/lower.rs | 1 + compiler/qsc_hir/src/hir.rs | 4 ++ compiler/qsc_lowerer/src/lib.rs | 2 +- compiler/qsc_parse/src/item/tests.rs | 17 +++++++ language_service/src/completion.rs | 1 + npm/qsharp/src/compiler/compiler.ts | 10 +++++ npm/qsharp/src/main.ts | 7 ++- vscode/src/testExplorer.ts | 67 ++++++++++++++++------------ wasm/src/lib.rs | 3 ++ wasm/src/test_explorer.rs | 54 ++++++++++++++++++++++ 10 files changed, 136 insertions(+), 30 deletions(-) create mode 100644 wasm/src/test_explorer.rs diff --git a/compiler/qsc_frontend/src/lower.rs b/compiler/qsc_frontend/src/lower.rs index c349b203bb..4df5d49526 100644 --- a/compiler/qsc_frontend/src/lower.rs +++ b/compiler/qsc_frontend/src/lower.rs @@ -443,6 +443,7 @@ impl With<'_> { None } }, + Ok(hir::Attr::Test) => Some(hir::Attr::Test), Err(()) => { self.lowerer.errors.push(Error::UnknownAttr( attr.name.name.to_string(), diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs index 5c22bdd0fe..664511a3f4 100644 --- a/compiler/qsc_hir/src/hir.rs +++ b/compiler/qsc_hir/src/hir.rs @@ -1359,6 +1359,8 @@ pub enum Attr { /// Indicates that an intrinsic callable is a reset. This means that the operation will be marked as /// "irreversible" in the generated QIR. Reset, + /// Indicates that a callable is a test case. + Test, } impl Attr { @@ -1376,6 +1378,7 @@ The `not` operator is also supported to negate the attribute, e.g. `not Adaptive Attr::SimulatableIntrinsic => "Indicates that an item should be treated as an intrinsic callable for QIR code generation and any implementation should only be used during simulation.", Attr::Measurement => "Indicates that an intrinsic callable is a measurement. This means that the operation will be marked as \"irreversible\" in the generated QIR, and output Result types will be moved to the arguments.", Attr::Reset => "Indicates that an intrinsic callable is a reset. This means that the operation will be marked as \"irreversible\" in the generated QIR.", + Attr::Test => "Indicates that a callable is a test case.", } } } @@ -1391,6 +1394,7 @@ impl FromStr for Attr { "SimulatableIntrinsic" => Ok(Self::SimulatableIntrinsic), "Measurement" => Ok(Self::Measurement), "Reset" => Ok(Self::Reset), + "Test" => Ok(Self::Test), _ => Err(()), } } diff --git a/compiler/qsc_lowerer/src/lib.rs b/compiler/qsc_lowerer/src/lib.rs index 3fb0084127..658cfe405f 100644 --- a/compiler/qsc_lowerer/src/lib.rs +++ b/compiler/qsc_lowerer/src/lib.rs @@ -943,7 +943,7 @@ fn lower_attrs(attrs: &[hir::Attr]) -> Vec { hir::Attr::EntryPoint => Some(fir::Attr::EntryPoint), hir::Attr::Measurement => Some(fir::Attr::Measurement), hir::Attr::Reset => Some(fir::Attr::Reset), - hir::Attr::SimulatableIntrinsic | hir::Attr::Unimplemented | hir::Attr::Config => None, + hir::Attr::SimulatableIntrinsic | hir::Attr::Unimplemented | hir::Attr::Config | hir::Attr::Test => None, }) .collect() } diff --git a/compiler/qsc_parse/src/item/tests.rs b/compiler/qsc_parse/src/item/tests.rs index e94d7168c8..a408e6fb43 100644 --- a/compiler/qsc_parse/src/item/tests.rs +++ b/compiler/qsc_parse/src/item/tests.rs @@ -2396,3 +2396,20 @@ fn top_level_nodes_error_recovery() { ]"#]], ); } + +#[test] +fn test_attribute() { + check( + parse, + "@Test() function Foo() : Unit {}", + &expect![[r#" + Item _id_ [0-32]: + Attr _id_ [0-7] (Ident _id_ [1-5] "Test"): + Expr _id_ [5-7]: Unit + Callable _id_ [8-32] (Function): + name: Ident _id_ [17-20] "Foo" + input: Pat _id_ [20-22]: Unit + output: Type _id_ [25-29]: Path: Path _id_ [25-29] (Ident _id_ [25-29] "Unit") + body: Block: Block _id_ [30-32]: "#]], + ); +} \ No newline at end of file diff --git a/language_service/src/completion.rs b/language_service/src/completion.rs index 74ad3500e2..45f92b07d2 100644 --- a/language_service/src/completion.rs +++ b/language_service/src/completion.rs @@ -165,6 +165,7 @@ fn collect_hardcoded_words(expected: WordKinds) -> Vec { ), Completion::new("Measurement".to_string(), CompletionItemKind::Interface), Completion::new("Reset".to_string(), CompletionItemKind::Interface), + Completion::new("Test".to_string(), CompletionItemKind::Interface), ]); } HardcodedIdentKind::Size => { diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts index 67345326c3..a0daa28e57 100644 --- a/npm/qsharp/src/compiler/compiler.ts +++ b/npm/qsharp/src/compiler/compiler.ts @@ -9,6 +9,7 @@ import { IProgramConfig as wasmIProgramConfig, TargetProfile, type VSDiagnostic, + IProgramConfig, } from "../../lib/web/qsc_wasm.js"; import { log } from "../log.js"; import { @@ -77,6 +78,10 @@ export interface ICompiler { exerciseSources: string[], eventHandler: IQscEventTarget, ): Promise; + + collectTestCallables( + program: IProgramConfig, + ): Promise; } /** @@ -243,6 +248,10 @@ export class Compiler implements ICompiler { return success; } + + async collectTestCallables(program: IProgramConfig): Promise { + return this.wasm.collect_test_callables(program); + } } /** @@ -326,6 +335,7 @@ export const compilerProtocol: ServiceProtocol = { run: "requestWithProgress", runWithPauliNoise: "requestWithProgress", checkExerciseSolution: "requestWithProgress", + collectTestCallables: "request", }, eventNames: ["DumpMachine", "Matrix", "Message", "Result"], }; diff --git a/npm/qsharp/src/main.ts b/npm/qsharp/src/main.ts index 647c1b6f9c..1aef7bfb7b 100644 --- a/npm/qsharp/src/main.ts +++ b/npm/qsharp/src/main.ts @@ -26,7 +26,7 @@ import { } from "./language-service/language-service.js"; import { log } from "./log.js"; import { createProxy } from "./workers/node.js"; -import type { ProjectLoader } from "../lib/web/qsc_wasm.js"; +import type { IProgramConfig, ProjectLoader } from "../lib/web/qsc_wasm.js"; import { IProjectHost } from "./browser.js"; export { qsharpLibraryUriScheme }; @@ -91,4 +91,9 @@ export function getLanguageServiceWorker(): ILanguageServiceWorker { ); } +export function collectTestCallables(config: IProgramConfig): string[] { + ensureWasm(); + return wasm!.collect_test_callables(config); +} + export * as utils from "./utils.js"; diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 126a0e950a..798df4d801 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -4,46 +4,57 @@ import * as vscode from 'vscode'; import { loadProject } from './projectSystem'; -import { IProjectConfig, log } from "qsharp-lang"; +import { getCompilerWorker, IProjectConfig, log } from "qsharp-lang"; import { getActiveQSharpDocumentUri } from './programConfig'; +import { IProgramConfig } from '../../npm/qsharp/lib/web/qsc_wasm'; +import { getTarget } from './config'; + +// TODO(sezna) testrunprofile, running tests export async function initTestExplorer(context: vscode.ExtensionContext) { const ctrl: vscode.TestController = vscode.tests.createTestController('qsharpTestController', 'Q# Tests'); context.subscriptions.push(ctrl); - const item = ctrl.createTestItem("Q# test","test fn"); - ctrl.items.add(item); ctrl.refreshHandler = async () => { - log.info("1"); if (!vscode.workspace.workspaceFolders) { log.info("No workspace detected; not starting test explorer") return; - } + } - log.info("2"); - const docUri = getActiveQSharpDocumentUri(); + const docUri = getActiveQSharpDocumentUri(); if (!docUri) { log.info("No active document detected; not starting test explorer") return; } - const projectConfig: IProjectConfig = await loadProject(docUri); if (!projectConfig) { log.info("No project detected; not starting test explorer") return; } - log.info("3"); - const sources = projectConfig.packageGraphSources.root.sources; - for (const [sourceUrl, sourceContent] of sources) { - const testItem = ctrl.createTestItem(sourceUrl, sourceUrl); + let programConfig: IProgramConfig = { + profile: getTarget(), + ...projectConfig + }; + const compilerWorkerScriptPath = vscode.Uri.joinPath( + context.extensionUri, + "./out/compilerWorker.js", + ).toString(); + const worker = getCompilerWorker(compilerWorkerScriptPath); + + const testCallables = await worker.collectTestCallables(programConfig); + + + testCallables.forEach((testCallable) => { + const testItem = ctrl.createTestItem( + testCallable, testCallable); ctrl.items.add(testItem); - } - log.info("4"); + }); + + - }; const fileChangedEmitter = new vscode.EventEmitter(); @@ -146,19 +157,19 @@ function getWorkspaceTestPatterns() { function startWatchingWorkspace(controller: vscode.TestController, fileChangedEmitter: vscode.EventEmitter) { return getWorkspaceTestPatterns().map(({ pattern }) => { const watcher = vscode.workspace.createFileSystemWatcher(pattern); -/* - watcher.onDidCreate(uri => { - getOrCreateFile(controller, uri); - fileChangedEmitter.fire(uri); - }); - watcher.onDidChange(async uri => { - const { file, data } = getOrCreateFile(controller, uri); - if (data.didResolve) { - await data.updateFromDisk(controller, file); - } - fileChangedEmitter.fire(uri); - }); - */ + /* + watcher.onDidCreate(uri => { + getOrCreateFile(controller, uri); + fileChangedEmitter.fire(uri); + }); + watcher.onDidChange(async uri => { + const { file, data } = getOrCreateFile(controller, uri); + if (data.didResolve) { + await data.updateFromDisk(controller, file); + } + fileChangedEmitter.fire(uri); + }); + */ watcher.onDidDelete(uri => controller.items.delete(uri.toString())); // findInitialFiles(controller, pattern); diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index ad09a451e2..8b60eba9e7 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -36,6 +36,9 @@ mod line_column; mod logging; mod project_system; mod serializable_type; +mod test_explorer; + +pub use test_explorer::collect_test_callables; #[cfg(test)] mod tests; diff --git a/wasm/src/test_explorer.rs b/wasm/src/test_explorer.rs new file mode 100644 index 0000000000..deaba6d415 --- /dev/null +++ b/wasm/src/test_explorer.rs @@ -0,0 +1,54 @@ +use qsc::{compile, hir::{Attr, PatKind}, PackageType}; +use wasm_bindgen::prelude::wasm_bindgen; + +use crate::{project_system::{into_qsc_args, ProgramConfig}, STORE_CORE_STD}; + +#[wasm_bindgen] +pub fn collect_test_callables(config: ProgramConfig) -> Result, String>{ + let (source_map, capabilities, language_features, _store, _deps) = + into_qsc_args(config, None).map_err(super::compile_errors_into_qsharp_errors_json)?; + + let package = STORE_CORE_STD.with(|(store, std)| { + let (unit, _) = compile::compile( + store, + &[(*std, None)], + source_map, + PackageType::Lib, + capabilities, + language_features, + ); + unit.package + }); + + + let items_with_test_attribute = package.items.iter().filter(|(_, item)| { + { + item.attrs.iter().any(|attr| *attr == Attr::Test) + } + }); + + + let (callables, others): (Vec<_>, Vec<_>) = items_with_test_attribute.partition(|(_, item)| { + matches!(item.kind, qsc::hir::ItemKind::Callable(_)) + }); + + if !others.is_empty() { + todo!("Return pretty error for non-callable with test attribute") + } + + let callable_names = callables.iter().filter_map(|(_, item)| { + if let qsc::hir::ItemKind::Callable(callable) = &item.kind { + if !callable.generics.is_empty() { + todo!("Return pretty error for generic callable with test attribute") + } + if callable.input.kind != PatKind::Tuple(vec![]) { + todo!("Return pretty error for callable with input") + } + Some(callable.name.name.to_string()) + } else { + None + } + }).collect(); + + Ok(callable_names) +} \ No newline at end of file From 0495a6dbc679f0884e46e43ed503667a4a6e3723 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 10 Dec 2024 14:43:55 -0800 Subject: [PATCH 04/82] tests run --- vscode/src/common.ts | 8 +- vscode/src/debugger/session.ts | 4 +- vscode/src/language-service/codeLens.ts | 4 +- vscode/src/language-service/completion.ts | 4 +- vscode/src/language-service/format.ts | 4 +- vscode/src/language-service/hover.ts | 4 +- vscode/src/language-service/rename.ts | 4 +- vscode/src/testExplorer.ts | 190 +++++++++++++++------- 8 files changed, 146 insertions(+), 76 deletions(-) diff --git a/vscode/src/common.ts b/vscode/src/common.ts index 71de7e08b6..9b980fda00 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -32,7 +32,7 @@ export function basename(path: string): string | undefined { return path.replace(/\/+$/, "").split("/").pop(); } -export function toVscodeRange(range: IRange): Range { +export function toVsCodeRange(range: IRange): Range { return new Range( range.start.line, range.start.character, @@ -42,7 +42,7 @@ export function toVscodeRange(range: IRange): Range { } export function toVscodeLocation(location: ILocation): any { - return new Location(Uri.parse(location.source), toVscodeRange(location.span)); + return new Location(Uri.parse(location.source), toVsCodeRange(location.span)); } export function toVscodeWorkspaceEdit( @@ -52,7 +52,7 @@ export function toVscodeWorkspaceEdit( for (const [source, edits] of iWorkspaceEdit.changes) { const uri = vscode.Uri.parse(source, true); const vsEdits = edits.map((edit) => { - return new vscode.TextEdit(toVscodeRange(edit.range), edit.newText); + return new vscode.TextEdit(toVsCodeRange(edit.range), edit.newText); }); workspaceEdit.set(uri, vsEdits); } @@ -73,7 +73,7 @@ export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { break; } const vscodeDiagnostic = new vscode.Diagnostic( - toVscodeRange(d.range), + toVsCodeRange(d.range), d.message, severity, ); diff --git a/vscode/src/debugger/session.ts b/vscode/src/debugger/session.ts index 0db074e737..9108ca43fc 100644 --- a/vscode/src/debugger/session.ts +++ b/vscode/src/debugger/session.ts @@ -30,7 +30,7 @@ import { log, } from "qsharp-lang"; import { updateCircuitPanel } from "../circuit"; -import { basename, isQsharpDocument, toVscodeRange } from "../common"; +import { basename, isQsharpDocument, toVsCodeRange } from "../common"; import { DebugEvent, EventType, @@ -134,7 +134,7 @@ export class QscDebugSession extends LoggingDebugSession { ), }; return { - range: toVscodeRange(location.range), + range: toVsCodeRange(location.range), uiLocation, breakpoint: this.createBreakpoint(location.id, uiLocation), } as IBreakpointLocationData; diff --git a/vscode/src/language-service/codeLens.ts b/vscode/src/language-service/codeLens.ts index 98672811cb..f5d952237b 100644 --- a/vscode/src/language-service/codeLens.ts +++ b/vscode/src/language-service/codeLens.ts @@ -7,7 +7,7 @@ import { qsharpLibraryUriScheme, } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange } from "../common"; +import { toVsCodeRange } from "../common"; export function createCodeLensProvider(languageService: ILanguageService) { return new QSharpCodeLensProvider(languageService); @@ -71,7 +71,7 @@ function mapCodeLens(cl: ICodeLens): vscode.CodeLens { break; } - return new vscode.CodeLens(toVscodeRange(cl.range), { + return new vscode.CodeLens(toVsCodeRange(cl.range), { title, command, arguments: args, diff --git a/vscode/src/language-service/completion.ts b/vscode/src/language-service/completion.ts index 92f2fc8bc8..444a46cdfd 100644 --- a/vscode/src/language-service/completion.ts +++ b/vscode/src/language-service/completion.ts @@ -4,7 +4,7 @@ import { ILanguageService, samples } from "qsharp-lang"; import * as vscode from "vscode"; import { CompletionItem } from "vscode"; -import { toVscodeRange } from "../common"; +import { toVsCodeRange } from "../common"; import { EventType, sendTelemetryEvent } from "../telemetry"; export function createCompletionItemProvider( @@ -84,7 +84,7 @@ class QSharpCompletionItemProvider implements vscode.CompletionItemProvider { item.sortText = c.sortText; item.detail = c.detail; item.additionalTextEdits = c.additionalTextEdits?.map((edit) => { - return new vscode.TextEdit(toVscodeRange(edit.range), edit.newText); + return new vscode.TextEdit(toVsCodeRange(edit.range), edit.newText); }); return item; }); diff --git a/vscode/src/language-service/format.ts b/vscode/src/language-service/format.ts index fb9275dfd5..a3a2b7f71e 100644 --- a/vscode/src/language-service/format.ts +++ b/vscode/src/language-service/format.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange } from "../common"; +import { toVsCodeRange } from "../common"; import { EventType, FormatEvent, sendTelemetryEvent } from "../telemetry"; import { getRandomGuid } from "../utils"; @@ -50,7 +50,7 @@ class QSharpFormattingProvider } let edits = lsEdits.map( - (edit) => new vscode.TextEdit(toVscodeRange(edit.range), edit.newText), + (edit) => new vscode.TextEdit(toVsCodeRange(edit.range), edit.newText), ); if (range) { diff --git a/vscode/src/language-service/hover.ts b/vscode/src/language-service/hover.ts index 4307174099..be17cf20b8 100644 --- a/vscode/src/language-service/hover.ts +++ b/vscode/src/language-service/hover.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange } from "../common"; +import { toVsCodeRange } from "../common"; export function createHoverProvider(languageService: ILanguageService) { return new QSharpHoverProvider(languageService); @@ -21,7 +21,7 @@ class QSharpHoverProvider implements vscode.HoverProvider { hover && new vscode.Hover( new vscode.MarkdownString(hover.contents), - toVscodeRange(hover.span), + toVsCodeRange(hover.span), ) ); } diff --git a/vscode/src/language-service/rename.ts b/vscode/src/language-service/rename.ts index 02060ab4f5..d9726d045e 100644 --- a/vscode/src/language-service/rename.ts +++ b/vscode/src/language-service/rename.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange, toVscodeWorkspaceEdit } from "../common"; +import { toVsCodeRange, toVscodeWorkspaceEdit } from "../common"; export function createRenameProvider(languageService: ILanguageService) { return new QSharpRenameProvider(languageService); @@ -40,7 +40,7 @@ class QSharpRenameProvider implements vscode.RenameProvider { ); if (prepareRename) { return { - range: toVscodeRange(prepareRename.range), + range: toVsCodeRange(prepareRename.range), placeholder: prepareRename.newText, }; } else { diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 798df4d801..e3b2890d11 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -4,104 +4,173 @@ import * as vscode from 'vscode'; import { loadProject } from './projectSystem'; -import { getCompilerWorker, IProjectConfig, log } from "qsharp-lang"; +import { getCompilerWorker, ICompilerWorker, IProjectConfig, log, QscEventTarget } from "qsharp-lang"; import { getActiveQSharpDocumentUri } from './programConfig'; import { IProgramConfig } from '../../npm/qsharp/lib/web/qsc_wasm'; import { getTarget } from './config'; +import { toVsCodeRange } from './common'; -// TODO(sezna) testrunprofile, running tests -export async function initTestExplorer(context: vscode.ExtensionContext) { - const ctrl: vscode.TestController = vscode.tests.createTestController('qsharpTestController', 'Q# Tests'); - context.subscriptions.push(ctrl); +function localGetCompilerWorker(context: vscode.ExtensionContext): ICompilerWorker { + const compilerWorkerScriptPath = vscode.Uri.joinPath( + context.extensionUri, + "./out/compilerWorker.js", + ).toString(); + const worker = getCompilerWorker(compilerWorkerScriptPath); + return worker; +} +async function getProgramConfig(): Promise { + if (!vscode.workspace.workspaceFolders) { + log.info("No workspace detected; not starting test explorer") + return null; + } - ctrl.refreshHandler = async () => { - if (!vscode.workspace.workspaceFolders) { - log.info("No workspace detected; not starting test explorer") - return; - } + const docUri = getActiveQSharpDocumentUri(); + if (!docUri) { + log.info("No active document detected; not starting test explorer") + return null; + } - const docUri = getActiveQSharpDocumentUri(); - if (!docUri) { - log.info("No active document detected; not starting test explorer") - return; - } + const projectConfig: IProjectConfig = await loadProject(docUri); + if (!projectConfig) { + log.info("No project detected; not starting test explorer") + return null; + } - const projectConfig: IProjectConfig = await loadProject(docUri); - if (!projectConfig) { - log.info("No project detected; not starting test explorer") + return { + profile: getTarget(), + ...projectConfig + } +} +function mkRefreshHandler(ctrl: vscode.TestController, context: vscode.ExtensionContext) { + return async () => { + const programConfig = await getProgramConfig(); + if (!programConfig) { return; } - - let programConfig: IProgramConfig = { - profile: getTarget(), - ...projectConfig - }; - const compilerWorkerScriptPath = vscode.Uri.joinPath( - context.extensionUri, - "./out/compilerWorker.js", - ).toString(); - const worker = getCompilerWorker(compilerWorkerScriptPath); + const worker = localGetCompilerWorker(context); const testCallables = await worker.collectTestCallables(programConfig); - testCallables.forEach((testCallable) => { const testItem = ctrl.createTestItem( testCallable, testCallable); ctrl.items.add(testItem); }); + } +} + +const fileChangedEmitter = new vscode.EventEmitter(); +// TODO(sezna) testrunprofile, running tests +export async function initTestExplorer(context: vscode.ExtensionContext) { + const ctrl: vscode.TestController = vscode.tests.createTestController('qsharpTestController', 'Q# Tests'); + context.subscriptions.push(ctrl); + + // construct the handler that runs when the user presses the refresh button in the test explorer + const refreshHandler = mkRefreshHandler(ctrl, context); + // initially populate tests + await refreshHandler(); + + ctrl.refreshHandler = refreshHandler; + + const runHandler = (request: vscode.TestRunRequest, cancellation: vscode.CancellationToken) => { + if (!request.continuous) { + return startTestRun(request); + } }; - const fileChangedEmitter = new vscode.EventEmitter(); - const watchingTests = new Map(); - fileChangedEmitter.event(uri => { - if (watchingTests.has('ALL')) { - startTestRun(new vscode.TestRunRequest(undefined, undefined, watchingTests.get('ALL'), true)); + const startTestRun = async (request: vscode.TestRunRequest) => { + // use the compiler worker to run the test in the interpreter + + log.info("Starting test run, request was", JSON.stringify(request)); + const worker = localGetCompilerWorker(context); + + let program = await getProgramConfig(); + if (!program) { return; } - const include: vscode.TestItem[] = []; - let profile: vscode.TestRunProfile | undefined; - for (const [item, thisProfile] of watchingTests) { - const cast = item as vscode.TestItem; - if (cast.uri?.toString() == uri.toString()) { - include.push(cast); - profile = thisProfile; + const queue = []; + + for (const testCase of request.include || []) { + const run = ctrl.createTestRun(request); + const testRunFunc = async () => { + const evtTarget = new QscEventTarget(false); + evtTarget.addEventListener('Message', (msg) => { + run.appendOutput(`Test ${testCase.label}: ${msg.detail}\r\n`); + + }) + + evtTarget.addEventListener('Result', (msg) => { + if (msg.detail.success) { + run.passed(testCase); + } else { + let message: vscode.TestMessage = { + message: msg.detail.value.message, + location: { + range: toVsCodeRange(msg.detail.value.range), + uri: vscode.Uri.parse(msg.detail.value.uri || "") + } + } + run.failed(testCase, message); + } + run.end(); + }) + const callableExpr = `Main.${testCase.label}()`; + log.info("about to run test", callableExpr); + try { + await worker.run(program, callableExpr, 1, evtTarget); + } catch (error) { + log.error(`Error running test ${testCase.label}:`, error); + run.appendOutput(`Error running test ${testCase.label}: ${error}\r\n`); + } + log.info("ran test", testCase.label); + } - } - if (include.length) { - startTestRun(new vscode.TestRunRequest(include, undefined, profile, true)); + queue.push(testRunFunc); } - }); - const runHandler = (request: vscode.TestRunRequest, cancellation: vscode.CancellationToken) => { - if (!request.continuous) { - return startTestRun(request); + for (const func of queue) { + await func(); } - if (request.include === undefined) { - watchingTests.set('ALL', request.profile); - cancellation.onCancellationRequested(() => watchingTests.delete('ALL')); - } else { - request.include.forEach(item => watchingTests.set(item, request.profile)); - cancellation.onCancellationRequested(() => request.include!.forEach(item => watchingTests.delete(item))); - } - }; - const startTestRun = (request: vscode.TestRunRequest) => { - const queue: { test: vscode.TestItem; data: string }[] = []; - const run = ctrl.createTestRun(request); + + /* + example: + { + "include":[ + { + "id":"Main", + "children":[], + "label":"Main", + "canResolveChildren":false, + "busy":false, + "tags":[] + } + ], + "exclude":[], + "profile":{ + "controllerId":"qsharpTestController", + "profileId":1933983363, + "kind":1, + "g":"Run Tests", + "j":true + }, + "continuous":false, + "preserveFocus":true} + */ + // map of file uris to statements on each line: // const coveredLines = new Map(); // run the test TODO - vscode.window.showInformationMessage('Running tests...'); }; + ctrl.createRunProfile('Run Tests', vscode.TestRunProfileKind.Run, runHandler, true, undefined, true); ctrl.resolveHandler = async item => { @@ -136,6 +205,7 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { } + function gatherTestItems(collection: vscode.TestItemCollection) { const items: vscode.TestItem[] = []; collection.forEach(item => items.push(item)); From eae1b4d3dd4eecf1599b999f0b7b51493eac1b6d Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 10 Dec 2024 14:45:06 -0800 Subject: [PATCH 05/82] add todos --- vscode/src/testExplorer.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index e3b2890d11..39d705a8e1 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -11,6 +11,11 @@ import { getTarget } from './config'; import { toVsCodeRange } from './common'; +// TODO(sezna): +// - construct fully qualified callable name instead of assuming `Main` +// - handle running all tests +// - Auto-populate newly discovered tests +// - CodeLens function localGetCompilerWorker(context: vscode.ExtensionContext): ICompilerWorker { const compilerWorkerScriptPath = vscode.Uri.joinPath( context.extensionUri, @@ -65,7 +70,6 @@ function mkRefreshHandler(ctrl: vscode.TestController, context: vscode.Extension const fileChangedEmitter = new vscode.EventEmitter(); -// TODO(sezna) testrunprofile, running tests export async function initTestExplorer(context: vscode.ExtensionContext) { const ctrl: vscode.TestController = vscode.tests.createTestController('qsharpTestController', 'Q# Tests'); context.subscriptions.push(ctrl); From 759036f4dd28f502c272f27880ee2480418b770d Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 10 Dec 2024 15:15:26 -0800 Subject: [PATCH 06/82] switching to namespaces included with callable names --- language_service/src/code_lens.rs | 17 ++++-- language_service/src/protocol.rs | 1 + vscode/src/language-service/codeLens.ts | 5 ++ vscode/src/testExplorer.ts | 12 ++++- vscode/src/webviewPanel.ts | 1 + wasm/src/language_service.rs | 3 +- wasm/src/test_explorer.rs | 72 ++++++++++++++++--------- 7 files changed, 82 insertions(+), 29 deletions(-) diff --git a/language_service/src/code_lens.rs b/language_service/src/code_lens.rs index bed22671fe..efea2431e6 100644 --- a/language_service/src/code_lens.rs +++ b/language_service/src/code_lens.rs @@ -43,8 +43,9 @@ pub(crate) fn get_code_lenses( let namespace = ns.name(); let range = into_range(position_encoding, decl.span, &user_unit.sources); let name = decl.name.name.clone(); + let is_test_case = decl.attrs.iter().any(|attr| *attr == qsc::hir::Attr::Test); - return Some((item, range, namespace, name, Some(item_id) == entry_item_id)); + return Some((item, range, namespace, name, Some(item_id) == entry_item_id, is_test_case)); } } } @@ -52,8 +53,8 @@ pub(crate) fn get_code_lenses( }); callables - .flat_map(|(item, range, namespace, name, is_entry_point)| { - if is_entry_point { + .flat_map(|(item, range, namespace, name, is_entry_point, is_test_case)| { + let mut lenses = if is_entry_point { vec![ CodeLens { range, @@ -87,7 +88,17 @@ pub(crate) fn get_code_lenses( }]; } vec![] + }; + + if is_test_case { + lenses.push(CodeLens { + range, + command: CodeLensCommand::RunTest, + }); } + + lenses + }) .collect() } diff --git a/language_service/src/protocol.rs b/language_service/src/protocol.rs index 4a75b701ca..45e217eb82 100644 --- a/language_service/src/protocol.rs +++ b/language_service/src/protocol.rs @@ -184,6 +184,7 @@ pub enum CodeLensCommand { Run, Estimate, Circuit(Option), + RunTest, } #[derive(Debug)] diff --git a/vscode/src/language-service/codeLens.ts b/vscode/src/language-service/codeLens.ts index f5d952237b..e47eb848fa 100644 --- a/vscode/src/language-service/codeLens.ts +++ b/vscode/src/language-service/codeLens.ts @@ -69,6 +69,11 @@ function mapCodeLens(cl: ICodeLens): vscode.CodeLens { args = [cl.args]; } break; + case "runTest": + title = "Run Test", + command = "qsharp-vscode.runTest"; + tooltip = "Run test"; + break; } return new vscode.CodeLens(toVsCodeRange(cl.range), { diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 39d705a8e1..f049a06ca0 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -16,6 +16,8 @@ import { toVsCodeRange } from './common'; // - handle running all tests // - Auto-populate newly discovered tests // - CodeLens +// - use namespace hierarchy to populate test items +// - Cancellation tokens function localGetCompilerWorker(context: vscode.ExtensionContext): ICompilerWorker { const compilerWorkerScriptPath = vscode.Uri.joinPath( context.extensionUri, @@ -48,6 +50,7 @@ async function getProgramConfig(): Promise { ...projectConfig } } + function mkRefreshHandler(ctrl: vscode.TestController, context: vscode.ExtensionContext) { return async () => { const programConfig = await getProgramConfig(); @@ -73,6 +76,13 @@ const fileChangedEmitter = new vscode.EventEmitter(); export async function initTestExplorer(context: vscode.ExtensionContext) { const ctrl: vscode.TestController = vscode.tests.createTestController('qsharpTestController', 'Q# Tests'); context.subscriptions.push(ctrl); + context.subscriptions.push( + vscode.commands.registerCommand( + "qsharp-vscode.runTest", + // TODO: codelens callback + () => {}, + ) + ) // construct the handler that runs when the user presses the refresh button in the test explorer const refreshHandler = mkRefreshHandler(ctrl, context); @@ -124,7 +134,7 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { } run.end(); }) - const callableExpr = `Main.${testCase.label}()`; + const callableExpr = `${testCase.label}()`; log.info("about to run test", callableExpr); try { await worker.run(program, callableExpr, 1, evtTarget); diff --git a/vscode/src/webviewPanel.ts b/vscode/src/webviewPanel.ts index 7344a6c6c6..c6e8486a86 100644 --- a/vscode/src/webviewPanel.ts +++ b/vscode/src/webviewPanel.ts @@ -379,6 +379,7 @@ export function registerWebViewCommands(context: ExtensionContext) { ), ); + context.subscriptions.push( commands.registerCommand("qsharp-vscode.showDocumentation", async () => { await showDocumentationCommand(context.extensionUri); diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs index 5e60a3f9fb..0c559d9bdc 100644 --- a/wasm/src/language_service.rs +++ b/wasm/src/language_service.rs @@ -312,6 +312,7 @@ impl LanguageService { total_num_qubits: args.total_num_qubits, }), ), + qsls::protocol::CodeLensCommand::RunTest => ("runTest", None), }; CodeLens { range, @@ -502,7 +503,7 @@ serializable_type! { }, r#"export type ICodeLens = { range: IRange; - command: "histogram" | "estimate" | "debug" | "run"; + command: "histogram" | "estimate" | "debug" | "run" | "runTest"; } | { range: IRange; command: "circuit"; diff --git a/wasm/src/test_explorer.rs b/wasm/src/test_explorer.rs index deaba6d415..60cff6ccad 100644 --- a/wasm/src/test_explorer.rs +++ b/wasm/src/test_explorer.rs @@ -1,12 +1,19 @@ -use qsc::{compile, hir::{Attr, PatKind}, PackageType}; +use qsc::{ + compile, + hir::{Attr, PatKind}, + PackageType, +}; use wasm_bindgen::prelude::wasm_bindgen; -use crate::{project_system::{into_qsc_args, ProgramConfig}, STORE_CORE_STD}; +use crate::{ + project_system::{into_qsc_args, ProgramConfig}, + STORE_CORE_STD, +}; #[wasm_bindgen] -pub fn collect_test_callables(config: ProgramConfig) -> Result, String>{ +pub fn collect_test_callables(config: ProgramConfig) -> Result, String> { let (source_map, capabilities, language_features, _store, _deps) = - into_qsc_args(config, None).map_err(super::compile_errors_into_qsharp_errors_json)?; + into_qsc_args(config, None).map_err(super::compile_errors_into_qsharp_errors_json)?; let package = STORE_CORE_STD.with(|(store, std)| { let (unit, _) = compile::compile( @@ -20,15 +27,13 @@ pub fn collect_test_callables(config: ProgramConfig) -> Result, Stri unit.package }); - - let items_with_test_attribute = package.items.iter().filter(|(_, item)| { - { - item.attrs.iter().any(|attr| *attr == Attr::Test) - } - }); - + let items_with_test_attribute = package + .items + .iter() + .filter(|(_, item)| item.attrs.iter().any(|attr| *attr == Attr::Test)); let (callables, others): (Vec<_>, Vec<_>) = items_with_test_attribute.partition(|(_, item)| { + log::info!("item parent: {:?}", item.parent); matches!(item.kind, qsc::hir::ItemKind::Callable(_)) }); @@ -36,19 +41,38 @@ pub fn collect_test_callables(config: ProgramConfig) -> Result, Stri todo!("Return pretty error for non-callable with test attribute") } - let callable_names = callables.iter().filter_map(|(_, item)| { - if let qsc::hir::ItemKind::Callable(callable) = &item.kind { - if !callable.generics.is_empty() { - todo!("Return pretty error for generic callable with test attribute") - } - if callable.input.kind != PatKind::Tuple(vec![]) { - todo!("Return pretty error for callable with input") + let callable_names = callables + .iter() + .filter_map(|(_, item)| { + if let qsc::hir::ItemKind::Callable(callable) = &item.kind { + if !callable.generics.is_empty() { + todo!("Return pretty error for generic callable with test attribute") + } + if callable.input.kind != PatKind::Tuple(vec![]) { + todo!("Return pretty error for callable with input") + } + // this is indeed a test callable, so let's grab its parent name + let name = match item.parent { + None => Default::default(), + Some(parent_id) => { + let parent_item = package + .items + .get(parent_id) + .expect("Parent item did not exist in package"); + if let qsc::hir::ItemKind::Namespace(ns, _) = &parent_item.kind { + format!("{}.{}", ns.name(), callable.name.name) + } else { + callable.name.name.to_string() + } + } + }; + + Some(name) + } else { + None } - Some(callable.name.name.to_string()) - } else { - None - } - }).collect(); + }) + .collect(); Ok(callable_names) -} \ No newline at end of file +} From cec4bf987d6def9fed011e6f7fa3664382af05e3 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 10 Dec 2024 15:24:17 -0800 Subject: [PATCH 07/82] scoped test names --- vscode/src/testExplorer.ts | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index f049a06ca0..942bf54716 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -51,8 +51,13 @@ async function getProgramConfig(): Promise { } } -function mkRefreshHandler(ctrl: vscode.TestController, context: vscode.ExtensionContext) { +function mkRefreshHandler(ctrl: vscode.TestController, context: vscode.ExtensionContext, shouldDeleteOldTests: boolean = true) { return async () => { + if (shouldDeleteOldTests) { + for (const [id, _] of ctrl.items) { + ctrl.items.delete(id); + } + } const programConfig = await getProgramConfig(); if (!programConfig) { return; @@ -62,9 +67,17 @@ function mkRefreshHandler(ctrl: vscode.TestController, context: vscode.Extension const testCallables = await worker.collectTestCallables(programConfig); testCallables.forEach((testCallable) => { - const testItem = ctrl.createTestItem( - testCallable, testCallable); - ctrl.items.add(testItem); + const parts = testCallable.split('.'); + let parent = ctrl.items; + + parts.forEach((part, index) => { + let item = parent.get(part); + if (!item) { + item = ctrl.createTestItem(testCallable, part); + parent.add(item); + } + parent = item.children; + }); }); } } @@ -115,7 +128,7 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { const testRunFunc = async () => { const evtTarget = new QscEventTarget(false); evtTarget.addEventListener('Message', (msg) => { - run.appendOutput(`Test ${testCase.label}: ${msg.detail}\r\n`); + run.appendOutput(`Test ${testCase.id}: ${msg.detail}\r\n`); }) @@ -134,15 +147,15 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { } run.end(); }) - const callableExpr = `${testCase.label}()`; + const callableExpr = `${testCase.id}()`; log.info("about to run test", callableExpr); try { await worker.run(program, callableExpr, 1, evtTarget); } catch (error) { - log.error(`Error running test ${testCase.label}:`, error); - run.appendOutput(`Error running test ${testCase.label}: ${error}\r\n`); + log.error(`Error running test ${testCase.id}:`, error); + run.appendOutput(`Error running test ${testCase.id}: ${error}\r\n`); } - log.info("ran test", testCase.label); + log.info("ran test", testCase.id); } From ad7d3e2f88ebc7df9f57d39ddd600bfaa49e8712 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 12 Dec 2024 10:17:01 -0800 Subject: [PATCH 08/82] deduplicate parent items --- vscode/src/testExplorer.ts | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 942bf54716..466346f8dc 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -12,11 +12,9 @@ import { toVsCodeRange } from './common'; // TODO(sezna): -// - construct fully qualified callable name instead of assuming `Main` // - handle running all tests // - Auto-populate newly discovered tests // - CodeLens -// - use namespace hierarchy to populate test items // - Cancellation tokens function localGetCompilerWorker(context: vscode.ExtensionContext): ICompilerWorker { const compilerWorkerScriptPath = vscode.Uri.joinPath( @@ -51,6 +49,11 @@ async function getProgramConfig(): Promise { } } +/** + * Constructs the handler to pass to the `TestController` that refreshes the discovered tests. + * if `shouldDeleteOldTests` is `true`, then clear out previously discovered tests. If `false`, add new tests but don't dissolve old ones. + * + */ function mkRefreshHandler(ctrl: vscode.TestController, context: vscode.ExtensionContext, shouldDeleteOldTests: boolean = true) { return async () => { if (shouldDeleteOldTests) { @@ -66,19 +69,24 @@ function mkRefreshHandler(ctrl: vscode.TestController, context: vscode.Extension const testCallables = await worker.collectTestCallables(programConfig); - testCallables.forEach((testCallable) => { - const parts = testCallable.split('.'); - let parent = ctrl.items; + // break down the test callable into its parts, so we can construct + // the namespace hierarchy in the test explorer + const hierarchy: {[key: string]: vscode.TestItem} = {}; - parts.forEach((part, index) => { - let item = parent.get(part); - if (!item) { - item = ctrl.createTestItem(testCallable, part); - parent.add(item); + for (const testCallable of testCallables) { + const parts = testCallable.split('.'); + + // for an individual test case, e.g. foo.bar.baz, create a hierarchy of items + let rover = ctrl.items; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const id = i === parts.length - 1 ? testCallable : part; + if (!rover.get(part)) { + rover.add(ctrl.createTestItem(id, part)); } - parent = item.children; - }); - }); + rover = rover.get(id)!.children; + } + } } } From d9f288d4351eb3890cf0ea66212cdda0899b10a1 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 12 Dec 2024 10:30:56 -0800 Subject: [PATCH 09/82] make running child items work --- vscode/src/testExplorer.ts | 88 ++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 466346f8dc..f51d4f8575 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -4,7 +4,7 @@ import * as vscode from 'vscode'; import { loadProject } from './projectSystem'; -import { getCompilerWorker, ICompilerWorker, IProjectConfig, log, QscEventTarget } from "qsharp-lang"; +import { getCompilerWorker, ICompilerWorker, IProjectConfig, log, ProgramConfig, QscEventTarget } from "qsharp-lang"; import { getActiveQSharpDocumentUri } from './programConfig'; import { IProgramConfig } from '../../npm/qsharp/lib/web/qsc_wasm'; import { getTarget } from './config'; @@ -71,11 +71,9 @@ function mkRefreshHandler(ctrl: vscode.TestController, context: vscode.Extension // break down the test callable into its parts, so we can construct // the namespace hierarchy in the test explorer - const hierarchy: {[key: string]: vscode.TestItem} = {}; - for (const testCallable of testCallables) { const parts = testCallable.split('.'); - + // for an individual test case, e.g. foo.bar.baz, create a hierarchy of items let rover = ctrl.items; for (let i = 0; i < parts.length; i++) { @@ -99,11 +97,11 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { context.subscriptions.push(ctrl); context.subscriptions.push( vscode.commands.registerCommand( - "qsharp-vscode.runTest", - // TODO: codelens callback - () => {}, + "qsharp-vscode.runTest", + // TODO: codelens callback + () => { }, ) - ) + ) // construct the handler that runs when the user presses the refresh button in the test explorer const refreshHandler = mkRefreshHandler(ctrl, context); @@ -132,42 +130,14 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { const queue = []; for (const testCase of request.include || []) { - const run = ctrl.createTestRun(request); - const testRunFunc = async () => { - const evtTarget = new QscEventTarget(false); - evtTarget.addEventListener('Message', (msg) => { - run.appendOutput(`Test ${testCase.id}: ${msg.detail}\r\n`); - - }) - - evtTarget.addEventListener('Result', (msg) => { - if (msg.detail.success) { - run.passed(testCase); - } else { - let message: vscode.TestMessage = { - message: msg.detail.value.message, - location: { - range: toVsCodeRange(msg.detail.value.range), - uri: vscode.Uri.parse(msg.detail.value.uri || "") - } - } - run.failed(testCase, message); - } - run.end(); - }) - const callableExpr = `${testCase.id}()`; - log.info("about to run test", callableExpr); - try { - await worker.run(program, callableExpr, 1, evtTarget); - } catch (error) { - log.error(`Error running test ${testCase.id}:`, error); - run.appendOutput(`Error running test ${testCase.id}: ${error}\r\n`); + if (testCase.children.size > 0) { + for (const childTestCase of testCase.children) { + queue.push(async () => runTestCase(ctrl, childTestCase[1], request, worker, program)); } - log.info("ran test", testCase.id); - } - - queue.push(testRunFunc); + else { + queue.push(async () => runTestCase(ctrl, testCase, request, worker, program)); + } } for (const func of queue) { @@ -281,4 +251,38 @@ function startWatchingWorkspace(controller: vscode.TestController, fileChangedEm return watcher; }); +} + +async function runTestCase(ctrl: vscode.TestController, testCase: vscode.TestItem, request: vscode.TestRunRequest, worker: ICompilerWorker, program: ProgramConfig): Promise { + const run = ctrl.createTestRun(request); + const evtTarget = new QscEventTarget(false); + evtTarget.addEventListener('Message', (msg) => { + run.appendOutput(`Test ${testCase.id}: ${msg.detail}\r\n`); + + }) + + evtTarget.addEventListener('Result', (msg) => { + if (msg.detail.success) { + run.passed(testCase); + } else { + let message: vscode.TestMessage = { + message: msg.detail.value.message, + location: { + range: toVsCodeRange(msg.detail.value.range), + uri: vscode.Uri.parse(msg.detail.value.uri || "") + } + } + run.failed(testCase, message); + } + run.end(); + }) + + const callableExpr = `${testCase.id}()`; + try { + await worker.run(program, callableExpr, 1, evtTarget); + } catch (error) { + log.error(`Error running test ${testCase.id}:`, error); + run.appendOutput(`Error running test ${testCase.id}: ${error}\r\n`); + } + log.info("ran test", testCase.id); } \ No newline at end of file From 9ec9f415e6506a4871e053e882366f3fa370e364 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 12 Dec 2024 10:38:39 -0800 Subject: [PATCH 10/82] auto-refresh test cases --- vscode/src/testExplorer.ts | 83 ++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index f51d4f8575..e18ea70cb8 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -180,7 +180,7 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { ctrl.resolveHandler = async item => { if (!item) { - context.subscriptions.push(...startWatchingWorkspace(ctrl, fileChangedEmitter)); + context.subscriptions.push(...startWatchingWorkspace(ctrl, fileChangedEmitter, context)); return; } @@ -229,23 +229,20 @@ function getWorkspaceTestPatterns() { } -function startWatchingWorkspace(controller: vscode.TestController, fileChangedEmitter: vscode.EventEmitter) { +function startWatchingWorkspace(controller: vscode.TestController, fileChangedEmitter: vscode.EventEmitter, context: vscode.ExtensionContext) { return getWorkspaceTestPatterns().map(({ pattern }) => { + const refresher = mkRefreshHandler(controller, context, true) const watcher = vscode.workspace.createFileSystemWatcher(pattern); - /* - watcher.onDidCreate(uri => { - getOrCreateFile(controller, uri); - fileChangedEmitter.fire(uri); - }); - watcher.onDidChange(async uri => { - const { file, data } = getOrCreateFile(controller, uri); - if (data.didResolve) { - await data.updateFromDisk(controller, file); - } - fileChangedEmitter.fire(uri); - }); - */ - watcher.onDidDelete(uri => controller.items.delete(uri.toString())); + watcher.onDidCreate(async uri => { + await refresher(); + }); + watcher.onDidChange(async uri => { + await refresher(); + }); + + watcher.onDidDelete(async uri => { + await refresher(); + }); // findInitialFiles(controller, pattern); @@ -255,34 +252,34 @@ function startWatchingWorkspace(controller: vscode.TestController, fileChangedEm async function runTestCase(ctrl: vscode.TestController, testCase: vscode.TestItem, request: vscode.TestRunRequest, worker: ICompilerWorker, program: ProgramConfig): Promise { const run = ctrl.createTestRun(request); - const evtTarget = new QscEventTarget(false); - evtTarget.addEventListener('Message', (msg) => { - run.appendOutput(`Test ${testCase.id}: ${msg.detail}\r\n`); - - }) - - evtTarget.addEventListener('Result', (msg) => { - if (msg.detail.success) { - run.passed(testCase); - } else { - let message: vscode.TestMessage = { - message: msg.detail.value.message, - location: { - range: toVsCodeRange(msg.detail.value.range), - uri: vscode.Uri.parse(msg.detail.value.uri || "") - } + const evtTarget = new QscEventTarget(false); + evtTarget.addEventListener('Message', (msg) => { + run.appendOutput(`Test ${testCase.id}: ${msg.detail}\r\n`); + + }) + + evtTarget.addEventListener('Result', (msg) => { + if (msg.detail.success) { + run.passed(testCase); + } else { + let message: vscode.TestMessage = { + message: msg.detail.value.message, + location: { + range: toVsCodeRange(msg.detail.value.range), + uri: vscode.Uri.parse(msg.detail.value.uri || "") } - run.failed(testCase, message); } - run.end(); - }) - - const callableExpr = `${testCase.id}()`; - try { - await worker.run(program, callableExpr, 1, evtTarget); - } catch (error) { - log.error(`Error running test ${testCase.id}:`, error); - run.appendOutput(`Error running test ${testCase.id}: ${error}\r\n`); + run.failed(testCase, message); } - log.info("ran test", testCase.id); + run.end(); + }) + + const callableExpr = `${testCase.id}()`; + try { + await worker.run(program, callableExpr, 1, evtTarget); + } catch (error) { + log.error(`Error running test ${testCase.id}:`, error); + run.appendOutput(`Error running test ${testCase.id}: ${error}\r\n`); + } + log.info("ran test", testCase.id); } \ No newline at end of file From a03ed1487b329dedbcecac0523c5ef63ca64a557 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 12 Dec 2024 12:39:14 -0800 Subject: [PATCH 11/82] wip -- checkpoint --- language_service/src/code_lens.rs | 10 ++++-- language_service/src/protocol.rs | 3 +- vscode/src/language-service/codeLens.ts | 3 ++ vscode/src/testExplorer.ts | 48 +++++++++++++++++-------- wasm/src/language_service.rs | 22 ++++++++---- 5 files changed, 60 insertions(+), 26 deletions(-) diff --git a/language_service/src/code_lens.rs b/language_service/src/code_lens.rs index efea2431e6..6fd649ccb4 100644 --- a/language_service/src/code_lens.rs +++ b/language_service/src/code_lens.rs @@ -43,7 +43,9 @@ pub(crate) fn get_code_lenses( let namespace = ns.name(); let range = into_range(position_encoding, decl.span, &user_unit.sources); let name = decl.name.name.clone(); - let is_test_case = decl.attrs.iter().any(|attr| *attr == qsc::hir::Attr::Test); + let is_test_case = if decl.attrs.iter().any(|attr| *attr == qsc::hir::Attr::Test) { + Some(format!("{}.{}", ns.name(), decl.name.name.clone())) + } else { None }; return Some((item, range, namespace, name, Some(item_id) == entry_item_id, is_test_case)); } @@ -90,10 +92,12 @@ pub(crate) fn get_code_lenses( vec![] }; - if is_test_case { + if let Some(test_name) = is_test_case { lenses.push(CodeLens { range, - command: CodeLensCommand::RunTest, + command: CodeLensCommand::RunTest( + test_name + ), }); } diff --git a/language_service/src/protocol.rs b/language_service/src/protocol.rs index 45e217eb82..5c74f1c53d 100644 --- a/language_service/src/protocol.rs +++ b/language_service/src/protocol.rs @@ -184,7 +184,8 @@ pub enum CodeLensCommand { Run, Estimate, Circuit(Option), - RunTest, + // The string represents the callable name to call to run the test + RunTest(String), } #[derive(Debug)] diff --git a/vscode/src/language-service/codeLens.ts b/vscode/src/language-service/codeLens.ts index e47eb848fa..3ebbc7b559 100644 --- a/vscode/src/language-service/codeLens.ts +++ b/vscode/src/language-service/codeLens.ts @@ -73,6 +73,9 @@ function mapCodeLens(cl: ICodeLens): vscode.CodeLens { title = "Run Test", command = "qsharp-vscode.runTest"; tooltip = "Run test"; + if (cl.testName) { + args = [cl.testName]; + } break; } diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index e18ea70cb8..50ccdbc02b 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -6,16 +6,15 @@ import * as vscode from 'vscode'; import { loadProject } from './projectSystem'; import { getCompilerWorker, ICompilerWorker, IProjectConfig, log, ProgramConfig, QscEventTarget } from "qsharp-lang"; import { getActiveQSharpDocumentUri } from './programConfig'; -import { IProgramConfig } from '../../npm/qsharp/lib/web/qsc_wasm'; +import { IOperationInfo, IProgramConfig } from '../../npm/qsharp/lib/web/qsc_wasm'; import { getTarget } from './config'; import { toVsCodeRange } from './common'; // TODO(sezna): -// - handle running all tests -// - Auto-populate newly discovered tests // - CodeLens // - Cancellation tokens +// - add tests to samples function localGetCompilerWorker(context: vscode.ExtensionContext): ICompilerWorker { const compilerWorkerScriptPath = vscode.Uri.joinPath( context.extensionUri, @@ -97,9 +96,13 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { context.subscriptions.push(ctrl); context.subscriptions.push( vscode.commands.registerCommand( - "qsharp-vscode.runTest", - // TODO: codelens callback - () => { }, + "${qsharpExtensionId}.runTest", + async (testName: string) => { + log.info("beginning manual test run", testName); + const program = await getProgramConfig(); + if (!program) { return; } + await runTestCaseCodeLens(ctrl, testName, localGetCompilerWorker(context)); + }, ) ) @@ -209,14 +212,6 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { ); } - - -function gatherTestItems(collection: vscode.TestItemCollection) { - const items: vscode.TestItem[] = []; - collection.forEach(item => items.push(item)); - return items; -} - function getWorkspaceTestPatterns() { if (!vscode.workspace.workspaceFolders) { return []; @@ -228,7 +223,6 @@ function getWorkspaceTestPatterns() { })); } - function startWatchingWorkspace(controller: vscode.TestController, fileChangedEmitter: vscode.EventEmitter, context: vscode.ExtensionContext) { return getWorkspaceTestPatterns().map(({ pattern }) => { const refresher = mkRefreshHandler(controller, context, true) @@ -282,4 +276,28 @@ async function runTestCase(ctrl: vscode.TestController, testCase: vscode.TestIte run.appendOutput(`Error running test ${testCase.id}: ${error}\r\n`); } log.info("ran test", testCase.id); +} + +async function runTestCaseCodeLens(ctrl: vscode.TestController, testName: string, worker: ICompilerWorker): Promise { + const program = await getProgramConfig(); + if (!program) { + return; + } + + const evtTarget = new QscEventTarget(false); + evtTarget.addEventListener('Message', (msg) => { + // TODO + }) + + evtTarget.addEventListener('Result', (msg) => { + // TODO + }) + + const callableExpr = `${testName}()`; + + try { + await worker.run(program, callableExpr, 1, evtTarget); + } catch (error) { + log.error(`Error running test ${testName}:`, error); + } } \ No newline at end of file diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs index 0c559d9bdc..54472fa035 100644 --- a/wasm/src/language_service.rs +++ b/wasm/src/language_service.rs @@ -300,24 +300,26 @@ impl LanguageService { .into_iter() .map(|lens| { let range = lens.range.into(); - let (command, args) = match lens.command { - qsls::protocol::CodeLensCommand::Histogram => ("histogram", None), - qsls::protocol::CodeLensCommand::Debug => ("debug", None), - qsls::protocol::CodeLensCommand::Run => ("run", None), - qsls::protocol::CodeLensCommand::Estimate => ("estimate", None), + let (command, args, test_name) = match lens.command { + qsls::protocol::CodeLensCommand::Histogram => ("histogram", None, None), + qsls::protocol::CodeLensCommand::Debug => ("debug", None, None), + qsls::protocol::CodeLensCommand::Run => ("run", None, None), + qsls::protocol::CodeLensCommand::Estimate => ("estimate", None, None), qsls::protocol::CodeLensCommand::Circuit(args) => ( "circuit", args.map(|args| OperationInfo { operation: args.operation, total_num_qubits: args.total_num_qubits, }), + None, ), - qsls::protocol::CodeLensCommand::RunTest => ("runTest", None), + qsls::protocol::CodeLensCommand::RunTest(name) => ("runTest", None, Some(name)), }; CodeLens { range, command: command.to_string(), args, + test_name } .into() }) @@ -500,14 +502,20 @@ serializable_type! { command: String, #[serde(skip_serializing_if = "Option::is_none")] args: Option, + #[serde(skip_serializing_if = "Option::is_none")] + test_name: Option, }, r#"export type ICodeLens = { range: IRange; - command: "histogram" | "estimate" | "debug" | "run" | "runTest"; + command: "histogram" | "estimate" | "debug" | "run"; } | { range: IRange; command: "circuit"; args?: IOperationInfo + } | { + range: IRange; + command: "runTest"; + testName: string; }"#, ICodeLens } From b247c352657470a2e9203d2a88b7c244aedf31b7 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 12 Dec 2024 12:59:08 -0800 Subject: [PATCH 12/82] Remove codelens stuff --- language_service/src/protocol.rs | 2 -- vscode/src/language-service/codeLens.ts | 4 ++-- wasm/src/language_service.rs | 15 +++++---------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/language_service/src/protocol.rs b/language_service/src/protocol.rs index 5c74f1c53d..4a75b701ca 100644 --- a/language_service/src/protocol.rs +++ b/language_service/src/protocol.rs @@ -184,8 +184,6 @@ pub enum CodeLensCommand { Run, Estimate, Circuit(Option), - // The string represents the callable name to call to run the test - RunTest(String), } #[derive(Debug)] diff --git a/vscode/src/language-service/codeLens.ts b/vscode/src/language-service/codeLens.ts index 98672811cb..f5d952237b 100644 --- a/vscode/src/language-service/codeLens.ts +++ b/vscode/src/language-service/codeLens.ts @@ -7,7 +7,7 @@ import { qsharpLibraryUriScheme, } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeRange } from "../common"; +import { toVsCodeRange } from "../common"; export function createCodeLensProvider(languageService: ILanguageService) { return new QSharpCodeLensProvider(languageService); @@ -71,7 +71,7 @@ function mapCodeLens(cl: ICodeLens): vscode.CodeLens { break; } - return new vscode.CodeLens(toVscodeRange(cl.range), { + return new vscode.CodeLens(toVsCodeRange(cl.range), { title, command, arguments: args, diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs index 467e437397..5e60a3f9fb 100644 --- a/wasm/src/language_service.rs +++ b/wasm/src/language_service.rs @@ -300,26 +300,23 @@ impl LanguageService { .into_iter() .map(|lens| { let range = lens.range.into(); - let (command, args, test_name) = match lens.command { - qsls::protocol::CodeLensCommand::Histogram => ("histogram", None, None), - qsls::protocol::CodeLensCommand::Debug => ("debug", None, None), - qsls::protocol::CodeLensCommand::Run => ("run", None, None), - qsls::protocol::CodeLensCommand::Estimate => ("estimate", None, None), + let (command, args) = match lens.command { + qsls::protocol::CodeLensCommand::Histogram => ("histogram", None), + qsls::protocol::CodeLensCommand::Debug => ("debug", None), + qsls::protocol::CodeLensCommand::Run => ("run", None), + qsls::protocol::CodeLensCommand::Estimate => ("estimate", None), qsls::protocol::CodeLensCommand::Circuit(args) => ( "circuit", args.map(|args| OperationInfo { operation: args.operation, total_num_qubits: args.total_num_qubits, }), - None, ), - qsls::protocol::CodeLensCommand::RunTest(name) => ("runTest", None, Some(name)), }; CodeLens { range, command: command.to_string(), args, - test_name } .into() }) @@ -502,8 +499,6 @@ serializable_type! { command: String, #[serde(skip_serializing_if = "Option::is_none")] args: Option, - #[serde(skip_serializing_if = "Option::is_none")] - test_name: Option, }, r#"export type ICodeLens = { range: IRange; From e75f65b6276d6a4f9357c2bc1f240142692fc9c7 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 12 Dec 2024 13:10:53 -0800 Subject: [PATCH 13/82] update libraries to use new testing harness --- compiler/qsc_lowerer/src/lib.rs | 5 ++++- compiler/qsc_parse/src/item/tests.rs | 2 +- library/fixed_point/src/Tests.qs | 8 +++----- library/qtest/src/Tests.qs | 20 +++++++++++++++----- library/rotations/src/Tests.qs | 7 ++----- library/signed/src/Tests.qs | 18 +++++++----------- vscode/src/testExplorer.ts | 2 +- 7 files changed, 33 insertions(+), 29 deletions(-) diff --git a/compiler/qsc_lowerer/src/lib.rs b/compiler/qsc_lowerer/src/lib.rs index 658cfe405f..34024504ba 100644 --- a/compiler/qsc_lowerer/src/lib.rs +++ b/compiler/qsc_lowerer/src/lib.rs @@ -943,7 +943,10 @@ fn lower_attrs(attrs: &[hir::Attr]) -> Vec { hir::Attr::EntryPoint => Some(fir::Attr::EntryPoint), hir::Attr::Measurement => Some(fir::Attr::Measurement), hir::Attr::Reset => Some(fir::Attr::Reset), - hir::Attr::SimulatableIntrinsic | hir::Attr::Unimplemented | hir::Attr::Config | hir::Attr::Test => None, + hir::Attr::SimulatableIntrinsic + | hir::Attr::Unimplemented + | hir::Attr::Config + | hir::Attr::Test => None, }) .collect() } diff --git a/compiler/qsc_parse/src/item/tests.rs b/compiler/qsc_parse/src/item/tests.rs index a408e6fb43..64dd8796e8 100644 --- a/compiler/qsc_parse/src/item/tests.rs +++ b/compiler/qsc_parse/src/item/tests.rs @@ -2412,4 +2412,4 @@ fn test_attribute() { output: Type _id_ [25-29]: Path: Path _id_ [25-29] (Ident _id_ [25-29] "Unit") body: Block: Block _id_ [30-32]: "#]], ); -} \ No newline at end of file +} diff --git a/library/fixed_point/src/Tests.qs b/library/fixed_point/src/Tests.qs index 24826d0765..29568bea78 100644 --- a/library/fixed_point/src/Tests.qs +++ b/library/fixed_point/src/Tests.qs @@ -9,11 +9,7 @@ import Std.Convert.IntAsDouble; import Std.Math.AbsD; import Operations.*; -operation Main() : Unit { - FxpMeasurementTest(); - FxpOperationTests(); -} - +@Test() operation FxpMeasurementTest() : Unit { for numQubits in 3..12 { for numIntBits in 2..numQubits { @@ -43,6 +39,7 @@ operation TestConstantMeasurement(constant : Double, registerWidth : Int, intege ResetAll(register); } +@Test() operation FxpOperationTests() : Unit { for i in 0..10 { let constant1 = 0.2 * IntAsDouble(i); @@ -54,6 +51,7 @@ operation FxpOperationTests() : Unit { TestSquare(constant1); } } + operation TestSquare(a : Double) : Unit { Message($"Testing Square({a})"); use resultRegister = Qubit[30]; diff --git a/library/qtest/src/Tests.qs b/library/qtest/src/Tests.qs index 229caa5126..ff9b931893 100644 --- a/library/qtest/src/Tests.qs +++ b/library/qtest/src/Tests.qs @@ -3,24 +3,34 @@ import Std.Diagnostics.Fact; -function Main() : Unit { - let sample_tests = [ + +function SampleTestData() : (String, () -> Int, Int)[] { + [ ("Should return 42", TestCaseOne, 43), ("Should add one", () -> AddOne(5), 42), ("Should add one", () -> AddOne(5), 6) - ]; + ] +} +@Test() +function ReturnsFalseForFailingTest() : Unit { Fact( - not Functions.CheckAllTestCases(sample_tests), + not Functions.CheckAllTestCases(SampleTestData()), "Test harness failed to return false for a failing tests." ); +} +@Test() +function ReturnsTrueForPassingTest() : Unit { Fact( Functions.CheckAllTestCases([("always returns true", () -> true, true)]), "Test harness failed to return true for a passing test" ); +} - let run_all_result = Functions.RunAllTestCases(sample_tests); +@Test() +function RunAllTests() : Unit { + let run_all_result = Functions.RunAllTestCases(SampleTestData()); Fact( Length(run_all_result) == 3, diff --git a/library/rotations/src/Tests.qs b/library/rotations/src/Tests.qs index 0d9267e52c..8bb9a2f7fc 100644 --- a/library/rotations/src/Tests.qs +++ b/library/rotations/src/Tests.qs @@ -6,11 +6,7 @@ import Std.Math.HammingWeightI, Std.Math.PI; import HammingWeightPhasing.HammingWeightPhasing, HammingWeightPhasing.WithHammingWeight; -operation Main() : Unit { - TestHammingWeight(); - TestPhasing(); -} - +@Test() operation TestHammingWeight() : Unit { // exhaustive use qs = Qubit[4]; @@ -41,6 +37,7 @@ operation TestHammingWeight() : Unit { } } +@Test() operation TestPhasing() : Unit { for theta in [1.0, 2.0, 0.0, -0.5, 5.0 * PI()] { for numQubits in 1..6 { diff --git a/library/signed/src/Tests.qs b/library/signed/src/Tests.qs index b8afe37441..a276f2d667 100644 --- a/library/signed/src/Tests.qs +++ b/library/signed/src/Tests.qs @@ -5,16 +5,9 @@ import Std.Diagnostics.Fact; import Operations.Invert2sSI; import Measurement.MeasureSignedInteger; -/// This entrypoint runs tests for the signed integer library. -operation Main() : Unit { - UnsignedOpTests(); - Fact(Qtest.Operations.CheckAllTestCases(MeasureSignedIntTests()), "SignedInt tests failed"); - SignedOpTests(); - -} - -function MeasureSignedIntTests() : (String, Int, (Qubit[]) => (), (Qubit[]) => Int, Int)[] { - [ +@Test() +operation MeasureSignedIntTests() : Unit { + let testCases = [ ("0b0001 == 1", 4, (qs) => X(qs[0]), (qs) => MeasureSignedInteger(qs, 4), 1), ("0b1111 == -1", 4, (qs) => { X(qs[0]); X(qs[1]); X(qs[2]); X(qs[3]); }, (qs) => MeasureSignedInteger(qs, 4), -1), ("0b01000 == 8", 5, (qs) => X(qs[3]), (qs) => MeasureSignedInteger(qs, 5), 8), @@ -25,9 +18,11 @@ function MeasureSignedIntTests() : (String, Int, (Qubit[]) => (), (Qubit[]) => I X(qs[4]); }, (qs) => MeasureSignedInteger(qs, 5), -2), ("0b11000 == -8", 5, (qs) => { X(qs[3]); X(qs[4]); }, (qs) => MeasureSignedInteger(qs, 5), -8) - ] + ]; + Fact(Qtest.Operations.CheckAllTestCases(testCases), "SignedInt tests failed"); } +@Test() operation SignedOpTests() : Unit { use a = Qubit[32]; use b = Qubit[32]; @@ -54,6 +49,7 @@ operation SignedOpTests() : Unit { } +@Test() operation UnsignedOpTests() : Unit { use a = Qubit[2]; use b = Qubit[2]; diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index c645d43dc6..c61594e7a1 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -163,7 +163,7 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { runHandler, true, undefined, - true, + false, ); ctrl.resolveHandler = async (item) => { From 131a341d37c1f197fec07577a67d5d2d95b52210 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 12 Dec 2024 13:11:17 -0800 Subject: [PATCH 14/82] remove bad imports --- vscode/src/testExplorer.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index c61594e7a1..d81b2d6b3f 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -13,16 +13,11 @@ import { } from "qsharp-lang"; import { getActiveQSharpDocumentUri } from "./programConfig"; import { - IOperationInfo, IProgramConfig, } from "../../npm/qsharp/lib/web/qsc_wasm"; import { getTarget } from "./config"; import { toVsCodeRange } from "./common"; -import { createDebugConsoleEventTarget } from "./debugger/output"; -// TODO(sezna): -// - Cancellation tokens -// - add tests to samples function localGetCompilerWorker( context: vscode.ExtensionContext, ): ICompilerWorker { From b52ad25721e19382df69436f40786d76b33532d7 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 12 Dec 2024 13:20:51 -0800 Subject: [PATCH 15/82] document some functions --- vscode/src/testExplorer.ts | 45 ++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index d81b2d6b3f..94b398cf48 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -12,9 +12,7 @@ import { QscEventTarget, } from "qsharp-lang"; import { getActiveQSharpDocumentUri } from "./programConfig"; -import { - IProgramConfig, -} from "../../npm/qsharp/lib/web/qsc_wasm"; +import { IProgramConfig } from "../../npm/qsharp/lib/web/qsc_wasm"; import { getTarget } from "./config"; import { toVsCodeRange } from "./common"; @@ -65,7 +63,7 @@ function mkRefreshHandler( ) { return async () => { if (shouldDeleteOldTests) { - for (const [id, _] of ctrl.items) { + for (const [id] of ctrl.items) { ctrl.items.delete(id); } } @@ -96,8 +94,6 @@ function mkRefreshHandler( }; } -const fileChangedEmitter = new vscode.EventEmitter(); - export async function initTestExplorer(context: vscode.ExtensionContext) { const ctrl: vscode.TestController = vscode.tests.createTestController( "qsharpTestController", @@ -111,22 +107,21 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { ctrl.refreshHandler = refreshHandler; - const runHandler = ( - request: vscode.TestRunRequest, - cancellation: vscode.CancellationToken, - ) => { + const runHandler = (request: vscode.TestRunRequest) => { if (!request.continuous) { return startTestRun(request); } }; + // runs an individual test run + // or test group (a test run where there are child tests) const startTestRun = async (request: vscode.TestRunRequest) => { // use the compiler worker to run the test in the interpreter log.info("Starting test run, request was", JSON.stringify(request)); const worker = localGetCompilerWorker(context); - let program = await getProgramConfig(); + const program = await getProgramConfig(); if (!program) { return; } @@ -163,9 +158,7 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { ctrl.resolveHandler = async (item) => { if (!item) { - context.subscriptions.push( - ...startWatchingWorkspace(ctrl, fileChangedEmitter, context), - ); + context.subscriptions.push(...startWatchingWorkspace(ctrl, context)); return; } }; @@ -192,6 +185,11 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { ); } +/** + * If there are no workspace folders, then we can't watch anything. In general, though, there is a workspace since this extension + * is only activated when a .qs file is opened. + **/ + function getWorkspaceTestPatterns() { if (!vscode.workspace.workspaceFolders) { return []; @@ -203,22 +201,25 @@ function getWorkspaceTestPatterns() { })); } +/** + * Watches *.qs files and triggers the test discovery function on update/creation/deletion, ensuring we detect new tests without + * the user having to manually refresh the test explorer. + **/ function startWatchingWorkspace( controller: vscode.TestController, - fileChangedEmitter: vscode.EventEmitter, context: vscode.ExtensionContext, ) { return getWorkspaceTestPatterns().map(({ pattern }) => { const refresher = mkRefreshHandler(controller, context, true); const watcher = vscode.workspace.createFileSystemWatcher(pattern); - watcher.onDidCreate(async (uri) => { + watcher.onDidCreate(async () => { await refresher(); }); - watcher.onDidChange(async (uri) => { + watcher.onDidChange(async () => { await refresher(); }); - watcher.onDidDelete(async (uri) => { + watcher.onDidDelete(async () => { await refresher(); }); @@ -228,6 +229,12 @@ function startWatchingWorkspace( }); } +/** + * Given a single test case, run it in the worker (which runs the interpreter) and report results back to the + * `TestController` as a side effect. + * + * This function manages its own event target for the results of the test run and uses the controller to render the output in the VS Code UI. + **/ async function runTestCase( ctrl: vscode.TestController, testCase: vscode.TestItem, @@ -245,7 +252,7 @@ async function runTestCase( if (msg.detail.success) { run.passed(testCase); } else { - let message: vscode.TestMessage = { + const message: vscode.TestMessage = { message: msg.detail.value.message, location: { range: toVsCodeRange(msg.detail.value.range), From 180368ed27a53891170c338ed202fe8af7131caa Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 12 Dec 2024 13:22:32 -0800 Subject: [PATCH 16/82] Add comment --- vscode/src/testExplorer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 94b398cf48..6324faa009 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +// This file uses the VS Code Test Explorer API (https://code.visualstudio.com/docs/editor/testing) + import * as vscode from "vscode"; import { loadProject } from "./projectSystem"; import { From e960916de71ac83c7d808c4cbab54233a08bd0a8 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 12 Dec 2024 13:34:41 -0800 Subject: [PATCH 17/82] Remove todos --- wasm/src/test_explorer.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/wasm/src/test_explorer.rs b/wasm/src/test_explorer.rs index 60cff6ccad..695a938ab4 100644 --- a/wasm/src/test_explorer.rs +++ b/wasm/src/test_explorer.rs @@ -32,24 +32,17 @@ pub fn collect_test_callables(config: ProgramConfig) -> Result, Stri .iter() .filter(|(_, item)| item.attrs.iter().any(|attr| *attr == Attr::Test)); - let (callables, others): (Vec<_>, Vec<_>) = items_with_test_attribute.partition(|(_, item)| { - log::info!("item parent: {:?}", item.parent); - matches!(item.kind, qsc::hir::ItemKind::Callable(_)) - }); - - if !others.is_empty() { - todo!("Return pretty error for non-callable with test attribute") - } + let callables = items_with_test_attribute + .filter(|(_, item)| matches!(item.kind, qsc::hir::ItemKind::Callable(_))); let callable_names = callables - .iter() - .filter_map(|(_, item)| { + .filter_map(|(_, item)| -> Option>{ if let qsc::hir::ItemKind::Callable(callable) = &item.kind { if !callable.generics.is_empty() { - todo!("Return pretty error for generic callable with test attribute") + return Some(Err(format!("Callable {} has generic type parameters. Test callables cannot have generic type parameters.", callable.name.name))); } if callable.input.kind != PatKind::Tuple(vec![]) { - todo!("Return pretty error for callable with input") + return Some(Err(format!("Callable {} has input parameters. Test callables cannot have input parameters.", callable.name.name))); } // this is indeed a test callable, so let's grab its parent name let name = match item.parent { @@ -67,12 +60,12 @@ pub fn collect_test_callables(config: ProgramConfig) -> Result, Stri } }; - Some(name) + Some(Ok(name)) } else { None } }) - .collect(); + .collect::>()?; Ok(callable_names) } From 7d2aadc70a74f93516ab8423c919a0cedcc4340b Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 12 Dec 2024 15:09:02 -0800 Subject: [PATCH 18/82] Fix nested tests --- library/signed/src/Tests.qs | 2 +- vscode/src/testExplorer.ts | 26 ++++++++------------------ 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/library/signed/src/Tests.qs b/library/signed/src/Tests.qs index a276f2d667..617b371b01 100644 --- a/library/signed/src/Tests.qs +++ b/library/signed/src/Tests.qs @@ -8,7 +8,7 @@ import Measurement.MeasureSignedInteger; @Test() operation MeasureSignedIntTests() : Unit { let testCases = [ - ("0b0001 == 1", 4, (qs) => X(qs[0]), (qs) => MeasureSignedInteger(qs, 4), 1), + ("0b0001 == 1", 4, (qs) => X(qs[0]), (qs) => MeasureSignedInteger(qs, 6), 11), ("0b1111 == -1", 4, (qs) => { X(qs[0]); X(qs[1]); X(qs[2]); X(qs[3]); }, (qs) => MeasureSignedInteger(qs, 4), -1), ("0b01000 == 8", 5, (qs) => X(qs[3]), (qs) => MeasureSignedInteger(qs, 5), 8), ("0b11110 == -2", 5, (qs) => { diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 6324faa009..459abebdb5 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -128,24 +128,8 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { return; } - const queue = []; - for (const testCase of request.include || []) { - if (testCase.children.size > 0) { - for (const childTestCase of testCase.children) { - queue.push(async () => - runTestCase(ctrl, childTestCase[1], request, worker, program), - ); - } - } else { - queue.push(async () => - runTestCase(ctrl, testCase, request, worker, program), - ); - } - } - - for (const func of queue) { - await func(); + await runTestCase(ctrl, testCase, request, worker, program); } }; @@ -244,6 +228,12 @@ async function runTestCase( worker: ICompilerWorker, program: ProgramConfig, ): Promise { + if (testCase.children.size > 0) { + for (const childTestCase of testCase.children) { + await runTestCase(ctrl, childTestCase[1], request, worker, program); + } + return; + } const run = ctrl.createTestRun(request); const evtTarget = new QscEventTarget(false); evtTarget.addEventListener("Message", (msg) => { @@ -273,5 +263,5 @@ async function runTestCase( log.error(`Error running test ${testCase.id}:`, error); run.appendOutput(`Error running test ${testCase.id}: ${error}\r\n`); } - log.info("ran test", testCase.id); + log.trace("ran test:", testCase.id); } From ac8bfd7a24c793a4a537523a8b3605da66fb79d3 Mon Sep 17 00:00:00 2001 From: sezna Date: Fri, 13 Dec 2024 14:16:21 -0800 Subject: [PATCH 19/82] initial round of PR feedback --- compiler/qsc_frontend/src/lower.rs | 13 ++++++++++++- vscode/src/testExplorer.ts | 23 ++++++----------------- wasm/src/test_explorer.rs | 3 +++ 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/compiler/qsc_frontend/src/lower.rs b/compiler/qsc_frontend/src/lower.rs index 4df5d49526..67c3cb6309 100644 --- a/compiler/qsc_frontend/src/lower.rs +++ b/compiler/qsc_frontend/src/lower.rs @@ -443,7 +443,18 @@ impl With<'_> { None } }, - Ok(hir::Attr::Test) => Some(hir::Attr::Test), + Ok(hir::Attr::Test) => { + // verify that no args are passed to the attribute + match &*attr.arg.kind { + ast::ExprKind::Tuple(args) if args.is_empty() => {} + _ => { + self.lowerer + .errors + .push(Error::InvalidAttrArgs("()".to_string(), attr.arg.span)); + } + } + Some(hir::Attr::Test) + } Err(()) => { self.lowerer.errors.push(Error::UnknownAttr( attr.name.name.to_string(), diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 459abebdb5..ff45d0333a 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -12,11 +12,11 @@ import { log, ProgramConfig, QscEventTarget, + IProgramConfig, } from "qsharp-lang"; import { getActiveQSharpDocumentUri } from "./programConfig"; -import { IProgramConfig } from "../../npm/qsharp/lib/web/qsc_wasm"; import { getTarget } from "./config"; -import { toVsCodeRange } from "./common"; +import { isQsharpDocument, toVsCodeRange } from "./common"; function localGetCompilerWorker( context: vscode.ExtensionContext, @@ -55,19 +55,14 @@ async function getProgramConfig(): Promise { /** * Constructs the handler to pass to the `TestController` that refreshes the discovered tests. - * if `shouldDeleteOldTests` is `true`, then clear out previously discovered tests. If `false`, add new tests but don't dissolve old ones. - * */ function mkRefreshHandler( ctrl: vscode.TestController, context: vscode.ExtensionContext, - shouldDeleteOldTests: boolean = true, ) { return async () => { - if (shouldDeleteOldTests) { - for (const [id] of ctrl.items) { - ctrl.items.delete(id); - } + for (const [id] of ctrl.items) { + ctrl.items.delete(id); } const programConfig = await getProgramConfig(); if (!programConfig) { @@ -150,11 +145,7 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { }; function updateNodeForDocument(e: vscode.TextDocument) { - if (e.uri.scheme !== "file") { - return; - } - - if (!e.uri.path.endsWith(".qs")) { + if (!isQsharpDocument(e)) { return; } } @@ -196,7 +187,7 @@ function startWatchingWorkspace( context: vscode.ExtensionContext, ) { return getWorkspaceTestPatterns().map(({ pattern }) => { - const refresher = mkRefreshHandler(controller, context, true); + const refresher = mkRefreshHandler(controller, context); const watcher = vscode.workspace.createFileSystemWatcher(pattern); watcher.onDidCreate(async () => { await refresher(); @@ -209,8 +200,6 @@ function startWatchingWorkspace( await refresher(); }); - // findInitialFiles(controller, pattern); - return watcher; }); } diff --git a/wasm/src/test_explorer.rs b/wasm/src/test_explorer.rs index 695a938ab4..c7c5a8e344 100644 --- a/wasm/src/test_explorer.rs +++ b/wasm/src/test_explorer.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + use qsc::{ compile, hir::{Attr, PatKind}, From c22d14dac1c98c1abf6662fa3fe440994f4f400f Mon Sep 17 00:00:00 2001 From: sezna Date: Fri, 13 Dec 2024 15:54:39 -0800 Subject: [PATCH 20/82] move discovery of test items into compiler layer --- compiler/qsc_hir/src/hir.rs | 47 ++++++++++++++++++++++++++++ npm/qsharp/src/compiler/compiler.ts | 2 +- vscode/src/testExplorer.ts | 3 +- wasm/src/test_explorer.rs | 48 ++--------------------------- 4 files changed, 51 insertions(+), 49 deletions(-) diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs index 664511a3f4..196b6706c2 100644 --- a/compiler/qsc_hir/src/hir.rs +++ b/compiler/qsc_hir/src/hir.rs @@ -279,6 +279,53 @@ impl Display for Package { } } +impl Package { + /// Returns a collection of the fully qualified names of any callables annotated with `@Test()` + pub fn collect_test_callables(&self) -> std::result::Result, String> { + let items_with_test_attribute = self + .items + .iter() + .filter(|(_, item)| item.attrs.iter().any(|attr| *attr == Attr::Test)); + + let callables = items_with_test_attribute + .filter(|(_, item)| matches!(item.kind, ItemKind::Callable(_))); + + let callable_names = callables + .filter_map(|(_, item)| -> Option>{ + if let ItemKind::Callable(callable) = &item.kind { + if !callable.generics.is_empty() { + return Some(Err(format!("Callable {} has generic type parameters. Test callables cannot have generic type parameters.", callable.name.name))); + } + if callable.input.kind != PatKind::Tuple(vec![]) { + return Some(Err(format!("Callable {} has input parameters. Test callables cannot have input parameters.", callable.name.name))); + } + // this is indeed a test callable, so let's grab its parent name + let name = match item.parent { + None => Default::default(), + Some(parent_id) => { + let parent_item = self + .items + .get(parent_id) + .expect("Parent item did not exist in package"); + if let ItemKind::Namespace(ns, _) = &parent_item.kind { + format!("{}.{}", ns.name(), callable.name.name) + } else { + callable.name.name.to_string() + } + } + }; + + Some(Ok(name)) + } else { + None + } + }) + .collect::>()?; + + Ok(callable_names) + } +} + /// An item. #[derive(Clone, Debug, PartialEq)] pub struct Item { diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts index d8f8d08ca5..192ad2f47e 100644 --- a/npm/qsharp/src/compiler/compiler.ts +++ b/npm/qsharp/src/compiler/compiler.ts @@ -79,7 +79,7 @@ export interface ICompiler { eventHandler: IQscEventTarget, ): Promise; - collectTestCallables(program: IProgramConfig): Promise; + collectTestCallables(program: ProgramConfig): Promise; } /** diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index ff45d0333a..fe6cd8d145 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -12,7 +12,6 @@ import { log, ProgramConfig, QscEventTarget, - IProgramConfig, } from "qsharp-lang"; import { getActiveQSharpDocumentUri } from "./programConfig"; import { getTarget } from "./config"; @@ -29,7 +28,7 @@ function localGetCompilerWorker( return worker; } -async function getProgramConfig(): Promise { +async function getProgramConfig(): Promise { if (!vscode.workspace.workspaceFolders) { log.info("No workspace detected; not starting test explorer"); return null; diff --git a/wasm/src/test_explorer.rs b/wasm/src/test_explorer.rs index c7c5a8e344..0c4af5e0aa 100644 --- a/wasm/src/test_explorer.rs +++ b/wasm/src/test_explorer.rs @@ -1,11 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use qsc::{ - compile, - hir::{Attr, PatKind}, - PackageType, -}; +use qsc::{compile, PackageType}; use wasm_bindgen::prelude::wasm_bindgen; use crate::{ @@ -30,45 +26,5 @@ pub fn collect_test_callables(config: ProgramConfig) -> Result, Stri unit.package }); - let items_with_test_attribute = package - .items - .iter() - .filter(|(_, item)| item.attrs.iter().any(|attr| *attr == Attr::Test)); - - let callables = items_with_test_attribute - .filter(|(_, item)| matches!(item.kind, qsc::hir::ItemKind::Callable(_))); - - let callable_names = callables - .filter_map(|(_, item)| -> Option>{ - if let qsc::hir::ItemKind::Callable(callable) = &item.kind { - if !callable.generics.is_empty() { - return Some(Err(format!("Callable {} has generic type parameters. Test callables cannot have generic type parameters.", callable.name.name))); - } - if callable.input.kind != PatKind::Tuple(vec![]) { - return Some(Err(format!("Callable {} has input parameters. Test callables cannot have input parameters.", callable.name.name))); - } - // this is indeed a test callable, so let's grab its parent name - let name = match item.parent { - None => Default::default(), - Some(parent_id) => { - let parent_item = package - .items - .get(parent_id) - .expect("Parent item did not exist in package"); - if let qsc::hir::ItemKind::Namespace(ns, _) = &parent_item.kind { - format!("{}.{}", ns.name(), callable.name.name) - } else { - callable.name.name.to_string() - } - } - }; - - Some(Ok(name)) - } else { - None - } - }) - .collect::>()?; - - Ok(callable_names) + package.collect_test_callables() } From 5a28ef104b5d9c0e23ec2d2c76c9c66d797f42a7 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 16 Dec 2024 09:43:47 -0800 Subject: [PATCH 21/82] Use a pass to detect test attribute errors and report them nicely --- compiler/qsc_passes/src/lib.rs | 6 ++ compiler/qsc_passes/src/test_attribute.rs | 45 +++++++++++ .../qsc_passes/src/test_attribute/tests.rs | 79 +++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 compiler/qsc_passes/src/test_attribute.rs create mode 100644 compiler/qsc_passes/src/test_attribute/tests.rs diff --git a/compiler/qsc_passes/src/lib.rs b/compiler/qsc_passes/src/lib.rs index c20a3816bf..7283814099 100644 --- a/compiler/qsc_passes/src/lib.rs +++ b/compiler/qsc_passes/src/lib.rs @@ -15,6 +15,7 @@ mod measurement; mod replace_qubit_allocation; mod reset; mod spec_gen; +mod test_attribute; use callable_limits::CallableLimits; use capabilitiesck::{check_supported_capabilities, lower_store, run_rca_pass}; @@ -52,6 +53,7 @@ pub enum Error { Measurement(measurement::Error), Reset(reset::Error), SpecGen(spec_gen::Error), + TestAttribute(test_attribute::TestAttributeError), } #[derive(Clone, Copy, Debug, PartialEq)] @@ -121,6 +123,9 @@ impl PassContext { ReplaceQubitAllocation::new(core, assigner).visit_package(package); Validator::default().visit_package(package); + let test_attribute_errors = test_attribute::validate_test_attributes(package); + Validator::default().visit_package(package); + callable_errors .into_iter() .map(Error::CallableLimits) @@ -130,6 +135,7 @@ impl PassContext { .chain(entry_point_errors) .chain(measurement_decl_errors.into_iter().map(Error::Measurement)) .chain(reset_decl_errors.into_iter().map(Error::Reset)) + .chain(test_attribute_errors.into_iter().map(Error::TestAttribute)) .collect() } diff --git a/compiler/qsc_passes/src/test_attribute.rs b/compiler/qsc_passes/src/test_attribute.rs new file mode 100644 index 0000000000..dd298b4c32 --- /dev/null +++ b/compiler/qsc_passes/src/test_attribute.rs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use miette::Diagnostic; +use qsc_data_structures::span::Span; +use qsc_hir::{hir::Attr, visit::Visitor}; +use thiserror::Error; + +#[cfg(test)] +mod tests; + +#[derive(Clone, Debug, Diagnostic, Error)] +pub enum TestAttributeError { + #[error("This callable has parameters. Tests cannot have parameters.")] + CallableHasParameters(#[label] Span), + #[error("This callable has type parameters. Tests cannot have type parameters.")] + CallableHaSTypeParameters(#[label] Span), +} + +pub(crate) fn validate_test_attributes( + package: &mut qsc_hir::hir::Package, +) -> Vec { + let mut validator = TestAttributeValidator { errors: Vec::new() }; + validator.visit_package(package); + validator.errors +} + +struct TestAttributeValidator { + errors: Vec, +} + +impl<'a> Visitor<'a> for TestAttributeValidator { + fn visit_callable_decl(&mut self, decl: &'a qsc_hir::hir::CallableDecl) { + if decl.attrs.iter().any(|attr| matches!(attr, Attr::Test)) { + if !decl.generics.is_empty() { + self.errors + .push(TestAttributeError::CallableHaSTypeParameters(decl.span)); + } + if decl.input.ty != qsc_hir::ty::Ty::UNIT { + self.errors + .push(TestAttributeError::CallableHasParameters(decl.span)); + } + } + } +} diff --git a/compiler/qsc_passes/src/test_attribute/tests.rs b/compiler/qsc_passes/src/test_attribute/tests.rs new file mode 100644 index 0000000000..87c2d8f9fd --- /dev/null +++ b/compiler/qsc_passes/src/test_attribute/tests.rs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use expect_test::{expect, Expect}; +use indoc::indoc; +use qsc_data_structures::{language_features::LanguageFeatures, target::TargetCapabilityFlags}; +use qsc_frontend::compile::{self, compile, PackageStore, SourceMap}; +use qsc_hir::{validate::Validator, visit::Visitor}; + +use crate::test_attribute::validate_test_attributes; + +fn check(file: &str, expect: &Expect) { + let store = PackageStore::new(compile::core()); + let sources = SourceMap::new([("test".into(), file.into())], None); + let mut unit = compile( + &store, + &[], + sources, + TargetCapabilityFlags::all(), + LanguageFeatures::default(), + ); + assert!(unit.errors.is_empty(), "{:?}", unit.errors); + + let errors = validate_test_attributes(&mut unit.package); + Validator::default().visit_package(&unit.package); + if errors.is_empty() { + expect.assert_eq(&unit.package.to_string()); + } else { + expect.assert_debug_eq(&errors); + } +} + +#[test] +fn callable_cant_have_params() { + check( + indoc! {" + namespace test { + @Test() + operation A(q : Qubit) : Unit { + + } + } + "}, + &expect![[r#" + [ + CallableHasParameters( + Span { + lo: 33, + hi: 71, + }, + ), + ] + "#]], + ); +} + +#[test] +fn callable_cant_have_type_params() { + check( + indoc! {" + namespace test { + @Test() + operation A<'T>() : Unit { + + } + } + "}, + &expect![[r#" + [ + CallableHaSTypeParameters( + Span { + lo: 33, + hi: 66, + }, + ), + ] + "#]], + ); +} From 16a2aedae7a3275b31771ab0a0a5a295c14491ba Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 16 Dec 2024 09:53:04 -0800 Subject: [PATCH 22/82] use getActiveProgram --- vscode/src/testExplorer.ts | 43 +++++++++++--------------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index fe6cd8d145..c8a95b232e 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -13,7 +13,7 @@ import { ProgramConfig, QscEventTarget, } from "qsharp-lang"; -import { getActiveQSharpDocumentUri } from "./programConfig"; +import { getActiveProgram } from "./programConfig"; import { getTarget } from "./config"; import { isQsharpDocument, toVsCodeRange } from "./common"; @@ -28,30 +28,6 @@ function localGetCompilerWorker( return worker; } -async function getProgramConfig(): Promise { - if (!vscode.workspace.workspaceFolders) { - log.info("No workspace detected; not starting test explorer"); - return null; - } - - const docUri = getActiveQSharpDocumentUri(); - if (!docUri) { - log.info("No active document detected; not starting test explorer"); - return null; - } - - const projectConfig: IProjectConfig = await loadProject(docUri); - if (!projectConfig) { - log.info("No project detected; not starting test explorer"); - return null; - } - - return { - profile: getTarget(), - ...projectConfig, - }; -} - /** * Constructs the handler to pass to the `TestController` that refreshes the discovered tests. */ @@ -63,10 +39,13 @@ function mkRefreshHandler( for (const [id] of ctrl.items) { ctrl.items.delete(id); } - const programConfig = await getProgramConfig(); - if (!programConfig) { - return; + const program = await getActiveProgram(); + if (!program.success) { + throw new Error(program.errorMsg); } + + const programConfig = program.programConfig; + const worker = localGetCompilerWorker(context); const testCallables = await worker.collectTestCallables(programConfig); @@ -117,11 +96,13 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { log.info("Starting test run, request was", JSON.stringify(request)); const worker = localGetCompilerWorker(context); - const program = await getProgramConfig(); - if (!program) { - return; + const programResult = await getActiveProgram(); + if (!programResult.success) { + throw new Error(programResult.errorMsg); } + const program = programResult.programConfig; + for (const testCase of request.include || []) { await runTestCase(ctrl, testCase, request, worker, program); } From 214097c447ec7b158b12e8c096fec10f29b10b88 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 16 Dec 2024 10:27:19 -0800 Subject: [PATCH 23/82] remove unnecessary api in main.ts --- compiler/qsc_passes/src/test_attribute.rs | 10 +++++----- npm/qsharp/src/main.ts | 5 ----- vscode/src/testExplorer.ts | 1 + 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/compiler/qsc_passes/src/test_attribute.rs b/compiler/qsc_passes/src/test_attribute.rs index dd298b4c32..ec548ba2e0 100644 --- a/compiler/qsc_passes/src/test_attribute.rs +++ b/compiler/qsc_passes/src/test_attribute.rs @@ -11,10 +11,10 @@ mod tests; #[derive(Clone, Debug, Diagnostic, Error)] pub enum TestAttributeError { - #[error("This callable has parameters. Tests cannot have parameters.")] + #[error("test callables cannot take arguments")] CallableHasParameters(#[label] Span), - #[error("This callable has type parameters. Tests cannot have type parameters.")] - CallableHaSTypeParameters(#[label] Span), + #[error("test callables cannot have type parameters")] + CallableHasTypeParameters(#[label] Span), } pub(crate) fn validate_test_attributes( @@ -34,11 +34,11 @@ impl<'a> Visitor<'a> for TestAttributeValidator { if decl.attrs.iter().any(|attr| matches!(attr, Attr::Test)) { if !decl.generics.is_empty() { self.errors - .push(TestAttributeError::CallableHaSTypeParameters(decl.span)); + .push(TestAttributeError::CallableHasTypeParameters(decl.name.span)); } if decl.input.ty != qsc_hir::ty::Ty::UNIT { self.errors - .push(TestAttributeError::CallableHasParameters(decl.span)); + .push(TestAttributeError::CallableHasParameters(decl.name.span)); } } } diff --git a/npm/qsharp/src/main.ts b/npm/qsharp/src/main.ts index 1aef7bfb7b..d2cf72f710 100644 --- a/npm/qsharp/src/main.ts +++ b/npm/qsharp/src/main.ts @@ -91,9 +91,4 @@ export function getLanguageServiceWorker(): ILanguageServiceWorker { ); } -export function collectTestCallables(config: IProgramConfig): string[] { - ensureWasm(); - return wasm!.collect_test_callables(config); -} - export * as utils from "./utils.js"; diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index c8a95b232e..052c09ebfd 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -172,6 +172,7 @@ function startWatchingWorkspace( watcher.onDidCreate(async () => { await refresher(); }); + watcher.onDidChange(async () => { await refresher(); }); From adbc4b20afa2fa185b5908afd31bd38521008b69 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 16 Dec 2024 10:53:47 -0800 Subject: [PATCH 24/82] Fmt --- compiler/qsc_passes/src/test_attribute.rs | 4 +++- vscode/src/testExplorer.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/compiler/qsc_passes/src/test_attribute.rs b/compiler/qsc_passes/src/test_attribute.rs index ec548ba2e0..1c865ac0b5 100644 --- a/compiler/qsc_passes/src/test_attribute.rs +++ b/compiler/qsc_passes/src/test_attribute.rs @@ -34,7 +34,9 @@ impl<'a> Visitor<'a> for TestAttributeValidator { if decl.attrs.iter().any(|attr| matches!(attr, Attr::Test)) { if !decl.generics.is_empty() { self.errors - .push(TestAttributeError::CallableHasTypeParameters(decl.name.span)); + .push(TestAttributeError::CallableHasTypeParameters( + decl.name.span, + )); } if decl.input.ty != qsc_hir::ty::Ty::UNIT { self.errors diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 052c09ebfd..23be23bd0c 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -172,7 +172,7 @@ function startWatchingWorkspace( watcher.onDidCreate(async () => { await refresher(); }); - + watcher.onDidChange(async () => { await refresher(); }); From 5b6a1f8919e61a06d05c1adc404ceea39f771650 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 16 Dec 2024 11:12:56 -0800 Subject: [PATCH 25/82] fix lints --- npm/qsharp/src/main.ts | 2 +- vscode/src/testExplorer.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/npm/qsharp/src/main.ts b/npm/qsharp/src/main.ts index d2cf72f710..647c1b6f9c 100644 --- a/npm/qsharp/src/main.ts +++ b/npm/qsharp/src/main.ts @@ -26,7 +26,7 @@ import { } from "./language-service/language-service.js"; import { log } from "./log.js"; import { createProxy } from "./workers/node.js"; -import type { IProgramConfig, ProjectLoader } from "../lib/web/qsc_wasm.js"; +import type { ProjectLoader } from "../lib/web/qsc_wasm.js"; import { IProjectHost } from "./browser.js"; export { qsharpLibraryUriScheme }; diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 23be23bd0c..e50eb2ced6 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -4,17 +4,14 @@ // This file uses the VS Code Test Explorer API (https://code.visualstudio.com/docs/editor/testing) import * as vscode from "vscode"; -import { loadProject } from "./projectSystem"; import { getCompilerWorker, ICompilerWorker, - IProjectConfig, log, ProgramConfig, QscEventTarget, } from "qsharp-lang"; import { getActiveProgram } from "./programConfig"; -import { getTarget } from "./config"; import { isQsharpDocument, toVsCodeRange } from "./common"; function localGetCompilerWorker( From 7cee53208cbaba916b60ccce6eeb0e5f3d2f5500 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 16 Dec 2024 11:29:22 -0800 Subject: [PATCH 26/82] update tests --- compiler/qsc_passes/src/test_attribute/tests.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/qsc_passes/src/test_attribute/tests.rs b/compiler/qsc_passes/src/test_attribute/tests.rs index 87c2d8f9fd..85cb292346 100644 --- a/compiler/qsc_passes/src/test_attribute/tests.rs +++ b/compiler/qsc_passes/src/test_attribute/tests.rs @@ -45,8 +45,8 @@ fn callable_cant_have_params() { [ CallableHasParameters( Span { - lo: 33, - hi: 71, + lo: 43, + hi: 44, }, ), ] @@ -67,10 +67,10 @@ fn callable_cant_have_type_params() { "}, &expect![[r#" [ - CallableHaSTypeParameters( + CallableHasTypeParameters( Span { - lo: 33, - hi: 66, + lo: 43, + hi: 44, }, ), ] From be6410545ada7987a452c1c42692db53f0c948ab Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 16 Dec 2024 12:30:39 -0800 Subject: [PATCH 27/82] filter out invalid test items --- compiler/qsc_hir/src/hir.rs | 55 ++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs index 196b6706c2..b3cf5c08ff 100644 --- a/compiler/qsc_hir/src/hir.rs +++ b/compiler/qsc_hir/src/hir.rs @@ -291,36 +291,35 @@ impl Package { .filter(|(_, item)| matches!(item.kind, ItemKind::Callable(_))); let callable_names = callables - .filter_map(|(_, item)| -> Option>{ - if let ItemKind::Callable(callable) = &item.kind { - if !callable.generics.is_empty() { - return Some(Err(format!("Callable {} has generic type parameters. Test callables cannot have generic type parameters.", callable.name.name))); - } - if callable.input.kind != PatKind::Tuple(vec![]) { - return Some(Err(format!("Callable {} has input parameters. Test callables cannot have input parameters.", callable.name.name))); - } - // this is indeed a test callable, so let's grab its parent name - let name = match item.parent { - None => Default::default(), - Some(parent_id) => { - let parent_item = self - .items - .get(parent_id) - .expect("Parent item did not exist in package"); - if let ItemKind::Namespace(ns, _) = &parent_item.kind { - format!("{}.{}", ns.name(), callable.name.name) - } else { - callable.name.name.to_string() - } + .filter_map(|(_, item)| -> Option> { + if let ItemKind::Callable(callable) = &item.kind { + if !callable.generics.is_empty() + || callable.input.kind != PatKind::Tuple(vec![]) + { + return None; } - }; + // this is indeed a test callable, so let's grab its parent name + let name = match item.parent { + None => Default::default(), + Some(parent_id) => { + let parent_item = self + .items + .get(parent_id) + .expect("Parent item did not exist in package"); + if let ItemKind::Namespace(ns, _) = &parent_item.kind { + format!("{}.{}", ns.name(), callable.name.name) + } else { + callable.name.name.to_string() + } + } + }; - Some(Ok(name)) - } else { - None - } - }) - .collect::>()?; + Some(Ok(name)) + } else { + None + } + }) + .collect::>()?; Ok(callable_names) } From fa1ca4285468905d2a94ccc4b051996b50acb041 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Wed, 18 Dec 2024 13:12:44 -0800 Subject: [PATCH 28/82] wip: start to add locations to the return type for test callables --- compiler/qsc/src/lib.rs | 38 ++++++++++++++++++++++++++++++++ compiler/qsc_hir/src/hir.rs | 10 ++++++--- wasm/src/test_explorer.rs | 43 ++++++++++++++++++++++++++++--------- 3 files changed, 78 insertions(+), 13 deletions(-) diff --git a/compiler/qsc/src/lib.rs b/compiler/qsc/src/lib.rs index 5847ab4449..12c69e38e7 100644 --- a/compiler/qsc/src/lib.rs +++ b/compiler/qsc/src/lib.rs @@ -9,6 +9,44 @@ pub mod interpret; pub mod location; pub mod packages; pub mod target; +pub mod test_callables { + use qsc_data_structures::line_column::{Encoding, Range}; + use qsc_frontend::compile::CompileUnit; + + use crate::location::Location; + + pub struct TestDescriptor { + pub callable_name: String, + pub location: Location, + } + + pub fn collect_test_callables( + unit: &CompileUnit + ) -> Result + '_, String> { + let test_callables = unit.package.collect_test_callables()?; + + Ok(test_callables.into_iter().map(|(name, span)| { + let source = unit + .sources + .find_by_offset(span.lo) + .expect("source should exist for offset"); + + let location = Location { + source: source.name.clone(), + range: Range::from_span( + // TODO(@sezna) ask @minestarks if this is correct + Encoding::Utf8, + &source.contents, + &(span - source.offset), + ), + }; + TestDescriptor { + callable_name: name, + location, + } + })) + } +} pub use qsc_formatter::formatter; diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs index b3cf5c08ff..701d114c30 100644 --- a/compiler/qsc_hir/src/hir.rs +++ b/compiler/qsc_hir/src/hir.rs @@ -279,9 +279,11 @@ impl Display for Package { } } +pub type TestCallableName = String; + impl Package { /// Returns a collection of the fully qualified names of any callables annotated with `@Test()` - pub fn collect_test_callables(&self) -> std::result::Result, String> { + pub fn collect_test_callables(&self) -> std::result::Result, String> { let items_with_test_attribute = self .items .iter() @@ -291,7 +293,7 @@ impl Package { .filter(|(_, item)| matches!(item.kind, ItemKind::Callable(_))); let callable_names = callables - .filter_map(|(_, item)| -> Option> { + .filter_map(|(_, item)| -> Option> { if let ItemKind::Callable(callable) = &item.kind { if !callable.generics.is_empty() || callable.input.kind != PatKind::Tuple(vec![]) @@ -314,7 +316,9 @@ impl Package { } }; - Some(Ok(name)) + let span = item.span; + + Some(Ok((name,span))) } else { None } diff --git a/wasm/src/test_explorer.rs b/wasm/src/test_explorer.rs index 0c4af5e0aa..6f383f1bc5 100644 --- a/wasm/src/test_explorer.rs +++ b/wasm/src/test_explorer.rs @@ -1,30 +1,53 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use qsc::{compile, PackageType}; +use qsc::{compile, location::Location, PackageType}; use wasm_bindgen::prelude::wasm_bindgen; use crate::{ - project_system::{into_qsc_args, ProgramConfig}, - STORE_CORE_STD, + project_system::{into_qsc_args, ProgramConfig}, serializable_type, STORE_CORE_STD }; +serializable_type! { + TestDescriptor, + { + pub callable_name: String, + pub location: Location, + }, + r#"export interface ITestDescriptor { + callable_name: string; + location: ILocation; + }"#, + ITestDescriptor +} + #[wasm_bindgen] -pub fn collect_test_callables(config: ProgramConfig) -> Result, String> { - let (source_map, capabilities, language_features, _store, _deps) = +pub fn collect_test_callables(config: ProgramConfig) -> Result, String> { + let (source_map, capabilities, language_features, store, _deps) = into_qsc_args(config, None).map_err(super::compile_errors_into_qsharp_errors_json)?; - let package = STORE_CORE_STD.with(|(store, std)| { - let (unit, _) = compile::compile( + let compile_unit = STORE_CORE_STD.with(|(store, std)| { + let (unit, errs) = compile::compile( store, &[(*std, None)], source_map, PackageType::Lib, capabilities, language_features, - ); - unit.package + ); + unit }); - package.collect_test_callables() + + let test_descriptors = qsc::test_callables::collect_test_callables( + &compile_unit + )?; + + Ok(test_descriptors.map(|qsc::test_callables::TestDescriptor { callable_name, location }| { + TestDescriptor { + callable_name, + location, + }.into() + }).collect()) + } From 790c29b890ed0c9cde549da463c2ccb86e510966 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Wed, 18 Dec 2024 14:11:13 -0800 Subject: [PATCH 29/82] get spans/ranges hooked up --- npm/qsharp/src/compiler/compiler.ts | 5 +++-- vscode/src/testExplorer.ts | 14 +++++++++----- wasm/src/test_explorer.rs | 11 +++++++---- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts index 192ad2f47e..1cff51dc3a 100644 --- a/npm/qsharp/src/compiler/compiler.ts +++ b/npm/qsharp/src/compiler/compiler.ts @@ -10,6 +10,7 @@ import { TargetProfile, type VSDiagnostic, IProgramConfig, + ITestDescriptor, } from "../../lib/web/qsc_wasm.js"; import { log } from "../log.js"; import { @@ -79,7 +80,7 @@ export interface ICompiler { eventHandler: IQscEventTarget, ): Promise; - collectTestCallables(program: ProgramConfig): Promise; + collectTestCallables(program: ProgramConfig): Promise; } /** @@ -247,7 +248,7 @@ export class Compiler implements ICompiler { return success; } - async collectTestCallables(program: IProgramConfig): Promise { + async collectTestCallables(program: IProgramConfig): Promise { return this.wasm.collect_test_callables(program); } } diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index e50eb2ced6..a83b8637a3 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -12,7 +12,7 @@ import { QscEventTarget, } from "qsharp-lang"; import { getActiveProgram } from "./programConfig"; -import { isQsharpDocument, toVsCodeRange } from "./common"; +import { isQsharpDocument, toVscodeLocation, toVsCodeRange } from "./common"; function localGetCompilerWorker( context: vscode.ExtensionContext, @@ -49,16 +49,20 @@ function mkRefreshHandler( // break down the test callable into its parts, so we can construct // the namespace hierarchy in the test explorer - for (const testCallable of testCallables) { - const parts = testCallable.split("."); + log.info("test callables:", JSON.stringify(testCallables)); + for (const {callableName, location } of testCallables) { + const vscLocation = toVscodeLocation(location); + const parts = callableName.split("."); // for an individual test case, e.g. foo.bar.baz, create a hierarchy of items let rover = ctrl.items; for (let i = 0; i < parts.length; i++) { const part = parts[i]; - const id = i === parts.length - 1 ? testCallable : part; + const id = i === parts.length - 1 ? callableName : part; if (!rover.get(part)) { - rover.add(ctrl.createTestItem(id, part)); + let testItem = ctrl.createTestItem(id, part, vscLocation.uri); + testItem.range = vscLocation.range; + rover.add(testItem); } rover = rover.get(id)!.children; } diff --git a/wasm/src/test_explorer.rs b/wasm/src/test_explorer.rs index 6f383f1bc5..35bba3126b 100644 --- a/wasm/src/test_explorer.rs +++ b/wasm/src/test_explorer.rs @@ -3,6 +3,8 @@ use qsc::{compile, location::Location, PackageType}; use wasm_bindgen::prelude::wasm_bindgen; +use serde::{Serialize, Deserialize}; + use crate::{ project_system::{into_qsc_args, ProgramConfig}, serializable_type, STORE_CORE_STD @@ -11,11 +13,12 @@ use crate::{ serializable_type! { TestDescriptor, { + #[serde(rename = "callableName")] pub callable_name: String, - pub location: Location, + pub location: crate::line_column::Location, }, r#"export interface ITestDescriptor { - callable_name: string; + callableName: string; location: ILocation; }"#, ITestDescriptor @@ -27,7 +30,7 @@ pub fn collect_test_callables(config: ProgramConfig) -> Result Result Date: Fri, 20 Dec 2024 11:22:24 -0800 Subject: [PATCH 30/82] abstract compiler worker generation into a common singleton worker --- vscode/src/common.ts | 25 ++++++++++++++++++++++++- vscode/src/language-service/activate.ts | 2 +- vscode/src/testExplorer.ts | 20 ++++---------------- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/vscode/src/common.ts b/vscode/src/common.ts index 9b980fda00..182c05f6a6 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -3,7 +3,7 @@ import { TextDocument, Uri, Range, Location } from "vscode"; import { Utils } from "vscode-uri"; -import { ILocation, IRange, IWorkspaceEdit, VSDiagnostic } from "qsharp-lang"; +import { ILocation, IRange, IWorkspaceEdit, VSDiagnostic , getCompilerWorker, ICompilerWorker } from "qsharp-lang"; import * as vscode from "vscode"; export const qsharpLanguageId = "qsharp"; @@ -95,3 +95,26 @@ export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { } return vscodeDiagnostic; } + + +// the below worker is common to multiple consumers in the language extension. +let worker = null; +/** + * Returns a singleton instance of the compiler worker. + * @param context The extension context. + * @returns The compiler worker. + * + * This function is used to get a *common* compiler worker. It should only be used for performance-light + * and safe (infallible) operations. For performance-intensive, blocking operations, or for fallible operations, + * use `getCompilerWorker` instead. + **/ +export function getCommonCompilerWorker(context: vscode.ExtensionContext) : ICompilerWorker { + + + const compilerWorkerScriptPath = vscode.Uri.joinPath( + context.extensionUri, + "./out/compilerWorker.js", + ).toString(); + worker = getCompilerWorker(compilerWorkerScriptPath); + return worker; +} diff --git a/vscode/src/language-service/activate.ts b/vscode/src/language-service/activate.ts index d4815d8713..1a82eea9d6 100644 --- a/vscode/src/language-service/activate.ts +++ b/vscode/src/language-service/activate.ts @@ -80,7 +80,7 @@ export async function activateLanguageService(extensionUri: vscode.Uri) { createCompletionItemProvider(languageService), // Trigger characters should be kept in sync with the ones in `playground/src/main.tsx` "@", - ".", + "." ), ); diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index a83b8637a3..184d48bd55 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -5,25 +5,14 @@ import * as vscode from "vscode"; import { - getCompilerWorker, ICompilerWorker, log, ProgramConfig, QscEventTarget, } from "qsharp-lang"; import { getActiveProgram } from "./programConfig"; -import { isQsharpDocument, toVscodeLocation, toVsCodeRange } from "./common"; +import { getCommonCompilerWorker, isQsharpDocument, toVscodeLocation, toVsCodeRange } from "./common"; -function localGetCompilerWorker( - context: vscode.ExtensionContext, -): ICompilerWorker { - const compilerWorkerScriptPath = vscode.Uri.joinPath( - context.extensionUri, - "./out/compilerWorker.js", - ).toString(); - const worker = getCompilerWorker(compilerWorkerScriptPath); - return worker; -} /** * Constructs the handler to pass to the `TestController` that refreshes the discovered tests. @@ -43,13 +32,12 @@ function mkRefreshHandler( const programConfig = program.programConfig; - const worker = localGetCompilerWorker(context); + const worker = getCommonCompilerWorker(context); const testCallables = await worker.collectTestCallables(programConfig); // break down the test callable into its parts, so we can construct // the namespace hierarchy in the test explorer - log.info("test callables:", JSON.stringify(testCallables)); for (const {callableName, location } of testCallables) { const vscLocation = toVscodeLocation(location); const parts = callableName.split("."); @@ -94,8 +82,8 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { const startTestRun = async (request: vscode.TestRunRequest) => { // use the compiler worker to run the test in the interpreter - log.info("Starting test run, request was", JSON.stringify(request)); - const worker = localGetCompilerWorker(context); + log.trace("Starting test run, request was", JSON.stringify(request)); + const worker = getCommonCompilerWorker(context); const programResult = await getActiveProgram(); if (!programResult.success) { From 38e0f4bf6289a141ffef4f3e32de3aa9481dc1f5 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 20 Dec 2024 11:47:36 -0800 Subject: [PATCH 31/82] wipz --- .../src/language-service/language-service.ts | 6 ++++++ vscode/src/language-service/activate.ts | 18 ++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/npm/qsharp/src/language-service/language-service.ts b/npm/qsharp/src/language-service/language-service.ts index c27db6de25..94026828d1 100644 --- a/npm/qsharp/src/language-service/language-service.ts +++ b/npm/qsharp/src/language-service/language-service.ts @@ -139,8 +139,14 @@ export class QSharpLanguageService implements ILanguageService { documentUri: string, version: number, code: string, + emitter?: vscode.EventTarget, ): Promise { this.languageService.update_document(documentUri, version, code); + // this is used to trigger functionality outside of the language service. + // by firing an event here, we unify the points at which the language service + // recognizes an "update document" and when subscribers to the event react, avoiding + // multiple implementations of the same logic. + emitter && emitter.fire("updateDocument"); } async updateNotebookDocument( diff --git a/vscode/src/language-service/activate.ts b/vscode/src/language-service/activate.ts index 1a82eea9d6..a15211d581 100644 --- a/vscode/src/language-service/activate.ts +++ b/vscode/src/language-service/activate.ts @@ -168,9 +168,14 @@ async function loadLanguageService(baseUri: vscode.Uri) { ); return languageService; } -function registerDocumentUpdateHandlers(languageService: ILanguageService) { + +function registerDocumentUpdateHandlers(languageService: ILanguageService): vscode.EventTarget { + + // consumers can subscribe to this event to get notified when updateDocument finishes + const eventEmitter = new vscode.EventEmitter(); + vscode.workspace.textDocuments.forEach((document) => { - updateIfQsharpDocument(document); + updateIfQsharpDocument(document, eventEmitter); }); // we manually send an OpenDocument telemetry event if this is a Q# document, because the @@ -203,13 +208,13 @@ function registerDocumentUpdateHandlers(languageService: ILanguageService) { { linesOfCode: document.lineCount }, ); } - updateIfQsharpDocument(document); + updateIfQsharpDocument(document, eventEmitter); }), ); subscriptions.push( vscode.workspace.onDidChangeTextDocument((evt) => { - updateIfQsharpDocument(evt.document); + updateIfQsharpDocument(evt.document, eventEmitter); }), ); @@ -252,19 +257,20 @@ function registerDocumentUpdateHandlers(languageService: ILanguageService) { // Check that the document is on the same project as the manifest. document.fileName.startsWith(project_folder) ) { - updateIfQsharpDocument(document); + updateIfQsharpDocument(document, eventEmitter); } }); } } - function updateIfQsharpDocument(document: vscode.TextDocument) { + function updateIfQsharpDocument(document: vscode.TextDocument, emitter?: vscode.EventEmitter) { if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { // Regular (not notebook) Q# document. languageService.updateDocument( document.uri.toString(), document.version, document.getText(), + emitter, ); } } From 43f4e1759e6f7fe4385549e1589dcee325ddd9c5 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Fri, 20 Dec 2024 12:23:30 -0800 Subject: [PATCH 32/82] use updateDocument events for test discovery --- npm/qsharp/src/compiler/compiler.ts | 4 +- .../src/language-service/language-service.ts | 6 --- vscode/src/common.ts | 21 +++++++-- vscode/src/extension.ts | 6 ++- vscode/src/language-service/activate.ts | 46 ++++++++++++++----- vscode/src/testExplorer.ts | 33 +++++++------ 6 files changed, 74 insertions(+), 42 deletions(-) diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts index 1cff51dc3a..5c43172947 100644 --- a/npm/qsharp/src/compiler/compiler.ts +++ b/npm/qsharp/src/compiler/compiler.ts @@ -248,7 +248,9 @@ export class Compiler implements ICompiler { return success; } - async collectTestCallables(program: IProgramConfig): Promise { + async collectTestCallables( + program: IProgramConfig, + ): Promise { return this.wasm.collect_test_callables(program); } } diff --git a/npm/qsharp/src/language-service/language-service.ts b/npm/qsharp/src/language-service/language-service.ts index 94026828d1..c27db6de25 100644 --- a/npm/qsharp/src/language-service/language-service.ts +++ b/npm/qsharp/src/language-service/language-service.ts @@ -139,14 +139,8 @@ export class QSharpLanguageService implements ILanguageService { documentUri: string, version: number, code: string, - emitter?: vscode.EventTarget, ): Promise { this.languageService.update_document(documentUri, version, code); - // this is used to trigger functionality outside of the language service. - // by firing an event here, we unify the points at which the language service - // recognizes an "update document" and when subscribers to the event react, avoiding - // multiple implementations of the same logic. - emitter && emitter.fire("updateDocument"); } async updateNotebookDocument( diff --git a/vscode/src/common.ts b/vscode/src/common.ts index 182c05f6a6..7b6a801cad 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -3,7 +3,14 @@ import { TextDocument, Uri, Range, Location } from "vscode"; import { Utils } from "vscode-uri"; -import { ILocation, IRange, IWorkspaceEdit, VSDiagnostic , getCompilerWorker, ICompilerWorker } from "qsharp-lang"; +import { + ILocation, + IRange, + IWorkspaceEdit, + VSDiagnostic, + getCompilerWorker, + ICompilerWorker, +} from "qsharp-lang"; import * as vscode from "vscode"; export const qsharpLanguageId = "qsharp"; @@ -96,9 +103,8 @@ export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { return vscodeDiagnostic; } - // the below worker is common to multiple consumers in the language extension. -let worker = null; +let worker: ICompilerWorker | null = null; /** * Returns a singleton instance of the compiler worker. * @param context The extension context. @@ -108,13 +114,18 @@ let worker = null; * and safe (infallible) operations. For performance-intensive, blocking operations, or for fallible operations, * use `getCompilerWorker` instead. **/ -export function getCommonCompilerWorker(context: vscode.ExtensionContext) : ICompilerWorker { +export function getCommonCompilerWorker( + context: vscode.ExtensionContext, +): ICompilerWorker { + if (worker !== null) { + return worker; + } - const compilerWorkerScriptPath = vscode.Uri.joinPath( context.extensionUri, "./out/compilerWorker.js", ).toString(); worker = getCompilerWorker(compilerWorkerScriptPath); + return worker; } diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index adec34f8bc..5761c49881 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -68,15 +68,17 @@ export async function activate( context.subscriptions.push(...activateTargetProfileStatusBarItem()); + const eventEmitter = new vscode.EventEmitter(); + context.subscriptions.push( - ...(await activateLanguageService(context.extensionUri)), + ...(await activateLanguageService(context.extensionUri, eventEmitter)), ); context.subscriptions.push(...startOtherQSharpDiagnostics()); context.subscriptions.push(...registerQSharpNotebookHandlers()); - initTestExplorer(context); + initTestExplorer(context, eventEmitter.event); initAzureWorkspaces(context); initCodegen(context); activateDebugger(context); diff --git a/vscode/src/language-service/activate.ts b/vscode/src/language-service/activate.ts index a15211d581..59e3730f1a 100644 --- a/vscode/src/language-service/activate.ts +++ b/vscode/src/language-service/activate.ts @@ -38,7 +38,14 @@ import { createReferenceProvider } from "./references.js"; import { createRenameProvider } from "./rename.js"; import { createSignatureHelpProvider } from "./signature.js"; -export async function activateLanguageService(extensionUri: vscode.Uri) { +/** + * Returns all of the subscriptions that should be registered for the language service. + * Additionally, if an `eventEmitter` is passed in, will fire an event when a document is updated. + */ +export async function activateLanguageService( + extensionUri: vscode.Uri, + eventEmitter?: vscode.EventEmitter, +): Promise { const subscriptions: vscode.Disposable[] = []; const languageService = await loadLanguageService(extensionUri); @@ -47,7 +54,9 @@ export async function activateLanguageService(extensionUri: vscode.Uri) { subscriptions.push(...startLanguageServiceDiagnostics(languageService)); // synchronize document contents - subscriptions.push(...registerDocumentUpdateHandlers(languageService)); + subscriptions.push( + ...registerDocumentUpdateHandlers(languageService, eventEmitter), + ); // synchronize notebook cell contents subscriptions.push( @@ -80,7 +89,7 @@ export async function activateLanguageService(extensionUri: vscode.Uri) { createCompletionItemProvider(languageService), // Trigger characters should be kept in sync with the ones in `playground/src/main.tsx` "@", - "." + ".", ), ); @@ -147,7 +156,9 @@ export async function activateLanguageService(extensionUri: vscode.Uri) { return subscriptions; } -async function loadLanguageService(baseUri: vscode.Uri) { +async function loadLanguageService( + baseUri: vscode.Uri, +): Promise { const start = performance.now(); const wasmUri = vscode.Uri.joinPath(baseUri, "./wasm/qsc_wasm_bg.wasm"); const wasmBytes = await vscode.workspace.fs.readFile(wasmUri); @@ -169,11 +180,14 @@ async function loadLanguageService(baseUri: vscode.Uri) { return languageService; } -function registerDocumentUpdateHandlers(languageService: ILanguageService): vscode.EventTarget { - - // consumers can subscribe to this event to get notified when updateDocument finishes - const eventEmitter = new vscode.EventEmitter(); - +/** + * This function returns all of the subscriptions that should be registered for the language service. + * Additionally, if an `eventEmitter` is passed in, will fire an event when a document is updated. + */ +function registerDocumentUpdateHandlers( + languageService: ILanguageService, + eventEmitter?: vscode.EventEmitter, +): vscode.Disposable[] { vscode.workspace.textDocuments.forEach((document) => { updateIfQsharpDocument(document, eventEmitter); }); @@ -263,15 +277,25 @@ function registerDocumentUpdateHandlers(languageService: ILanguageService): vsco } } - function updateIfQsharpDocument(document: vscode.TextDocument, emitter?: vscode.EventEmitter) { + function updateIfQsharpDocument( + document: vscode.TextDocument, + emitter?: vscode.EventEmitter, + ) { if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { // Regular (not notebook) Q# document. languageService.updateDocument( document.uri.toString(), document.version, document.getText(), - emitter, ); + + if (emitter) { + // this is used to trigger functionality outside of the language service. + // by firing an event here, we unify the points at which the language service + // recognizes an "update document" and when subscribers to the event react, avoiding + // multiple implementations of the same logic. + emitter.fire(document.uri.toString()); + } } } diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 184d48bd55..43b2e39723 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -11,8 +11,12 @@ import { QscEventTarget, } from "qsharp-lang"; import { getActiveProgram } from "./programConfig"; -import { getCommonCompilerWorker, isQsharpDocument, toVscodeLocation, toVsCodeRange } from "./common"; - +import { + getCommonCompilerWorker, + isQsharpDocument, + toVscodeLocation, + toVsCodeRange, +} from "./common"; /** * Constructs the handler to pass to the `TestController` that refreshes the discovered tests. @@ -38,7 +42,7 @@ function mkRefreshHandler( // break down the test callable into its parts, so we can construct // the namespace hierarchy in the test explorer - for (const {callableName, location } of testCallables) { + for (const { callableName, location } of testCallables) { const vscLocation = toVscodeLocation(location); const parts = callableName.split("."); @@ -58,7 +62,10 @@ function mkRefreshHandler( }; } -export async function initTestExplorer(context: vscode.ExtensionContext) { +export async function initTestExplorer( + context: vscode.ExtensionContext, + updateDocumentEvent: vscode.Event, +) { const ctrl: vscode.TestController = vscode.tests.createTestController( "qsharpTestController", "Q# Tests", @@ -108,7 +115,9 @@ export async function initTestExplorer(context: vscode.ExtensionContext) { ctrl.resolveHandler = async (item) => { if (!item) { - context.subscriptions.push(...startWatchingWorkspace(ctrl, context)); + context.subscriptions.push( + ...startWatchingWorkspace(ctrl, context, updateDocumentEvent), + ); return; } }; @@ -154,22 +163,12 @@ function getWorkspaceTestPatterns() { function startWatchingWorkspace( controller: vscode.TestController, context: vscode.ExtensionContext, + updateDocumentEvent: vscode.Event, ) { return getWorkspaceTestPatterns().map(({ pattern }) => { const refresher = mkRefreshHandler(controller, context); const watcher = vscode.workspace.createFileSystemWatcher(pattern); - watcher.onDidCreate(async () => { - await refresher(); - }); - - watcher.onDidChange(async () => { - await refresher(); - }); - - watcher.onDidDelete(async () => { - await refresher(); - }); - + updateDocumentEvent(refresher); return watcher; }); } From bcd6d34893732f4cbd4432dd7eaf7928d28b9a65 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 23 Dec 2024 13:10:20 -0800 Subject: [PATCH 33/82] it works, but i'd rather not have the tests collapse on auto refresh --- vscode/src/common.ts | 2 +- vscode/src/extension.ts | 2 +- vscode/src/language-service/activate.ts | 8 +-- vscode/src/testExplorer.ts | 84 ++++++++++--------------- 4 files changed, 38 insertions(+), 58 deletions(-) diff --git a/vscode/src/common.ts b/vscode/src/common.ts index 7b6a801cad..dadc4a8755 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -48,7 +48,7 @@ export function toVsCodeRange(range: IRange): Range { ); } -export function toVscodeLocation(location: ILocation): any { +export function toVscodeLocation(location: ILocation): Location { return new Location(Uri.parse(location.source), toVsCodeRange(location.span)); } diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 5761c49881..a22ffe892a 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -68,7 +68,7 @@ export async function activate( context.subscriptions.push(...activateTargetProfileStatusBarItem()); - const eventEmitter = new vscode.EventEmitter(); + const eventEmitter = new vscode.EventEmitter(); context.subscriptions.push( ...(await activateLanguageService(context.extensionUri, eventEmitter)), diff --git a/vscode/src/language-service/activate.ts b/vscode/src/language-service/activate.ts index 59e3730f1a..823a55ed6b 100644 --- a/vscode/src/language-service/activate.ts +++ b/vscode/src/language-service/activate.ts @@ -44,7 +44,7 @@ import { createSignatureHelpProvider } from "./signature.js"; */ export async function activateLanguageService( extensionUri: vscode.Uri, - eventEmitter?: vscode.EventEmitter, + eventEmitter?: vscode.EventEmitter, ): Promise { const subscriptions: vscode.Disposable[] = []; @@ -186,7 +186,7 @@ async function loadLanguageService( */ function registerDocumentUpdateHandlers( languageService: ILanguageService, - eventEmitter?: vscode.EventEmitter, + eventEmitter?: vscode.EventEmitter, ): vscode.Disposable[] { vscode.workspace.textDocuments.forEach((document) => { updateIfQsharpDocument(document, eventEmitter); @@ -279,7 +279,7 @@ function registerDocumentUpdateHandlers( function updateIfQsharpDocument( document: vscode.TextDocument, - emitter?: vscode.EventEmitter, + emitter?: vscode.EventEmitter, ) { if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { // Regular (not notebook) Q# document. @@ -294,7 +294,7 @@ function registerDocumentUpdateHandlers( // by firing an event here, we unify the points at which the language service // recognizes an "update document" and when subscribers to the event react, avoiding // multiple implementations of the same logic. - emitter.fire(document.uri.toString()); + emitter.fire(document.uri); } } } diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 43b2e39723..97aa8baf51 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -25,24 +25,38 @@ function mkRefreshHandler( ctrl: vscode.TestController, context: vscode.ExtensionContext, ) { - return async () => { - for (const [id] of ctrl.items) { - ctrl.items.delete(id); + /// if `uri` is null, then we are performing a full refresh and scanning the entire program + return async (uri: vscode.Uri | null = null) => { + log.info("Refreshing tests for uri", uri); + // clear out old tests + for (const [id, testItem] of ctrl.items) { + // if the uri is null, delete all test items, as we are going to repopulate + // all tests. + // if the uri is some value, and the test item is from this same URI, + // delete it because we are about to repopulate tests from that document. + if (uri === null || testItem.uri?.toString() == uri.toString()) { + ctrl.items.delete(id); + } } + const program = await getActiveProgram(); if (!program.success) { throw new Error(program.errorMsg); } const programConfig = program.programConfig; - const worker = getCommonCompilerWorker(context); + const allTestCallables = await worker.collectTestCallables(programConfig); - const testCallables = await worker.collectTestCallables(programConfig); + // only update test callables from this Uri + const scopedTestCallables = uri === null ? allTestCallables : allTestCallables.filter(({callableName, location}) => { + const vscLocation = toVscodeLocation(location); + return vscLocation.uri.toString() === uri.toString(); + }); // break down the test callable into its parts, so we can construct // the namespace hierarchy in the test explorer - for (const { callableName, location } of testCallables) { + for (const { callableName, location } of scopedTestCallables) { const vscLocation = toVscodeLocation(location); const parts = callableName.split("."); @@ -62,21 +76,29 @@ function mkRefreshHandler( }; } +/** + * Initializes the test explorer with the Q# tests in the active document. + **/ export async function initTestExplorer( context: vscode.ExtensionContext, - updateDocumentEvent: vscode.Event, + updateDocumentEvent: vscode.Event, ) { const ctrl: vscode.TestController = vscode.tests.createTestController( "qsharpTestController", "Q# Tests", ); context.subscriptions.push(ctrl); - // construct the handler that runs when the user presses the refresh button in the test explorer + const refreshHandler = mkRefreshHandler(ctrl, context); // initially populate tests - await refreshHandler(); + await refreshHandler(null); + + // when the refresh button is pressed, refresh all tests by passing in a null uri + ctrl.refreshHandler = () => refreshHandler(null); - ctrl.refreshHandler = refreshHandler; + // when the language service detects an updateDocument, this event fires. + // we call the test refresher when that happens + updateDocumentEvent(refreshHandler); const runHandler = (request: vscode.TestRunRequest) => { if (!request.continuous) { @@ -113,15 +135,6 @@ export async function initTestExplorer( false, ); - ctrl.resolveHandler = async (item) => { - if (!item) { - context.subscriptions.push( - ...startWatchingWorkspace(ctrl, context, updateDocumentEvent), - ); - return; - } - }; - function updateNodeForDocument(e: vscode.TextDocument) { if (!isQsharpDocument(e)) { return; @@ -140,39 +153,6 @@ export async function initTestExplorer( ); } -/** - * If there are no workspace folders, then we can't watch anything. In general, though, there is a workspace since this extension - * is only activated when a .qs file is opened. - **/ - -function getWorkspaceTestPatterns() { - if (!vscode.workspace.workspaceFolders) { - return []; - } - - return vscode.workspace.workspaceFolders.map((workspaceFolder) => ({ - workspaceFolder, - pattern: new vscode.RelativePattern(workspaceFolder, "**/*.qs"), - })); -} - -/** - * Watches *.qs files and triggers the test discovery function on update/creation/deletion, ensuring we detect new tests without - * the user having to manually refresh the test explorer. - **/ -function startWatchingWorkspace( - controller: vscode.TestController, - context: vscode.ExtensionContext, - updateDocumentEvent: vscode.Event, -) { - return getWorkspaceTestPatterns().map(({ pattern }) => { - const refresher = mkRefreshHandler(controller, context); - const watcher = vscode.workspace.createFileSystemWatcher(pattern); - updateDocumentEvent(refresher); - return watcher; - }); -} - /** * Given a single test case, run it in the worker (which runs the interpreter) and report results back to the * `TestController` as a side effect. From 5421cb28d4f263266d51f70842559b7b6afc23dd Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 23 Dec 2024 13:30:32 -0800 Subject: [PATCH 34/82] Fmt --- vscode/src/testExplorer.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 97aa8baf51..b6d38e71c8 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -26,8 +26,8 @@ function mkRefreshHandler( context: vscode.ExtensionContext, ) { /// if `uri` is null, then we are performing a full refresh and scanning the entire program - return async (uri: vscode.Uri | null = null) => { - log.info("Refreshing tests for uri", uri); + return async (uri: vscode.Uri | null = null) => { + log.trace("Refreshing tests for uri", uri?.toString()); // clear out old tests for (const [id, testItem] of ctrl.items) { // if the uri is null, delete all test items, as we are going to repopulate @@ -49,10 +49,13 @@ function mkRefreshHandler( const allTestCallables = await worker.collectTestCallables(programConfig); // only update test callables from this Uri - const scopedTestCallables = uri === null ? allTestCallables : allTestCallables.filter(({callableName, location}) => { - const vscLocation = toVscodeLocation(location); - return vscLocation.uri.toString() === uri.toString(); - }); + const scopedTestCallables = + uri === null + ? allTestCallables + : allTestCallables.filter(({ callableName, location }) => { + const vscLocation = toVscodeLocation(location); + return vscLocation.uri.toString() === uri.toString(); + }); // break down the test callable into its parts, so we can construct // the namespace hierarchy in the test explorer @@ -76,7 +79,7 @@ function mkRefreshHandler( }; } -/** +/** * Initializes the test explorer with the Q# tests in the active document. **/ export async function initTestExplorer( @@ -96,7 +99,7 @@ export async function initTestExplorer( // when the refresh button is pressed, refresh all tests by passing in a null uri ctrl.refreshHandler = () => refreshHandler(null); - // when the language service detects an updateDocument, this event fires. + // when the language service detects an updateDocument, this event fires. // we call the test refresher when that happens updateDocumentEvent(refreshHandler); @@ -127,7 +130,7 @@ export async function initTestExplorer( }; ctrl.createRunProfile( - "Run Tests", + "Interpreter", vscode.TestRunProfileKind.Run, runHandler, true, @@ -166,6 +169,7 @@ async function runTestCase( worker: ICompilerWorker, program: ProgramConfig, ): Promise { + log.trace("Running Q# test: ", testCase.id); if (testCase.children.size > 0) { for (const childTestCase of testCase.children) { await runTestCase(ctrl, childTestCase[1], request, worker, program); From 804fb0bdb7ccbe34802b4f0f98815797c2604b81 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 23 Dec 2024 13:36:56 -0800 Subject: [PATCH 35/82] rename Vscode to VsCode --- vscode/src/common.ts | 6 +++--- vscode/src/language-service/codeActions.ts | 4 ++-- vscode/src/language-service/definition.ts | 4 ++-- vscode/src/language-service/references.ts | 4 ++-- vscode/src/language-service/rename.ts | 4 ++-- vscode/src/testExplorer.ts | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/vscode/src/common.ts b/vscode/src/common.ts index dadc4a8755..5dd6b3392c 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -48,11 +48,11 @@ export function toVsCodeRange(range: IRange): Range { ); } -export function toVscodeLocation(location: ILocation): Location { +export function toVsCodeLocation(location: ILocation): Location { return new Location(Uri.parse(location.source), toVsCodeRange(location.span)); } -export function toVscodeWorkspaceEdit( +export function toVsCodeWorkspaceEdit( iWorkspaceEdit: IWorkspaceEdit, ): vscode.WorkspaceEdit { const workspaceEdit = new vscode.WorkspaceEdit(); @@ -95,7 +95,7 @@ export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { if (d.related) { vscodeDiagnostic.relatedInformation = d.related.map((r) => { return new vscode.DiagnosticRelatedInformation( - toVscodeLocation(r.location), + toVsCodeLocation(r.location), r.message, ); }); diff --git a/vscode/src/language-service/codeActions.ts b/vscode/src/language-service/codeActions.ts index 513f28fe88..c3c29b73bc 100644 --- a/vscode/src/language-service/codeActions.ts +++ b/vscode/src/language-service/codeActions.ts @@ -3,7 +3,7 @@ import { ILanguageService, ICodeAction } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeWorkspaceEdit } from "../common"; +import { toVsCodeWorkspaceEdit } from "../common"; export function createCodeActionsProvider(languageService: ILanguageService) { return new QSharpCodeActionProvider(languageService); @@ -31,7 +31,7 @@ function toCodeAction(iCodeAction: ICodeAction): vscode.CodeAction { toCodeActionKind(iCodeAction.kind), ); if (iCodeAction.edit) { - codeAction.edit = toVscodeWorkspaceEdit(iCodeAction.edit); + codeAction.edit = toVsCodeWorkspaceEdit(iCodeAction.edit); } codeAction.isPreferred = iCodeAction.isPreferred; return codeAction; diff --git a/vscode/src/language-service/definition.ts b/vscode/src/language-service/definition.ts index fb2f6a6a23..3b2f8e1607 100644 --- a/vscode/src/language-service/definition.ts +++ b/vscode/src/language-service/definition.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeLocation } from "../common"; +import { toVsCodeLocation } from "../common"; export function createDefinitionProvider(languageService: ILanguageService) { return new QSharpDefinitionProvider(languageService); @@ -21,6 +21,6 @@ class QSharpDefinitionProvider implements vscode.DefinitionProvider { position, ); if (!definition) return null; - return toVscodeLocation(definition); + return toVsCodeLocation(definition); } } diff --git a/vscode/src/language-service/references.ts b/vscode/src/language-service/references.ts index 528038c189..84ada029ac 100644 --- a/vscode/src/language-service/references.ts +++ b/vscode/src/language-service/references.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVscodeLocation } from "../common"; +import { toVsCodeLocation } from "../common"; export function createReferenceProvider(languageService: ILanguageService) { return new QSharpReferenceProvider(languageService); @@ -24,6 +24,6 @@ class QSharpReferenceProvider implements vscode.ReferenceProvider { context.includeDeclaration, ); if (!lsReferences) return []; - return lsReferences.map(toVscodeLocation); + return lsReferences.map(toVsCodeLocation); } } diff --git a/vscode/src/language-service/rename.ts b/vscode/src/language-service/rename.ts index d9726d045e..7ae45ce218 100644 --- a/vscode/src/language-service/rename.ts +++ b/vscode/src/language-service/rename.ts @@ -3,7 +3,7 @@ import { ILanguageService } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVsCodeRange, toVscodeWorkspaceEdit } from "../common"; +import { toVsCodeRange, toVsCodeWorkspaceEdit } from "../common"; export function createRenameProvider(languageService: ILanguageService) { return new QSharpRenameProvider(languageService); @@ -25,7 +25,7 @@ class QSharpRenameProvider implements vscode.RenameProvider { newName, ); if (!rename) return null; - return toVscodeWorkspaceEdit(rename); + return toVsCodeWorkspaceEdit(rename); } async prepareRename( diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index b6d38e71c8..4da29e6b8d 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -14,7 +14,7 @@ import { getActiveProgram } from "./programConfig"; import { getCommonCompilerWorker, isQsharpDocument, - toVscodeLocation, + toVsCodeLocation, toVsCodeRange, } from "./common"; @@ -53,14 +53,14 @@ function mkRefreshHandler( uri === null ? allTestCallables : allTestCallables.filter(({ callableName, location }) => { - const vscLocation = toVscodeLocation(location); + const vscLocation = toVsCodeLocation(location); return vscLocation.uri.toString() === uri.toString(); }); // break down the test callable into its parts, so we can construct // the namespace hierarchy in the test explorer for (const { callableName, location } of scopedTestCallables) { - const vscLocation = toVscodeLocation(location); + const vscLocation = toVsCodeLocation(location); const parts = callableName.split("."); // for an individual test case, e.g. foo.bar.baz, create a hierarchy of items From ffab7246f3ba250059b777058f87835f0244969f Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 23 Dec 2024 13:39:21 -0800 Subject: [PATCH 36/82] rename collectTestCallables to getTestCallables --- compiler/qsc/src/lib.rs | 4 ++-- compiler/qsc_hir/src/hir.rs | 2 +- npm/qsharp/src/compiler/compiler.ts | 8 ++++---- vscode/src/testExplorer.ts | 2 +- wasm/src/lib.rs | 2 +- wasm/src/test_explorer.rs | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/compiler/qsc/src/lib.rs b/compiler/qsc/src/lib.rs index 12c69e38e7..a9af948069 100644 --- a/compiler/qsc/src/lib.rs +++ b/compiler/qsc/src/lib.rs @@ -20,10 +20,10 @@ pub mod test_callables { pub location: Location, } - pub fn collect_test_callables( + pub fn get_test_callables( unit: &CompileUnit ) -> Result + '_, String> { - let test_callables = unit.package.collect_test_callables()?; + let test_callables = unit.package.get_test_callables()?; Ok(test_callables.into_iter().map(|(name, span)| { let source = unit diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs index 701d114c30..432898ff80 100644 --- a/compiler/qsc_hir/src/hir.rs +++ b/compiler/qsc_hir/src/hir.rs @@ -283,7 +283,7 @@ pub type TestCallableName = String; impl Package { /// Returns a collection of the fully qualified names of any callables annotated with `@Test()` - pub fn collect_test_callables(&self) -> std::result::Result, String> { + pub fn get_test_callables(&self) -> std::result::Result, String> { let items_with_test_attribute = self .items .iter() diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts index 5c43172947..b5e4db467a 100644 --- a/npm/qsharp/src/compiler/compiler.ts +++ b/npm/qsharp/src/compiler/compiler.ts @@ -80,7 +80,7 @@ export interface ICompiler { eventHandler: IQscEventTarget, ): Promise; - collectTestCallables(program: ProgramConfig): Promise; + getTestCallables(program: ProgramConfig): Promise; } /** @@ -248,10 +248,10 @@ export class Compiler implements ICompiler { return success; } - async collectTestCallables( + async getTestCallables( program: IProgramConfig, ): Promise { - return this.wasm.collect_test_callables(program); + return this.wasm.get_test_callables(program); } } @@ -336,7 +336,7 @@ export const compilerProtocol: ServiceProtocol = { run: "requestWithProgress", runWithPauliNoise: "requestWithProgress", checkExerciseSolution: "requestWithProgress", - collectTestCallables: "request", + getTestCallables: "request", }, eventNames: ["DumpMachine", "Matrix", "Message", "Result"], }; diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 4da29e6b8d..48b1b07506 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -46,7 +46,7 @@ function mkRefreshHandler( const programConfig = program.programConfig; const worker = getCommonCompilerWorker(context); - const allTestCallables = await worker.collectTestCallables(programConfig); + const allTestCallables = await worker.getTestCallables(programConfig); // only update test callables from this Uri const scopedTestCallables = diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 8b60eba9e7..9d289cbb16 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -38,7 +38,7 @@ mod project_system; mod serializable_type; mod test_explorer; -pub use test_explorer::collect_test_callables; +pub use test_explorer::get_test_callables; #[cfg(test)] mod tests; diff --git a/wasm/src/test_explorer.rs b/wasm/src/test_explorer.rs index 35bba3126b..25124e611b 100644 --- a/wasm/src/test_explorer.rs +++ b/wasm/src/test_explorer.rs @@ -25,7 +25,7 @@ serializable_type! { } #[wasm_bindgen] -pub fn collect_test_callables(config: ProgramConfig) -> Result, String> { +pub fn get_test_callables(config: ProgramConfig) -> Result, String> { let (source_map, capabilities, language_features, store, _deps) = into_qsc_args(config, None).map_err(super::compile_errors_into_qsharp_errors_json)?; @@ -42,7 +42,7 @@ pub fn collect_test_callables(config: ProgramConfig) -> Result Date: Mon, 23 Dec 2024 14:18:03 -0800 Subject: [PATCH 37/82] switch to debug event target; remove unnecessary result --- compiler/qsc/src/lib.rs | 9 ++++----- compiler/qsc_hir/src/hir.rs | 11 ++++++----- npm/qsharp/src/compiler/compiler.ts | 4 +--- vscode/src/debugger/output.ts | 8 ++++++-- vscode/src/testExplorer.ts | 10 +++++----- wasm/src/test_explorer.rs | 6 +++--- 6 files changed, 25 insertions(+), 23 deletions(-) diff --git a/compiler/qsc/src/lib.rs b/compiler/qsc/src/lib.rs index a9af948069..742e25c540 100644 --- a/compiler/qsc/src/lib.rs +++ b/compiler/qsc/src/lib.rs @@ -22,10 +22,10 @@ pub mod test_callables { pub fn get_test_callables( unit: &CompileUnit - ) -> Result + '_, String> { - let test_callables = unit.package.get_test_callables()?; + ) -> impl Iterator + '_ { + let test_callables = unit.package.get_test_callables(); - Ok(test_callables.into_iter().map(|(name, span)| { + test_callables.into_iter().map(|(name, span)| { let source = unit .sources .find_by_offset(span.lo) @@ -34,7 +34,6 @@ pub mod test_callables { let location = Location { source: source.name.clone(), range: Range::from_span( - // TODO(@sezna) ask @minestarks if this is correct Encoding::Utf8, &source.contents, &(span - source.offset), @@ -44,7 +43,7 @@ pub mod test_callables { callable_name: name, location, } - })) + }) } } diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs index 432898ff80..20bf445a3e 100644 --- a/compiler/qsc_hir/src/hir.rs +++ b/compiler/qsc_hir/src/hir.rs @@ -279,11 +279,12 @@ impl Display for Package { } } +/// The name of a test callable, including its parent namespace. pub type TestCallableName = String; impl Package { /// Returns a collection of the fully qualified names of any callables annotated with `@Test()` - pub fn get_test_callables(&self) -> std::result::Result, String> { + pub fn get_test_callables(&self) -> Vec<(TestCallableName, Span)> { let items_with_test_attribute = self .items .iter() @@ -293,7 +294,7 @@ impl Package { .filter(|(_, item)| matches!(item.kind, ItemKind::Callable(_))); let callable_names = callables - .filter_map(|(_, item)| -> Option> { + .filter_map(|(_, item)| -> Option<_> { if let ItemKind::Callable(callable) = &item.kind { if !callable.generics.is_empty() || callable.input.kind != PatKind::Tuple(vec![]) @@ -318,14 +319,14 @@ impl Package { let span = item.span; - Some(Ok((name,span))) + Some((name,span)) } else { None } }) - .collect::>()?; + .collect::>(); - Ok(callable_names) + callable_names } } diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts index b5e4db467a..c3d9f93c22 100644 --- a/npm/qsharp/src/compiler/compiler.ts +++ b/npm/qsharp/src/compiler/compiler.ts @@ -248,9 +248,7 @@ export class Compiler implements ICompiler { return success; } - async getTestCallables( - program: IProgramConfig, - ): Promise { + async getTestCallables(program: IProgramConfig): Promise { return this.wasm.get_test_callables(program); } } diff --git a/vscode/src/debugger/output.ts b/vscode/src/debugger/output.ts index 00dbb0da74..bcc1c7d0db 100644 --- a/vscode/src/debugger/output.ts +++ b/vscode/src/debugger/output.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { QscEventTarget } from "qsharp-lang"; +import { log, QscEventTarget } from "qsharp-lang"; function formatComplex(real: number, imag: number) { // Format -0 as 0 @@ -72,7 +72,11 @@ export function createDebugConsoleEventTarget(out: (message: string) => void) { }); eventTarget.addEventListener("Result", (evt) => { - out(`${evt.detail.value}`); + if (evt.detail.success) { + out(`${evt.detail.value}`); + } else { + out(`${evt.detail.value.message}`); + } }); return eventTarget; diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index 48b1b07506..d42033fb89 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -17,6 +17,7 @@ import { toVsCodeLocation, toVsCodeRange, } from "./common"; +import { createDebugConsoleEventTarget } from "./debugger/output"; /** * Constructs the handler to pass to the `TestController` that refreshes the discovered tests. @@ -52,7 +53,7 @@ function mkRefreshHandler( const scopedTestCallables = uri === null ? allTestCallables - : allTestCallables.filter(({ callableName, location }) => { + : allTestCallables.filter(({ location }) => { const vscLocation = toVsCodeLocation(location); return vscLocation.uri.toString() === uri.toString(); }); @@ -177,11 +178,9 @@ async function runTestCase( return; } const run = ctrl.createTestRun(request); - const evtTarget = new QscEventTarget(false); - evtTarget.addEventListener("Message", (msg) => { - run.appendOutput(`Test ${testCase.id}: ${msg.detail}\r\n`); + const evtTarget = createDebugConsoleEventTarget((msg) => { + run.appendOutput(msg); }); - evtTarget.addEventListener("Result", (msg) => { if (msg.detail.success) { run.passed(testCase); @@ -199,6 +198,7 @@ async function runTestCase( }); const callableExpr = `${testCase.id}()`; + try { await worker.run(program, callableExpr, 1, evtTarget); } catch (error) { diff --git a/wasm/src/test_explorer.rs b/wasm/src/test_explorer.rs index 25124e611b..f3855e32eb 100644 --- a/wasm/src/test_explorer.rs +++ b/wasm/src/test_explorer.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use qsc::{compile, location::Location, PackageType}; +use qsc::{compile, PackageType}; use wasm_bindgen::prelude::wasm_bindgen; use serde::{Serialize, Deserialize}; @@ -26,7 +26,7 @@ serializable_type! { #[wasm_bindgen] pub fn get_test_callables(config: ProgramConfig) -> Result, String> { - let (source_map, capabilities, language_features, store, _deps) = + let (source_map, capabilities, language_features, _store, _deps) = into_qsc_args(config, None).map_err(super::compile_errors_into_qsharp_errors_json)?; let compile_unit = STORE_CORE_STD.with(|(store, std)| { @@ -44,7 +44,7 @@ pub fn get_test_callables(config: ProgramConfig) -> Result, let test_descriptors = qsc::test_callables::get_test_callables( &compile_unit - )?; + ); Ok(test_descriptors.map(|qsc::test_callables::TestDescriptor { callable_name, location }| { TestDescriptor { From eea9581305c3031683c5e4f97ad921e967997bae Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 23 Dec 2024 14:20:11 -0800 Subject: [PATCH 38/82] rename test explorer to test discovery --- vscode/src/testExplorer.ts | 1 - wasm/src/lib.rs | 4 ++-- wasm/src/{test_explorer.rs => test_discovery.rs} | 0 3 files changed, 2 insertions(+), 3 deletions(-) rename wasm/src/{test_explorer.rs => test_discovery.rs} (100%) diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index d42033fb89..bff3ae7280 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -8,7 +8,6 @@ import { ICompilerWorker, log, ProgramConfig, - QscEventTarget, } from "qsharp-lang"; import { getActiveProgram } from "./programConfig"; import { diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 9d289cbb16..c6b94c680f 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -36,9 +36,9 @@ mod line_column; mod logging; mod project_system; mod serializable_type; -mod test_explorer; +mod test_discovery; -pub use test_explorer::get_test_callables; +pub use test_discovery::get_test_callables; #[cfg(test)] mod tests; diff --git a/wasm/src/test_explorer.rs b/wasm/src/test_discovery.rs similarity index 100% rename from wasm/src/test_explorer.rs rename to wasm/src/test_discovery.rs From 540de0bfdfc5704da114756b0d0b02fdc68a261e Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 23 Dec 2024 14:32:19 -0800 Subject: [PATCH 39/82] fmt --- compiler/qsc/src/lib.rs | 10 ++-------- compiler/qsc_hir/src/hir.rs | 2 +- vscode/src/debugger/output.ts | 2 +- vscode/src/testExplorer.ts | 10 +++------- wasm/src/test_discovery.rs | 36 +++++++++++++++++++---------------- 5 files changed, 27 insertions(+), 33 deletions(-) diff --git a/compiler/qsc/src/lib.rs b/compiler/qsc/src/lib.rs index 742e25c540..4d0e97dd85 100644 --- a/compiler/qsc/src/lib.rs +++ b/compiler/qsc/src/lib.rs @@ -20,9 +20,7 @@ pub mod test_callables { pub location: Location, } - pub fn get_test_callables( - unit: &CompileUnit - ) -> impl Iterator + '_ { + pub fn get_test_callables(unit: &CompileUnit) -> impl Iterator + '_ { let test_callables = unit.package.get_test_callables(); test_callables.into_iter().map(|(name, span)| { @@ -33,11 +31,7 @@ pub mod test_callables { let location = Location { source: source.name.clone(), - range: Range::from_span( - Encoding::Utf8, - &source.contents, - &(span - source.offset), - ), + range: Range::from_span(Encoding::Utf8, &source.contents, &(span - source.offset)), }; TestDescriptor { callable_name: name, diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs index 20bf445a3e..bcefb8ed9c 100644 --- a/compiler/qsc_hir/src/hir.rs +++ b/compiler/qsc_hir/src/hir.rs @@ -319,7 +319,7 @@ impl Package { let span = item.span; - Some((name,span)) + Some((name, span)) } else { None } diff --git a/vscode/src/debugger/output.ts b/vscode/src/debugger/output.ts index bcc1c7d0db..de888fce1e 100644 --- a/vscode/src/debugger/output.ts +++ b/vscode/src/debugger/output.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { log, QscEventTarget } from "qsharp-lang"; +import { QscEventTarget } from "qsharp-lang"; function formatComplex(real: number, imag: number) { // Format -0 as 0 diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts index bff3ae7280..b4b6887950 100644 --- a/vscode/src/testExplorer.ts +++ b/vscode/src/testExplorer.ts @@ -4,11 +4,7 @@ // This file uses the VS Code Test Explorer API (https://code.visualstudio.com/docs/editor/testing) import * as vscode from "vscode"; -import { - ICompilerWorker, - log, - ProgramConfig, -} from "qsharp-lang"; +import { ICompilerWorker, log, ProgramConfig } from "qsharp-lang"; import { getActiveProgram } from "./programConfig"; import { getCommonCompilerWorker, @@ -69,7 +65,7 @@ function mkRefreshHandler( const part = parts[i]; const id = i === parts.length - 1 ? callableName : part; if (!rover.get(part)) { - let testItem = ctrl.createTestItem(id, part, vscLocation.uri); + const testItem = ctrl.createTestItem(id, part, vscLocation.uri); testItem.range = vscLocation.range; rover.add(testItem); } @@ -178,7 +174,7 @@ async function runTestCase( } const run = ctrl.createTestRun(request); const evtTarget = createDebugConsoleEventTarget((msg) => { - run.appendOutput(msg); + run.appendOutput(`${msg}\n`); }); evtTarget.addEventListener("Result", (msg) => { if (msg.detail.success) { diff --git a/wasm/src/test_discovery.rs b/wasm/src/test_discovery.rs index f3855e32eb..1600e672f8 100644 --- a/wasm/src/test_discovery.rs +++ b/wasm/src/test_discovery.rs @@ -2,12 +2,12 @@ // Licensed under the MIT License. use qsc::{compile, PackageType}; +use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::wasm_bindgen; -use serde::{Serialize, Deserialize}; - use crate::{ - project_system::{into_qsc_args, ProgramConfig}, serializable_type, STORE_CORE_STD + project_system::{into_qsc_args, ProgramConfig}, + serializable_type, STORE_CORE_STD, }; serializable_type! { @@ -37,20 +37,24 @@ pub fn get_test_callables(config: ProgramConfig) -> Result, PackageType::Lib, capabilities, language_features, - ); + ); unit }); - - let test_descriptors = qsc::test_callables::get_test_callables( - &compile_unit - ); - - Ok(test_descriptors.map(|qsc::test_callables::TestDescriptor { callable_name, location }| { - TestDescriptor { - callable_name, - location: location.into(), - }.into() - }).collect()) - + let test_descriptors = qsc::test_callables::get_test_callables(&compile_unit); + + Ok(test_descriptors + .map( + |qsc::test_callables::TestDescriptor { + callable_name, + location, + }| { + TestDescriptor { + callable_name, + location: location.into(), + } + .into() + }, + ) + .collect()) } From f17114114d18d25964b251e40c01fa4564456244 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 31 Dec 2024 13:47:30 -0800 Subject: [PATCH 40/82] update tests for test attribute --- .../qsc_passes/src/test_attribute/tests.rs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/compiler/qsc_passes/src/test_attribute/tests.rs b/compiler/qsc_passes/src/test_attribute/tests.rs index 85cb292346..9411ac03c7 100644 --- a/compiler/qsc_passes/src/test_attribute/tests.rs +++ b/compiler/qsc_passes/src/test_attribute/tests.rs @@ -77,3 +77,34 @@ fn callable_cant_have_type_params() { "#]], ); } + +#[test] +fn callable_is_valid_test_callable() { + check( + indoc! {" + namespace test { + @Test() + operation A() : Unit { + + } + } + "}, + &expect![[r#" + Package: + Item 0 [0-64] (Public): + Namespace (Ident 5 [10-14] "test"): Item 1 + Item 1 [21-62] (Internal): + Parent: 0 + Test + Callable 0 [33-62] (operation): + name: Ident 1 [43-44] "A" + input: Pat 2 [44-46] [Type Unit]: Unit + output: Unit + functors: empty set + body: SpecDecl 3 [33-62]: Impl: + Block 4 [54-62]: + adj: + ctl: + ctl-adj: "#]], + ); +} From 8e3c9a409d84b7bae8163969acaabd65a9cdf2a8 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Wed, 1 Jan 2025 16:41:08 -0800 Subject: [PATCH 41/82] wip --- language_service/src/compilation.rs | 7 ++ language_service/src/lib.rs | 6 +- language_service/src/protocol.rs | 6 ++ language_service/src/state.rs | 12 ++- language_service/src/state/tests.rs | 155 ++++++++++++++++++++++------ language_service/src/test_utils.rs | 3 + language_service/src/tests.rs | 22 ++-- wasm/src/language_service.rs | 49 ++++++++- 8 files changed, 213 insertions(+), 47 deletions(-) diff --git a/language_service/src/compilation.rs b/language_service/src/compilation.rs index 253eab560e..13ef56a1dd 100644 --- a/language_service/src/compilation.rs +++ b/language_service/src/compilation.rs @@ -37,6 +37,7 @@ pub(crate) struct Compilation { pub compile_errors: Vec, pub kind: CompilationKind, pub dependencies: FxHashMap>, + pub test_cases: Vec<(String, Span)>, } #[derive(Debug)] @@ -102,6 +103,8 @@ impl Compilation { run_linter_passes(&mut compile_errors, &package_store, unit, lints_config); + let test_cases = unit.package.get_test_callables(); + Self { package_store, user_package_id: package_id, @@ -111,6 +114,7 @@ impl Compilation { compile_errors, project_errors, dependencies: user_code_dependencies.into_iter().collect(), + test_cases, } } @@ -218,12 +222,15 @@ impl Compilation { .chain(once((source_package_id, None))) .collect(); + let test_cases = unit.package.get_test_callables(); + Self { package_store, user_package_id: package_id, compile_errors: errors, project_errors: project.as_ref().map_or_else(Vec::new, |p| p.errors.clone()), kind: CompilationKind::Notebook { project }, + test_cases, dependencies, } } diff --git a/language_service/src/lib.rs b/language_service/src/lib.rs index 983a58a8bb..c511f03c5f 100644 --- a/language_service/src/lib.rs +++ b/language_service/src/lib.rs @@ -26,7 +26,7 @@ use futures_util::StreamExt; use log::{trace, warn}; use protocol::{ CodeAction, CodeLens, CompletionList, DiagnosticUpdate, Hover, NotebookMetadata, SignatureHelp, - TextEdit, WorkspaceConfigurationUpdate, + TestCallables, TextEdit, WorkspaceConfigurationUpdate, }; use qsc::{ line_column::{Encoding, Position, Range}, @@ -67,6 +67,9 @@ impl LanguageService { pub fn create_update_worker<'a>( &mut self, diagnostics_receiver: impl Fn(DiagnosticUpdate) + 'a, + // Callback which receives detected test callables and does something with them + // in the case of VS Code, updates the test explorer with them + test_callable_receiver: impl Fn(TestCallables) + 'a, project_host: impl JSProjectHost + 'static, ) -> UpdateWorker<'a> { assert!(self.state_updater.is_none()); @@ -75,6 +78,7 @@ impl LanguageService { updater: CompilationStateUpdater::new( self.state.clone(), diagnostics_receiver, + test_callable_receiver, project_host, ), recv, diff --git a/language_service/src/protocol.rs b/language_service/src/protocol.rs index 4a75b701ca..1b7e039fc6 100644 --- a/language_service/src/protocol.rs +++ b/language_service/src/protocol.rs @@ -33,6 +33,12 @@ pub struct DiagnosticUpdate { pub errors: Vec, } +#[derive(Debug)] +pub struct TestCallables { + pub callables: Vec<(String, qsc::Span)>, + pub version: Option, +} + #[derive(Debug)] pub enum CodeActionKind { Empty, diff --git a/language_service/src/state.rs b/language_service/src/state.rs index 3eaed8c13c..c271cabd51 100644 --- a/language_service/src/state.rs +++ b/language_service/src/state.rs @@ -5,12 +5,12 @@ mod tests; use super::compilation::Compilation; -use super::protocol::{DiagnosticUpdate, NotebookMetadata}; -use crate::protocol::{ErrorKind, WorkspaceConfigurationUpdate}; +use super::protocol::{ + DiagnosticUpdate, ErrorKind, NotebookMetadata, TestCallables, WorkspaceConfigurationUpdate, +}; use log::{debug, trace}; use miette::Diagnostic; -use qsc::{compile, project}; -use qsc::{target::Profile, LanguageFeatures, PackageType}; +use qsc::{compile, project, target::Profile, LanguageFeatures, PackageType}; use qsc_linter::LintConfig; use qsc_project::{FileSystemAsync, JSProjectHost, PackageCache, Project}; use rustc_hash::{FxHashMap, FxHashSet}; @@ -102,6 +102,8 @@ pub(super) struct CompilationStateUpdater<'a> { /// Callback which will receive diagnostics (compilation errors) /// whenever a (re-)compilation occurs. diagnostics_receiver: Box, + /// Callback which will receive test callables whenever a (re-)compilation occurs. + test_callable_receiver: Box, cache: RefCell, /// Functions to interact with the host filesystem for project system operations. project_host: Box, @@ -111,6 +113,7 @@ impl<'a> CompilationStateUpdater<'a> { pub fn new( state: Rc>, diagnostics_receiver: impl Fn(DiagnosticUpdate) + 'a, + test_callable_receiver: impl Fn(TestCallables) + 'a, project_host: impl JSProjectHost + 'static, ) -> Self { Self { @@ -118,6 +121,7 @@ impl<'a> CompilationStateUpdater<'a> { configuration: Configuration::default(), documents_with_errors: FxHashSet::default(), diagnostics_receiver: Box::new(diagnostics_receiver), + test_callable_receiver: Box::new(test_callable_receiver), cache: RefCell::default(), project_host: Box::new(project_host), } diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index 2cf949e2c0..859d519d9c 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -6,7 +6,9 @@ use super::{CompilationState, CompilationStateUpdater}; use crate::{ - protocol::{DiagnosticUpdate, ErrorKind, NotebookMetadata, WorkspaceConfigurationUpdate}, + protocol::{ + DiagnosticUpdate, ErrorKind, NotebookMetadata, TestCallables, WorkspaceConfigurationUpdate, + }, tests::test_fs::{dir, file, FsNode, TestProjectHost}, }; use expect_test::{expect, Expect}; @@ -17,7 +19,8 @@ use std::{cell::RefCell, fmt::Write, rc::Rc}; #[tokio::test] async fn no_error() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); updater .update_document( @@ -38,7 +41,8 @@ async fn no_error() { #[tokio::test] async fn clear_error() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); updater .update_document("single/foo.qs", 1, "namespace {") @@ -107,7 +111,8 @@ async fn clear_error() { #[tokio::test] async fn close_last_doc_in_project() { let received_errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&received_errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&received_errors, &test_cases); updater .update_document( @@ -216,7 +221,9 @@ async fn close_last_doc_in_project() { #[tokio::test] async fn clear_on_document_close() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + + let mut updater = new_updater(&errors, &test_cases); updater .update_document("single/foo.qs", 1, "namespace {") @@ -277,7 +284,8 @@ async fn clear_on_document_close() { #[tokio::test] async fn compile_error() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); updater .update_document("single/foo.qs", 1, "badsyntax") @@ -320,7 +328,8 @@ async fn compile_error() { #[tokio::test] async fn rca_errors_are_reported_when_compilation_succeeds() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); updater.update_configuration(WorkspaceConfigurationUpdate { target_profile: Some(Profile::AdaptiveRI), @@ -374,7 +383,8 @@ async fn rca_errors_are_reported_when_compilation_succeeds() { #[tokio::test] async fn base_profile_rca_errors_are_reported_when_compilation_succeeds() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); updater.update_configuration(WorkspaceConfigurationUpdate { target_profile: Some(Profile::Base), @@ -438,7 +448,8 @@ async fn base_profile_rca_errors_are_reported_when_compilation_succeeds() { #[tokio::test] async fn package_type_update_causes_error() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); updater.update_configuration(WorkspaceConfigurationUpdate { package_type: Some(PackageType::Lib), @@ -491,7 +502,8 @@ async fn package_type_update_causes_error() { #[tokio::test] async fn target_profile_update_fixes_error() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); updater.update_configuration(WorkspaceConfigurationUpdate { target_profile: Some(Profile::Base), @@ -559,7 +571,8 @@ async fn target_profile_update_fixes_error() { #[tokio::test] async fn target_profile_update_causes_error_in_stdlib() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); updater.update_document( "single/foo.qs", @@ -610,7 +623,8 @@ async fn target_profile_update_causes_error_in_stdlib() { #[tokio::test] async fn notebook_document_no_errors() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); updater .update_notebook_document( @@ -635,7 +649,8 @@ async fn notebook_document_no_errors() { #[tokio::test] async fn notebook_document_errors() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); updater .update_notebook_document( @@ -697,7 +712,8 @@ async fn notebook_document_errors() { #[tokio::test] async fn notebook_document_lints() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); updater .update_notebook_document( @@ -779,7 +795,8 @@ async fn notebook_document_lints() { #[tokio::test] async fn notebook_update_remove_cell_clears_errors() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); updater .update_notebook_document( @@ -863,7 +880,8 @@ async fn notebook_update_remove_cell_clears_errors() { #[tokio::test] async fn close_notebook_clears_errors() { let errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); updater .update_notebook_document( @@ -960,7 +978,9 @@ async fn update_notebook_with_valid_dependencies() { let fs = Rc::new(RefCell::new(fs)); let errors = RefCell::new(Vec::new()); - let mut updater = new_updater_with_file_system(&errors, &fs); + let test_cases = RefCell::new(Vec::new()); + + let mut updater = new_updater_with_file_system(&errors, &test_cases, &fs); updater .update_notebook_document( @@ -1005,7 +1025,9 @@ async fn update_notebook_reports_errors_from_dependencies() { let fs = Rc::new(RefCell::new(fs)); let errors = RefCell::new(Vec::new()); - let mut updater = new_updater_with_file_system(&errors, &fs); + let test_cases = RefCell::new(Vec::new()); + + let mut updater = new_updater_with_file_system(&errors, &test_cases, &fs); updater .update_notebook_document( @@ -1154,7 +1176,9 @@ async fn update_notebook_reports_errors_from_dependency_of_dependencies() { let fs = Rc::new(RefCell::new(fs)); let errors = RefCell::new(Vec::new()); - let mut updater = new_updater_with_file_system(&errors, &fs); + let test_cases = RefCell::new(Vec::new()); + + let mut updater = new_updater_with_file_system(&errors, &test_cases, &fs); updater .update_notebook_document( @@ -1204,7 +1228,8 @@ async fn update_notebook_reports_errors_from_dependency_of_dependencies() { #[tokio::test] async fn update_doc_updates_project() { let received_errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&received_errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&received_errors, &test_cases); updater .update_document( @@ -1301,7 +1326,8 @@ async fn update_doc_updates_project() { #[tokio::test] async fn close_doc_prioritizes_fs() { let received_errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&received_errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&received_errors, &test_cases); updater .update_document( @@ -1395,7 +1421,8 @@ async fn close_doc_prioritizes_fs() { #[tokio::test] async fn delete_manifest() { let received_errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&received_errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&received_errors, &test_cases); updater .update_document( @@ -1480,7 +1507,8 @@ async fn delete_manifest() { #[tokio::test] async fn delete_manifest_then_close() { let received_errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&received_errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&received_errors, &test_cases); updater .update_document( @@ -1539,7 +1567,8 @@ async fn delete_manifest_then_close() { #[tokio::test] async fn doc_switches_project() { let received_errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&received_errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&received_errors, &test_cases); updater .update_document("nested_projects/src/subdir/src/a.qs", 1, "namespace A {}") @@ -1647,7 +1676,8 @@ async fn doc_switches_project() { #[tokio::test] async fn doc_switches_project_on_close() { let received_errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&received_errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&received_errors, &test_cases); updater .update_document("nested_projects/src/subdir/src/a.qs", 1, "namespace A {}") @@ -1771,7 +1801,9 @@ async fn loading_lints_config_from_manifest() { let fs = Rc::new(RefCell::new(fs)); let received_errors = RefCell::new(Vec::new()); - let updater = new_updater_with_file_system(&received_errors, &fs); + let test_cases = RefCell::new(Vec::new()); + + let updater = new_updater_with_file_system(&received_errors, &test_cases, &fs); // Check the LintConfig. check_lints_config( @@ -1823,7 +1855,9 @@ async fn lints_update_after_manifest_change() { let fs = Rc::new(RefCell::new(fs)); let received_errors = RefCell::new(Vec::new()); - let mut updater = new_updater_with_file_system(&received_errors, &fs); + let test_cases = RefCell::new(Vec::new()); + + let mut updater = new_updater_with_file_system(&received_errors, &test_cases, &fs); // Trigger a document update. updater @@ -1956,7 +1990,8 @@ async fn lints_prefer_workspace_over_defaults() { "namespace Foo { @EntryPoint() function Main() : Unit { let x = 5 / 0 + (2 ^ 4); } }"; let received_errors = RefCell::new(Vec::new()); - let mut updater = new_updater(&received_errors); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&received_errors, &test_cases); updater.update_configuration(WorkspaceConfigurationUpdate { lints_config: Some(vec![LintConfig { kind: LintKind::Ast(AstLint::DivisionByZero), @@ -2016,7 +2051,8 @@ async fn lints_prefer_manifest_over_workspace() { let fs = Rc::new(RefCell::new(fs)); let received_errors = RefCell::new(Vec::new()); - let mut updater = new_updater_with_file_system(&received_errors, &fs); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater_with_file_system(&received_errors, &test_cases, &fs); updater.update_configuration(WorkspaceConfigurationUpdate { lints_config: Some(vec![LintConfig { kind: LintKind::Ast(AstLint::DivisionByZero), @@ -2053,9 +2089,10 @@ async fn missing_dependency_reported() { let fs = Rc::new(RefCell::new(fs)); let received_errors = RefCell::new(Vec::new()); - let mut updater = new_updater_with_file_system(&received_errors, &fs); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater_with_file_system(&received_errors, &test_cases, &fs); - // Triger a document update. + // Trigger a document update. updater .update_document("parent/src/main.qs", 1, "function Main() : Unit {}") .await; @@ -2106,9 +2143,10 @@ async fn error_from_dependency_reported() { let fs = Rc::new(RefCell::new(fs)); let received_errors = RefCell::new(Vec::new()); - let mut updater = new_updater_with_file_system(&received_errors, &fs); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater_with_file_system(&received_errors, &test_cases, &fs); - // Triger a document update. + // Trigger a document update. updater .update_document("parent/src/main.qs", 1, "function Main() : Unit {}") .await; @@ -2143,6 +2181,39 @@ async fn error_from_dependency_reported() { .assert_debug_eq(&received_errors.borrow()); } +#[tokio::test] +async fn test_case_detected() { + let fs = FsNode::Dir( + [dir( + "parent", + [ + file("qsharp.json", r#"{}"#), + dir("src", [file("main.qs", "function MyTestCase() : Unit {}")]), + ], + )] + .into_iter() + .collect(), + ); + + let fs = Rc::new(RefCell::new(fs)); + let received_errors = RefCell::new(Vec::new()); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater_with_file_system(&received_errors, &test_cases, &fs); + + // Trigger a document update. + updater + .update_document( + "parent/src/main.qs", + 1, + "@Test() function MyTestCase() : Unit {}", + ) + .await; + + expect![[r#" + "#]] + .assert_debug_eq(&test_cases.borrow()); +} + type ErrorInfo = ( String, Option, @@ -2150,7 +2221,10 @@ type ErrorInfo = ( Vec, ); -fn new_updater(received_errors: &RefCell>) -> CompilationStateUpdater<'_> { +fn new_updater<'a>( + received_errors: &'a RefCell>, + received_test_cases: &'a RefCell>, +) -> CompilationStateUpdater<'a> { let diagnostic_receiver = move |update: DiagnosticUpdate| { let project_errors = update.errors.iter().filter_map(|error| match error { ErrorKind::Project(error) => Some(error.clone()), @@ -2171,9 +2245,15 @@ fn new_updater(received_errors: &RefCell>) -> CompilationStateUpd )); }; + let test_callable_receiver = move |update: TestCallables| { + let mut v = received_test_cases.borrow_mut(); + v.push(update); + }; + CompilationStateUpdater::new( Rc::new(RefCell::new(CompilationState::default())), diagnostic_receiver, + test_callable_receiver, TestProjectHost { fs: TEST_FS.with(Clone::clone), }, @@ -2182,6 +2262,7 @@ fn new_updater(received_errors: &RefCell>) -> CompilationStateUpd fn new_updater_with_file_system<'a>( received_errors: &'a RefCell>, + received_test_cases: &'a RefCell>, fs: &Rc>, ) -> CompilationStateUpdater<'a> { let diagnostic_receiver = move |update: DiagnosticUpdate| { @@ -2204,9 +2285,15 @@ fn new_updater_with_file_system<'a>( )); }; + let test_callable_receiver = move |update: TestCallables| { + let mut v = received_test_cases.borrow_mut(); + v.push(update); + }; + CompilationStateUpdater::new( Rc::new(RefCell::new(CompilationState::default())), diagnostic_receiver, + test_callable_receiver, TestProjectHost { fs: fs.clone() }, ) } diff --git a/language_service/src/test_utils.rs b/language_service/src/test_utils.rs index 5667e455d1..67ff4bb2b3 100644 --- a/language_service/src/test_utils.rs +++ b/language_service/src/test_utils.rs @@ -223,6 +223,7 @@ fn compile_project_with_markers_cursor_optional( LanguageFeatures::default(), ); + let test_cases = unit.package.get_test_callables(); let package_id = package_store.insert(unit); ( @@ -235,6 +236,7 @@ fn compile_project_with_markers_cursor_optional( compile_errors: errors, project_errors: Vec::new(), dependencies: dependencies.into_iter().collect(), + test_cases, }, cursor_location, target_spans, @@ -294,6 +296,7 @@ where kind: CompilationKind::Notebook { project: None }, project_errors: Vec::new(), dependencies: [(source_package_id, None)].into_iter().collect(), + test_cases: Default::default(), } } diff --git a/language_service/src/tests.rs b/language_service/src/tests.rs index 645c1467f9..66808bd36a 100644 --- a/language_service/src/tests.rs +++ b/language_service/src/tests.rs @@ -2,7 +2,7 @@ // Licensed under the MIT License. use crate::{ - protocol::{DiagnosticUpdate, ErrorKind}, + protocol::{DiagnosticUpdate, ErrorKind, TestCallables}, Encoding, LanguageService, UpdateWorker, }; use expect_test::{expect, Expect}; @@ -15,8 +15,9 @@ pub(crate) mod test_fs; #[tokio::test] async fn single_document() { let received_errors = RefCell::new(Vec::new()); + let test_cases = RefCell::new(Vec::new()); let mut ls = LanguageService::new(Encoding::Utf8); - let mut worker = create_update_worker(&mut ls, &received_errors); + let mut worker = create_update_worker(&mut ls, &received_errors, &test_cases); ls.update_document("foo.qs", 1, "namespace Foo { }"); @@ -49,8 +50,9 @@ async fn single_document() { #[allow(clippy::too_many_lines)] async fn single_document_update() { let received_errors = RefCell::new(Vec::new()); + let test_cases = RefCell::new(Vec::new()); let mut ls = LanguageService::new(Encoding::Utf8); - let mut worker = create_update_worker(&mut ls, &received_errors); + let mut worker = create_update_worker(&mut ls, &received_errors, &test_cases); ls.update_document("foo.qs", 1, "namespace Foo { }"); @@ -114,8 +116,9 @@ async fn single_document_update() { #[allow(clippy::too_many_lines)] async fn document_in_project() { let received_errors = RefCell::new(Vec::new()); + let test_cases = RefCell::new(Vec::new()); let mut ls = LanguageService::new(Encoding::Utf8); - let mut worker = create_update_worker(&mut ls, &received_errors); + let mut worker = create_update_worker(&mut ls, &received_errors, &test_cases); ls.update_document("project/src/this_file.qs", 1, "namespace Foo { }"); @@ -167,8 +170,9 @@ async fn document_in_project() { #[tokio::test] async fn completions_requested_before_document_load() { let errors = RefCell::new(Vec::new()); + let test_cases = RefCell::new(Vec::new()); let mut ls = LanguageService::new(Encoding::Utf8); - let _worker = create_update_worker(&mut ls, &errors); + let _worker = create_update_worker(&mut ls, &errors, &test_cases); ls.update_document( "foo.qs", @@ -195,8 +199,9 @@ async fn completions_requested_before_document_load() { #[tokio::test] async fn completions_requested_after_document_load() { let errors = RefCell::new(Vec::new()); + let test_cases = RefCell::new(Vec::new()); let mut ls = LanguageService::new(Encoding::Utf8); - let mut worker = create_update_worker(&mut ls, &errors); + let mut worker = create_update_worker(&mut ls, &errors, &test_cases); // this test is a contrast to `completions_requested_before_document_load` // we want to ensure that completions load when the update_document call has been awaited @@ -264,6 +269,7 @@ type ErrorInfo = ( fn create_update_worker<'a>( ls: &mut LanguageService, received_errors: &'a RefCell>, + received_test_cases: &'a RefCell>, ) -> UpdateWorker<'a> { let worker = ls.create_update_worker( |update: DiagnosticUpdate| { @@ -285,6 +291,10 @@ fn create_update_worker<'a>( project_errors.collect(), )); }, + move |update: TestCallables| { + let mut v = received_test_cases.borrow_mut(); + v.push(update); + }, TestProjectHost { fs: TEST_FS.with(Clone::clone), }, diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs index 5e60a3f9fb..a41cc67297 100644 --- a/wasm/src/language_service.rs +++ b/wasm/src/language_service.rs @@ -11,7 +11,7 @@ use qsc::{ self, line_column::Encoding, linter::LintConfig, target::Profile, LanguageFeatures, PackageType, }; use qsc_project::Manifest; -use qsls::protocol::DiagnosticUpdate; +use qsls::protocol::{DiagnosticUpdate, TestCallables}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -32,6 +32,7 @@ impl LanguageService { pub fn start_background_work( &mut self, diagnostics_callback: DiagnosticsCallback, + test_callables_callback: TestCallableCallback, host: ProjectHost, ) -> js_sys::Promise { let diagnostics_callback = @@ -58,7 +59,45 @@ impl LanguageService { ) .expect("callback should succeed"); }; - let mut worker = self.0.create_update_worker(diagnostics_callback, host); + + let test_callables_callback = test_callables_callback + .dyn_ref::() + .expect("expected a valid JS function") + .clone(); + + let test_callables_callback = move |update: TestCallables| { + let callables = update + .callables + .iter() + // TODO(sezna) -- convert span to Range + .map(|(name, _range)| -> (String, Range) { + ( + name.clone(), + crate::line_column::Range { + start: crate::line_column::Position { + line: 0, + character: 0, + }, + end: crate::line_column::Position { + line: 0, + character: 0, + }, + }, + ) + }) + .collect::>(); + + let _ = test_callables_callback + .call1( + &JsValue::NULL, + &serde_wasm_bindgen::to_value(&callables) + .expect("conversion to TestCallables should succeed"), + ) + .expect("callback should succeed"); + }; + let mut worker = + self.0 + .create_update_worker(diagnostics_callback, test_callables_callback, host); future_to_promise(async move { worker.run().await; @@ -587,3 +626,9 @@ extern "C" { )] pub type DiagnosticsCallback; } + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "(callables: [string, IRange][]) => void")] + pub type TestCallableCallback; +} From f18dc39163ac16d4bb5be0c5d206ac8318fb0b31 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 2 Jan 2025 14:05:25 -0800 Subject: [PATCH 42/82] wip: refactor: it works --- language_service/src/protocol.rs | 3 +- language_service/src/state.rs | 37 +++- npm/qsharp/src/browser.ts | 2 +- npm/qsharp/src/compiler/compiler.ts | 5 + .../src/language-service/language-service.ts | 27 ++- vscode/src/extension.ts | 2 - vscode/src/language-service/diagnostics.ts | 46 +++- vscode/src/testExplorer.ts | 204 ------------------ wasm/src/language_service.rs | 19 +- 9 files changed, 111 insertions(+), 234 deletions(-) delete mode 100644 vscode/src/testExplorer.ts diff --git a/language_service/src/protocol.rs b/language_service/src/protocol.rs index 1b7e039fc6..86dfb5bd81 100644 --- a/language_service/src/protocol.rs +++ b/language_service/src/protocol.rs @@ -3,6 +3,7 @@ use miette::Diagnostic; use qsc::line_column::Range; +use qsc::location::Location; use qsc::{compile, project}; use qsc::{linter::LintConfig, project::Manifest, target::Profile, LanguageFeatures, PackageType}; use thiserror::Error; @@ -35,7 +36,7 @@ pub struct DiagnosticUpdate { #[derive(Debug)] pub struct TestCallables { - pub callables: Vec<(String, qsc::Span)>, + pub callables: Vec<(String, Location)>, pub version: Option, } diff --git a/language_service/src/state.rs b/language_service/src/state.rs index c271cabd51..efbd55a79a 100644 --- a/language_service/src/state.rs +++ b/language_service/src/state.rs @@ -178,7 +178,7 @@ impl<'a> CompilationStateUpdater<'a> { self.insert_buffer_aware_compilation(project); - self.publish_diagnostics(); + self.publish_diagnostics_and_test_callables(); } /// Attempts to resolve a manifest for the given document uri. @@ -279,7 +279,7 @@ impl<'a> CompilationStateUpdater<'a> { } } - self.publish_diagnostics(); + self.publish_diagnostics_and_test_callables(); } /// Removes a document from the open documents map. If the @@ -376,7 +376,7 @@ impl<'a> CompilationStateUpdater<'a> { (compilation, notebook_configuration), ); }); - self.publish_diagnostics(); + self.publish_diagnostics_and_test_callables(); } pub(super) fn close_notebook_document(&mut self, notebook_uri: &str) { @@ -394,18 +394,19 @@ impl<'a> CompilationStateUpdater<'a> { state.compilations.remove(notebook_uri); }); - self.publish_diagnostics(); + self.publish_diagnostics_and_test_callables(); } // It gets really messy knowing when to clear diagnostics // when the document changes ownership between compilations, etc. // So let's do it the simplest way possible. Republish all the diagnostics every time. - fn publish_diagnostics(&mut self) { + fn publish_diagnostics_and_test_callables(&mut self) { let last_docs_with_errors = take(&mut self.documents_with_errors); let mut docs_with_errors = FxHashSet::default(); self.with_state(|state| { for (compilation_uri, compilation) in &state.compilations { + self.publish_test_callables(&compilation_uri, &compilation.0); trace!("publishing diagnostics for {compilation_uri}"); for (uri, errors) in map_errors_to_docs( @@ -507,7 +508,7 @@ impl<'a> CompilationStateUpdater<'a> { } }); - self.publish_diagnostics(); + self.publish_diagnostics_and_test_callables(); } /// Borrows the compilation state immutably and invokes `f`. @@ -537,6 +538,30 @@ impl<'a> CompilationStateUpdater<'a> { let mut state = self.state.borrow_mut(); f(&mut state) } + + fn publish_test_callables(&self, uri: &Arc, compilation: &Compilation) { + let callables = TestCallables { + callables: compilation + .test_cases + .iter() + .map(|(name, span)| { + ( + name.clone(), + // TODO(sezna) verify encoding + crate::qsc_utils::into_location( + qsc::line_column::Encoding::Utf16, + &compilation, + *span, + compilation.user_package_id, + ), + ) + }) + .collect(), + version: None, + }; + + (self.test_callable_receiver)(callables); + } } impl CompilationState { diff --git a/npm/qsharp/src/browser.ts b/npm/qsharp/src/browser.ts index 258d80176e..0834ba2616 100644 --- a/npm/qsharp/src/browser.ts +++ b/npm/qsharp/src/browser.ts @@ -173,7 +173,7 @@ export type { export { type Dump, type ShotResult } from "./compiler/common.js"; export { type CompilerState, type ProgramConfig } from "./compiler/compiler.js"; export { QscEventTarget } from "./compiler/events.js"; -export type { LanguageServiceEvent } from "./language-service/language-service.js"; +export type { LanguageServiceDiagnosticEvent, LanguageServiceEvent, LanguageServiceTestCallablesEvent } from "./language-service/language-service.js"; export { default as samples } from "./samples.generated.js"; export { log, type LogLevel, type TargetProfile }; export type { diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts index c3d9f93c22..542bbb7283 100644 --- a/npm/qsharp/src/compiler/compiler.ts +++ b/npm/qsharp/src/compiler/compiler.ts @@ -11,6 +11,8 @@ import { type VSDiagnostic, IProgramConfig, ITestDescriptor, + IRange, + ILocation, } from "../../lib/web/qsc_wasm.js"; import { log } from "../log.js"; import { @@ -125,6 +127,9 @@ export class Compiler implements ICompiler { (uri: string, version: number | undefined, errors: VSDiagnostic[]) => { diags = errors; }, + (callables: [string, ILocation][]) => { + // do nothing; test callables are not reported in checkCode + }, { readFile: async () => null, listDirectory: async () => [], diff --git a/npm/qsharp/src/language-service/language-service.ts b/npm/qsharp/src/language-service/language-service.ts index c27db6de25..cf914c3a11 100644 --- a/npm/qsharp/src/language-service/language-service.ts +++ b/npm/qsharp/src/language-service/language-service.ts @@ -26,8 +26,7 @@ import { } from "../workers/common.js"; type QscWasm = typeof import("../../lib/web/qsc_wasm.js"); -// Only one event type for now -export type LanguageServiceEvent = { +export type LanguageServiceDiagnosticEvent = { type: "diagnostics"; detail: { uri: string; @@ -36,6 +35,15 @@ export type LanguageServiceEvent = { }; }; +export type LanguageServiceTestCallablesEvent = { + type: "testCallables"; + detail: { + callables: [string, ILocation][]; + }; +}; + +export type LanguageServiceEvent = LanguageServiceDiagnosticEvent | LanguageServiceTestCallablesEvent; + // These need to be async/promise results for when communicating across a WebWorker, however // for running the compiler in the same thread the result will be synchronous (a resolved promise). export interface ILanguageService { @@ -127,6 +135,7 @@ export class QSharpLanguageService implements ILanguageService { this.backgroundWork = this.languageService.start_background_work( this.onDiagnostics.bind(this), + this.onTestCallables.bind(this), host, ); } @@ -274,6 +283,18 @@ export class QSharpLanguageService implements ILanguageService { log.error("Error in onDiagnostics", e); } } + + async onTestCallables(callables: [string, ILocation][]) { + try { + const event = new Event("testCallables") as LanguageServiceEvent & Event; + event.detail = { + callables, + }; + this.eventHandler.dispatchEvent(event); + } catch (e) { + log.error("Error in onTestCallables", e); + } + } } /** @@ -283,7 +304,7 @@ export class QSharpLanguageService implements ILanguageService { */ export const languageServiceProtocol: ServiceProtocol< ILanguageService, - LanguageServiceEvent + LanguageServiceDiagnosticEvent > = { class: QSharpLanguageService, methods: { diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index a22ffe892a..f642e6e6e2 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -28,7 +28,6 @@ import { initCodegen } from "./qirGeneration.js"; import { activateTargetProfileStatusBarItem } from "./statusbar.js"; import { initTelemetry } from "./telemetry.js"; import { registerWebViewCommands } from "./webviewPanel.js"; -import { initTestExplorer } from "./testExplorer.js"; export async function activate( context: vscode.ExtensionContext, @@ -78,7 +77,6 @@ export async function activate( context.subscriptions.push(...registerQSharpNotebookHandlers()); - initTestExplorer(context, eventEmitter.event); initAzureWorkspaces(context); initCodegen(context); activateDebugger(context); diff --git a/vscode/src/language-service/diagnostics.ts b/vscode/src/language-service/diagnostics.ts index cbd0e9ac1d..ed4e259aa5 100644 --- a/vscode/src/language-service/diagnostics.ts +++ b/vscode/src/language-service/diagnostics.ts @@ -5,9 +5,11 @@ import { ILanguageService, VSDiagnostic, qsharpLibraryUriScheme, + IRange, + ILocation, } from "qsharp-lang"; import * as vscode from "vscode"; -import { qsharpLanguageId, toVsCodeDiagnostic } from "../common"; +import { qsharpLanguageId, toVsCodeDiagnostic , toVsCodeLocation, toVsCodeRange} from "../common"; export function startLanguageServiceDiagnostics( languageService: ILanguageService, @@ -15,6 +17,11 @@ export function startLanguageServiceDiagnostics( const diagCollection = vscode.languages.createDiagnosticCollection(qsharpLanguageId); + const testController: vscode.TestController = vscode.tests.createTestController( + "qsharpTestController", + "Q# Tests", + ); + async function onDiagnostics(evt: { detail: { uri: string; @@ -36,14 +43,51 @@ export function startLanguageServiceDiagnostics( ); } + async function onTestCallables(evt: { + detail: { + callables: [string, ILocation][]; + }; + }) { + + for (const [id, testItem] of testController.items) { + testController.items.delete(id); + } + + + // break down the test callable into its parts, so we can construct + // the namespace hierarchy in the test explorer + for (const [ callableName, location ] of evt.detail.callables) { + const vscRange = toVsCodeLocation(location); + const parts = callableName.split("."); + + // for an individual test case, e.g. foo.bar.baz, create a hierarchy of items + let rover = testController.items; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const id = i === parts.length - 1 ? callableName : part; + if (!rover.get(part)) { + // TODO get URI of test + const testItem = testController.createTestItem(id, part, vscRange.uri); + testItem.range = vscRange.range; + rover.add(testItem); + } + rover = rover.get(id)!.children; + } + } + + } + languageService.addEventListener("diagnostics", onDiagnostics); + languageService.addEventListener("testCallables", onTestCallables); return [ { dispose: () => { languageService.removeEventListener("diagnostics", onDiagnostics); + languageService.removeEventListener("testCallables", onTestCallables); }, }, diagCollection, + testController, ]; } diff --git a/vscode/src/testExplorer.ts b/vscode/src/testExplorer.ts deleted file mode 100644 index b4b6887950..0000000000 --- a/vscode/src/testExplorer.ts +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// This file uses the VS Code Test Explorer API (https://code.visualstudio.com/docs/editor/testing) - -import * as vscode from "vscode"; -import { ICompilerWorker, log, ProgramConfig } from "qsharp-lang"; -import { getActiveProgram } from "./programConfig"; -import { - getCommonCompilerWorker, - isQsharpDocument, - toVsCodeLocation, - toVsCodeRange, -} from "./common"; -import { createDebugConsoleEventTarget } from "./debugger/output"; - -/** - * Constructs the handler to pass to the `TestController` that refreshes the discovered tests. - */ -function mkRefreshHandler( - ctrl: vscode.TestController, - context: vscode.ExtensionContext, -) { - /// if `uri` is null, then we are performing a full refresh and scanning the entire program - return async (uri: vscode.Uri | null = null) => { - log.trace("Refreshing tests for uri", uri?.toString()); - // clear out old tests - for (const [id, testItem] of ctrl.items) { - // if the uri is null, delete all test items, as we are going to repopulate - // all tests. - // if the uri is some value, and the test item is from this same URI, - // delete it because we are about to repopulate tests from that document. - if (uri === null || testItem.uri?.toString() == uri.toString()) { - ctrl.items.delete(id); - } - } - - const program = await getActiveProgram(); - if (!program.success) { - throw new Error(program.errorMsg); - } - - const programConfig = program.programConfig; - const worker = getCommonCompilerWorker(context); - const allTestCallables = await worker.getTestCallables(programConfig); - - // only update test callables from this Uri - const scopedTestCallables = - uri === null - ? allTestCallables - : allTestCallables.filter(({ location }) => { - const vscLocation = toVsCodeLocation(location); - return vscLocation.uri.toString() === uri.toString(); - }); - - // break down the test callable into its parts, so we can construct - // the namespace hierarchy in the test explorer - for (const { callableName, location } of scopedTestCallables) { - const vscLocation = toVsCodeLocation(location); - const parts = callableName.split("."); - - // for an individual test case, e.g. foo.bar.baz, create a hierarchy of items - let rover = ctrl.items; - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - const id = i === parts.length - 1 ? callableName : part; - if (!rover.get(part)) { - const testItem = ctrl.createTestItem(id, part, vscLocation.uri); - testItem.range = vscLocation.range; - rover.add(testItem); - } - rover = rover.get(id)!.children; - } - } - }; -} - -/** - * Initializes the test explorer with the Q# tests in the active document. - **/ -export async function initTestExplorer( - context: vscode.ExtensionContext, - updateDocumentEvent: vscode.Event, -) { - const ctrl: vscode.TestController = vscode.tests.createTestController( - "qsharpTestController", - "Q# Tests", - ); - context.subscriptions.push(ctrl); - - const refreshHandler = mkRefreshHandler(ctrl, context); - // initially populate tests - await refreshHandler(null); - - // when the refresh button is pressed, refresh all tests by passing in a null uri - ctrl.refreshHandler = () => refreshHandler(null); - - // when the language service detects an updateDocument, this event fires. - // we call the test refresher when that happens - updateDocumentEvent(refreshHandler); - - const runHandler = (request: vscode.TestRunRequest) => { - if (!request.continuous) { - return startTestRun(request); - } - }; - - // runs an individual test run - // or test group (a test run where there are child tests) - const startTestRun = async (request: vscode.TestRunRequest) => { - // use the compiler worker to run the test in the interpreter - - log.trace("Starting test run, request was", JSON.stringify(request)); - const worker = getCommonCompilerWorker(context); - - const programResult = await getActiveProgram(); - if (!programResult.success) { - throw new Error(programResult.errorMsg); - } - - const program = programResult.programConfig; - - for (const testCase of request.include || []) { - await runTestCase(ctrl, testCase, request, worker, program); - } - }; - - ctrl.createRunProfile( - "Interpreter", - vscode.TestRunProfileKind.Run, - runHandler, - true, - undefined, - false, - ); - - function updateNodeForDocument(e: vscode.TextDocument) { - if (!isQsharpDocument(e)) { - return; - } - } - - for (const document of vscode.workspace.textDocuments) { - updateNodeForDocument(document); - } - - context.subscriptions.push( - vscode.workspace.onDidOpenTextDocument(updateNodeForDocument), - vscode.workspace.onDidChangeTextDocument((e) => - updateNodeForDocument(e.document), - ), - ); -} - -/** - * Given a single test case, run it in the worker (which runs the interpreter) and report results back to the - * `TestController` as a side effect. - * - * This function manages its own event target for the results of the test run and uses the controller to render the output in the VS Code UI. - **/ -async function runTestCase( - ctrl: vscode.TestController, - testCase: vscode.TestItem, - request: vscode.TestRunRequest, - worker: ICompilerWorker, - program: ProgramConfig, -): Promise { - log.trace("Running Q# test: ", testCase.id); - if (testCase.children.size > 0) { - for (const childTestCase of testCase.children) { - await runTestCase(ctrl, childTestCase[1], request, worker, program); - } - return; - } - const run = ctrl.createTestRun(request); - const evtTarget = createDebugConsoleEventTarget((msg) => { - run.appendOutput(`${msg}\n`); - }); - evtTarget.addEventListener("Result", (msg) => { - if (msg.detail.success) { - run.passed(testCase); - } else { - const message: vscode.TestMessage = { - message: msg.detail.value.message, - location: { - range: toVsCodeRange(msg.detail.value.range), - uri: vscode.Uri.parse(msg.detail.value.uri || ""), - }, - }; - run.failed(testCase, message); - } - run.end(); - }); - - const callableExpr = `${testCase.id}()`; - - try { - await worker.run(program, callableExpr, 1, evtTarget); - } catch (error) { - log.error(`Error running test ${testCase.id}:`, error); - run.appendOutput(`Error running test ${testCase.id}: ${error}\r\n`); - } - log.trace("ran test:", testCase.id); -} diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs index a41cc67297..4d8dc85aa1 100644 --- a/wasm/src/language_service.rs +++ b/wasm/src/language_service.rs @@ -69,21 +69,8 @@ impl LanguageService { let callables = update .callables .iter() - // TODO(sezna) -- convert span to Range - .map(|(name, _range)| -> (String, Range) { - ( - name.clone(), - crate::line_column::Range { - start: crate::line_column::Position { - line: 0, - character: 0, - }, - end: crate::line_column::Position { - line: 0, - character: 0, - }, - }, - ) + .map(|(name, location)| -> (String, Location) { + (name.clone(), location.clone().into()) }) .collect::>(); @@ -629,6 +616,6 @@ extern "C" { #[wasm_bindgen] extern "C" { - #[wasm_bindgen(typescript_type = "(callables: [string, IRange][]) => void")] + #[wasm_bindgen(typescript_type = "(callables: [string, ILocation][]) => void")] pub type TestCallableCallback; } From cf35b190f30a298fa26f5502e927ca75174b7f87 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 2 Jan 2025 14:26:27 -0800 Subject: [PATCH 43/82] Remove comment --- vscode/src/language-service/diagnostics.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/vscode/src/language-service/diagnostics.ts b/vscode/src/language-service/diagnostics.ts index ed4e259aa5..936904fe33 100644 --- a/vscode/src/language-service/diagnostics.ts +++ b/vscode/src/language-service/diagnostics.ts @@ -66,7 +66,6 @@ export function startLanguageServiceDiagnostics( const part = parts[i]; const id = i === parts.length - 1 ? callableName : part; if (!rover.get(part)) { - // TODO get URI of test const testItem = testController.createTestItem(id, part, vscRange.uri); testItem.range = vscRange.range; rover.add(testItem); From 36abbd1f0f854a42fdbdcde295639c1ef3b2c941 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 2 Jan 2025 14:29:49 -0800 Subject: [PATCH 44/82] wip --- vscode/src/language-service/diagnostics.ts | 41 +++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/vscode/src/language-service/diagnostics.ts b/vscode/src/language-service/diagnostics.ts index 936904fe33..49e3bd0733 100644 --- a/vscode/src/language-service/diagnostics.ts +++ b/vscode/src/language-service/diagnostics.ts @@ -7,9 +7,11 @@ import { qsharpLibraryUriScheme, IRange, ILocation, + log, } from "qsharp-lang"; import * as vscode from "vscode"; -import { qsharpLanguageId, toVsCodeDiagnostic , toVsCodeLocation, toVsCodeRange} from "../common"; +import { getCommonCompilerWorker, qsharpLanguageId, toVsCodeDiagnostic , toVsCodeLocation, toVsCodeRange} from "../common"; +import { getActiveProgram } from "../programConfig"; export function startLanguageServiceDiagnostics( languageService: ILanguageService, @@ -43,6 +45,43 @@ export function startLanguageServiceDiagnostics( ); } + // test explorer features + + const runHandler = (request: vscode.TestRunRequest) => { + if (!request.continuous) { + return startTestRun(request); + } + }; + + // runs an individual test run + // or test group (a test run where there are child tests) + const startTestRun = async (request: vscode.TestRunRequest) => { + // use the compiler worker to run the test in the interpreter + + log.trace("Starting test run, request was", JSON.stringify(request)); + const worker = getCommonCompilerWorker(context); + + const programResult = await getActiveProgram(); + if (!programResult.success) { + throw new Error(programResult.errorMsg); + } + + const program = programResult.programConfig; + + for (const testCase of request.include || []) { + await runTestCase(testController, testCase, request, worker, program); + } + }; + + testController.createRunProfile( + "Interpreter", + vscode.TestRunProfileKind.Run, + runHandler, + true, + undefined, + false, + ); + async function onTestCallables(evt: { detail: { callables: [string, ILocation][]; From bf8b277b7a26503609778059d5413fce87d15cb3 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 2 Jan 2025 14:30:33 -0800 Subject: [PATCH 45/82] wip --- vscode/src/language-service/diagnostics.ts | 55 ++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/vscode/src/language-service/diagnostics.ts b/vscode/src/language-service/diagnostics.ts index 49e3bd0733..f6b90d793f 100644 --- a/vscode/src/language-service/diagnostics.ts +++ b/vscode/src/language-service/diagnostics.ts @@ -8,10 +8,13 @@ import { IRange, ILocation, log, + ICompilerWorker, + ProgramConfig, } from "qsharp-lang"; import * as vscode from "vscode"; import { getCommonCompilerWorker, qsharpLanguageId, toVsCodeDiagnostic , toVsCodeLocation, toVsCodeRange} from "../common"; import { getActiveProgram } from "../programConfig"; +import { createDebugConsoleEventTarget } from "../debugger/output"; export function startLanguageServiceDiagnostics( languageService: ILanguageService, @@ -73,6 +76,58 @@ export function startLanguageServiceDiagnostics( } }; + /** + * Given a single test case, run it in the worker (which runs the interpreter) and report results back to the + * `TestController` as a side effect. + * + * This function manages its own event target for the results of the test run and uses the controller to render the output in the VS Code UI. + **/ + async function runTestCase( + ctrl: vscode.TestController, + testCase: vscode.TestItem, + request: vscode.TestRunRequest, + worker: ICompilerWorker, + program: ProgramConfig, + ): Promise { + log.trace("Running Q# test: ", testCase.id); + if (testCase.children.size > 0) { + for (const childTestCase of testCase.children) { + await runTestCase(ctrl, childTestCase[1], request, worker, program); + } + return; + } + const run = ctrl.createTestRun(request); + const evtTarget = createDebugConsoleEventTarget((msg) => { + run.appendOutput(`${msg}\n`); + }); + evtTarget.addEventListener("Result", (msg) => { + if (msg.detail.success) { + run.passed(testCase); + } else { + const message: vscode.TestMessage = { + message: msg.detail.value.message, + location: { + range: toVsCodeRange(msg.detail.value.range), + uri: vscode.Uri.parse(msg.detail.value.uri || ""), + }, + }; + run.failed(testCase, message); + } + run.end(); + }); + + const callableExpr = `${testCase.id}()`; + + try { + await worker.run(program, callableExpr, 1, evtTarget); + } catch (error) { + log.error(`Error running test ${testCase.id}:`, error); + run.appendOutput(`Error running test ${testCase.id}: ${error}\r\n`); + } + log.trace("ran test:", testCase.id); + } + + testController.createRunProfile( "Interpreter", vscode.TestRunProfileKind.Run, From ff31000278b05d9afbc888f152bf70f78409dfe8 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 7 Jan 2025 10:48:38 -0800 Subject: [PATCH 46/82] individual test parity tracking works --- vscode/src/extension.ts | 4 +- vscode/src/language-service/activate.ts | 27 +++----- vscode/src/language-service/diagnostics.ts | 71 ++++++++++++++++++---- 3 files changed, 69 insertions(+), 33 deletions(-) diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index f642e6e6e2..e942dbb182 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -67,10 +67,8 @@ export async function activate( context.subscriptions.push(...activateTargetProfileStatusBarItem()); - const eventEmitter = new vscode.EventEmitter(); - context.subscriptions.push( - ...(await activateLanguageService(context.extensionUri, eventEmitter)), + ...(await activateLanguageService(context)), ); context.subscriptions.push(...startOtherQSharpDiagnostics()); diff --git a/vscode/src/language-service/activate.ts b/vscode/src/language-service/activate.ts index 823a55ed6b..abb0a31970 100644 --- a/vscode/src/language-service/activate.ts +++ b/vscode/src/language-service/activate.ts @@ -40,22 +40,21 @@ import { createSignatureHelpProvider } from "./signature.js"; /** * Returns all of the subscriptions that should be registered for the language service. - * Additionally, if an `eventEmitter` is passed in, will fire an event when a document is updated. */ export async function activateLanguageService( - extensionUri: vscode.Uri, - eventEmitter?: vscode.EventEmitter, + context: vscode.ExtensionContext, ): Promise { + const extensionUri = context.extensionUri; const subscriptions: vscode.Disposable[] = []; const languageService = await loadLanguageService(extensionUri); // diagnostics - subscriptions.push(...startLanguageServiceDiagnostics(languageService)); + subscriptions.push(...startLanguageServiceDiagnostics(languageService, context)); // synchronize document contents subscriptions.push( - ...registerDocumentUpdateHandlers(languageService, eventEmitter), + ...registerDocumentUpdateHandlers(languageService), ); // synchronize notebook cell contents @@ -186,10 +185,9 @@ async function loadLanguageService( */ function registerDocumentUpdateHandlers( languageService: ILanguageService, - eventEmitter?: vscode.EventEmitter, ): vscode.Disposable[] { vscode.workspace.textDocuments.forEach((document) => { - updateIfQsharpDocument(document, eventEmitter); + updateIfQsharpDocument(document); }); // we manually send an OpenDocument telemetry event if this is a Q# document, because the @@ -222,13 +220,13 @@ function registerDocumentUpdateHandlers( { linesOfCode: document.lineCount }, ); } - updateIfQsharpDocument(document, eventEmitter); + updateIfQsharpDocument(document); }), ); subscriptions.push( vscode.workspace.onDidChangeTextDocument((evt) => { - updateIfQsharpDocument(evt.document, eventEmitter); + updateIfQsharpDocument(evt.document); }), ); @@ -271,7 +269,7 @@ function registerDocumentUpdateHandlers( // Check that the document is on the same project as the manifest. document.fileName.startsWith(project_folder) ) { - updateIfQsharpDocument(document, eventEmitter); + updateIfQsharpDocument(document); } }); } @@ -279,7 +277,6 @@ function registerDocumentUpdateHandlers( function updateIfQsharpDocument( document: vscode.TextDocument, - emitter?: vscode.EventEmitter, ) { if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { // Regular (not notebook) Q# document. @@ -288,14 +285,6 @@ function registerDocumentUpdateHandlers( document.version, document.getText(), ); - - if (emitter) { - // this is used to trigger functionality outside of the language service. - // by firing an event here, we unify the points at which the language service - // recognizes an "update document" and when subscribers to the event react, avoiding - // multiple implementations of the same logic. - emitter.fire(document.uri); - } } } diff --git a/vscode/src/language-service/diagnostics.ts b/vscode/src/language-service/diagnostics.ts index f6b90d793f..e5134fd648 100644 --- a/vscode/src/language-service/diagnostics.ts +++ b/vscode/src/language-service/diagnostics.ts @@ -10,14 +10,17 @@ import { log, ICompilerWorker, ProgramConfig, + getCompilerWorker, } from "qsharp-lang"; import * as vscode from "vscode"; -import { getCommonCompilerWorker, qsharpLanguageId, toVsCodeDiagnostic , toVsCodeLocation, toVsCodeRange} from "../common"; +import { qsharpLanguageId, toVsCodeDiagnostic, toVsCodeLocation, toVsCodeRange, getCommonCompilerWorker } from "../common"; import { getActiveProgram } from "../programConfig"; -import { createDebugConsoleEventTarget } from "../debugger/output"; +import { createDebugConsoleEventTarget } from "../debugger/output" + export function startLanguageServiceDiagnostics( languageService: ILanguageService, + context: vscode.ExtensionContext, ): vscode.Disposable[] { const diagCollection = vscode.languages.createDiagnosticCollection(qsharpLanguageId); @@ -55,8 +58,8 @@ export function startLanguageServiceDiagnostics( return startTestRun(request); } }; - - // runs an individual test run + + // runs an individual test run // or test group (a test run where there are child tests) const startTestRun = async (request: vscode.TestRunRequest) => { // use the compiler worker to run the test in the interpreter @@ -115,9 +118,9 @@ export function startLanguageServiceDiagnostics( } run.end(); }); - + const callableExpr = `${testCase.id}()`; - + try { await worker.run(program, callableExpr, 1, evtTarget); } catch (error) { @@ -126,7 +129,7 @@ export function startLanguageServiceDiagnostics( } log.trace("ran test:", testCase.id); } - + testController.createRunProfile( "Interpreter", @@ -137,20 +140,28 @@ export function startLanguageServiceDiagnostics( false, ); + const testMetadata = new WeakMap(); + + async function onTestCallables(evt: { detail: { callables: [string, ILocation][]; }; }) { - + let current_parity = 0; for (const [id, testItem] of testController.items) { - testController.items.delete(id); + // set the parity of this run + const discoveredParity = testMetadata.get(testItem) || 0; + // increment the parity + current_parity = discoveredParity + 1; + break; } + // use a parity check to find old items // break down the test callable into its parts, so we can construct // the namespace hierarchy in the test explorer - for (const [ callableName, location ] of evt.detail.callables) { + for (const [callableName, location] of evt.detail.callables) { const vscRange = toVsCodeLocation(location); const parts = callableName.split("."); @@ -159,17 +170,35 @@ export function startLanguageServiceDiagnostics( for (let i = 0; i < parts.length; i++) { const part = parts[i]; const id = i === parts.length - 1 ? callableName : part; - if (!rover.get(part)) { + const discoveredTestItem = rover.get(part); + if (!discoveredTestItem) { const testItem = testController.createTestItem(id, part, vscRange.uri); testItem.range = vscRange.range; + testMetadata.set(testItem, current_parity); rover.add(testItem); + } else { + testMetadata.set(discoveredTestItem, current_parity); } + rover = rover.get(id)!.children; } } + + // delete old items + deleteItemsNotOfParity(current_parity, testController.items, testController); } + function deleteItemsNotOfParity(parity: number, items: vscode.TestItemCollection, testController: vscode.TestController) { + for (const [id, testItem] of items) { + deleteItemsNotOfParity(parity, testItem.children, testController) + if (testMetadata.get(testItem) !== parity) { + items.delete(id); + } + } + } + + languageService.addEventListener("diagnostics", onDiagnostics); languageService.addEventListener("testCallables", onTestCallables); @@ -184,3 +213,23 @@ export function startLanguageServiceDiagnostics( testController, ]; } + + + +let worker: ICompilerWorker | null = null; + +function getLocalCompilerWorker( + context: vscode.ExtensionContext, +): ICompilerWorker { + if (worker !== null) { + return worker; + } + + const compilerWorkerScriptPath = vscode.Uri.joinPath( + context.extensionUri, + "./out/compilerWorker.js", + ).toString(); + worker = getCompilerWorker(compilerWorkerScriptPath); + + return worker; +} From b72ca758ce56a35d8bdf918949f4bead0e9dffc6 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 7 Jan 2025 10:50:43 -0800 Subject: [PATCH 47/82] Rename from parity to version --- vscode/src/language-service/diagnostics.ts | 23 +++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/vscode/src/language-service/diagnostics.ts b/vscode/src/language-service/diagnostics.ts index e5134fd648..43c180ae2e 100644 --- a/vscode/src/language-service/diagnostics.ts +++ b/vscode/src/language-service/diagnostics.ts @@ -148,16 +148,15 @@ export function startLanguageServiceDiagnostics( callables: [string, ILocation][]; }; }) { - let current_parity = 0; + let currentVersion = 0; for (const [id, testItem] of testController.items) { - // set the parity of this run - const discoveredParity = testMetadata.get(testItem) || 0; - // increment the parity - current_parity = discoveredParity + 1; + // set the version of this run + const discoveredVersion = testMetadata.get(testItem) || 0; + // increment the version + currentVersion = (discoveredVersion + 1) % 1000; break; } - // use a parity check to find old items // break down the test callable into its parts, so we can construct // the namespace hierarchy in the test explorer @@ -174,10 +173,10 @@ export function startLanguageServiceDiagnostics( if (!discoveredTestItem) { const testItem = testController.createTestItem(id, part, vscRange.uri); testItem.range = vscRange.range; - testMetadata.set(testItem, current_parity); + testMetadata.set(testItem, currentVersion); rover.add(testItem); } else { - testMetadata.set(discoveredTestItem, current_parity); + testMetadata.set(discoveredTestItem, currentVersion); } rover = rover.get(id)!.children; @@ -186,13 +185,13 @@ export function startLanguageServiceDiagnostics( // delete old items - deleteItemsNotOfParity(current_parity, testController.items, testController); + deleteItemsNotOfVersion(currentVersion, testController.items, testController); } - function deleteItemsNotOfParity(parity: number, items: vscode.TestItemCollection, testController: vscode.TestController) { + function deleteItemsNotOfVersion(version: number, items: vscode.TestItemCollection, testController: vscode.TestController) { for (const [id, testItem] of items) { - deleteItemsNotOfParity(parity, testItem.children, testController) - if (testMetadata.get(testItem) !== parity) { + deleteItemsNotOfVersion(version, testItem.children, testController) + if (testMetadata.get(testItem) !== version) { items.delete(id); } } From f52330d09d0f75e033862a5a8234e943e27fb12e Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 7 Jan 2025 11:03:51 -0800 Subject: [PATCH 48/82] pull test explorer out into its own file --- npm/qsharp/src/browser.ts | 6 +- npm/qsharp/src/compiler/compiler.ts | 2 +- .../src/language-service/language-service.ts | 4 +- vscode/src/extension.ts | 4 +- vscode/src/language-service/activate.ts | 14 +- vscode/src/language-service/diagnostics.ts | 193 +----------------- vscode/src/language-service/testExplorer.ts | 180 ++++++++++++++++ 7 files changed, 204 insertions(+), 199 deletions(-) create mode 100644 vscode/src/language-service/testExplorer.ts diff --git a/npm/qsharp/src/browser.ts b/npm/qsharp/src/browser.ts index 0834ba2616..dd0e557f95 100644 --- a/npm/qsharp/src/browser.ts +++ b/npm/qsharp/src/browser.ts @@ -173,7 +173,11 @@ export type { export { type Dump, type ShotResult } from "./compiler/common.js"; export { type CompilerState, type ProgramConfig } from "./compiler/compiler.js"; export { QscEventTarget } from "./compiler/events.js"; -export type { LanguageServiceDiagnosticEvent, LanguageServiceEvent, LanguageServiceTestCallablesEvent } from "./language-service/language-service.js"; +export type { + LanguageServiceDiagnosticEvent, + LanguageServiceEvent, + LanguageServiceTestCallablesEvent, +} from "./language-service/language-service.js"; export { default as samples } from "./samples.generated.js"; export { log, type LogLevel, type TargetProfile }; export type { diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts index 542bbb7283..d88905fceb 100644 --- a/npm/qsharp/src/compiler/compiler.ts +++ b/npm/qsharp/src/compiler/compiler.ts @@ -127,7 +127,7 @@ export class Compiler implements ICompiler { (uri: string, version: number | undefined, errors: VSDiagnostic[]) => { diags = errors; }, - (callables: [string, ILocation][]) => { + (callables: [string, ILocation][]) => { // do nothing; test callables are not reported in checkCode }, { diff --git a/npm/qsharp/src/language-service/language-service.ts b/npm/qsharp/src/language-service/language-service.ts index cf914c3a11..781715df0f 100644 --- a/npm/qsharp/src/language-service/language-service.ts +++ b/npm/qsharp/src/language-service/language-service.ts @@ -42,7 +42,9 @@ export type LanguageServiceTestCallablesEvent = { }; }; -export type LanguageServiceEvent = LanguageServiceDiagnosticEvent | LanguageServiceTestCallablesEvent; +export type LanguageServiceEvent = + | LanguageServiceDiagnosticEvent + | LanguageServiceTestCallablesEvent; // These need to be async/promise results for when communicating across a WebWorker, however // for running the compiler in the same thread the result will be synchronous (a resolved promise). diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index e942dbb182..d5b54f6eb9 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -67,9 +67,7 @@ export async function activate( context.subscriptions.push(...activateTargetProfileStatusBarItem()); - context.subscriptions.push( - ...(await activateLanguageService(context)), - ); + context.subscriptions.push(...(await activateLanguageService(context))); context.subscriptions.push(...startOtherQSharpDiagnostics()); diff --git a/vscode/src/language-service/activate.ts b/vscode/src/language-service/activate.ts index abb0a31970..da55d3f513 100644 --- a/vscode/src/language-service/activate.ts +++ b/vscode/src/language-service/activate.ts @@ -37,6 +37,7 @@ import { registerQSharpNotebookCellUpdateHandlers } from "./notebook.js"; import { createReferenceProvider } from "./references.js"; import { createRenameProvider } from "./rename.js"; import { createSignatureHelpProvider } from "./signature.js"; +import { startTestDiscovery } from "./testExplorer.js"; /** * Returns all of the subscriptions that should be registered for the language service. @@ -50,12 +51,13 @@ export async function activateLanguageService( const languageService = await loadLanguageService(extensionUri); // diagnostics - subscriptions.push(...startLanguageServiceDiagnostics(languageService, context)); + subscriptions.push(...startLanguageServiceDiagnostics(languageService)); + + // test explorer + subscriptions.push(...startTestDiscovery(languageService, context)); // synchronize document contents - subscriptions.push( - ...registerDocumentUpdateHandlers(languageService), - ); + subscriptions.push(...registerDocumentUpdateHandlers(languageService)); // synchronize notebook cell contents subscriptions.push( @@ -275,9 +277,7 @@ function registerDocumentUpdateHandlers( } } - function updateIfQsharpDocument( - document: vscode.TextDocument, - ) { + function updateIfQsharpDocument(document: vscode.TextDocument) { if (isQsharpDocument(document) && !isQsharpNotebookCell(document)) { // Regular (not notebook) Q# document. languageService.updateDocument( diff --git a/vscode/src/language-service/diagnostics.ts b/vscode/src/language-service/diagnostics.ts index 43c180ae2e..5f9b949b2c 100644 --- a/vscode/src/language-service/diagnostics.ts +++ b/vscode/src/language-service/diagnostics.ts @@ -5,31 +5,22 @@ import { ILanguageService, VSDiagnostic, qsharpLibraryUriScheme, - IRange, - ILocation, - log, - ICompilerWorker, - ProgramConfig, - getCompilerWorker, } from "qsharp-lang"; import * as vscode from "vscode"; -import { qsharpLanguageId, toVsCodeDiagnostic, toVsCodeLocation, toVsCodeRange, getCommonCompilerWorker } from "../common"; -import { getActiveProgram } from "../programConfig"; -import { createDebugConsoleEventTarget } from "../debugger/output" - +import { + qsharpLanguageId, + toVsCodeDiagnostic, + toVsCodeLocation, + toVsCodeRange, + getCommonCompilerWorker, +} from "../common"; export function startLanguageServiceDiagnostics( languageService: ILanguageService, - context: vscode.ExtensionContext, ): vscode.Disposable[] { const diagCollection = vscode.languages.createDiagnosticCollection(qsharpLanguageId); - const testController: vscode.TestController = vscode.tests.createTestController( - "qsharpTestController", - "Q# Tests", - ); - async function onDiagnostics(evt: { detail: { uri: string; @@ -51,184 +42,14 @@ export function startLanguageServiceDiagnostics( ); } - // test explorer features - - const runHandler = (request: vscode.TestRunRequest) => { - if (!request.continuous) { - return startTestRun(request); - } - }; - - // runs an individual test run - // or test group (a test run where there are child tests) - const startTestRun = async (request: vscode.TestRunRequest) => { - // use the compiler worker to run the test in the interpreter - - log.trace("Starting test run, request was", JSON.stringify(request)); - const worker = getCommonCompilerWorker(context); - - const programResult = await getActiveProgram(); - if (!programResult.success) { - throw new Error(programResult.errorMsg); - } - - const program = programResult.programConfig; - - for (const testCase of request.include || []) { - await runTestCase(testController, testCase, request, worker, program); - } - }; - - /** - * Given a single test case, run it in the worker (which runs the interpreter) and report results back to the - * `TestController` as a side effect. - * - * This function manages its own event target for the results of the test run and uses the controller to render the output in the VS Code UI. - **/ - async function runTestCase( - ctrl: vscode.TestController, - testCase: vscode.TestItem, - request: vscode.TestRunRequest, - worker: ICompilerWorker, - program: ProgramConfig, - ): Promise { - log.trace("Running Q# test: ", testCase.id); - if (testCase.children.size > 0) { - for (const childTestCase of testCase.children) { - await runTestCase(ctrl, childTestCase[1], request, worker, program); - } - return; - } - const run = ctrl.createTestRun(request); - const evtTarget = createDebugConsoleEventTarget((msg) => { - run.appendOutput(`${msg}\n`); - }); - evtTarget.addEventListener("Result", (msg) => { - if (msg.detail.success) { - run.passed(testCase); - } else { - const message: vscode.TestMessage = { - message: msg.detail.value.message, - location: { - range: toVsCodeRange(msg.detail.value.range), - uri: vscode.Uri.parse(msg.detail.value.uri || ""), - }, - }; - run.failed(testCase, message); - } - run.end(); - }); - - const callableExpr = `${testCase.id}()`; - - try { - await worker.run(program, callableExpr, 1, evtTarget); - } catch (error) { - log.error(`Error running test ${testCase.id}:`, error); - run.appendOutput(`Error running test ${testCase.id}: ${error}\r\n`); - } - log.trace("ran test:", testCase.id); - } - - - testController.createRunProfile( - "Interpreter", - vscode.TestRunProfileKind.Run, - runHandler, - true, - undefined, - false, - ); - - const testMetadata = new WeakMap(); - - - async function onTestCallables(evt: { - detail: { - callables: [string, ILocation][]; - }; - }) { - let currentVersion = 0; - for (const [id, testItem] of testController.items) { - // set the version of this run - const discoveredVersion = testMetadata.get(testItem) || 0; - // increment the version - currentVersion = (discoveredVersion + 1) % 1000; - break; - } - - - // break down the test callable into its parts, so we can construct - // the namespace hierarchy in the test explorer - for (const [callableName, location] of evt.detail.callables) { - const vscRange = toVsCodeLocation(location); - const parts = callableName.split("."); - - // for an individual test case, e.g. foo.bar.baz, create a hierarchy of items - let rover = testController.items; - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - const id = i === parts.length - 1 ? callableName : part; - const discoveredTestItem = rover.get(part); - if (!discoveredTestItem) { - const testItem = testController.createTestItem(id, part, vscRange.uri); - testItem.range = vscRange.range; - testMetadata.set(testItem, currentVersion); - rover.add(testItem); - } else { - testMetadata.set(discoveredTestItem, currentVersion); - } - - rover = rover.get(id)!.children; - } - } - - - // delete old items - deleteItemsNotOfVersion(currentVersion, testController.items, testController); - } - - function deleteItemsNotOfVersion(version: number, items: vscode.TestItemCollection, testController: vscode.TestController) { - for (const [id, testItem] of items) { - deleteItemsNotOfVersion(version, testItem.children, testController) - if (testMetadata.get(testItem) !== version) { - items.delete(id); - } - } - } - - languageService.addEventListener("diagnostics", onDiagnostics); - languageService.addEventListener("testCallables", onTestCallables); return [ { dispose: () => { languageService.removeEventListener("diagnostics", onDiagnostics); - languageService.removeEventListener("testCallables", onTestCallables); }, }, diagCollection, - testController, ]; } - - - -let worker: ICompilerWorker | null = null; - -function getLocalCompilerWorker( - context: vscode.ExtensionContext, -): ICompilerWorker { - if (worker !== null) { - return worker; - } - - const compilerWorkerScriptPath = vscode.Uri.joinPath( - context.extensionUri, - "./out/compilerWorker.js", - ).toString(); - worker = getCompilerWorker(compilerWorkerScriptPath); - - return worker; -} diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts new file mode 100644 index 0000000000..d544e7ad21 --- /dev/null +++ b/vscode/src/language-service/testExplorer.ts @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + ICompilerWorker, + ILanguageService, + ILocation, + LanguageServiceTestCallablesEvent, + log, + ProgramConfig, +} from "qsharp-lang"; +import * as vscode from "vscode"; +import { + getCommonCompilerWorker, + toVsCodeLocation, + toVsCodeRange, +} from "../common"; +import { getActiveProgram } from "../programConfig"; +import { createDebugConsoleEventTarget } from "../debugger/output"; + +export function startTestDiscovery( + languageService: ILanguageService, + context: vscode.ExtensionContext, +): vscode.Disposable[] { + // test explorer features + const testController: vscode.TestController = + vscode.tests.createTestController("qsharpTestController", "Q# Tests"); + const runHandler = (request: vscode.TestRunRequest) => { + if (!request.continuous) { + return startTestRun(request); + } + }; + + // runs an individual test run + // or test group (a test run where there are child tests) + const startTestRun = async (request: vscode.TestRunRequest) => { + // use the compiler worker to run the test in the interpreter + + log.trace("Starting test run, request was", JSON.stringify(request)); + const worker = getCommonCompilerWorker(context); + + const programResult = await getActiveProgram(); + if (!programResult.success) { + throw new Error(programResult.errorMsg); + } + + const program = programResult.programConfig; + + for (const testCase of request.include || []) { + await runTestCase(testController, testCase, request, worker, program); + } + }; + + /** + * Given a single test case, run it in the worker (which runs the interpreter) and report results back to the + * `TestController` as a side effect. + * + * This function manages its own event target for the results of the test run and uses the controller to render the output in the VS Code UI. + **/ + async function runTestCase( + ctrl: vscode.TestController, + testCase: vscode.TestItem, + request: vscode.TestRunRequest, + worker: ICompilerWorker, + program: ProgramConfig, + ): Promise { + log.trace("Running Q# test: ", testCase.id); + if (testCase.children.size > 0) { + for (const childTestCase of testCase.children) { + await runTestCase(ctrl, childTestCase[1], request, worker, program); + } + return; + } + const run = ctrl.createTestRun(request); + const evtTarget = createDebugConsoleEventTarget((msg) => { + run.appendOutput(`${msg}\n`); + }); + evtTarget.addEventListener("Result", (msg) => { + if (msg.detail.success) { + run.passed(testCase); + } else { + const message: vscode.TestMessage = { + message: msg.detail.value.message, + location: { + range: toVsCodeRange(msg.detail.value.range), + uri: vscode.Uri.parse(msg.detail.value.uri || ""), + }, + }; + run.failed(testCase, message); + } + run.end(); + }); + + const callableExpr = `${testCase.id}()`; + + try { + await worker.run(program, callableExpr, 1, evtTarget); + } catch (error) { + log.error(`Error running test ${testCase.id}:`, error); + run.appendOutput(`Error running test ${testCase.id}: ${error}\r\n`); + } + log.trace("ran test:", testCase.id); + } + + testController.createRunProfile( + "Interpreter", + vscode.TestRunProfileKind.Run, + runHandler, + true, + undefined, + false, + ); + + const testMetadata = new WeakMap(); + + async function onTestCallables(evt: { + detail: { + callables: [string, ILocation][]; + }; + }) { + let currentVersion = 0; + for (const [, testItem] of testController.items) { + currentVersion = (testMetadata.get(testItem) || 0) + 1; + break; + } + + for (const [callableName, location] of evt.detail.callables) { + const vscLocation = toVsCodeLocation(location); + const parts = callableName.split("."); + + let rover = testController.items; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const id = + i === parts.length - 1 + ? callableName + : parts.slice(0, i + 1).join("."); + let testItem = rover.get(id); + if (!testItem) { + testItem = testController.createTestItem(id, part, vscLocation.uri); + testItem.range = vscLocation.range; + rover.add(testItem); + } + testMetadata.set(testItem, currentVersion); + rover = testItem.children; + } + } + + // delete old items from previous versions that were not updated + deleteItemsNotOfVersion( + currentVersion, + testController.items, + testController, + ); + } + + function deleteItemsNotOfVersion( + version: number, + items: vscode.TestItemCollection, + testController: vscode.TestController, + ) { + for (const [id, testItem] of items) { + deleteItemsNotOfVersion(version, testItem.children, testController); + if (testMetadata.get(testItem) !== version) { + items.delete(id); + } + } + } + + languageService.addEventListener("testCallables", onTestCallables); + + return [ + { + dispose: () => { + languageService.removeEventListener("testCallables", onTestCallables); + }, + }, + testController, + ]; +} From 865cce1a504a2d3ad2365d3249edbdc09117268c Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 7 Jan 2025 11:07:00 -0800 Subject: [PATCH 49/82] lint fixes --- npm/qsharp/src/compiler/compiler.ts | 4 +--- vscode/src/language-service/diagnostics.ts | 8 +------- vscode/src/language-service/testExplorer.ts | 1 - 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts index d88905fceb..a9fe4f484b 100644 --- a/npm/qsharp/src/compiler/compiler.ts +++ b/npm/qsharp/src/compiler/compiler.ts @@ -11,8 +11,6 @@ import { type VSDiagnostic, IProgramConfig, ITestDescriptor, - IRange, - ILocation, } from "../../lib/web/qsc_wasm.js"; import { log } from "../log.js"; import { @@ -127,7 +125,7 @@ export class Compiler implements ICompiler { (uri: string, version: number | undefined, errors: VSDiagnostic[]) => { diags = errors; }, - (callables: [string, ILocation][]) => { + () => { // do nothing; test callables are not reported in checkCode }, { diff --git a/vscode/src/language-service/diagnostics.ts b/vscode/src/language-service/diagnostics.ts index 5f9b949b2c..cbd0e9ac1d 100644 --- a/vscode/src/language-service/diagnostics.ts +++ b/vscode/src/language-service/diagnostics.ts @@ -7,13 +7,7 @@ import { qsharpLibraryUriScheme, } from "qsharp-lang"; import * as vscode from "vscode"; -import { - qsharpLanguageId, - toVsCodeDiagnostic, - toVsCodeLocation, - toVsCodeRange, - getCommonCompilerWorker, -} from "../common"; +import { qsharpLanguageId, toVsCodeDiagnostic } from "../common"; export function startLanguageServiceDiagnostics( languageService: ILanguageService, diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index d544e7ad21..825fb27646 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -5,7 +5,6 @@ import { ICompilerWorker, ILanguageService, ILocation, - LanguageServiceTestCallablesEvent, log, ProgramConfig, } from "qsharp-lang"; From 1f88c976e8a72f0dfecc8e93a83b3cd6e1d40c34 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 7 Jan 2025 11:14:42 -0800 Subject: [PATCH 50/82] rust lint updates --- language_service/src/state.rs | 6 +++--- wasm/src/language_service.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/language_service/src/state.rs b/language_service/src/state.rs index efbd55a79a..d1c94998c9 100644 --- a/language_service/src/state.rs +++ b/language_service/src/state.rs @@ -406,7 +406,7 @@ impl<'a> CompilationStateUpdater<'a> { self.with_state(|state| { for (compilation_uri, compilation) in &state.compilations { - self.publish_test_callables(&compilation_uri, &compilation.0); + self.publish_test_callables(&compilation.0); trace!("publishing diagnostics for {compilation_uri}"); for (uri, errors) in map_errors_to_docs( @@ -539,7 +539,7 @@ impl<'a> CompilationStateUpdater<'a> { f(&mut state) } - fn publish_test_callables(&self, uri: &Arc, compilation: &Compilation) { + fn publish_test_callables(&self, compilation: &Compilation) { let callables = TestCallables { callables: compilation .test_cases @@ -550,7 +550,7 @@ impl<'a> CompilationStateUpdater<'a> { // TODO(sezna) verify encoding crate::qsc_utils::into_location( qsc::line_column::Encoding::Utf16, - &compilation, + compilation, *span, compilation.user_package_id, ), diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs index 4d8dc85aa1..5c9121a0a1 100644 --- a/wasm/src/language_service.rs +++ b/wasm/src/language_service.rs @@ -32,7 +32,7 @@ impl LanguageService { pub fn start_background_work( &mut self, diagnostics_callback: DiagnosticsCallback, - test_callables_callback: TestCallableCallback, + test_callables_callback: &TestCallableCallback, host: ProjectHost, ) -> js_sys::Promise { let diagnostics_callback = From e0347c6bcd1f3ae6636329aa826a5c0ff1b4e40c Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 7 Jan 2025 11:19:21 -0800 Subject: [PATCH 51/82] update expect test --- language_service/src/state/tests.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index 859d519d9c..81e4937d06 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -2210,6 +2210,29 @@ async fn test_case_detected() { .await; expect![[r#" + [ + TestCallables { + callables: [ + ( + "main.MyTestCase", + Location { + source: "parent/src/main.qs", + range: Range { + start: Position { + line: 0, + column: 0, + }, + end: Position { + line: 0, + column: 39, + }, + }, + }, + ), + ], + version: None, + }, + ] "#]] .assert_debug_eq(&test_cases.borrow()); } From 468345b62e27e898c4d9d5bd5ec810870bbdfcfa Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 7 Jan 2025 11:26:28 -0800 Subject: [PATCH 52/82] update test case for new test callables --- language_service/src/state/tests.rs | 384 ++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index 81e4937d06..ab0e8c20cf 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -2237,6 +2237,390 @@ async fn test_case_detected() { .assert_debug_eq(&test_cases.borrow()); } +#[tokio::test] +async fn test_case_removed() { + let fs = FsNode::Dir( + [dir( + "parent", + [ + file("qsharp.json", r#"{}"#), + dir("src", [file("main.qs", "@Test() function MyTestCase() : Unit {}")]), + ], + )] + .into_iter() + .collect(), + ); + + let fs = Rc::new(RefCell::new(fs)); + let received_errors = RefCell::new(Vec::new()); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater_with_file_system(&received_errors, &test_cases, &fs); + + // Trigger a document update. + updater + .update_document( + "parent/src/main.qs", + 1, + "function MyTestCase() : Unit {}", + ) + .await; + + expect![[r#" + [ + TestCallables { + callables: [], + version: None, + }, + ] + "#]] + .assert_debug_eq(&test_cases.borrow()); +} + +#[tokio::test] +async fn test_case_modified() { + let fs = FsNode::Dir( + [dir( + "parent", + [ + file("qsharp.json", r#"{}"#), + dir("src", [file("main.qs", "@Test() function MyTestCase() : Unit {}")]), + ], + )] + .into_iter() + .collect(), + ); + + let fs = Rc::new(RefCell::new(fs)); + let received_errors = RefCell::new(Vec::new()); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater_with_file_system(&received_errors, &test_cases, &fs); + + // Trigger a document update. + updater + .update_document( + "parent/src/main.qs", + 1, + "@Test() function MyTestCase() : Unit {}", + ) + .await; + + updater + .update_document( + "parent/src/main.qs", + 2, + "@Test() function MyTestCase2() : Unit { }", + ) + .await; + + expect![[r#" + [ + TestCallables { + callables: [ + ( + "main.MyTestCase", + Location { + source: "parent/src/main.qs", + range: Range { + start: Position { + line: 0, + column: 0, + }, + end: Position { + line: 0, + column: 39, + }, + }, + }, + ), + ], + version: None, + }, + TestCallables { + callables: [ + ( + "main.MyTestCase2", + Location { + source: "parent/src/main.qs", + range: Range { + start: Position { + line: 0, + column: 0, + }, + end: Position { + line: 0, + column: 41, + }, + }, + }, + ), + ], + version: None, + }, + ] + "#]] + .assert_debug_eq(&test_cases.borrow()); +} + +#[tokio::test] +async fn test_annotation_removed() { + let fs = FsNode::Dir( + [dir( + "parent", + [ + file("qsharp.json", r#"{}"#), + dir("src", [file("main.qs", "@Test() function MyTestCase() : Unit {}")]), + ], + )] + .into_iter() + .collect(), + ); + + let fs = Rc::new(RefCell::new(fs)); + let received_errors = RefCell::new(Vec::new()); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater_with_file_system(&received_errors, &test_cases, &fs); + + // Trigger a document update. + updater + .update_document( + "parent/src/main.qs", + 1, + "@Test() function MyTestCase() : Unit {}", + ) + .await; + + updater + .update_document( + "parent/src/main.qs", + 2, + "function MyTestCase() : Unit {}", + ) + .await; + + expect![[r#" + [ + TestCallables { + callables: [ + ( + "main.MyTestCase", + Location { + source: "parent/src/main.qs", + range: Range { + start: Position { + line: 0, + column: 0, + }, + end: Position { + line: 0, + column: 39, + }, + }, + }, + ), + ], + version: None, + }, + TestCallables { + callables: [], + version: None, + }, + ] + "#]].assert_debug_eq(&test_cases.borrow()); +} + +#[tokio::test] +async fn multiple_tests() { + let fs = FsNode::Dir( + [dir( + "parent", + [ + file("qsharp.json", r#"{}"#), + dir( + "src", + [ + file("main.qs", "@Test() function Test1() : Unit {} @Test() function Test2() : Unit {}"), + ], + ), + ], + )] + .into_iter() + .collect(), + ); + + let fs = Rc::new(RefCell::new(fs)); + let received_errors = RefCell::new(Vec::new()); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater_with_file_system(&received_errors, &test_cases, &fs); + + // Trigger a document update. + updater + .update_document( + "parent/src/main.qs", + 1, + "@Test() function Test1() : Unit {} @Test() function Test2() : Unit {}", + ) + .await; + + expect![[r#" + [ + TestCallables { + callables: [ + ( + "main.Test1", + Location { + source: "parent/src/main.qs", + range: Range { + start: Position { + line: 0, + column: 0, + }, + end: Position { + line: 0, + column: 34, + }, + }, + }, + ), + ( + "main.Test2", + Location { + source: "parent/src/main.qs", + range: Range { + start: Position { + line: 0, + column: 35, + }, + end: Position { + line: 0, + column: 69, + }, + }, + }, + ), + ], + version: None, + }, + ] + "#]] + .assert_debug_eq(&test_cases.borrow()); +} + +#[tokio::test] +async fn test_case_in_different_files() { + let fs = FsNode::Dir( + [dir( + "parent", + [ + file("qsharp.json", r#"{}"#), + dir( + "src", + [ + file("test1.qs", "@Test() function Test1() : Unit {}"), + file("test2.qs", "@Test() function Test2() : Unit {}"), + ], + ), + ], + )] + .into_iter() + .collect(), + ); + + let fs = Rc::new(RefCell::new(fs)); + let received_errors = RefCell::new(Vec::new()); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater_with_file_system(&received_errors, &test_cases, &fs); + + // Trigger a document update for the first test file. + updater + .update_document("parent/src/test1.qs", 1, "@Test() function Test1() : Unit {}") + .await; + + // Trigger a document update for the second test file. + updater + .update_document("parent/src/test2.qs", 1, "@Test() function Test2() : Unit {}") + .await; + + expect![[r#" + [ + TestCallables { + callables: [ + ( + "test1.Test1", + Location { + source: "parent/src/test1.qs", + range: Range { + start: Position { + line: 0, + column: 0, + }, + end: Position { + line: 0, + column: 34, + }, + }, + }, + ), + ( + "test2.Test2", + Location { + source: "parent/src/test2.qs", + range: Range { + start: Position { + line: 0, + column: 0, + }, + end: Position { + line: 0, + column: 34, + }, + }, + }, + ), + ], + version: None, + }, + TestCallables { + callables: [ + ( + "test1.Test1", + Location { + source: "parent/src/test1.qs", + range: Range { + start: Position { + line: 0, + column: 0, + }, + end: Position { + line: 0, + column: 34, + }, + }, + }, + ), + ( + "test2.Test2", + Location { + source: "parent/src/test2.qs", + range: Range { + start: Position { + line: 0, + column: 0, + }, + end: Position { + line: 0, + column: 34, + }, + }, + }, + ), + ], + version: None, + }, + ] + "#]] + .assert_debug_eq(&test_cases.borrow()); +} + type ErrorInfo = ( String, Option, From 00d5f4fe13ea97573af268b5320773c76fc2280e Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 7 Jan 2025 11:35:46 -0800 Subject: [PATCH 53/82] basics.js test --- language_service/src/state/tests.rs | 51 ++++++++++++++++++----------- npm/qsharp/test/basics.js | 31 ++++++++++++++++++ 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index ab0e8c20cf..da84de39b6 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -2244,7 +2244,10 @@ async fn test_case_removed() { "parent", [ file("qsharp.json", r#"{}"#), - dir("src", [file("main.qs", "@Test() function MyTestCase() : Unit {}")]), + dir( + "src", + [file("main.qs", "@Test() function MyTestCase() : Unit {}")], + ), ], )] .into_iter() @@ -2258,11 +2261,7 @@ async fn test_case_removed() { // Trigger a document update. updater - .update_document( - "parent/src/main.qs", - 1, - "function MyTestCase() : Unit {}", - ) + .update_document("parent/src/main.qs", 1, "function MyTestCase() : Unit {}") .await; expect![[r#" @@ -2283,7 +2282,10 @@ async fn test_case_modified() { "parent", [ file("qsharp.json", r#"{}"#), - dir("src", [file("main.qs", "@Test() function MyTestCase() : Unit {}")]), + dir( + "src", + [file("main.qs", "@Test() function MyTestCase() : Unit {}")], + ), ], )] .into_iter() @@ -2368,7 +2370,10 @@ async fn test_annotation_removed() { "parent", [ file("qsharp.json", r#"{}"#), - dir("src", [file("main.qs", "@Test() function MyTestCase() : Unit {}")]), + dir( + "src", + [file("main.qs", "@Test() function MyTestCase() : Unit {}")], + ), ], )] .into_iter() @@ -2390,14 +2395,10 @@ async fn test_annotation_removed() { .await; updater - .update_document( - "parent/src/main.qs", - 2, - "function MyTestCase() : Unit {}", - ) + .update_document("parent/src/main.qs", 2, "function MyTestCase() : Unit {}") .await; - expect![[r#" + expect![[r#" [ TestCallables { callables: [ @@ -2425,7 +2426,8 @@ async fn test_annotation_removed() { version: None, }, ] - "#]].assert_debug_eq(&test_cases.borrow()); + "#]] + .assert_debug_eq(&test_cases.borrow()); } #[tokio::test] @@ -2437,9 +2439,10 @@ async fn multiple_tests() { file("qsharp.json", r#"{}"#), dir( "src", - [ - file("main.qs", "@Test() function Test1() : Unit {} @Test() function Test2() : Unit {}"), - ], + [file( + "main.qs", + "@Test() function Test1() : Unit {} @Test() function Test2() : Unit {}", + )], ), ], )] @@ -2532,12 +2535,20 @@ async fn test_case_in_different_files() { // Trigger a document update for the first test file. updater - .update_document("parent/src/test1.qs", 1, "@Test() function Test1() : Unit {}") + .update_document( + "parent/src/test1.qs", + 1, + "@Test() function Test1() : Unit {}", + ) .await; // Trigger a document update for the second test file. updater - .update_document("parent/src/test2.qs", 1, "@Test() function Test2() : Unit {}") + .update_document( + "parent/src/test2.qs", + 1, + "@Test() function Test2() : Unit {}", + ) .await; expect![[r#" diff --git a/npm/qsharp/test/basics.js b/npm/qsharp/test/basics.js index 5372aa65d0..2f8341e686 100644 --- a/npm/qsharp/test/basics.js +++ b/npm/qsharp/test/basics.js @@ -485,6 +485,37 @@ test("language service diagnostics", async () => { assert(gotDiagnostics); }); +test("testCallableDiscovery", async () => { + const languageService = getLanguageService(); + let gotTests = false; + languageService.addEventListener("testCallables", (event) => { + gotTests = true; + assert.equal(event.type, "testCallables"); + assert.equal(event.detail.callables.length, 1); + assert.equal( + event.detail.callables[0][0], + "Sample.main", + ); + }); + await languageService.updateDocument( + "test.qs", + 1, + `namespace Sample { + @Test() + operation main() : Unit { + use q1 = Qubit(); + Ry(q1); + let m1 = M(q1); + return [m1]; + } +}`, + ); + + // dispose() will complete when the language service has processed all the updates. + await languageService.dispose(); + assert(gotTests); +}); + test("diagnostics with related spans", async () => { const languageService = getLanguageService(); let gotDiagnostics = false; From 57bc6c16557cefa0cff925e84f528329a4d1410d Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 7 Jan 2025 12:27:11 -0800 Subject: [PATCH 54/82] add basics.js tests --- npm/qsharp/test/basics.js | 65 ++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/npm/qsharp/test/basics.js b/npm/qsharp/test/basics.js index 2f8341e686..5fb9e6b00c 100644 --- a/npm/qsharp/test/basics.js +++ b/npm/qsharp/test/basics.js @@ -485,29 +485,34 @@ test("language service diagnostics", async () => { assert(gotDiagnostics); }); -test("testCallableDiscovery", async () => { +test("test callable discovery", async () => { const languageService = getLanguageService(); let gotTests = false; languageService.addEventListener("testCallables", (event) => { gotTests = true; assert.equal(event.type, "testCallables"); assert.equal(event.detail.callables.length, 1); - assert.equal( - event.detail.callables[0][0], - "Sample.main", - ); + assert.equal(event.detail.callables[0][0], "Sample.main"); + assert.deepStrictEqual(event.detail.callables[0][1], { + source: "test.qs", + span: { + end: { + character: 30, + line: 2, + }, + start: { + character: 4, + line: 1, + }, + }, + }); }); await languageService.updateDocument( "test.qs", 1, `namespace Sample { @Test() - operation main() : Unit { - use q1 = Qubit(); - Ry(q1); - let m1 = M(q1); - return [m1]; - } + operation main() : Unit {} }`, ); @@ -516,6 +521,44 @@ test("testCallableDiscovery", async () => { assert(gotTests); }); +test("test callable discovery", async () => { + const languageService = getLanguageService(); + let gotTests = false; + languageService.addEventListener("testCallables", (event) => { + gotTests = true; + assert.equal(event.type, "testCallables"); + assert.equal(event.detail.callables.length, 4); + assert.equal(event.detail.callables[0][0], "Sample.test1"); + assert.equal(event.detail.callables[1][0], "Sample.test2"); + assert.equal(event.detail.callables[2][0], "Sample2.test1"); + assert.equal(event.detail.callables[3][0], "Sample2.test2"); + }); + await languageService.updateDocument( + "test.qs", + 1, + `namespace Sample { + @Test() + operation test1() : Unit {} + + @Test() + function test2() : Unit {} +} +namespace Sample2 { + @Test() + operation test1() : Unit {} + + @Test() + function test2() : Unit {} + } +} +`, + ); + + // dispose() will complete when the language service has processed all the updates. + await languageService.dispose(); + assert(gotTests); +}); + test("diagnostics with related spans", async () => { const languageService = getLanguageService(); let gotDiagnostics = false; From d1267c4aef76df68dec89d318f8470a13d8d287d Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 7 Jan 2025 12:36:57 -0800 Subject: [PATCH 55/82] undo common compiler worker --- vscode/src/common.ts | 36 +-------------------- vscode/src/language-service/testExplorer.ts | 31 ++++++++++++++---- 2 files changed, 26 insertions(+), 41 deletions(-) diff --git a/vscode/src/common.ts b/vscode/src/common.ts index 5dd6b3392c..5d3c156d1f 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -3,14 +3,7 @@ import { TextDocument, Uri, Range, Location } from "vscode"; import { Utils } from "vscode-uri"; -import { - ILocation, - IRange, - IWorkspaceEdit, - VSDiagnostic, - getCompilerWorker, - ICompilerWorker, -} from "qsharp-lang"; +import { ILocation, IRange, IWorkspaceEdit, VSDiagnostic } from "qsharp-lang"; import * as vscode from "vscode"; export const qsharpLanguageId = "qsharp"; @@ -102,30 +95,3 @@ export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { } return vscodeDiagnostic; } - -// the below worker is common to multiple consumers in the language extension. -let worker: ICompilerWorker | null = null; -/** - * Returns a singleton instance of the compiler worker. - * @param context The extension context. - * @returns The compiler worker. - * - * This function is used to get a *common* compiler worker. It should only be used for performance-light - * and safe (infallible) operations. For performance-intensive, blocking operations, or for fallible operations, - * use `getCompilerWorker` instead. - **/ -export function getCommonCompilerWorker( - context: vscode.ExtensionContext, -): ICompilerWorker { - if (worker !== null) { - return worker; - } - - const compilerWorkerScriptPath = vscode.Uri.joinPath( - context.extensionUri, - "./out/compilerWorker.js", - ).toString(); - worker = getCompilerWorker(compilerWorkerScriptPath); - - return worker; -} diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index 825fb27646..f74d9c4fb6 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { + getCompilerWorker, ICompilerWorker, ILanguageService, ILocation, @@ -9,14 +10,32 @@ import { ProgramConfig, } from "qsharp-lang"; import * as vscode from "vscode"; -import { - getCommonCompilerWorker, - toVsCodeLocation, - toVsCodeRange, -} from "../common"; +import { toVsCodeLocation, toVsCodeRange } from "../common"; import { getActiveProgram } from "../programConfig"; import { createDebugConsoleEventTarget } from "../debugger/output"; +let worker: ICompilerWorker | null = null; +/** + * Returns a singleton instance of the compiler worker. + * @param context The extension context. + * @returns The compiler worker. + **/ +function getLocalCompilerWorker( + context: vscode.ExtensionContext, +): ICompilerWorker { + if (worker !== null) { + return worker; + } + + const compilerWorkerScriptPath = vscode.Uri.joinPath( + context.extensionUri, + "./out/compilerWorker.js", + ).toString(); + worker = getCompilerWorker(compilerWorkerScriptPath); + + return worker; +} + export function startTestDiscovery( languageService: ILanguageService, context: vscode.ExtensionContext, @@ -36,7 +55,7 @@ export function startTestDiscovery( // use the compiler worker to run the test in the interpreter log.trace("Starting test run, request was", JSON.stringify(request)); - const worker = getCommonCompilerWorker(context); + const worker = getLocalCompilerWorker(context); const programResult = await getActiveProgram(); if (!programResult.success) { From 1d5ef8e9b7e0cbf8cb03a55ccb3426c84908d73f Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 9 Jan 2025 10:20:52 -0800 Subject: [PATCH 56/82] Fix output --- vscode/src/debugger/output.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/vscode/src/debugger/output.ts b/vscode/src/debugger/output.ts index de888fce1e..00dbb0da74 100644 --- a/vscode/src/debugger/output.ts +++ b/vscode/src/debugger/output.ts @@ -72,11 +72,7 @@ export function createDebugConsoleEventTarget(out: (message: string) => void) { }); eventTarget.addEventListener("Result", (evt) => { - if (evt.detail.success) { - out(`${evt.detail.value}`); - } else { - out(`${evt.detail.value.message}`); - } + out(`${evt.detail.value}`); }); return eventTarget; From 6ca81d0029812d7dd5f7151b43ed8ce2a843511c Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 9 Jan 2025 11:22:02 -0800 Subject: [PATCH 57/82] mid PR review feedback --- compiler/qsc/src/lib.rs | 31 -------------- npm/qsharp/src/browser.ts | 1 + npm/qsharp/src/compiler/compiler.ts | 9 ---- .../src/language-service/language-service.ts | 5 ++- npm/qsharp/test/basics.js | 2 +- playground/src/editor.tsx | 4 +- vscode/src/language-service/testExplorer.ts | 7 ++-- wasm/src/language_service.rs | 2 +- wasm/src/lib.rs | 2 - wasm/src/test_discovery.rs | 42 +------------------ 10 files changed, 12 insertions(+), 93 deletions(-) diff --git a/compiler/qsc/src/lib.rs b/compiler/qsc/src/lib.rs index 4d0e97dd85..5847ab4449 100644 --- a/compiler/qsc/src/lib.rs +++ b/compiler/qsc/src/lib.rs @@ -9,37 +9,6 @@ pub mod interpret; pub mod location; pub mod packages; pub mod target; -pub mod test_callables { - use qsc_data_structures::line_column::{Encoding, Range}; - use qsc_frontend::compile::CompileUnit; - - use crate::location::Location; - - pub struct TestDescriptor { - pub callable_name: String, - pub location: Location, - } - - pub fn get_test_callables(unit: &CompileUnit) -> impl Iterator + '_ { - let test_callables = unit.package.get_test_callables(); - - test_callables.into_iter().map(|(name, span)| { - let source = unit - .sources - .find_by_offset(span.lo) - .expect("source should exist for offset"); - - let location = Location { - source: source.name.clone(), - range: Range::from_span(Encoding::Utf8, &source.contents, &(span - source.offset)), - }; - TestDescriptor { - callable_name: name, - location, - } - }) - } -} pub use qsc_formatter::formatter; diff --git a/npm/qsharp/src/browser.ts b/npm/qsharp/src/browser.ts index dd0e557f95..607b0116df 100644 --- a/npm/qsharp/src/browser.ts +++ b/npm/qsharp/src/browser.ts @@ -168,6 +168,7 @@ export type { IStructStepResult, IWorkspaceEdit, ProjectLoader, + ITestDescriptor, VSDiagnostic, } from "../lib/web/qsc_wasm.js"; export { type Dump, type ShotResult } from "./compiler/common.js"; diff --git a/npm/qsharp/src/compiler/compiler.ts b/npm/qsharp/src/compiler/compiler.ts index a9fe4f484b..f625f72e5c 100644 --- a/npm/qsharp/src/compiler/compiler.ts +++ b/npm/qsharp/src/compiler/compiler.ts @@ -9,8 +9,6 @@ import { IProgramConfig as wasmIProgramConfig, TargetProfile, type VSDiagnostic, - IProgramConfig, - ITestDescriptor, } from "../../lib/web/qsc_wasm.js"; import { log } from "../log.js"; import { @@ -79,8 +77,6 @@ export interface ICompiler { exerciseSources: string[], eventHandler: IQscEventTarget, ): Promise; - - getTestCallables(program: ProgramConfig): Promise; } /** @@ -250,10 +246,6 @@ export class Compiler implements ICompiler { return success; } - - async getTestCallables(program: IProgramConfig): Promise { - return this.wasm.get_test_callables(program); - } } /** @@ -337,7 +329,6 @@ export const compilerProtocol: ServiceProtocol = { run: "requestWithProgress", runWithPauliNoise: "requestWithProgress", checkExerciseSolution: "requestWithProgress", - getTestCallables: "request", }, eventNames: ["DumpMachine", "Matrix", "Message", "Result"], }; diff --git a/npm/qsharp/src/language-service/language-service.ts b/npm/qsharp/src/language-service/language-service.ts index 781715df0f..92ed210a85 100644 --- a/npm/qsharp/src/language-service/language-service.ts +++ b/npm/qsharp/src/language-service/language-service.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { ITestDescriptor } from "../../lib/node/qsc_wasm.cjs"; import type { ICodeAction, ICodeLens, @@ -38,7 +39,7 @@ export type LanguageServiceDiagnosticEvent = { export type LanguageServiceTestCallablesEvent = { type: "testCallables"; detail: { - callables: [string, ILocation][]; + callables: ITestDescriptor[]; }; }; @@ -286,7 +287,7 @@ export class QSharpLanguageService implements ILanguageService { } } - async onTestCallables(callables: [string, ILocation][]) { + async onTestCallables(callables: ITestDescriptor[]) { try { const event = new Event("testCallables") as LanguageServiceEvent & Event; event.detail = { diff --git a/npm/qsharp/test/basics.js b/npm/qsharp/test/basics.js index 5fb9e6b00c..5e8afb82c8 100644 --- a/npm/qsharp/test/basics.js +++ b/npm/qsharp/test/basics.js @@ -521,7 +521,7 @@ test("test callable discovery", async () => { assert(gotTests); }); -test("test callable discovery", async () => { +test("multiple test callable discovery", async () => { const languageService = getLanguageService(); let gotTests = false; languageService.addEventListener("testCallables", (event) => { diff --git a/playground/src/editor.tsx b/playground/src/editor.tsx index d8929e6126..801da9558d 100644 --- a/playground/src/editor.tsx +++ b/playground/src/editor.tsx @@ -8,12 +8,12 @@ import { CompilerState, ICompilerWorker, ILanguageServiceWorker, - LanguageServiceEvent, QscEventTarget, VSDiagnostic, log, ProgramConfig, TargetProfile, + LanguageServiceDiagnosticEvent, } from "qsharp-lang"; import { Exercise, getExerciseSources } from "qsharp-lang/katas-md"; import { codeToCompressedBase64, lsRangeToMonacoRange } from "./utils.js"; @@ -296,7 +296,7 @@ export function Editor(props: { : [{ lint: "needlessOperation", level: "warn" }], }); - function onDiagnostics(evt: LanguageServiceEvent) { + function onDiagnostics(evt: LanguageServiceDiagnosticEvent) { const diagnostics = evt.detail.diagnostics; errMarks.current.checkDiags = diagnostics; markErrors(); diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index f74d9c4fb6..e55fb88312 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -5,7 +5,7 @@ import { getCompilerWorker, ICompilerWorker, ILanguageService, - ILocation, + ITestDescriptor, log, ProgramConfig, } from "qsharp-lang"; @@ -130,10 +130,9 @@ export function startTestDiscovery( ); const testMetadata = new WeakMap(); - async function onTestCallables(evt: { detail: { - callables: [string, ILocation][]; + callables: ITestDescriptor[]; }; }) { let currentVersion = 0; @@ -142,7 +141,7 @@ export function startTestDiscovery( break; } - for (const [callableName, location] of evt.detail.callables) { + for (const { callableName, location } of evt.detail.callables) { const vscLocation = toVsCodeLocation(location); const parts = callableName.split("."); diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs index 5c9121a0a1..fccabedcd2 100644 --- a/wasm/src/language_service.rs +++ b/wasm/src/language_service.rs @@ -616,6 +616,6 @@ extern "C" { #[wasm_bindgen] extern "C" { - #[wasm_bindgen(typescript_type = "(callables: [string, ILocation][]) => void")] + #[wasm_bindgen(typescript_type = "(callables: ITestDescriptor[]) => void")] pub type TestCallableCallback; } diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index c6b94c680f..bdabefa201 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -38,8 +38,6 @@ mod project_system; mod serializable_type; mod test_discovery; -pub use test_discovery::get_test_callables; - #[cfg(test)] mod tests; diff --git a/wasm/src/test_discovery.rs b/wasm/src/test_discovery.rs index 1600e672f8..0a918052e0 100644 --- a/wasm/src/test_discovery.rs +++ b/wasm/src/test_discovery.rs @@ -1,19 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use qsc::{compile, PackageType}; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::wasm_bindgen; -use crate::{ - project_system::{into_qsc_args, ProgramConfig}, - serializable_type, STORE_CORE_STD, -}; +use crate::serializable_type; serializable_type! { TestDescriptor, { - #[serde(rename = "callableName")] pub callable_name: String, pub location: crate::line_column::Location, }, @@ -23,38 +18,3 @@ serializable_type! { }"#, ITestDescriptor } - -#[wasm_bindgen] -pub fn get_test_callables(config: ProgramConfig) -> Result, String> { - let (source_map, capabilities, language_features, _store, _deps) = - into_qsc_args(config, None).map_err(super::compile_errors_into_qsharp_errors_json)?; - - let compile_unit = STORE_CORE_STD.with(|(store, std)| { - let (unit, _errs) = compile::compile( - store, - &[(*std, None)], - source_map, - PackageType::Lib, - capabilities, - language_features, - ); - unit - }); - - let test_descriptors = qsc::test_callables::get_test_callables(&compile_unit); - - Ok(test_descriptors - .map( - |qsc::test_callables::TestDescriptor { - callable_name, - location, - }| { - TestDescriptor { - callable_name, - location: location.into(), - } - .into() - }, - ) - .collect()) -} From 4235d03b7a4cf49e671951ee1f4b779fb110bf53 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 21 Jan 2025 11:49:21 -0800 Subject: [PATCH 58/82] Address large portion of pr feedback --- compiler/qsc_passes/src/test_attribute.rs | 2 +- language_service/src/protocol.rs | 1 - language_service/src/state.rs | 9 ++++++--- language_service/src/state/tests.rs | 9 --------- library/signed/src/Tests.qs | 2 +- samples/language/TestAttribute.qs | 18 ++++++++++++++++++ vscode/src/common.ts | 17 ++++++++++++++++- vscode/src/language-service/testExplorer.ts | 12 +++--------- 8 files changed, 45 insertions(+), 25 deletions(-) create mode 100644 samples/language/TestAttribute.qs diff --git a/compiler/qsc_passes/src/test_attribute.rs b/compiler/qsc_passes/src/test_attribute.rs index 1c865ac0b5..ac9d1a678b 100644 --- a/compiler/qsc_passes/src/test_attribute.rs +++ b/compiler/qsc_passes/src/test_attribute.rs @@ -40,7 +40,7 @@ impl<'a> Visitor<'a> for TestAttributeValidator { } if decl.input.ty != qsc_hir::ty::Ty::UNIT { self.errors - .push(TestAttributeError::CallableHasParameters(decl.name.span)); + .push(TestAttributeError::CallableHasParameters(decl.span)); } } } diff --git a/language_service/src/protocol.rs b/language_service/src/protocol.rs index 86dfb5bd81..3c4d2ebea1 100644 --- a/language_service/src/protocol.rs +++ b/language_service/src/protocol.rs @@ -37,7 +37,6 @@ pub struct DiagnosticUpdate { #[derive(Debug)] pub struct TestCallables { pub callables: Vec<(String, Location)>, - pub version: Option, } #[derive(Debug)] diff --git a/language_service/src/state.rs b/language_service/src/state.rs index d1c94998c9..e6431c3832 100644 --- a/language_service/src/state.rs +++ b/language_service/src/state.rs @@ -10,6 +10,7 @@ use super::protocol::{ }; use log::{debug, trace}; use miette::Diagnostic; +use qsc::line_column::Encoding; use qsc::{compile, project, target::Profile, LanguageFeatures, PackageType}; use qsc_linter::LintConfig; use qsc_project::{FileSystemAsync, JSProjectHost, PackageCache, Project}; @@ -107,6 +108,8 @@ pub(super) struct CompilationStateUpdater<'a> { cache: RefCell, /// Functions to interact with the host filesystem for project system operations. project_host: Box, + /// Encoding for converting between line/column and byte offsets. + position_encoding: Encoding, } impl<'a> CompilationStateUpdater<'a> { @@ -115,6 +118,7 @@ impl<'a> CompilationStateUpdater<'a> { diagnostics_receiver: impl Fn(DiagnosticUpdate) + 'a, test_callable_receiver: impl Fn(TestCallables) + 'a, project_host: impl JSProjectHost + 'static, + position_encoding: Encoding, ) -> Self { Self { state, @@ -124,6 +128,7 @@ impl<'a> CompilationStateUpdater<'a> { test_callable_receiver: Box::new(test_callable_receiver), cache: RefCell::default(), project_host: Box::new(project_host), + position_encoding, } } @@ -547,9 +552,8 @@ impl<'a> CompilationStateUpdater<'a> { .map(|(name, span)| { ( name.clone(), - // TODO(sezna) verify encoding crate::qsc_utils::into_location( - qsc::line_column::Encoding::Utf16, + self.position_encoding, compilation, *span, compilation.user_package_id, @@ -557,7 +561,6 @@ impl<'a> CompilationStateUpdater<'a> { ) }) .collect(), - version: None, }; (self.test_callable_receiver)(callables); diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index da84de39b6..ce740216cb 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -2542,15 +2542,6 @@ async fn test_case_in_different_files() { ) .await; - // Trigger a document update for the second test file. - updater - .update_document( - "parent/src/test2.qs", - 1, - "@Test() function Test2() : Unit {}", - ) - .await; - expect![[r#" [ TestCallables { diff --git a/library/signed/src/Tests.qs b/library/signed/src/Tests.qs index 617b371b01..3fbe29d3b9 100644 --- a/library/signed/src/Tests.qs +++ b/library/signed/src/Tests.qs @@ -8,7 +8,7 @@ import Measurement.MeasureSignedInteger; @Test() operation MeasureSignedIntTests() : Unit { let testCases = [ - ("0b0001 == 1", 4, (qs) => X(qs[0]), (qs) => MeasureSignedInteger(qs, 6), 11), + ("0b0001 == 1", 4, (qs) => X(qs[0]), (qs) => MeasureSignedInteger(qs, 6), 1), ("0b1111 == -1", 4, (qs) => { X(qs[0]); X(qs[1]); X(qs[2]); X(qs[3]); }, (qs) => MeasureSignedInteger(qs, 4), -1), ("0b01000 == 8", 5, (qs) => X(qs[3]), (qs) => MeasureSignedInteger(qs, 5), 8), ("0b11110 == -2", 5, (qs) => { diff --git a/samples/language/TestAttribute.qs b/samples/language/TestAttribute.qs new file mode 100644 index 0000000000..3ba39c181d --- /dev/null +++ b/samples/language/TestAttribute.qs @@ -0,0 +1,18 @@ +// # Sample +// Test Attribute +// +// # Description +// A Q# function or operation (callable) can be designated as a test case via the @Test() attribute. +// In VS Code, these tests will show up in the "test explorer" in the Activity Bar. +// If the test crashes, it is a failure. If it runs to completion, it is a success. + +// Tests must take zero parameters, and contain no generic types (type parameters). +@Test() +function TestPass() : Unit { + Std.Diagnostics.Fact(true, "This test should pass."); +} + +@Test() +function TestFail() : Unit { + Std.Diagnostics.Fact(false, "This test should fail."); +} \ No newline at end of file diff --git a/vscode/src/common.ts b/vscode/src/common.ts index 5d3c156d1f..a74afcc300 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -3,7 +3,14 @@ import { TextDocument, Uri, Range, Location } from "vscode"; import { Utils } from "vscode-uri"; -import { ILocation, IRange, IWorkspaceEdit, VSDiagnostic } from "qsharp-lang"; +import { + getCompilerWorker, + ICompilerWorker, + ILocation, + IRange, + IWorkspaceEdit, + VSDiagnostic, +} from "qsharp-lang"; import * as vscode from "vscode"; export const qsharpLanguageId = "qsharp"; @@ -95,3 +102,11 @@ export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { } return vscodeDiagnostic; } + +export function loadCompilerWorker(extensionUri: string): ICompilerWorker { + const compilerWorkerScriptPath = vscode.Uri.joinPath( + extensionUri, + "./out/compilerWorker.js", + ).toString(); + getCompilerWorker(compilerWorkerScriptPath); +} diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index e55fb88312..2790a45219 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -10,7 +10,7 @@ import { ProgramConfig, } from "qsharp-lang"; import * as vscode from "vscode"; -import { toVsCodeLocation, toVsCodeRange } from "../common"; +import { loadCompilerWorker, toVsCodeLocation, toVsCodeRange } from "../common"; import { getActiveProgram } from "../programConfig"; import { createDebugConsoleEventTarget } from "../debugger/output"; @@ -20,18 +20,12 @@ let worker: ICompilerWorker | null = null; * @param context The extension context. * @returns The compiler worker. **/ -function getLocalCompilerWorker( - context: vscode.ExtensionContext, -): ICompilerWorker { +function getLocalCompilerWorker(extensionUri: string): ICompilerWorker { if (worker !== null) { return worker; } - const compilerWorkerScriptPath = vscode.Uri.joinPath( - context.extensionUri, - "./out/compilerWorker.js", - ).toString(); - worker = getCompilerWorker(compilerWorkerScriptPath); + worker = loadCompilerWorker(extensionUri); return worker; } From cf99d0cbab75ddafdf1241ba668df49e4ab3fef6 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 21 Jan 2025 13:01:46 -0800 Subject: [PATCH 59/82] apply rest of PR feedback --- .../qsc_passes/src/test_attribute/tests.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/compiler/qsc_passes/src/test_attribute/tests.rs b/compiler/qsc_passes/src/test_attribute/tests.rs index 9411ac03c7..0e1aaf3924 100644 --- a/compiler/qsc_passes/src/test_attribute/tests.rs +++ b/compiler/qsc_passes/src/test_attribute/tests.rs @@ -78,6 +78,25 @@ fn callable_cant_have_type_params() { ); } +#[test] +fn conditionally_compile_out_test() { + check( + indoc! {" + namespace test { + @Test() + @Config(Base) + operation A<'T>() : Unit { + + } + } + "}, + &expect![[r#" + Package: + Item 0 [0-86] (Public): + Namespace (Ident 0 [10-14] "test"): "#]], + ); +} + #[test] fn callable_is_valid_test_callable() { check( From b66fab452958b4a040d10d24a446f5c6311df484 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 21 Jan 2025 13:55:15 -0800 Subject: [PATCH 60/82] Clean up lints --- compiler/qsc_passes/src/test_attribute/tests.rs | 4 ++-- language_service/src/lib.rs | 1 + language_service/src/state/tests.rs | 6 +++++- samples/language/TestAttribute.qs | 5 ++++- samples_test/src/tests/language.rs | 3 +++ vscode/src/language-service/testExplorer.ts | 1 - 6 files changed, 15 insertions(+), 5 deletions(-) diff --git a/compiler/qsc_passes/src/test_attribute/tests.rs b/compiler/qsc_passes/src/test_attribute/tests.rs index 0e1aaf3924..1597a38e5f 100644 --- a/compiler/qsc_passes/src/test_attribute/tests.rs +++ b/compiler/qsc_passes/src/test_attribute/tests.rs @@ -45,8 +45,8 @@ fn callable_cant_have_params() { [ CallableHasParameters( Span { - lo: 43, - hi: 44, + lo: 33, + hi: 71, }, ), ] diff --git a/language_service/src/lib.rs b/language_service/src/lib.rs index c511f03c5f..cf9f9ceb85 100644 --- a/language_service/src/lib.rs +++ b/language_service/src/lib.rs @@ -80,6 +80,7 @@ impl LanguageService { diagnostics_receiver, test_callable_receiver, project_host, + self.position_encoding, ), recv, }; diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index ce740216cb..d259efebd4 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -12,7 +12,9 @@ use crate::{ tests::test_fs::{dir, file, FsNode, TestProjectHost}, }; use expect_test::{expect, Expect}; -use qsc::{compile, project, target::Profile, LanguageFeatures, PackageType}; +use qsc::{ + compile, line_column::Encoding, project, target::Profile, LanguageFeatures, PackageType, +}; use qsc_linter::{AstLint, LintConfig, LintKind, LintLevel}; use std::{cell::RefCell, fmt::Write, rc::Rc}; @@ -2666,6 +2668,7 @@ fn new_updater<'a>( TestProjectHost { fs: TEST_FS.with(Clone::clone), }, + Encoding::Utf16, ) } @@ -2704,6 +2707,7 @@ fn new_updater_with_file_system<'a>( diagnostic_receiver, test_callable_receiver, TestProjectHost { fs: fs.clone() }, + Encoding::Utf16, ) } diff --git a/samples/language/TestAttribute.qs b/samples/language/TestAttribute.qs index 3ba39c181d..95f4e7371b 100644 --- a/samples/language/TestAttribute.qs +++ b/samples/language/TestAttribute.qs @@ -12,7 +12,10 @@ function TestPass() : Unit { Std.Diagnostics.Fact(true, "This test should pass."); } +// Because this function asserts `false`, it will crash and the test will fail. @Test() function TestFail() : Unit { Std.Diagnostics.Fact(false, "This test should fail."); -} \ No newline at end of file +} + +function Main() : Unit {} \ No newline at end of file diff --git a/samples_test/src/tests/language.rs b/samples_test/src/tests/language.rs index f2362a4736..4cd55b69fe 100644 --- a/samples_test/src/tests/language.rs +++ b/samples_test/src/tests/language.rs @@ -362,3 +362,6 @@ pub const CLASSCONSTRAINTS_EXPECT_DEBUG: Expect = expect![[r#" false true ()"#]]; + +pub const TESTATTRIBUTE_EXPECT: Expect = expect!["()"]; +pub const TESTATTRIBUTE_EXPECT_DEBUG: Expect = expect!["()"]; diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index 2790a45219..6453294efb 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import { - getCompilerWorker, ICompilerWorker, ILanguageService, ITestDescriptor, From 14043ad135a9ad85671ae899227005a20588f7f8 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 21 Jan 2025 14:03:49 -0800 Subject: [PATCH 61/82] final test updates; build.py passing --- language_service/src/state/tests.rs | 93 ++++++++--------------------- 1 file changed, 24 insertions(+), 69 deletions(-) diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index d259efebd4..3c2f847ba0 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -2232,7 +2232,6 @@ async fn test_case_detected() { }, ), ], - version: None, }, ] "#]] @@ -2270,7 +2269,6 @@ async fn test_case_removed() { [ TestCallables { callables: [], - version: None, }, ] "#]] @@ -2337,7 +2335,6 @@ async fn test_case_modified() { }, ), ], - version: None, }, TestCallables { callables: [ @@ -2358,7 +2355,6 @@ async fn test_case_modified() { }, ), ], - version: None, }, ] "#]] @@ -2401,34 +2397,32 @@ async fn test_annotation_removed() { .await; expect![[r#" - [ - TestCallables { - callables: [ - ( - "main.MyTestCase", - Location { - source: "parent/src/main.qs", - range: Range { - start: Position { - line: 0, - column: 0, - }, - end: Position { - line: 0, - column: 39, - }, + [ + TestCallables { + callables: [ + ( + "main.MyTestCase", + Location { + source: "parent/src/main.qs", + range: Range { + start: Position { + line: 0, + column: 0, + }, + end: Position { + line: 0, + column: 39, }, }, - ), - ], - version: None, - }, - TestCallables { - callables: [], - version: None, - }, - ] - "#]] + }, + ), + ], + }, + TestCallables { + callables: [], + }, + ] + "#]] .assert_debug_eq(&test_cases.borrow()); } @@ -2503,7 +2497,6 @@ async fn multiple_tests() { }, ), ], - version: None, }, ] "#]] @@ -2581,44 +2574,6 @@ async fn test_case_in_different_files() { }, ), ], - version: None, - }, - TestCallables { - callables: [ - ( - "test1.Test1", - Location { - source: "parent/src/test1.qs", - range: Range { - start: Position { - line: 0, - column: 0, - }, - end: Position { - line: 0, - column: 34, - }, - }, - }, - ), - ( - "test2.Test2", - Location { - source: "parent/src/test2.qs", - range: Range { - start: Position { - line: 0, - column: 0, - }, - end: Position { - line: 0, - column: 34, - }, - }, - }, - ), - ], - version: None, }, ] "#]] From 4c48051aadc2166ed24605ca77660125fab5b19f Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 21 Jan 2025 20:11:29 -0800 Subject: [PATCH 62/82] update ts types --- vscode/src/common.ts | 4 ++-- vscode/src/language-service/testExplorer.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vscode/src/common.ts b/vscode/src/common.ts index a74afcc300..c695bf7b1e 100644 --- a/vscode/src/common.ts +++ b/vscode/src/common.ts @@ -103,10 +103,10 @@ export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { return vscodeDiagnostic; } -export function loadCompilerWorker(extensionUri: string): ICompilerWorker { +export function loadCompilerWorker(extensionUri: vscode.Uri): ICompilerWorker { const compilerWorkerScriptPath = vscode.Uri.joinPath( extensionUri, "./out/compilerWorker.js", ).toString(); - getCompilerWorker(compilerWorkerScriptPath); + return getCompilerWorker(compilerWorkerScriptPath); } diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index 6453294efb..ad0da27afd 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -19,7 +19,7 @@ let worker: ICompilerWorker | null = null; * @param context The extension context. * @returns The compiler worker. **/ -function getLocalCompilerWorker(extensionUri: string): ICompilerWorker { +function getLocalCompilerWorker(extensionUri: vscode.Uri): ICompilerWorker { if (worker !== null) { return worker; } @@ -48,7 +48,7 @@ export function startTestDiscovery( // use the compiler worker to run the test in the interpreter log.trace("Starting test run, request was", JSON.stringify(request)); - const worker = getLocalCompilerWorker(context); + const worker = getLocalCompilerWorker(context.extensionUri); const programResult = await getActiveProgram(); if (!programResult.success) { From 2471cc6c607ef81f7e384a3d5f4031034a23de3b Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Wed, 22 Jan 2025 14:55:43 -0800 Subject: [PATCH 63/82] fix test explorer --- vscode/src/language-service/testExplorer.ts | 1 + wasm/src/language_service.rs | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index ad0da27afd..1bb7e1d82e 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -173,6 +173,7 @@ export function startTestDiscovery( deleteItemsNotOfVersion(version, testItem.children, testController); if (testMetadata.get(testItem) !== version) { items.delete(id); + } else { } } } diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs index 8808298b0f..84b996e584 100644 --- a/wasm/src/language_service.rs +++ b/wasm/src/language_service.rs @@ -6,6 +6,7 @@ use crate::{ line_column::{ILocation, IPosition, IRange, Location, Position, Range}, project_system::ProjectHost, serializable_type, + test_discovery::TestDescriptor, }; use qsc::{ self, line_column::Encoding, linter::LintConfig, target::Profile, LanguageFeatures, PackageType, @@ -69,8 +70,11 @@ impl LanguageService { let callables = update .callables .iter() - .map(|(name, location)| -> (String, Location) { - (name.clone(), location.clone().into()) + .map(|(callable_name, location)| -> TestDescriptor { + TestDescriptor { + callable_name: callable_name.into(), + location: location.clone().into(), + } }) .collect::>(); From 1bac92503ada05df5a4500bd57ad928e55d2803d Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Wed, 22 Jan 2025 14:59:58 -0800 Subject: [PATCH 64/82] Remove empty else block --- vscode/src/language-service/testExplorer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index 1bb7e1d82e..ad0da27afd 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -173,7 +173,6 @@ export function startTestDiscovery( deleteItemsNotOfVersion(version, testItem.children, testController); if (testMetadata.get(testItem) !== version) { items.delete(id); - } else { } } } From 33397382bb6a4a4d7b9afc2fdb40f092806f1b11 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Thu, 23 Jan 2025 08:05:15 -0800 Subject: [PATCH 65/82] Fix basics.js types --- npm/qsharp/test/basics.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/npm/qsharp/test/basics.js b/npm/qsharp/test/basics.js index 63bff9b5eb..5b18df543a 100644 --- a/npm/qsharp/test/basics.js +++ b/npm/qsharp/test/basics.js @@ -511,8 +511,8 @@ test("test callable discovery", async () => { gotTests = true; assert.equal(event.type, "testCallables"); assert.equal(event.detail.callables.length, 1); - assert.equal(event.detail.callables[0][0], "Sample.main"); - assert.deepStrictEqual(event.detail.callables[0][1], { + assert.equal(event.detail.callables[0].callableName, "Sample.main"); + assert.deepStrictEqual(event.detail.callables[0].location, { source: "test.qs", span: { end: { @@ -547,10 +547,10 @@ test("multiple test callable discovery", async () => { gotTests = true; assert.equal(event.type, "testCallables"); assert.equal(event.detail.callables.length, 4); - assert.equal(event.detail.callables[0][0], "Sample.test1"); - assert.equal(event.detail.callables[1][0], "Sample.test2"); - assert.equal(event.detail.callables[2][0], "Sample2.test1"); - assert.equal(event.detail.callables[3][0], "Sample2.test2"); + assert.equal(event.detail.callables[0].callableName, "Sample.test1"); + assert.equal(event.detail.callables[1].callableName, "Sample.test2"); + assert.equal(event.detail.callables[2].callableName, "Sample2.test1"); + assert.equal(event.detail.callables[3].callableName, "Sample2.test2"); }); await languageService.updateDocument( "test.qs", From 1b74be2a708a487c8df83d548c099368f423b0b9 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 27 Jan 2025 10:01:02 -0800 Subject: [PATCH 66/82] fix bugs; respond to feedback --- compiler/qsc_hir/src/hir.rs | 11 ++-- language_service/src/protocol.rs | 6 +- language_service/src/state.rs | 60 +++++++++++-------- language_service/src/state/tests.rs | 4 +- .../src/language-service/language-service.ts | 9 ++- vscode/src/debugger/output.ts | 9 ++- vscode/src/language-service/testExplorer.ts | 34 ++++++++--- wasm/src/language_service.rs | 5 +- wasm/src/project_system.rs | 9 --- 9 files changed, 87 insertions(+), 60 deletions(-) diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs index fba13f9b4f..45d9732abf 100644 --- a/compiler/qsc_hir/src/hir.rs +++ b/compiler/qsc_hir/src/hir.rs @@ -301,24 +301,25 @@ impl Package { { return None; } + // this is indeed a test callable, so let's grab its parent name - let name = match item.parent { + let (name, span) = match item.parent { None => Default::default(), Some(parent_id) => { let parent_item = self .items .get(parent_id) .expect("Parent item did not exist in package"); - if let ItemKind::Namespace(ns, _) = &parent_item.kind { + let name = if let ItemKind::Namespace(ns, _) = &parent_item.kind { format!("{}.{}", ns.name(), callable.name.name) } else { callable.name.name.to_string() - } + }; + let span = callable.name.span; + (name, span) } }; - let span = item.span; - Some((name, span)) } else { None diff --git a/language_service/src/protocol.rs b/language_service/src/protocol.rs index 3c4d2ebea1..a9efb6e0b4 100644 --- a/language_service/src/protocol.rs +++ b/language_service/src/protocol.rs @@ -34,9 +34,13 @@ pub struct DiagnosticUpdate { pub errors: Vec, } +/// This is a string that represents the interpreter-ready name of the test callable. +/// i.e. "Main.TestCase". Call it by adding parens to the end, e.g. `Main.TestCase()` +pub type CallableName = String; + #[derive(Debug)] pub struct TestCallables { - pub callables: Vec<(String, Location)>, + pub callables: Vec<(CallableName, Location)>, } #[derive(Debug)] diff --git a/language_service/src/state.rs b/language_service/src/state.rs index e6431c3832..6a16bf0932 100644 --- a/language_service/src/state.rs +++ b/language_service/src/state.rs @@ -159,6 +159,9 @@ impl<'a> CompilationStateUpdater<'a> { let compilation_uri = project.path.clone(); + // publish test callables only for the latest-updated compilation URI + self.publish_test_callables(&compilation_uri); + let prev_compilation_uri = self.with_state_mut(|state| { state .open_documents @@ -183,7 +186,7 @@ impl<'a> CompilationStateUpdater<'a> { self.insert_buffer_aware_compilation(project); - self.publish_diagnostics_and_test_callables(); + self.publish_diagnostics(); } /// Attempts to resolve a manifest for the given document uri. @@ -284,7 +287,7 @@ impl<'a> CompilationStateUpdater<'a> { } } - self.publish_diagnostics_and_test_callables(); + self.publish_diagnostics(); } /// Removes a document from the open documents map. If the @@ -381,7 +384,7 @@ impl<'a> CompilationStateUpdater<'a> { (compilation, notebook_configuration), ); }); - self.publish_diagnostics_and_test_callables(); + self.publish_diagnostics(); } pub(super) fn close_notebook_document(&mut self, notebook_uri: &str) { @@ -399,19 +402,18 @@ impl<'a> CompilationStateUpdater<'a> { state.compilations.remove(notebook_uri); }); - self.publish_diagnostics_and_test_callables(); + self.publish_diagnostics(); } // It gets really messy knowing when to clear diagnostics // when the document changes ownership between compilations, etc. // So let's do it the simplest way possible. Republish all the diagnostics every time. - fn publish_diagnostics_and_test_callables(&mut self) { + fn publish_diagnostics(&mut self) { let last_docs_with_errors = take(&mut self.documents_with_errors); let mut docs_with_errors = FxHashSet::default(); self.with_state(|state| { for (compilation_uri, compilation) in &state.compilations { - self.publish_test_callables(&compilation.0); trace!("publishing diagnostics for {compilation_uri}"); for (uri, errors) in map_errors_to_docs( @@ -513,7 +515,7 @@ impl<'a> CompilationStateUpdater<'a> { } }); - self.publish_diagnostics_and_test_callables(); + self.publish_diagnostics(); } /// Borrows the compilation state immutably and invokes `f`. @@ -544,26 +546,32 @@ impl<'a> CompilationStateUpdater<'a> { f(&mut state) } - fn publish_test_callables(&self, compilation: &Compilation) { - let callables = TestCallables { - callables: compilation - .test_cases - .iter() - .map(|(name, span)| { - ( - name.clone(), - crate::qsc_utils::into_location( - self.position_encoding, - compilation, - *span, - compilation.user_package_id, - ), - ) - }) - .collect(), - }; + fn publish_test_callables(&self, compilation_uri: &str) { + self.with_state(|state| { + let Some(compilation) = state.compilations.get(compilation_uri).map(|x| &x.0) else { + return; + }; + + let callables = TestCallables { + callables: compilation + .test_cases + .iter() + .map(|(name, span)| { + ( + name.clone(), + crate::qsc_utils::into_location( + self.position_encoding, + compilation, + *span, + compilation.user_package_id, + ), + ) + }) + .collect(), + }; - (self.test_callable_receiver)(callables); + (self.test_callable_receiver)(callables); + }); } } diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index 854dec97a2..ee5e9fd841 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -11,9 +11,7 @@ use crate::{ }; use expect_test::{expect, Expect}; use miette::Diagnostic; -use qsc::{ - compile, line_column::Encoding, project, target::Profile, LanguageFeatures, PackageType, -}; +use qsc::{line_column::Encoding, target::Profile, LanguageFeatures, PackageType}; use qsc_linter::{AstLint, LintConfig, LintKind, LintLevel}; use std::{ cell::RefCell, diff --git a/npm/qsharp/src/language-service/language-service.ts b/npm/qsharp/src/language-service/language-service.ts index 92ed210a85..268b742bd9 100644 --- a/npm/qsharp/src/language-service/language-service.ts +++ b/npm/qsharp/src/language-service/language-service.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ITestDescriptor } from "../../lib/node/qsc_wasm.cjs"; import type { ICodeAction, ICodeLens, @@ -17,6 +16,7 @@ import type { IWorkspaceEdit, LanguageService, VSDiagnostic, + ITestDescriptor, } from "../../lib/web/qsc_wasm.js"; import { IProjectHost } from "../browser.js"; import { log } from "../log.js"; @@ -275,7 +275,8 @@ export class QSharpLanguageService implements ILanguageService { diagnostics: VSDiagnostic[], ) { try { - const event = new Event("diagnostics") as LanguageServiceEvent & Event; + const event = new Event("diagnostics") as LanguageServiceDiagnosticEvent & + Event; event.detail = { uri, version: version ?? 0, @@ -289,7 +290,9 @@ export class QSharpLanguageService implements ILanguageService { async onTestCallables(callables: ITestDescriptor[]) { try { - const event = new Event("testCallables") as LanguageServiceEvent & Event; + const event = new Event( + "testCallables", + ) as LanguageServiceTestCallablesEvent & Event; event.detail = { callables, }; diff --git a/vscode/src/debugger/output.ts b/vscode/src/debugger/output.ts index 00dbb0da74..bf180c247e 100644 --- a/vscode/src/debugger/output.ts +++ b/vscode/src/debugger/output.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { QscEventTarget } from "qsharp-lang"; +import { QscEventTarget, VSDiagnostic } from "qsharp-lang"; function formatComplex(real: number, imag: number) { // Format -0 as 0 @@ -72,7 +72,12 @@ export function createDebugConsoleEventTarget(out: (message: string) => void) { }); eventTarget.addEventListener("Result", (evt) => { - out(`${evt.detail.value}`); + // sometimes these are VS Diagnostics + if ((evt.detail.value as VSDiagnostic).message !== undefined) { + out(`${(evt.detail.value as VSDiagnostic).message}`); + } else { + out(`${evt.detail.value}`); + } }); return eventTarget; diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index ad0da27afd..1ea0e5e09a 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -56,9 +56,19 @@ export function startTestDiscovery( } const program = programResult.programConfig; - - for (const testCase of request.include || []) { - await runTestCase(testController, testCase, request, worker, program); + // request.include is an array of test cases to run, and it is only provided if a specific set of tests were selected. + if (request.include !== undefined) { + for (const testCase of request.include || []) { + await runTestCase(testController, testCase, request, worker, program); + } + } else { + // alternatively, if there is no include specified, we run all tests that are not in the exclude list + for (const [, testCase] of testController.items) { + if (request.exclude && request.exclude.includes(testCase)) { + continue; + } + await runTestCase(testController, testCase, request, worker, program); + } } }; @@ -90,12 +100,22 @@ export function startTestDiscovery( if (msg.detail.success) { run.passed(testCase); } else { + const failureLocation = + msg.detail?.value?.uri || + (msg.detail?.value?.related && + msg.detail.value.related[0].location?.source) || + null; + + log.info("msg: ", JSON.stringify(msg, null, 2)); const message: vscode.TestMessage = { message: msg.detail.value.message, - location: { - range: toVsCodeRange(msg.detail.value.range), - uri: vscode.Uri.parse(msg.detail.value.uri || ""), - }, + location: + failureLocation === null + ? undefined + : { + range: toVsCodeRange(msg.detail.value.range), + uri: vscode.Uri.parse(failureLocation), + }, }; run.failed(testCase, message); } diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs index 84b996e584..7908aa1952 100644 --- a/wasm/src/language_service.rs +++ b/wasm/src/language_service.rs @@ -32,13 +32,10 @@ impl LanguageService { pub fn start_background_work( &mut self, - diagnostics_callback: DiagnosticsCallback, + diagnostics_callback: &DiagnosticsCallback, test_callables_callback: &TestCallableCallback, host: ProjectHost, ) -> js_sys::Promise { - let diagnostics_callback = - crate::project_system::to_js_function(diagnostics_callback.obj, "diagnostics_callback"); - let diagnostics_callback = diagnostics_callback .dyn_ref::() .expect("expected a valid JS function") diff --git a/wasm/src/project_system.rs b/wasm/src/project_system.rs index f1b7b62613..a15699b1cc 100644 --- a/wasm/src/project_system.rs +++ b/wasm/src/project_system.rs @@ -73,15 +73,6 @@ extern "C" { fn profile(this: &ProgramConfig) -> String; } -pub(crate) fn to_js_function(val: JsValue, help_text_panic: &'static str) -> js_sys::Function { - let js_ty = val.js_typeof(); - assert!( - val.is_function(), - "expected a valid JS function ({help_text_panic}), received {js_ty:?}" - ); - Into::::into(val) -} - thread_local! { static PACKAGE_CACHE: Rc> = Rc::default(); } /// a minimal implementation for interacting with async JS filesystem callbacks to From 96763f9f0dcfc815cb42b378ae67c9ff075edfbe Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 27 Jan 2025 10:10:24 -0800 Subject: [PATCH 67/82] update expects --- language_service/src/state/tests.rs | 138 ++-------------------------- 1 file changed, 8 insertions(+), 130 deletions(-) diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index ee5e9fd841..9edfda8827 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -1620,28 +1620,7 @@ async fn test_case_detected() { .await; expect![[r#" - [ - TestCallables { - callables: [ - ( - "main.MyTestCase", - Location { - source: "parent/src/main.qs", - range: Range { - start: Position { - line: 0, - column: 0, - }, - end: Position { - line: 0, - column: 39, - }, - }, - }, - ), - ], - }, - ] + [] "#]] .assert_debug_eq(&test_cases.borrow()); } @@ -1674,11 +1653,7 @@ async fn test_case_removed() { .await; expect![[r#" - [ - TestCallables { - callables: [], - }, - ] + [] "#]] .assert_debug_eq(&test_cases.borrow()); } @@ -1733,31 +1708,11 @@ async fn test_case_modified() { range: Range { start: Position { line: 0, - column: 0, + column: 17, }, end: Position { line: 0, - column: 39, - }, - }, - }, - ), - ], - }, - TestCallables { - callables: [ - ( - "main.MyTestCase2", - Location { - source: "parent/src/main.qs", - range: Range { - start: Position { - line: 0, - column: 0, - }, - end: Position { - line: 0, - column: 41, + column: 27, }, }, }, @@ -1815,20 +1770,17 @@ async fn test_annotation_removed() { range: Range { start: Position { line: 0, - column: 0, + column: 17, }, end: Position { line: 0, - column: 39, + column: 27, }, }, }, ), ], }, - TestCallables { - callables: [], - }, ] "#]] .assert_debug_eq(&test_cases.borrow()); @@ -1869,44 +1821,7 @@ async fn multiple_tests() { .await; expect![[r#" - [ - TestCallables { - callables: [ - ( - "main.Test1", - Location { - source: "parent/src/main.qs", - range: Range { - start: Position { - line: 0, - column: 0, - }, - end: Position { - line: 0, - column: 34, - }, - }, - }, - ), - ( - "main.Test2", - Location { - source: "parent/src/main.qs", - range: Range { - start: Position { - line: 0, - column: 35, - }, - end: Position { - line: 0, - column: 69, - }, - }, - }, - ), - ], - }, - ] + [] "#]] .assert_debug_eq(&test_cases.borrow()); } @@ -1946,44 +1861,7 @@ async fn test_case_in_different_files() { .await; expect![[r#" - [ - TestCallables { - callables: [ - ( - "test1.Test1", - Location { - source: "parent/src/test1.qs", - range: Range { - start: Position { - line: 0, - column: 0, - }, - end: Position { - line: 0, - column: 34, - }, - }, - }, - ), - ( - "test2.Test2", - Location { - source: "parent/src/test2.qs", - range: Range { - start: Position { - line: 0, - column: 0, - }, - end: Position { - line: 0, - column: 34, - }, - }, - }, - ), - ], - }, - ] + [] "#]] .assert_debug_eq(&test_cases.borrow()); } From 637393b8e7c5cd523c4057f947144f0203ca13c5 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 27 Jan 2025 11:13:52 -0800 Subject: [PATCH 68/82] fix issue where callable parents have spans in test explorer --- vscode/src/language-service/testExplorer.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index 1ea0e5e09a..8d6d72249b 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -48,6 +48,7 @@ export function startTestDiscovery( // use the compiler worker to run the test in the interpreter log.trace("Starting test run, request was", JSON.stringify(request)); + const worker = getLocalCompilerWorker(context.extensionUri); const programResult = await getActiveProgram(); @@ -106,7 +107,6 @@ export function startTestDiscovery( msg.detail.value.related[0].location?.source) || null; - log.info("msg: ", JSON.stringify(msg, null, 2)); const message: vscode.TestMessage = { message: msg.detail.value.message, location: @@ -168,7 +168,9 @@ export function startTestDiscovery( let testItem = rover.get(id); if (!testItem) { testItem = testController.createTestItem(id, part, vscLocation.uri); - testItem.range = vscLocation.range; + if (i === parts.length - 1) { + testItem.range = vscLocation.range; + } rover.add(testItem); } testMetadata.set(testItem, currentVersion); From eadd1d890b1680520e0cd8ef76c784bd2e642cbf Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 27 Jan 2025 11:32:27 -0800 Subject: [PATCH 69/82] add top level file names to discovered test callables --- language_service/src/protocol.rs | 5 ++- language_service/src/state.rs | 40 ++++++++++----------- vscode/src/language-service/testExplorer.ts | 6 ++-- wasm/src/language_service.rs | 3 +- wasm/src/test_discovery.rs | 4 ++- 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/language_service/src/protocol.rs b/language_service/src/protocol.rs index a9efb6e0b4..09bce2b581 100644 --- a/language_service/src/protocol.rs +++ b/language_service/src/protocol.rs @@ -38,9 +38,12 @@ pub struct DiagnosticUpdate { /// i.e. "Main.TestCase". Call it by adding parens to the end, e.g. `Main.TestCase()` pub type CallableName = String; +/// A string that represents the originating compilation URI of this callable +pub type CompilationUri = String; + #[derive(Debug)] pub struct TestCallables { - pub callables: Vec<(CallableName, Location)>, + pub callables: Vec<(CompilationUri, CallableName, Location)>, } #[derive(Debug)] diff --git a/language_service/src/state.rs b/language_service/src/state.rs index 6a16bf0932..bd3b650513 100644 --- a/language_service/src/state.rs +++ b/language_service/src/state.rs @@ -159,9 +159,6 @@ impl<'a> CompilationStateUpdater<'a> { let compilation_uri = project.path.clone(); - // publish test callables only for the latest-updated compilation URI - self.publish_test_callables(&compilation_uri); - let prev_compilation_uri = self.with_state_mut(|state| { state .open_documents @@ -186,7 +183,7 @@ impl<'a> CompilationStateUpdater<'a> { self.insert_buffer_aware_compilation(project); - self.publish_diagnostics(); + self.publish_diagnostics_and_test_callables(); } /// Attempts to resolve a manifest for the given document uri. @@ -287,7 +284,7 @@ impl<'a> CompilationStateUpdater<'a> { } } - self.publish_diagnostics(); + self.publish_diagnostics_and_test_callables(); } /// Removes a document from the open documents map. If the @@ -384,7 +381,7 @@ impl<'a> CompilationStateUpdater<'a> { (compilation, notebook_configuration), ); }); - self.publish_diagnostics(); + self.publish_diagnostics_and_test_callables(); } pub(super) fn close_notebook_document(&mut self, notebook_uri: &str) { @@ -402,13 +399,14 @@ impl<'a> CompilationStateUpdater<'a> { state.compilations.remove(notebook_uri); }); - self.publish_diagnostics(); + self.publish_diagnostics_and_test_callables(); } // It gets really messy knowing when to clear diagnostics // when the document changes ownership between compilations, etc. // So let's do it the simplest way possible. Republish all the diagnostics every time. - fn publish_diagnostics(&mut self) { + fn publish_diagnostics_and_test_callables(&mut self) { + self.publish_test_callables(); let last_docs_with_errors = take(&mut self.documents_with_errors); let mut docs_with_errors = FxHashSet::default(); @@ -515,7 +513,7 @@ impl<'a> CompilationStateUpdater<'a> { } }); - self.publish_diagnostics(); + self.publish_diagnostics_and_test_callables(); } /// Borrows the compilation state immutably and invokes `f`. @@ -546,18 +544,16 @@ impl<'a> CompilationStateUpdater<'a> { f(&mut state) } - fn publish_test_callables(&self, compilation_uri: &str) { + fn publish_test_callables(&self) { self.with_state(|state| { - let Some(compilation) = state.compilations.get(compilation_uri).map(|x| &x.0) else { - return; - }; - - let callables = TestCallables { - callables: compilation - .test_cases - .iter() - .map(|(name, span)| { + // get test callables from each compilation + let callables: Vec<_> = state + .compilations + .iter() + .flat_map(|(compilation_uri, (compilation, _))| { + compilation.test_cases.iter().map(move |(name, span)| { ( + compilation_uri.to_string(), name.clone(), crate::qsc_utils::into_location( self.position_encoding, @@ -567,10 +563,10 @@ impl<'a> CompilationStateUpdater<'a> { ), ) }) - .collect(), - }; + }) + .collect(); - (self.test_callable_receiver)(callables); + (self.test_callable_receiver)(TestCallables { callables }); }); } } diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index 8d6d72249b..4848266f38 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -153,10 +153,10 @@ export function startTestDiscovery( currentVersion = (testMetadata.get(testItem) || 0) + 1; break; } - - for (const { callableName, location } of evt.detail.callables) { + + for (const { compilationUri, callableName, location } of evt.detail.callables) { const vscLocation = toVsCodeLocation(location); - const parts = callableName.split("."); + const parts = [compilationUri, ...callableName.split(".")]; let rover = testController.items; for (let i = 0; i < parts.length; i++) { diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs index 7908aa1952..ae11217786 100644 --- a/wasm/src/language_service.rs +++ b/wasm/src/language_service.rs @@ -67,8 +67,9 @@ impl LanguageService { let callables = update .callables .iter() - .map(|(callable_name, location)| -> TestDescriptor { + .map(|(compilation_uri, callable_name, location)| -> TestDescriptor { TestDescriptor { + compilation_uri: compilation_uri.clone(), callable_name: callable_name.into(), location: location.clone().into(), } diff --git a/wasm/src/test_discovery.rs b/wasm/src/test_discovery.rs index 0a918052e0..e989f9f802 100644 --- a/wasm/src/test_discovery.rs +++ b/wasm/src/test_discovery.rs @@ -11,10 +11,12 @@ serializable_type! { { pub callable_name: String, pub location: crate::line_column::Location, + pub compilation_uri: String, }, r#"export interface ITestDescriptor { - callableName: string; + callableName: string; location: ILocation; + compilationUri: string; }"#, ITestDescriptor } From bb7b88a3d1a688a3aa9b0ca8c1bb9e2dc676de5d Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 27 Jan 2025 11:57:54 -0800 Subject: [PATCH 70/82] switch to human readable names in test explorer --- language_service/src/compilation.rs | 17 +++++++++++++++++ language_service/src/completion.rs | 1 + language_service/src/protocol.rs | 5 ++++- language_service/src/state.rs | 2 ++ language_service/src/test_utils.rs | 1 + vscode/src/language-service/testExplorer.ts | 4 ++-- wasm/src/language_service.rs | 3 ++- wasm/src/test_discovery.rs | 2 ++ 8 files changed, 31 insertions(+), 4 deletions(-) diff --git a/language_service/src/compilation.rs b/language_service/src/compilation.rs index 13ef56a1dd..c75bf95034 100644 --- a/language_service/src/compilation.rs +++ b/language_service/src/compilation.rs @@ -47,6 +47,8 @@ pub(crate) enum CompilationKind { /// one or more sources, and a target profile. OpenProject { package_graph_sources: PackageGraphSources, + /// a human-readable name for the package (not a unique URI -- meant to be read by humans) + human_readable_name: Arc, }, /// A Q# notebook. In a notebook compilation, the user package /// contains multiple `Source`s, with each source corresponding @@ -63,6 +65,7 @@ impl Compilation { lints_config: &[LintConfig], package_graph_sources: PackageGraphSources, project_errors: Vec, + pretty_name: &Arc, ) -> Self { let mut buildable_program = prepare_package_store(target_profile.into(), package_graph_sources.clone()); @@ -110,6 +113,7 @@ impl Compilation { user_package_id: package_id, kind: CompilationKind::OpenProject { package_graph_sources, + human_readable_name: pretty_name.clone(), }, compile_errors, project_errors, @@ -235,6 +239,17 @@ impl Compilation { } } + pub fn human_readable_project_name(&self) -> Arc { + match &self.kind { + CompilationKind::OpenProject { human_readable_name, .. } => human_readable_name.clone(), + CompilationKind::Notebook { project } => { + project + .as_ref() + .map_or_else(|| Arc::from("Notebook"), |p| p.name.clone()) + } + } + } + /// Gets the `CompileUnit` associated with user (non-library) code. pub fn user_unit(&self) -> &CompileUnit { self.package_store @@ -329,6 +344,7 @@ impl Compilation { let new = match self.kind { CompilationKind::OpenProject { ref package_graph_sources, + ref human_readable_name, } => Self::new( package_type, target_profile, @@ -336,6 +352,7 @@ impl Compilation { lints_config, package_graph_sources.clone(), Vec::new(), // project errors will stay the same + human_readable_name ), CompilationKind::Notebook { ref project } => Self::new_notebook( sources.into_iter(), diff --git a/language_service/src/completion.rs b/language_service/src/completion.rs index 45f92b07d2..10884e7198 100644 --- a/language_service/src/completion.rs +++ b/language_service/src/completion.rs @@ -115,6 +115,7 @@ fn expected_word_kinds( match &compilation.kind { CompilationKind::OpenProject { package_graph_sources, + .. } => possible_words_at_offset_in_source( source_contents, Some(source_name_relative), diff --git a/language_service/src/protocol.rs b/language_service/src/protocol.rs index 09bce2b581..faaa510e43 100644 --- a/language_service/src/protocol.rs +++ b/language_service/src/protocol.rs @@ -41,9 +41,12 @@ pub type CallableName = String; /// A string that represents the originating compilation URI of this callable pub type CompilationUri = String; +/// A human readable name that represents the compilation. +pub type HumanReadableName = String; + #[derive(Debug)] pub struct TestCallables { - pub callables: Vec<(CompilationUri, CallableName, Location)>, + pub callables: Vec<(CompilationUri, CallableName, Location, HumanReadableName)>, } #[derive(Debug)] diff --git a/language_service/src/state.rs b/language_service/src/state.rs index bd3b650513..d41f4e6269 100644 --- a/language_service/src/state.rs +++ b/language_service/src/state.rs @@ -262,6 +262,7 @@ impl<'a> CompilationStateUpdater<'a> { &configuration.lints_config, loaded_project.package_graph_sources, loaded_project.errors, + &loaded_project.name ); state @@ -561,6 +562,7 @@ impl<'a> CompilationStateUpdater<'a> { *span, compilation.user_package_id, ), + compilation.human_readable_project_name().to_string(), ) }) }) diff --git a/language_service/src/test_utils.rs b/language_service/src/test_utils.rs index 67ff4bb2b3..a1adb96599 100644 --- a/language_service/src/test_utils.rs +++ b/language_service/src/test_utils.rs @@ -232,6 +232,7 @@ fn compile_project_with_markers_cursor_optional( user_package_id: package_id, kind: CompilationKind::OpenProject { package_graph_sources, + human_readable_name: Arc::from("test project") }, compile_errors: errors, project_errors: Vec::new(), diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index 4848266f38..32adae9bbe 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -154,9 +154,9 @@ export function startTestDiscovery( break; } - for (const { compilationUri, callableName, location } of evt.detail.callables) { + for (const { compilationUri, callableName, location, humanReadableName } of evt.detail.callables) { const vscLocation = toVsCodeLocation(location); - const parts = [compilationUri, ...callableName.split(".")]; + const parts = [humanReadableName, ...callableName.split(".")]; let rover = testController.items; for (let i = 0; i < parts.length; i++) { diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs index ae11217786..583db9810d 100644 --- a/wasm/src/language_service.rs +++ b/wasm/src/language_service.rs @@ -67,11 +67,12 @@ impl LanguageService { let callables = update .callables .iter() - .map(|(compilation_uri, callable_name, location)| -> TestDescriptor { + .map(|(compilation_uri, callable_name, location, human_readable_name)| -> TestDescriptor { TestDescriptor { compilation_uri: compilation_uri.clone(), callable_name: callable_name.into(), location: location.clone().into(), + human_readable_name: human_readable_name.clone(), } }) .collect::>(); diff --git a/wasm/src/test_discovery.rs b/wasm/src/test_discovery.rs index e989f9f802..118560a323 100644 --- a/wasm/src/test_discovery.rs +++ b/wasm/src/test_discovery.rs @@ -12,11 +12,13 @@ serializable_type! { pub callable_name: String, pub location: crate::line_column::Location, pub compilation_uri: String, + pub human_readable_name: String, }, r#"export interface ITestDescriptor { callableName: string; location: ILocation; compilationUri: string; + humanReadableName: string; }"#, ITestDescriptor } From 2baf59aef47e9bb9abdc10570c8565a94c2d6d32 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 27 Jan 2025 12:14:24 -0800 Subject: [PATCH 71/82] finish moving to globally scoped test callable discovery --- language_service/src/compilation.rs | 15 +++--- language_service/src/state.rs | 2 +- language_service/src/test_utils.rs | 2 +- vscode/src/language-service/testExplorer.ts | 55 ++++++++++++++------- vscode/src/projectSystem.ts | 8 +++ 5 files changed, 56 insertions(+), 26 deletions(-) diff --git a/language_service/src/compilation.rs b/language_service/src/compilation.rs index c75bf95034..61b3a3fb76 100644 --- a/language_service/src/compilation.rs +++ b/language_service/src/compilation.rs @@ -241,12 +241,13 @@ impl Compilation { pub fn human_readable_project_name(&self) -> Arc { match &self.kind { - CompilationKind::OpenProject { human_readable_name, .. } => human_readable_name.clone(), - CompilationKind::Notebook { project } => { - project - .as_ref() - .map_or_else(|| Arc::from("Notebook"), |p| p.name.clone()) - } + CompilationKind::OpenProject { + human_readable_name, + .. + } => human_readable_name.clone(), + CompilationKind::Notebook { project } => project + .as_ref() + .map_or_else(|| Arc::from("Notebook"), |p| p.name.clone()), } } @@ -352,7 +353,7 @@ impl Compilation { lints_config, package_graph_sources.clone(), Vec::new(), // project errors will stay the same - human_readable_name + human_readable_name, ), CompilationKind::Notebook { ref project } => Self::new_notebook( sources.into_iter(), diff --git a/language_service/src/state.rs b/language_service/src/state.rs index d41f4e6269..4701158733 100644 --- a/language_service/src/state.rs +++ b/language_service/src/state.rs @@ -262,7 +262,7 @@ impl<'a> CompilationStateUpdater<'a> { &configuration.lints_config, loaded_project.package_graph_sources, loaded_project.errors, - &loaded_project.name + &loaded_project.name, ); state diff --git a/language_service/src/test_utils.rs b/language_service/src/test_utils.rs index a1adb96599..af26958986 100644 --- a/language_service/src/test_utils.rs +++ b/language_service/src/test_utils.rs @@ -232,7 +232,7 @@ fn compile_project_with_markers_cursor_optional( user_package_id: package_id, kind: CompilationKind::OpenProject { package_graph_sources, - human_readable_name: Arc::from("test project") + human_readable_name: Arc::from("test project"), }, compile_errors: errors, project_errors: Vec::new(), diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index 32adae9bbe..847666dc92 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -10,9 +10,13 @@ import { } from "qsharp-lang"; import * as vscode from "vscode"; import { loadCompilerWorker, toVsCodeLocation, toVsCodeRange } from "../common"; -import { getActiveProgram } from "../programConfig"; +import { getActiveProgram, getProgramForDocument } from "../programConfig"; import { createDebugConsoleEventTarget } from "../debugger/output"; +interface TestMetadata { + compilationUri: vscode.Uri; +} + let worker: ICompilerWorker | null = null; /** * Returns a singleton instance of the compiler worker. @@ -51,16 +55,10 @@ export function startTestDiscovery( const worker = getLocalCompilerWorker(context.extensionUri); - const programResult = await getActiveProgram(); - if (!programResult.success) { - throw new Error(programResult.errorMsg); - } - - const program = programResult.programConfig; // request.include is an array of test cases to run, and it is only provided if a specific set of tests were selected. if (request.include !== undefined) { for (const testCase of request.include || []) { - await runTestCase(testController, testCase, request, worker, program); + await runTestCase(testController, testCase, request, worker); } } else { // alternatively, if there is no include specified, we run all tests that are not in the exclude list @@ -68,7 +66,7 @@ export function startTestDiscovery( if (request.exclude && request.exclude.includes(testCase)) { continue; } - await runTestCase(testController, testCase, request, worker, program); + await runTestCase(testController, testCase, request, worker); } } }; @@ -84,12 +82,11 @@ export function startTestDiscovery( testCase: vscode.TestItem, request: vscode.TestRunRequest, worker: ICompilerWorker, - program: ProgramConfig, ): Promise { log.trace("Running Q# test: ", testCase.id); if (testCase.children.size > 0) { for (const childTestCase of testCase.children) { - await runTestCase(ctrl, childTestCase[1], request, worker, program); + await runTestCase(ctrl, childTestCase[1], request, worker); } return; } @@ -123,6 +120,20 @@ export function startTestDiscovery( }); const callableExpr = `${testCase.id}()`; + const uri = testMetadata.get(testCase)?.compilationUri; + if (!uri) { + log.error(`No compilation URI for test ${testCase.id}`); + run.appendOutput(`No compilation URI for test ${testCase.id}\r\n`); + return; + } + const programResult = await getProgramForDocument(uri); + + log.info(JSON.stringify(programResult, null, 2)); + if (!programResult.success) { + throw new Error(programResult.errorMsg); + } + + const program = programResult.programConfig; try { await worker.run(program, callableExpr, 1, evtTarget); @@ -142,7 +153,8 @@ export function startTestDiscovery( false, ); - const testMetadata = new WeakMap(); + const testVersions = new WeakMap(); + const testMetadata = new WeakMap(); async function onTestCallables(evt: { detail: { callables: ITestDescriptor[]; @@ -150,11 +162,16 @@ export function startTestDiscovery( }) { let currentVersion = 0; for (const [, testItem] of testController.items) { - currentVersion = (testMetadata.get(testItem) || 0) + 1; + currentVersion = (testVersions.get(testItem) || 0) + 1; break; } - - for (const { compilationUri, callableName, location, humanReadableName } of evt.detail.callables) { + + for (const { + compilationUri, + callableName, + location, + humanReadableName, + } of evt.detail.callables) { const vscLocation = toVsCodeLocation(location); const parts = [humanReadableName, ...callableName.split(".")]; @@ -168,12 +185,16 @@ export function startTestDiscovery( let testItem = rover.get(id); if (!testItem) { testItem = testController.createTestItem(id, part, vscLocation.uri); + // if this is the actual test item, give it a range and a compilation uri if (i === parts.length - 1) { testItem.range = vscLocation.range; + testMetadata.set(testItem, { + compilationUri: vscode.Uri.parse(compilationUri), + }); } rover.add(testItem); } - testMetadata.set(testItem, currentVersion); + testVersions.set(testItem, currentVersion); rover = testItem.children; } } @@ -193,7 +214,7 @@ export function startTestDiscovery( ) { for (const [id, testItem] of items) { deleteItemsNotOfVersion(version, testItem.children, testController); - if (testMetadata.get(testItem) !== version) { + if (testVersions.get(testItem) !== version) { items.delete(id); } } diff --git a/vscode/src/projectSystem.ts b/vscode/src/projectSystem.ts index cff724769f..935231f43c 100644 --- a/vscode/src/projectSystem.ts +++ b/vscode/src/projectSystem.ts @@ -24,6 +24,14 @@ async function findManifestDocument( // vscode-vfs://github%2B7b2276223a312c22726566223a7b2274797065223a332c226964223a22383439227d7d/microsoft/qsharp/samples/shor.qs const currentDocumentUri = URI.parse(currentDocumentUriString); + // if this document is itself a manifest file, then we've found it + if (currentDocumentUri.path.endsWith("qsharp.json")) { + return { + directory: Utils.dirname(currentDocumentUri), + manifest: currentDocumentUri, + }; + } + // Untitled documents don't have a file location, thus can't have a manifest if (currentDocumentUri.scheme === "untitled") return null; From 321d65af3861705244c0478983a61dca3e72d1f9 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 27 Jan 2025 12:24:41 -0800 Subject: [PATCH 72/82] update to new test callables mode --- language_service/src/state/tests.rs | 146 +++++++++++++++++++++++++++- npm/qsharp/test/basics.js | 6 +- 2 files changed, 145 insertions(+), 7 deletions(-) diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index 9edfda8827..b539836aa9 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -1620,7 +1620,30 @@ async fn test_case_detected() { .await; expect![[r#" - [] + [ + TestCallables { + callables: [ + ( + "parent/qsharp.json", + "main.MyTestCase", + Location { + source: "parent/src/main.qs", + range: Range { + start: Position { + line: 0, + column: 17, + }, + end: Position { + line: 0, + column: 27, + }, + }, + }, + "parent", + ), + ], + }, + ] "#]] .assert_debug_eq(&test_cases.borrow()); } @@ -1653,7 +1676,11 @@ async fn test_case_removed() { .await; expect![[r#" - [] + [ + TestCallables { + callables: [], + }, + ] "#]] .assert_debug_eq(&test_cases.borrow()); } @@ -1702,6 +1729,7 @@ async fn test_case_modified() { TestCallables { callables: [ ( + "parent/qsharp.json", "main.MyTestCase", Location { source: "parent/src/main.qs", @@ -1716,6 +1744,29 @@ async fn test_case_modified() { }, }, }, + "parent", + ), + ], + }, + TestCallables { + callables: [ + ( + "parent/qsharp.json", + "main.MyTestCase2", + Location { + source: "parent/src/main.qs", + range: Range { + start: Position { + line: 0, + column: 17, + }, + end: Position { + line: 0, + column: 28, + }, + }, + }, + "parent", ), ], }, @@ -1764,6 +1815,7 @@ async fn test_annotation_removed() { TestCallables { callables: [ ( + "parent/qsharp.json", "main.MyTestCase", Location { source: "parent/src/main.qs", @@ -1778,9 +1830,13 @@ async fn test_annotation_removed() { }, }, }, + "parent", ), ], }, + TestCallables { + callables: [], + }, ] "#]] .assert_debug_eq(&test_cases.borrow()); @@ -1821,7 +1877,48 @@ async fn multiple_tests() { .await; expect![[r#" - [] + [ + TestCallables { + callables: [ + ( + "parent/qsharp.json", + "main.Test1", + Location { + source: "parent/src/main.qs", + range: Range { + start: Position { + line: 0, + column: 17, + }, + end: Position { + line: 0, + column: 22, + }, + }, + }, + "parent", + ), + ( + "parent/qsharp.json", + "main.Test2", + Location { + source: "parent/src/main.qs", + range: Range { + start: Position { + line: 0, + column: 52, + }, + end: Position { + line: 0, + column: 57, + }, + }, + }, + "parent", + ), + ], + }, + ] "#]] .assert_debug_eq(&test_cases.borrow()); } @@ -1861,7 +1958,48 @@ async fn test_case_in_different_files() { .await; expect![[r#" - [] + [ + TestCallables { + callables: [ + ( + "parent/qsharp.json", + "test1.Test1", + Location { + source: "parent/src/test1.qs", + range: Range { + start: Position { + line: 0, + column: 17, + }, + end: Position { + line: 0, + column: 22, + }, + }, + }, + "parent", + ), + ( + "parent/qsharp.json", + "test2.Test2", + Location { + source: "parent/src/test2.qs", + range: Range { + start: Position { + line: 0, + column: 17, + }, + end: Position { + line: 0, + column: 22, + }, + }, + }, + "parent", + ), + ], + }, + ] "#]] .assert_debug_eq(&test_cases.borrow()); } diff --git a/npm/qsharp/test/basics.js b/npm/qsharp/test/basics.js index 5b18df543a..abb787d993 100644 --- a/npm/qsharp/test/basics.js +++ b/npm/qsharp/test/basics.js @@ -516,12 +516,12 @@ test("test callable discovery", async () => { source: "test.qs", span: { end: { - character: 30, + character: 18, line: 2, }, start: { - character: 4, - line: 1, + character: 14, + line: 2, }, }, }); From f772acefa22e8cae9d7ab1bd9261d67cf43c0ec5 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 27 Jan 2025 12:28:27 -0800 Subject: [PATCH 73/82] remove unused imports --- vscode/src/language-service/testExplorer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index 847666dc92..16a8ab894a 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -6,11 +6,10 @@ import { ILanguageService, ITestDescriptor, log, - ProgramConfig, } from "qsharp-lang"; import * as vscode from "vscode"; import { loadCompilerWorker, toVsCodeLocation, toVsCodeRange } from "../common"; -import { getActiveProgram, getProgramForDocument } from "../programConfig"; +import { getProgramForDocument } from "../programConfig"; import { createDebugConsoleEventTarget } from "../debugger/output"; interface TestMetadata { From 8c6859c4ccc23a4069f9ff1f3d8528748e8f1881 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Mon, 27 Jan 2025 14:42:21 -0800 Subject: [PATCH 74/82] Remove notebook callables from test explorer --- language_service/src/compilation.rs | 10 +++++----- language_service/src/state.rs | 9 ++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/language_service/src/compilation.rs b/language_service/src/compilation.rs index 61b3a3fb76..01b5cd25fa 100644 --- a/language_service/src/compilation.rs +++ b/language_service/src/compilation.rs @@ -239,15 +239,15 @@ impl Compilation { } } - pub fn human_readable_project_name(&self) -> Arc { + /// Returns a human-readable compilation name if one exists. + /// Notebooks don't have human-readable compilation names. + pub fn human_readable_project_name(&self) -> Option> { match &self.kind { CompilationKind::OpenProject { human_readable_name, .. - } => human_readable_name.clone(), - CompilationKind::Notebook { project } => project - .as_ref() - .map_or_else(|| Arc::from("Notebook"), |p| p.name.clone()), + } => Some(human_readable_name.clone()), + CompilationKind::Notebook { .. } => None, } } diff --git a/language_service/src/state.rs b/language_service/src/state.rs index 4701158733..1c60208d12 100644 --- a/language_service/src/state.rs +++ b/language_service/src/state.rs @@ -553,7 +553,7 @@ impl<'a> CompilationStateUpdater<'a> { .iter() .flat_map(|(compilation_uri, (compilation, _))| { compilation.test_cases.iter().map(move |(name, span)| { - ( + Some(( compilation_uri.to_string(), name.clone(), crate::qsc_utils::into_location( @@ -562,10 +562,13 @@ impl<'a> CompilationStateUpdater<'a> { *span, compilation.user_package_id, ), - compilation.human_readable_project_name().to_string(), - ) + // notebooks don't have human readable names -- we use this + // to filter them out in the test explorer + compilation.human_readable_project_name()?.to_string(), + )) }) }) + .flatten() .collect(); (self.test_callable_receiver)(TestCallables { callables }); From 3810cca2bf1d11f281f75931e60f4f3045e0f4dd Mon Sep 17 00:00:00 2001 From: Alex Hansen Date: Tue, 28 Jan 2025 07:28:56 -0800 Subject: [PATCH 75/82] Update language_service/src/state/tests.rs Co-authored-by: Mine Starks <16928427+minestarks@users.noreply.github.com> --- language_service/src/state/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index b539836aa9..28596ca537 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -2062,7 +2062,7 @@ fn new_updater<'a>( TestProjectHost { fs: TEST_FS.with(Clone::clone), }, - Encoding::Utf16, + Encoding::Utf8, ) } From 2f7aff2af4ae13cbe28610c287abdeed71ca062e Mon Sep 17 00:00:00 2001 From: Alex Hansen Date: Tue, 28 Jan 2025 07:29:08 -0800 Subject: [PATCH 76/82] Update language_service/src/state/tests.rs Co-authored-by: Mine Starks <16928427+minestarks@users.noreply.github.com> --- language_service/src/state/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index 28596ca537..a013915414 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -2086,7 +2086,7 @@ fn new_updater_with_file_system<'a>( diagnostic_receiver, test_callable_receiver, TestProjectHost { fs: fs.clone() }, - Encoding::Utf16, + Encoding::Utf8, ) } From a38bc23922b211b4319689644f66b5cc53c762a4 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 28 Jan 2025 08:11:59 -0800 Subject: [PATCH 77/82] refactors from PR feedback --- language_service/src/compilation.rs | 17 +++++++--------- language_service/src/protocol.rs | 23 +++++++++++---------- language_service/src/state.rs | 14 +++++++------ language_service/src/test_utils.rs | 2 +- wasm/src/language_service.rs | 31 +++++++++++++++-------------- wasm/src/test_discovery.rs | 2 +- 6 files changed, 46 insertions(+), 43 deletions(-) diff --git a/language_service/src/compilation.rs b/language_service/src/compilation.rs index 01b5cd25fa..3521fad23d 100644 --- a/language_service/src/compilation.rs +++ b/language_service/src/compilation.rs @@ -48,7 +48,7 @@ pub(crate) enum CompilationKind { OpenProject { package_graph_sources: PackageGraphSources, /// a human-readable name for the package (not a unique URI -- meant to be read by humans) - human_readable_name: Arc, + friendly_name: Arc, }, /// A Q# notebook. In a notebook compilation, the user package /// contains multiple `Source`s, with each source corresponding @@ -65,7 +65,7 @@ impl Compilation { lints_config: &[LintConfig], package_graph_sources: PackageGraphSources, project_errors: Vec, - pretty_name: &Arc, + friendly_name: &Arc, ) -> Self { let mut buildable_program = prepare_package_store(target_profile.into(), package_graph_sources.clone()); @@ -113,7 +113,7 @@ impl Compilation { user_package_id: package_id, kind: CompilationKind::OpenProject { package_graph_sources, - human_readable_name: pretty_name.clone(), + friendly_name: friendly_name.clone(), }, compile_errors, project_errors, @@ -241,12 +241,9 @@ impl Compilation { /// Returns a human-readable compilation name if one exists. /// Notebooks don't have human-readable compilation names. - pub fn human_readable_project_name(&self) -> Option> { + pub fn friendly_project_name(&self) -> Option> { match &self.kind { - CompilationKind::OpenProject { - human_readable_name, - .. - } => Some(human_readable_name.clone()), + CompilationKind::OpenProject { friendly_name, .. } => Some(friendly_name.clone()), CompilationKind::Notebook { .. } => None, } } @@ -345,7 +342,7 @@ impl Compilation { let new = match self.kind { CompilationKind::OpenProject { ref package_graph_sources, - ref human_readable_name, + ref friendly_name, } => Self::new( package_type, target_profile, @@ -353,7 +350,7 @@ impl Compilation { lints_config, package_graph_sources.clone(), Vec::new(), // project errors will stay the same - human_readable_name, + friendly_name, ), CompilationKind::Notebook { ref project } => Self::new_notebook( sources.into_iter(), diff --git a/language_service/src/protocol.rs b/language_service/src/protocol.rs index faaa510e43..8a6c2f340d 100644 --- a/language_service/src/protocol.rs +++ b/language_service/src/protocol.rs @@ -34,19 +34,21 @@ pub struct DiagnosticUpdate { pub errors: Vec, } -/// This is a string that represents the interpreter-ready name of the test callable. -/// i.e. "Main.TestCase". Call it by adding parens to the end, e.g. `Main.TestCase()` -pub type CallableName = String; - -/// A string that represents the originating compilation URI of this callable -pub type CompilationUri = String; - -/// A human readable name that represents the compilation. -pub type HumanReadableName = String; +#[derive(Debug)] +pub struct TestCallable { + /// This is a string that represents the interpreter-ready name of the test callable. + /// i.e. "Main.TestCase". Call it by adding parens to the end, e.g. `Main.TestCase()` + pub callable_name: Arc, + /// A string that represents the originating compilation URI of this callable + pub compilation_uri: Arc, + pub location: Location, + /// A human readable name that represents the compilation. + pub friendly_name: Arc, +} #[derive(Debug)] pub struct TestCallables { - pub callables: Vec<(CompilationUri, CallableName, Location, HumanReadableName)>, + pub callables: Vec, } #[derive(Debug)] @@ -128,6 +130,7 @@ impl PartialEq for CompletionItem { impl Eq for CompletionItem {} use std::hash::{Hash, Hasher}; +use std::sync::Arc; impl Hash for CompletionItem { fn hash(&self, state: &mut H) { diff --git a/language_service/src/state.rs b/language_service/src/state.rs index 1c60208d12..00a726fe53 100644 --- a/language_service/src/state.rs +++ b/language_service/src/state.rs @@ -4,6 +4,8 @@ #[cfg(test)] mod tests; +use crate::protocol::TestCallable; + use super::compilation::Compilation; use super::protocol::{ DiagnosticUpdate, ErrorKind, NotebookMetadata, TestCallables, WorkspaceConfigurationUpdate, @@ -553,10 +555,10 @@ impl<'a> CompilationStateUpdater<'a> { .iter() .flat_map(|(compilation_uri, (compilation, _))| { compilation.test_cases.iter().map(move |(name, span)| { - Some(( - compilation_uri.to_string(), - name.clone(), - crate::qsc_utils::into_location( + Some(TestCallable { + compilation_uri: Arc::from(compilation_uri.as_ref()), + callable_name: Arc::from(name.as_ref()), + location: crate::qsc_utils::into_location( self.position_encoding, compilation, *span, @@ -564,8 +566,8 @@ impl<'a> CompilationStateUpdater<'a> { ), // notebooks don't have human readable names -- we use this // to filter them out in the test explorer - compilation.human_readable_project_name()?.to_string(), - )) + friendly_name: compilation.friendly_project_name()?, + }) }) }) .flatten() diff --git a/language_service/src/test_utils.rs b/language_service/src/test_utils.rs index af26958986..01c9fbf710 100644 --- a/language_service/src/test_utils.rs +++ b/language_service/src/test_utils.rs @@ -232,7 +232,7 @@ fn compile_project_with_markers_cursor_optional( user_package_id: package_id, kind: CompilationKind::OpenProject { package_graph_sources, - human_readable_name: Arc::from("test project"), + friendly_name: Arc::from("test project"), }, compile_errors: errors, project_errors: Vec::new(), diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs index 583db9810d..b187b089df 100644 --- a/wasm/src/language_service.rs +++ b/wasm/src/language_service.rs @@ -12,7 +12,7 @@ use qsc::{ self, line_column::Encoding, linter::LintConfig, target::Profile, LanguageFeatures, PackageType, }; use qsc_project::Manifest; -use qsls::protocol::{DiagnosticUpdate, TestCallables}; +use qsls::protocol::{DiagnosticUpdate, TestCallable, TestCallables}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -63,28 +63,29 @@ impl LanguageService { .expect("expected a valid JS function") .clone(); - let test_callables_callback = move |update: TestCallables| { - let callables = update + let test_callables_callback = + move |update: TestCallables| { + let callables = update .callables .iter() - .map(|(compilation_uri, callable_name, location, human_readable_name)| -> TestDescriptor { + .map(|TestCallable { compilation_uri, callable_name, location, friendly_name }| -> TestDescriptor { TestDescriptor { - compilation_uri: compilation_uri.clone(), - callable_name: callable_name.into(), + compilation_uri: compilation_uri.to_string(), + callable_name: callable_name.to_string(), location: location.clone().into(), - human_readable_name: human_readable_name.clone(), + friendly_name: friendly_name.to_string(), } }) .collect::>(); - let _ = test_callables_callback - .call1( - &JsValue::NULL, - &serde_wasm_bindgen::to_value(&callables) - .expect("conversion to TestCallables should succeed"), - ) - .expect("callback should succeed"); - }; + let _ = test_callables_callback + .call1( + &JsValue::NULL, + &serde_wasm_bindgen::to_value(&callables) + .expect("conversion to TestCallables should succeed"), + ) + .expect("callback should succeed"); + }; let mut worker = self.0 .create_update_worker(diagnostics_callback, test_callables_callback, host); diff --git a/wasm/src/test_discovery.rs b/wasm/src/test_discovery.rs index 118560a323..2f18c2dd27 100644 --- a/wasm/src/test_discovery.rs +++ b/wasm/src/test_discovery.rs @@ -12,7 +12,7 @@ serializable_type! { pub callable_name: String, pub location: crate::line_column::Location, pub compilation_uri: String, - pub human_readable_name: String, + pub friendly_name: String, }, r#"export interface ITestDescriptor { callableName: string; From a15699e680c79795ed214a4e82ac6fd40fa104ff Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 28 Jan 2025 08:20:28 -0800 Subject: [PATCH 78/82] PR feedback --- vscode/src/language-service/testExplorer.ts | 20 ++------- wasm/src/language_service.rs | 45 ++++++++++++--------- wasm/src/test_discovery.rs | 2 +- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index 16a8ab894a..17ac926fc4 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -12,10 +12,6 @@ import { loadCompilerWorker, toVsCodeLocation, toVsCodeRange } from "../common"; import { getProgramForDocument } from "../programConfig"; import { createDebugConsoleEventTarget } from "../debugger/output"; -interface TestMetadata { - compilationUri: vscode.Uri; -} - let worker: ICompilerWorker | null = null; /** * Returns a singleton instance of the compiler worker. @@ -119,7 +115,7 @@ export function startTestDiscovery( }); const callableExpr = `${testCase.id}()`; - const uri = testMetadata.get(testCase)?.compilationUri; + const uri = testCase.uri; if (!uri) { log.error(`No compilation URI for test ${testCase.id}`); run.appendOutput(`No compilation URI for test ${testCase.id}\r\n`); @@ -153,7 +149,6 @@ export function startTestDiscovery( ); const testVersions = new WeakMap(); - const testMetadata = new WeakMap(); async function onTestCallables(evt: { detail: { callables: ITestDescriptor[]; @@ -165,14 +160,10 @@ export function startTestDiscovery( break; } - for (const { - compilationUri, - callableName, - location, - humanReadableName, - } of evt.detail.callables) { + for (const { compilationUri, callableName, location, friendlyName } of evt + .detail.callables) { const vscLocation = toVsCodeLocation(location); - const parts = [humanReadableName, ...callableName.split(".")]; + const parts = [friendlyName, ...callableName.split(".")]; let rover = testController.items; for (let i = 0; i < parts.length; i++) { @@ -187,9 +178,6 @@ export function startTestDiscovery( // if this is the actual test item, give it a range and a compilation uri if (i === parts.length - 1) { testItem.range = vscLocation.range; - testMetadata.set(testItem, { - compilationUri: vscode.Uri.parse(compilationUri), - }); } rover.add(testItem); } diff --git a/wasm/src/language_service.rs b/wasm/src/language_service.rs index b187b089df..76ef3a9662 100644 --- a/wasm/src/language_service.rs +++ b/wasm/src/language_service.rs @@ -63,29 +63,36 @@ impl LanguageService { .expect("expected a valid JS function") .clone(); - let test_callables_callback = - move |update: TestCallables| { - let callables = update + let test_callables_callback = move |update: TestCallables| { + let callables = update .callables .iter() - .map(|TestCallable { compilation_uri, callable_name, location, friendly_name }| -> TestDescriptor { - TestDescriptor { - compilation_uri: compilation_uri.to_string(), - callable_name: callable_name.to_string(), - location: location.clone().into(), - friendly_name: friendly_name.to_string(), - } - }) + .map( + |TestCallable { + compilation_uri, + callable_name, + location, + friendly_name, + }| + -> TestDescriptor { + TestDescriptor { + compilation_uri: compilation_uri.to_string(), + callable_name: callable_name.to_string(), + location: location.clone().into(), + friendly_name: friendly_name.to_string(), + } + }, + ) .collect::>(); - let _ = test_callables_callback - .call1( - &JsValue::NULL, - &serde_wasm_bindgen::to_value(&callables) - .expect("conversion to TestCallables should succeed"), - ) - .expect("callback should succeed"); - }; + let _ = test_callables_callback + .call1( + &JsValue::NULL, + &serde_wasm_bindgen::to_value(&callables) + .expect("conversion to TestCallables should succeed"), + ) + .expect("callback should succeed"); + }; let mut worker = self.0 .create_update_worker(diagnostics_callback, test_callables_callback, host); diff --git a/wasm/src/test_discovery.rs b/wasm/src/test_discovery.rs index 2f18c2dd27..6314960515 100644 --- a/wasm/src/test_discovery.rs +++ b/wasm/src/test_discovery.rs @@ -18,7 +18,7 @@ serializable_type! { callableName: string; location: ILocation; compilationUri: string; - humanReadableName: string; + friendlyName: string; }"#, ITestDescriptor } From 52e224c39b177e49b9e0d561b3625252189e653a Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 28 Jan 2025 08:41:38 -0800 Subject: [PATCH 79/82] fix unused var --- vscode/src/language-service/testExplorer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index 17ac926fc4..0835733592 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -160,8 +160,8 @@ export function startTestDiscovery( break; } - for (const { compilationUri, callableName, location, friendlyName } of evt - .detail.callables) { + for (const { callableName, location, friendlyName } of evt.detail + .callables) { const vscLocation = toVsCodeLocation(location); const parts = [friendlyName, ...callableName.split(".")]; From 15e52f65d7440bacc506cd7fac6e3d74b0967acb Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 28 Jan 2025 08:58:21 -0800 Subject: [PATCH 80/82] update expect --- language_service/src/state/tests.rs | 96 ++++++++++++++--------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index a013915414..9e5d7a4a45 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -1623,10 +1623,10 @@ async fn test_case_detected() { [ TestCallables { callables: [ - ( - "parent/qsharp.json", - "main.MyTestCase", - Location { + TestCallable { + callable_name: "main.MyTestCase", + compilation_uri: "parent/qsharp.json", + location: Location { source: "parent/src/main.qs", range: Range { start: Position { @@ -1639,8 +1639,8 @@ async fn test_case_detected() { }, }, }, - "parent", - ), + friendly_name: "parent", + }, ], }, ] @@ -1728,10 +1728,10 @@ async fn test_case_modified() { [ TestCallables { callables: [ - ( - "parent/qsharp.json", - "main.MyTestCase", - Location { + TestCallable { + callable_name: "main.MyTestCase", + compilation_uri: "parent/qsharp.json", + location: Location { source: "parent/src/main.qs", range: Range { start: Position { @@ -1744,16 +1744,16 @@ async fn test_case_modified() { }, }, }, - "parent", - ), + friendly_name: "parent", + }, ], }, TestCallables { callables: [ - ( - "parent/qsharp.json", - "main.MyTestCase2", - Location { + TestCallable { + callable_name: "main.MyTestCase2", + compilation_uri: "parent/qsharp.json", + location: Location { source: "parent/src/main.qs", range: Range { start: Position { @@ -1766,8 +1766,8 @@ async fn test_case_modified() { }, }, }, - "parent", - ), + friendly_name: "parent", + }, ], }, ] @@ -1814,10 +1814,10 @@ async fn test_annotation_removed() { [ TestCallables { callables: [ - ( - "parent/qsharp.json", - "main.MyTestCase", - Location { + TestCallable { + callable_name: "main.MyTestCase", + compilation_uri: "parent/qsharp.json", + location: Location { source: "parent/src/main.qs", range: Range { start: Position { @@ -1830,8 +1830,8 @@ async fn test_annotation_removed() { }, }, }, - "parent", - ), + friendly_name: "parent", + }, ], }, TestCallables { @@ -1880,10 +1880,10 @@ async fn multiple_tests() { [ TestCallables { callables: [ - ( - "parent/qsharp.json", - "main.Test1", - Location { + TestCallable { + callable_name: "main.Test1", + compilation_uri: "parent/qsharp.json", + location: Location { source: "parent/src/main.qs", range: Range { start: Position { @@ -1896,12 +1896,12 @@ async fn multiple_tests() { }, }, }, - "parent", - ), - ( - "parent/qsharp.json", - "main.Test2", - Location { + friendly_name: "parent", + }, + TestCallable { + callable_name: "main.Test2", + compilation_uri: "parent/qsharp.json", + location: Location { source: "parent/src/main.qs", range: Range { start: Position { @@ -1914,8 +1914,8 @@ async fn multiple_tests() { }, }, }, - "parent", - ), + friendly_name: "parent", + }, ], }, ] @@ -1961,10 +1961,10 @@ async fn test_case_in_different_files() { [ TestCallables { callables: [ - ( - "parent/qsharp.json", - "test1.Test1", - Location { + TestCallable { + callable_name: "test1.Test1", + compilation_uri: "parent/qsharp.json", + location: Location { source: "parent/src/test1.qs", range: Range { start: Position { @@ -1977,12 +1977,12 @@ async fn test_case_in_different_files() { }, }, }, - "parent", - ), - ( - "parent/qsharp.json", - "test2.Test2", - Location { + friendly_name: "parent", + }, + TestCallable { + callable_name: "test2.Test2", + compilation_uri: "parent/qsharp.json", + location: Location { source: "parent/src/test2.qs", range: Range { start: Position { @@ -1995,8 +1995,8 @@ async fn test_case_in_different_files() { }, }, }, - "parent", - ), + friendly_name: "parent", + }, ], }, ] From 75474befc7f290afebde49c65612c2459ff928e0 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 28 Jan 2025 09:46:12 -0800 Subject: [PATCH 81/82] Remove a log --- vscode/src/language-service/testExplorer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index 0835733592..723a1c4369 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -123,7 +123,6 @@ export function startTestDiscovery( } const programResult = await getProgramForDocument(uri); - log.info(JSON.stringify(programResult, null, 2)); if (!programResult.success) { throw new Error(programResult.errorMsg); } From 92fbb1a6edc6f7f40f8db6f15ddec449a6855ac1 Mon Sep 17 00:00:00 2001 From: Alexander Hansen Date: Tue, 28 Jan 2025 10:30:31 -0800 Subject: [PATCH 82/82] Add explanatory comments to algorithm --- vscode/src/language-service/testExplorer.ts | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/vscode/src/language-service/testExplorer.ts b/vscode/src/language-service/testExplorer.ts index 723a1c4369..b6204d0a1e 100644 --- a/vscode/src/language-service/testExplorer.ts +++ b/vscode/src/language-service/testExplorer.ts @@ -162,19 +162,42 @@ export function startTestDiscovery( for (const { callableName, location, friendlyName } of evt.detail .callables) { const vscLocation = toVsCodeLocation(location); + // below, we transform `parts` into a tree structure for the test explorer + // e.g. if we have the following callables: + // - "TestSuite.Test1" + // - "TestSuite.Test2" + // they will be turned into the parts: + // - ["FriendlyName", "TestSuite", "Test1"] + // - ["FriendlyName", "TestSuite", "Test2"] + // and then into a tree structure: + // - FriendlyName + // - TestSuite + // - Test1 + // - Test2 const parts = [friendlyName, ...callableName.split(".")]; let rover = testController.items; for (let i = 0; i < parts.length; i++) { const part = parts[i]; + // the `id` is used to actually call the test item + // it is constructed in the test runner via: callableExpr = `${testCase.id}()`; + // so it should be the full path to the test item, not including the "friendly name" (since that isn't in the callable expr), + // if and only if it is a "leaf" (an actual test case) + // note that leaves are test cases and internal nodes are not test cases + // in teh above example, TestSuite would have the id `FriendlyName.TestSuite`, and Test1 would have the id `TestSuite.Test1` const id = i === parts.length - 1 ? callableName : parts.slice(0, i + 1).join("."); + // this test item may have already existed from a previous scan, so fetch it let testItem = rover.get(id); + + // if it doesn't exist, create it if (!testItem) { testItem = testController.createTestItem(id, part, vscLocation.uri); // if this is the actual test item, give it a range and a compilation uri + // this triggers the little "green play button" in the test explorer and in the left + // gutter of the editor if (i === parts.length - 1) { testItem.range = vscLocation.range; }