From b79c31e1c3cb7406181eafa7659b8c1b6d937300 Mon Sep 17 00:00:00 2001 From: sezna Date: Tue, 10 Dec 2024 06:04:19 -0800 Subject: [PATCH 01/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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()) }