From 519d591d163abf94e910c662aa54a7eb67b1370e Mon Sep 17 00:00:00 2001 From: seem Date: Thu, 18 Jun 2026 18:44:54 +0200 Subject: [PATCH 1/4] use disposable store --- .../vscode/src/test/code-cell-symbols.test.ts | 132 ++++++++---------- apps/vscode/src/test/test-utils.ts | 16 ++- 2 files changed, 69 insertions(+), 79 deletions(-) diff --git a/apps/vscode/src/test/code-cell-symbols.test.ts b/apps/vscode/src/test/code-cell-symbols.test.ts index 0607a357..34dbe24e 100644 --- a/apps/vscode/src/test/code-cell-symbols.test.ts +++ b/apps/vscode/src/test/code-cell-symbols.test.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode"; import * as assert from "assert"; import { openUniqueExampleDocument, wait } from "./test-utils"; +import { DisposableStore } from "core"; /** * Creates a fake document symbol provider that returns DocumentSymbol[] for virtual docs. @@ -69,6 +70,8 @@ function flattenSymbolNames(symbols: vscode.DocumentSymbol[]): string[] { } suite("Code Cell Symbols", function () { + const disposables = new DisposableStore(); + setup(async function () { await vscode.workspace .getConfiguration("quarto") @@ -77,6 +80,7 @@ suite("Code Cell Symbols", function () { }); teardown(async function () { + disposables.clear(); await vscode.workspace .getConfiguration("quarto") .update("symbols.showCodeCellsInOutline", undefined); @@ -102,106 +106,88 @@ suite("Code Cell Symbols", function () { // Register BEFORE opening the document // Use both scheme and language like the formatting tests - const provider = vscode.languages.registerDocumentSymbolProvider( + disposables.add(vscode.languages.registerDocumentSymbolProvider( { scheme: "file", pattern: "**/.vdoc.*" }, createFakeDocumentSymbolProvider(fakeSymbols) - ); + )); await wait(100); - const { doc, cleanup } = await openUniqueExampleDocument("format/basics.qmd"); - try { - await wait(800); + const { doc } = await openUniqueExampleDocument("format/basics.qmd", disposables); + await wait(800); - const symbols = await vscode.commands.executeCommand( - "vscode.executeDocumentSymbolProvider", - doc.uri - ); + const symbols = await vscode.commands.executeCommand( + "vscode.executeDocumentSymbolProvider", + doc.uri + ); - const names = flattenSymbolNames(symbols); - assert.ok( - names.includes("my_function"), - `Expected 'my_function' in symbols, got: ${names.join(", ")}` - ); - assert.ok( - names.includes("my_variable"), - `Expected 'my_variable' in symbols, got: ${names.join(", ")}` - ); - assert.ok( - names.includes("(code cell)"), - `Expected '(code cell)' in symbols, got: ${names.join(", ")}` - ); - } finally { - provider.dispose(); - await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); - cleanup(); - } + const names = flattenSymbolNames(symbols); + assert.ok( + names.includes("my_function"), + `Expected 'my_function' in symbols, got: ${names.join(", ")}` + ); + assert.ok( + names.includes("my_variable"), + `Expected 'my_variable' in symbols, got: ${names.join(", ")}` + ); + assert.ok( + names.includes("(code cell)"), + `Expected '(code cell)' in symbols, got: ${names.join(", ")}` + ); }); test("handles SymbolInformation[] from embedded provider", async function () { const symbolNames = ["info_function", "info_class"]; // Register BEFORE opening the document - const provider = vscode.languages.registerDocumentSymbolProvider( + disposables.add(vscode.languages.registerDocumentSymbolProvider( { scheme: "file", pattern: "**/.vdoc.*" }, createFakeSymbolInformationProvider(symbolNames) - ); + )); await wait(100); - const { doc, cleanup } = await openUniqueExampleDocument("format/basics.qmd"); - try { - await wait(800); + const { doc } = await openUniqueExampleDocument("format/basics.qmd", disposables); + await wait(800); - const symbols = await vscode.commands.executeCommand( - "vscode.executeDocumentSymbolProvider", - doc.uri - ); + const symbols = await vscode.commands.executeCommand( + "vscode.executeDocumentSymbolProvider", + doc.uri + ); - const names = flattenSymbolNames(symbols); - assert.ok( - names.includes("(code cell)"), - `Expected '(code cell)' in symbols, got: ${names.join(", ")}` - ); - assert.ok( - names.includes("info_function"), - `Expected 'info_function' in symbols, got: ${names.join(", ")}` - ); - assert.ok( - names.includes("info_class"), - `Expected 'info_class' in symbols, got: ${names.join(", ")}` - ); - } finally { - provider.dispose(); - await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); - cleanup(); - } + const names = flattenSymbolNames(symbols); + assert.ok( + names.includes("(code cell)"), + `Expected '(code cell)' in symbols, got: ${names.join(", ")}` + ); + assert.ok( + names.includes("info_function"), + `Expected 'info_function' in symbols, got: ${names.join(", ")}` + ); + assert.ok( + names.includes("info_class"), + `Expected 'info_class' in symbols, got: ${names.join(", ")}` + ); }); test("handles undefined from embedded provider without error", async function () { // Register BEFORE opening the document - const provider = vscode.languages.registerDocumentSymbolProvider( + disposables.add(vscode.languages.registerDocumentSymbolProvider( { scheme: "file", pattern: "**/.vdoc.*" }, createUndefinedSymbolProvider() - ); + )); await wait(100); - const { doc, cleanup } = await openUniqueExampleDocument("format/basics.qmd"); - try { - await wait(800); + const { doc } = await openUniqueExampleDocument("format/basics.qmd", disposables); + await wait(800); - const symbols = await vscode.commands.executeCommand( - "vscode.executeDocumentSymbolProvider", - doc.uri - ); + const symbols = await vscode.commands.executeCommand( + "vscode.executeDocumentSymbolProvider", + doc.uri + ); - const names = flattenSymbolNames(symbols); - assert.ok( - names.includes("(code cell)"), - `Expected '(code cell)' to still appear even when embedded provider returns undefined, got: ${names.join(", ")}` - ); - } finally { - provider.dispose(); - await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); - cleanup(); - } + const names = flattenSymbolNames(symbols); + assert.ok( + names.includes("(code cell)"), + `Expected '(code cell)' to still appear even when embedded provider returns undefined, got: ${names.join(", ")}` + ); }); }); diff --git a/apps/vscode/src/test/test-utils.ts b/apps/vscode/src/test/test-utils.ts index 3fc0918c..fa459f83 100644 --- a/apps/vscode/src/test/test-utils.ts +++ b/apps/vscode/src/test/test-utils.ts @@ -1,3 +1,4 @@ +import { DisposableStore } from "core"; import * as fs from "fs"; import * as path from "path"; import * as vscode from "vscode"; @@ -51,8 +52,8 @@ export async function openAndShowUri( } /** - * Opens a unique on-disk copy of an example file and returns it along with a - * `cleanup` function that deletes the copy. + * Opens a unique on-disk copy of an example file and registers a callback + * with a disposable store to delete the copy. * * Use this instead of `openAndShowExamplesTextDocument` when a test exercises a * provider command that caches results per document URI, such as @@ -64,21 +65,24 @@ export async function openAndShowUri( * actually runs. * * The copy is created alongside the original example file so workspace-relative - * behavior (LSP, configuration) is preserved. Always call `cleanup()` in a - * `finally` block. + * behavior (LSP, configuration) is preserved. Always dispose `disposables` in + * the `teardown` hook. */ -export async function openUniqueExampleDocument(fileName: string) { +export async function openUniqueExampleDocument(fileName: string, disposables: DisposableStore) { const sourcePath = path.join(WORKSPACE_PATH, fileName); const extension = path.extname(fileName); const uniqueName = `${path.basename(fileName, extension)}-${Date.now()}-${Math.random().toString(36).slice(2)}${extension}`; const uniquePath = path.join(path.dirname(sourcePath), uniqueName); + + // Ensure that the copy is deleted on dispose (usually, on test `teardown`). + disposables.add({ dispose: () => fs.rmSync(uniquePath, { force: true }) }); + fs.copyFileSync(sourcePath, uniquePath); const { doc, editor } = await openAndShowUri(vscode.Uri.file(uniquePath)); return { doc, editor, - cleanup: () => fs.rmSync(uniquePath, { force: true }), }; } From 58487d091eebfb28004096c1eadb78058501c0e5 Mon Sep 17 00:00:00 2001 From: seem Date: Thu, 18 Jun 2026 19:20:01 +0200 Subject: [PATCH 2/4] use the existing unique example function in diagnostics tests --- .../vscode/src/test/code-cell-symbols.test.ts | 8 +- apps/vscode/src/test/diagnostics.test.ts | 73 +++++++------------ apps/vscode/src/test/test-utils.ts | 17 +++-- 3 files changed, 40 insertions(+), 58 deletions(-) diff --git a/apps/vscode/src/test/code-cell-symbols.test.ts b/apps/vscode/src/test/code-cell-symbols.test.ts index 34dbe24e..f229d60e 100644 --- a/apps/vscode/src/test/code-cell-symbols.test.ts +++ b/apps/vscode/src/test/code-cell-symbols.test.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; import * as assert from "assert"; -import { openUniqueExampleDocument, wait } from "./test-utils"; +import { openAndShowUniqueExamplesDocument, wait } from "./test-utils"; import { DisposableStore } from "core"; /** @@ -112,7 +112,7 @@ suite("Code Cell Symbols", function () { )); await wait(100); - const { doc } = await openUniqueExampleDocument("format/basics.qmd", disposables); + const { doc } = await openAndShowUniqueExamplesDocument("format/basics.qmd", disposables); await wait(800); const symbols = await vscode.commands.executeCommand( @@ -145,7 +145,7 @@ suite("Code Cell Symbols", function () { )); await wait(100); - const { doc } = await openUniqueExampleDocument("format/basics.qmd", disposables); + const { doc } = await openAndShowUniqueExamplesDocument("format/basics.qmd", disposables); await wait(800); const symbols = await vscode.commands.executeCommand( @@ -176,7 +176,7 @@ suite("Code Cell Symbols", function () { )); await wait(100); - const { doc } = await openUniqueExampleDocument("format/basics.qmd", disposables); + const { doc } = await openAndShowUniqueExamplesDocument("format/basics.qmd", disposables); await wait(800); const symbols = await vscode.commands.executeCommand( diff --git a/apps/vscode/src/test/diagnostics.test.ts b/apps/vscode/src/test/diagnostics.test.ts index edf25e96..892a1827 100644 --- a/apps/vscode/src/test/diagnostics.test.ts +++ b/apps/vscode/src/test/diagnostics.test.ts @@ -1,8 +1,7 @@ import * as assert from "assert"; import * as vscode from "vscode"; -import { randomUUID } from "crypto"; import { LanguageClient } from "vscode-languageclient/node"; -import { examplesUri, raceTimeout } from "./test-utils"; +import { raceTimeout, uniqueExamplesUri } from "./test-utils"; import { testLanguageClient } from "./fixtures/test-language-client"; import { DidUpdateDiagnosticsEvent, EmbeddedDiagnosticsManager, VdocDisposeReason } from "../providers/diagnostics"; import { VIRTUAL_DOC_TEMP_DIRECTORY, deleteDocument, quartoVdocDir } from "../vdoc/vdoc-tempfile"; @@ -22,8 +21,6 @@ function createTestManager(disposables: DisposableStore, timeoutMs?: number) { suite("Diagnostics", function () { const disposables = new DisposableStore(); - /** Test docs to be deleted during teardown. See the note on {@link openExampleTextDocument} */ - const toDelete: vscode.TextDocument[] = []; /** All vdoc URIs created during tests, to check for leaks during teardown. */ const vdocUris: vscode.Uri[] = []; let client: LanguageClient; @@ -44,7 +41,6 @@ suite("Diagnostics", function () { teardown(async function () { disposables.clear(); - await Promise.all(toDelete.map(doc => deleteDocument(doc))); await client.stop(); await vscode.commands.executeCommand("workbench.action.closeAllEditors"); @@ -64,7 +60,7 @@ suite("Diagnostics", function () { }); test("receives diagnostics in the .qmd for embedded languages", async function () { - const { event } = await openAndAwaitDiagnostics(manager, "diagnostics-python-undefined.qmd", toDelete); + const { event } = await openAndAwaitDiagnostics(manager, "diagnostics-python-undefined.qmd", disposables); assert.strictEqual(event.diagnostics.length, 1, "Expected one diagnostic"); assert.strictEqual( @@ -80,7 +76,7 @@ suite("Diagnostics", function () { }); test("updates diagnostics when .qmd edited", async function () { - const { uri, event, doc } = await openAndAwaitDiagnostics(manager, "diagnostics-python-none.qmd", toDelete); + const { uri, event, doc } = await openAndAwaitDiagnostics(manager, "diagnostics-python-none.qmd", disposables); assert.strictEqual( event.diagnostics.length, @@ -106,8 +102,8 @@ suite("Diagnostics", function () { test("receives diagnostics for multiple languages independently", async function () { this.timeout(15000); - const doc = await openExampleTextDocument("diagnostics-multilang.qmd", toDelete); - const uri = doc.uri; + const uri = uniqueExamplesUri("diagnostics-multilang.qmd", disposables); + const doc = await vscode.workspace.openTextDocument(uri); // Subscribe before showing so we don't miss events fired during document open. const events: DidUpdateDiagnosticsEvent[] = []; @@ -139,7 +135,7 @@ suite("Diagnostics", function () { test("times out for unresponsive language servers without blocking others", async function () { // Julia has no language server registered in tests, so it will time out. // Python should still get its diagnostics independently. - const { event } = await openAndAwaitDiagnostics(manager, "diagnostics-timeout.qmd", toDelete); + const { event } = await openAndAwaitDiagnostics(manager, "diagnostics-timeout.qmd", disposables); // Python diagnostics should be present despite Julia timing out. assert.ok( @@ -154,7 +150,8 @@ suite("Diagnostics", function () { test("cleans up vdoc after timeout when language server does not respond", async function () { const shortTimeoutManager = createTestManager(disposables, 200); - const doc = await openExampleTextDocument("diagnostics-julia-only.qmd", toDelete); + const uri = uniqueExamplesUri("diagnostics-julia-only.qmd", disposables); + const doc = await vscode.workspace.openTextDocument(uri); const disposal = nextVdocDisposal(shortTimeoutManager, "timeout", "julia"); await vscode.window.showTextDocument(doc); @@ -170,7 +167,7 @@ suite("Diagnostics", function () { const shortTimeoutManager = createTestManager(disposables, 200); const { uri, doc } = await openAndAwaitDiagnostics( - shortTimeoutManager, "diagnostics-timeout.qmd", toDelete + shortTimeoutManager, "diagnostics-timeout.qmd", disposables ); assert.ok(vscode.languages.getDiagnostics(uri).length >= 1, "Should have Python diagnostics initially"); @@ -197,7 +194,7 @@ suite("Diagnostics", function () { }); test("clears diagnostics when error is fixed", async function () { - const { uri, doc } = await openAndAwaitDiagnostics(manager, "diagnostics-python-undefined.qmd", toDelete); + const { uri, doc } = await openAndAwaitDiagnostics(manager, "diagnostics-python-undefined.qmd", disposables); const cleared = nextDiagnostics(manager, uri); const editor = await vscode.window.showTextDocument(doc); @@ -216,7 +213,8 @@ suite("Diagnostics", function () { }); test("cleans up vdoc after diagnostics are received", async function () { - const doc = await openExampleTextDocument("diagnostics-python-undefined.qmd", toDelete); + const uri = uniqueExamplesUri("diagnostics-python-undefined.qmd", disposables); + const doc = await vscode.workspace.openTextDocument(uri); const disposal = nextVdocDisposal(manager, "diagnostics-received", "python"); await vscode.window.showTextDocument(doc); @@ -230,7 +228,8 @@ suite("Diagnostics", function () { test("cleans up vdoc when document is closed", async function () { // Julia (no LS in tests) so the vdoc stays alive long enough to be // disposed by closing the document rather than by receiving diagnostics. - const doc = await openExampleTextDocument("diagnostics-julia-only.qmd", toDelete); + const uri = uniqueExamplesUri("diagnostics-julia-only.qmd", disposables); + const doc = await vscode.workspace.openTextDocument(uri); const disposal = nextVdocDisposal(manager, "session-removed", "julia"); await vscode.window.showTextDocument(doc); @@ -245,7 +244,7 @@ suite("Diagnostics", function () { }); test("reports diagnostics from multiple cells of the same language", async function () { - const { event } = await openAndAwaitDiagnostics(manager, "diagnostics-python-multicell.qmd", toDelete); + const { event } = await openAndAwaitDiagnostics(manager, "diagnostics-python-multicell.qmd", disposables); const diagnostics = event.diagnostics; assert.strictEqual(diagnostics.length, 2, "Expected one diagnostic per cell"); @@ -259,7 +258,7 @@ suite("Diagnostics", function () { }); test("maps diagnostic line numbers correctly with content above cell", async function () { - const { event } = await openAndAwaitDiagnostics(manager, "diagnostics-python-offset.qmd", toDelete); + const { event } = await openAndAwaitDiagnostics(manager, "diagnostics-python-offset.qmd", disposables); const diagnostics = event.diagnostics; assert.strictEqual(diagnostics.length, 1, "Expected one diagnostic"); @@ -271,7 +270,7 @@ suite("Diagnostics", function () { }); test("clears diagnostics when all executable cells are removed", async function () { - const { uri, doc } = await openAndAwaitDiagnostics(manager, "diagnostics-python-undefined.qmd", toDelete); + const { uri, doc } = await openAndAwaitDiagnostics(manager, "diagnostics-python-undefined.qmd", disposables); // Remove the entire code cell, leaving only markdown. const cleared = nextDiagnostics(manager, uri); @@ -294,7 +293,7 @@ suite("Diagnostics", function () { }); test("clears diagnostics when document is closed", async function () { - const { uri, doc } = await openAndAwaitDiagnostics(manager, "diagnostics-python-offset.qmd", toDelete); + const { uri, doc } = await openAndAwaitDiagnostics(manager, "diagnostics-python-offset.qmd", disposables); const cleared = nextDiagnostics(manager, uri); await deleteDocument(doc); @@ -310,7 +309,7 @@ suite("Diagnostics", function () { suite("vdoc location", () => { test("places typescript vdoc in local directory", async function () { - const { uri, event } = await openAndAwaitVdocActivation(manager, "diagnostics-typescript.qmd", "ts", toDelete); + const { uri, event } = await openAndAwaitVdocActivation(manager, "diagnostics-typescript.qmd", "ts", disposables); const expectedDir = quartoVdocDir(uri.fsPath); assert.ok( event.uri.fsPath.startsWith(expectedDir), @@ -319,7 +318,7 @@ suite("Diagnostics", function () { }); test("places python vdoc in global temp directory", async function () { - const { event } = await openAndAwaitVdocActivation(manager, "diagnostics-python-undefined.qmd", "python", toDelete); + const { event } = await openAndAwaitVdocActivation(manager, "diagnostics-python-undefined.qmd", "python", disposables); assert.ok( event.uri.fsPath.startsWith(VIRTUAL_DOC_TEMP_DIRECTORY), `Expected Python vdoc in global temp dir (${VIRTUAL_DOC_TEMP_DIRECTORY}), got ${event.uri.fsPath}` @@ -332,7 +331,7 @@ suite("Diagnostics", function () { // code, tags, and relatedInformation (one URI pointing at the vdoc file // itself, one pointing at an external path). test("maps all diagnostic fields from the language server", async function () { - const { uri, event } = await openAndAwaitDiagnostics(manager, "diagnostics-rich.qmd", toDelete); + const { uri, event } = await openAndAwaitDiagnostics(manager, "diagnostics-rich.qmd", disposables); const rich = event.diagnostics.find(d => d.message.includes("rich diagnostic")); assert.ok(rich, "Expected a rich diagnostic"); @@ -367,26 +366,6 @@ suite("Diagnostics", function () { }); }); -/** - * Copy a fixture file to a unique URI and open it. - * - * VS Code keeps text documents in memory even after their editors are closed, - * so a fixture opened by one test remains in `workspace.textDocuments` for - * subsequent tests. When `EmbeddedDiagnosticsManager` is constructed it - * notifies the language server about all already-open documents. - * - * Copying to a fresh URI guarantees the document has never been seen before, - * and lets us delete it to fire onDidCloseTextDocument events. - */ -async function openExampleTextDocument(fixture: string, toDelete: vscode.TextDocument[]): Promise { - const source = examplesUri(fixture); - const dest = Uri.joinPath(source, "..", `tmp-${randomUUID()}-${fixture}`); - await vscode.workspace.fs.copy(source, dest); - const doc = await vscode.workspace.openTextDocument(dest); - toDelete.push(doc); - return doc; -} - function isUriEqual(a: vscode.Uri, b: vscode.Uri) { return a.toString() === b.toString(); } @@ -440,8 +419,9 @@ function nextVdocActivation( } /** Open a .qmd fixture and wait for its first diagnostics event. */ -async function openAndAwaitDiagnostics(manager: EmbeddedDiagnosticsManager, fixture: string, toDelete: vscode.TextDocument[]) { - const doc = await openExampleTextDocument(fixture, toDelete); +async function openAndAwaitDiagnostics(manager: EmbeddedDiagnosticsManager, fixture: string, disposables: DisposableStore) { + const uri = uniqueExamplesUri(fixture, disposables); + const doc = await vscode.workspace.openTextDocument(uri); const diagnostics = nextDiagnostics(manager, doc.uri); await vscode.window.showTextDocument(doc); const event = await raceTimeout(diagnostics, 4000); @@ -452,8 +432,9 @@ async function openAndAwaitDiagnostics(manager: EmbeddedDiagnosticsManager, fixt } /** Open a .qmd fixture and wait for its virtual document to activate for a given language. */ -async function openAndAwaitVdocActivation(manager: EmbeddedDiagnosticsManager, fixture: string, language: string, toDelete: vscode.TextDocument[]) { - const doc = await openExampleTextDocument(fixture, toDelete); +async function openAndAwaitVdocActivation(manager: EmbeddedDiagnosticsManager, fixture: string, language: string, disposables: DisposableStore) { + const uri = uniqueExamplesUri(fixture, disposables); + const doc = await vscode.workspace.openTextDocument(uri); const activation = nextVdocActivation(manager, doc.uri, language); await vscode.window.showTextDocument(doc); const event = await raceTimeout(activation, 4000); diff --git a/apps/vscode/src/test/test-utils.ts b/apps/vscode/src/test/test-utils.ts index fa459f83..d0ea656e 100644 --- a/apps/vscode/src/test/test-utils.ts +++ b/apps/vscode/src/test/test-utils.ts @@ -52,8 +52,8 @@ export async function openAndShowUri( } /** - * Opens a unique on-disk copy of an example file and registers a callback - * with a disposable store to delete the copy. + * Creates a unique on-disk copy of an example file and registers a callback + * with a disposable store to delete the copy on dispose. * * Use this instead of `openAndShowExamplesTextDocument` when a test exercises a * provider command that caches results per document URI, such as @@ -68,7 +68,12 @@ export async function openAndShowUri( * behavior (LSP, configuration) is preserved. Always dispose `disposables` in * the `teardown` hook. */ -export async function openUniqueExampleDocument(fileName: string, disposables: DisposableStore) { +export async function openAndShowUniqueExamplesDocument(fileName: string, disposables: DisposableStore) { + const uri = uniqueExamplesUri(fileName, disposables); + return openAndShowExamplesTextDocument(uri.fsPath); +} + +export function uniqueExamplesUri(fileName: string, disposables: DisposableStore) { const sourcePath = path.join(WORKSPACE_PATH, fileName); const extension = path.extname(fileName); const uniqueName = `${path.basename(fileName, extension)}-${Date.now()}-${Math.random().toString(36).slice(2)}${extension}`; @@ -79,11 +84,7 @@ export async function openUniqueExampleDocument(fileName: string, disposables: D fs.copyFileSync(sourcePath, uniquePath); - const { doc, editor } = await openAndShowUri(vscode.Uri.file(uniquePath)); - return { - doc, - editor, - }; + return vscode.Uri.file(uniquePath); } export const APPROX_TIME_TO_OPEN_VISUAL_EDITOR = 1700; From 116d11198d951f33e2eb74bddf32c74a539f4b1f Mon Sep 17 00:00:00 2001 From: seem Date: Thu, 18 Jun 2026 19:24:49 +0200 Subject: [PATCH 3/4] nit: use uris --- apps/vscode/src/test/test-utils.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/vscode/src/test/test-utils.ts b/apps/vscode/src/test/test-utils.ts index d0ea656e..85400c6b 100644 --- a/apps/vscode/src/test/test-utils.ts +++ b/apps/vscode/src/test/test-utils.ts @@ -70,21 +70,21 @@ export async function openAndShowUri( */ export async function openAndShowUniqueExamplesDocument(fileName: string, disposables: DisposableStore) { const uri = uniqueExamplesUri(fileName, disposables); - return openAndShowExamplesTextDocument(uri.fsPath); + return openAndShowUri(uri); } export function uniqueExamplesUri(fileName: string, disposables: DisposableStore) { - const sourcePath = path.join(WORKSPACE_PATH, fileName); + const sourceUri = examplesUri(fileName); const extension = path.extname(fileName); const uniqueName = `${path.basename(fileName, extension)}-${Date.now()}-${Math.random().toString(36).slice(2)}${extension}`; - const uniquePath = path.join(path.dirname(sourcePath), uniqueName); + const uniqueUri = vscode.Uri.joinPath(sourceUri, "..", uniqueName); // Ensure that the copy is deleted on dispose (usually, on test `teardown`). - disposables.add({ dispose: () => fs.rmSync(uniquePath, { force: true }) }); + disposables.add({ dispose: () => fs.rmSync(uniqueUri.fsPath, { force: true }) }); - fs.copyFileSync(sourcePath, uniquePath); + fs.copyFileSync(sourceUri.fsPath, uniqueUri.fsPath); - return vscode.Uri.file(uniquePath); + return uniqueUri; } export const APPROX_TIME_TO_OPEN_VISUAL_EDITOR = 1700; From efc2102bc1dd1cc7bce7f69a22180562c4cdf979 Mon Sep 17 00:00:00 2001 From: seem Date: Thu, 18 Jun 2026 20:35:18 +0200 Subject: [PATCH 4/4] fix failing tests --- apps/vscode/src/test/diagnostics.test.ts | 46 +++++++++++------------- apps/vscode/src/test/test-utils.ts | 19 ++++++---- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/apps/vscode/src/test/diagnostics.test.ts b/apps/vscode/src/test/diagnostics.test.ts index 892a1827..e1184525 100644 --- a/apps/vscode/src/test/diagnostics.test.ts +++ b/apps/vscode/src/test/diagnostics.test.ts @@ -1,7 +1,7 @@ import * as assert from "assert"; import * as vscode from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; -import { raceTimeout, uniqueExamplesUri } from "./test-utils"; +import { raceTimeout, openUniqueExamplesDocument } from "./test-utils"; import { testLanguageClient } from "./fixtures/test-language-client"; import { DidUpdateDiagnosticsEvent, EmbeddedDiagnosticsManager, VdocDisposeReason } from "../providers/diagnostics"; import { VIRTUAL_DOC_TEMP_DIRECTORY, deleteDocument, quartoVdocDir } from "../vdoc/vdoc-tempfile"; @@ -102,14 +102,13 @@ suite("Diagnostics", function () { test("receives diagnostics for multiple languages independently", async function () { this.timeout(15000); - const uri = uniqueExamplesUri("diagnostics-multilang.qmd", disposables); - const doc = await vscode.workspace.openTextDocument(uri); + const doc = await openUniqueExamplesDocument("diagnostics-multilang.qmd", disposables); // Subscribe before showing so we don't miss events fired during document open. const events: DidUpdateDiagnosticsEvent[] = []; const gotBoth = new Promise((resolve) => { const listener = manager.onDidUpdateDiagnostics((e) => { - if (isUriEqual(e.documentUri, uri)) { + if (isUriEqual(e.documentUri, doc.uri)) { events.push(e); if (events.length >= 2) { listener.dispose(); @@ -125,11 +124,9 @@ suite("Diagnostics", function () { assert.strictEqual(result, true, "Timed out waiting for multi-language diagnostics"); // The final published diagnostics should contain entries from both languages. - const finalDiagnostics = vscode.languages.getDiagnostics(uri); - assert.ok( - finalDiagnostics.length >= 2, - `Expected at least 2 diagnostics (one per language), got ${finalDiagnostics.length}` - ); + assert.ok(events.length === 2, `Expected 2 diagnostics events (one per language), got ${events.length}`); + assert.ok(events[0].diagnostics.length === 1, "Expected one diagnostic when the first language's diagnostics are received"); + assert.ok(events[1].diagnostics.length === 2, "Expected two diagnostics when the second language's diagnostics are received"); }); test("times out for unresponsive language servers without blocking others", async function () { @@ -150,8 +147,7 @@ suite("Diagnostics", function () { test("cleans up vdoc after timeout when language server does not respond", async function () { const shortTimeoutManager = createTestManager(disposables, 200); - const uri = uniqueExamplesUri("diagnostics-julia-only.qmd", disposables); - const doc = await vscode.workspace.openTextDocument(uri); + const doc = await openUniqueExamplesDocument("diagnostics-julia-only.qmd", disposables); const disposal = nextVdocDisposal(shortTimeoutManager, "timeout", "julia"); await vscode.window.showTextDocument(doc); @@ -166,14 +162,15 @@ suite("Diagnostics", function () { test("clears stale diagnostics after timeout", async function () { const shortTimeoutManager = createTestManager(disposables, 200); - const { uri, doc } = await openAndAwaitDiagnostics( + const { uri, doc, event } = await openAndAwaitDiagnostics( shortTimeoutManager, "diagnostics-timeout.qmd", disposables ); - assert.ok(vscode.languages.getDiagnostics(uri).length >= 1, "Should have Python diagnostics initially"); + assert.ok(event.diagnostics.length >= 1, "Should have Python diagnostics initially"); // Wait for the initial Julia timeout before editing, // otherwise nextDiagnostics catches that event instead. - await raceTimeout(nextVdocDisposal(shortTimeoutManager, "timeout", "julia"), 2000); + const disposal = await raceTimeout(nextVdocDisposal(shortTimeoutManager, "timeout", "julia"), 2000); + assert.ok(disposal, "Expected Julia vdoc to be disposed via timeout before editing"); // Delete the Python cell, keeping only Julia (which will timeout). const cleared = nextDiagnostics(shortTimeoutManager, uri); @@ -187,10 +184,11 @@ suite("Diagnostics", function () { ); }); - const event = await raceTimeout(cleared, 3000); - assert.ok(event, "Expected diagnostics update after timeout"); - assert.strictEqual(event.diagnostics.length, 0, - "Stale Python diagnostics should be cleared after Julia-only timeout"); + const clearedEvent = await raceTimeout(cleared, 3000); + assert.ok(clearedEvent, "Expected diagnostics update after timeout"); + assert.strictEqual(clearedEvent.diagnostics.length, 0, + "Stale Python diagnostics should be cleared after Julia-only timeout, got:\n" + + JSON.stringify(clearedEvent.diagnostics)); }); test("clears diagnostics when error is fixed", async function () { @@ -213,8 +211,7 @@ suite("Diagnostics", function () { }); test("cleans up vdoc after diagnostics are received", async function () { - const uri = uniqueExamplesUri("diagnostics-python-undefined.qmd", disposables); - const doc = await vscode.workspace.openTextDocument(uri); + const doc = await openUniqueExamplesDocument("diagnostics-python-undefined.qmd", disposables); const disposal = nextVdocDisposal(manager, "diagnostics-received", "python"); await vscode.window.showTextDocument(doc); @@ -228,8 +225,7 @@ suite("Diagnostics", function () { test("cleans up vdoc when document is closed", async function () { // Julia (no LS in tests) so the vdoc stays alive long enough to be // disposed by closing the document rather than by receiving diagnostics. - const uri = uniqueExamplesUri("diagnostics-julia-only.qmd", disposables); - const doc = await vscode.workspace.openTextDocument(uri); + const doc = await openUniqueExamplesDocument("diagnostics-julia-only.qmd", disposables); const disposal = nextVdocDisposal(manager, "session-removed", "julia"); await vscode.window.showTextDocument(doc); @@ -420,8 +416,7 @@ function nextVdocActivation( /** Open a .qmd fixture and wait for its first diagnostics event. */ async function openAndAwaitDiagnostics(manager: EmbeddedDiagnosticsManager, fixture: string, disposables: DisposableStore) { - const uri = uniqueExamplesUri(fixture, disposables); - const doc = await vscode.workspace.openTextDocument(uri); + const doc = await openUniqueExamplesDocument(fixture, disposables); const diagnostics = nextDiagnostics(manager, doc.uri); await vscode.window.showTextDocument(doc); const event = await raceTimeout(diagnostics, 4000); @@ -433,8 +428,7 @@ async function openAndAwaitDiagnostics(manager: EmbeddedDiagnosticsManager, fixt /** Open a .qmd fixture and wait for its virtual document to activate for a given language. */ async function openAndAwaitVdocActivation(manager: EmbeddedDiagnosticsManager, fixture: string, language: string, disposables: DisposableStore) { - const uri = uniqueExamplesUri(fixture, disposables); - const doc = await vscode.workspace.openTextDocument(uri); + const doc = await openUniqueExamplesDocument(fixture, disposables); const activation = nextVdocActivation(manager, doc.uri, language); await vscode.window.showTextDocument(doc); const event = await raceTimeout(activation, 4000); diff --git a/apps/vscode/src/test/test-utils.ts b/apps/vscode/src/test/test-utils.ts index 85400c6b..b701f58b 100644 --- a/apps/vscode/src/test/test-utils.ts +++ b/apps/vscode/src/test/test-utils.ts @@ -2,6 +2,7 @@ import { DisposableStore } from "core"; import * as fs from "fs"; import * as path from "path"; import * as vscode from "vscode"; +import { deleteDocument } from "../vdoc/vdoc-tempfile"; /** @@ -69,22 +70,26 @@ export async function openAndShowUri( * the `teardown` hook. */ export async function openAndShowUniqueExamplesDocument(fileName: string, disposables: DisposableStore) { - const uri = uniqueExamplesUri(fileName, disposables); - return openAndShowUri(uri); + const doc = await openUniqueExamplesDocument(fileName, disposables); + const editor = await vscode.window.showTextDocument(doc); + return { doc, editor }; } -export function uniqueExamplesUri(fileName: string, disposables: DisposableStore) { +export async function openUniqueExamplesDocument(fileName: string, disposables: DisposableStore) { const sourceUri = examplesUri(fileName); const extension = path.extname(fileName); const uniqueName = `${path.basename(fileName, extension)}-${Date.now()}-${Math.random().toString(36).slice(2)}${extension}`; const uniqueUri = vscode.Uri.joinPath(sourceUri, "..", uniqueName); - // Ensure that the copy is deleted on dispose (usually, on test `teardown`). - disposables.add({ dispose: () => fs.rmSync(uniqueUri.fsPath, { force: true }) }); + /** + * Ensure that the copy is deleted on dispose (usually, on test `teardown`). + * See the notes in {@link deleteDocument} for why we have to use that function. + */ + disposables.add({ dispose: () => deleteDocument(doc) }); fs.copyFileSync(sourceUri.fsPath, uniqueUri.fsPath); - - return uniqueUri; + const doc = await vscode.workspace.openTextDocument(uniqueUri); + return doc; } export const APPROX_TIME_TO_OPEN_VISUAL_EDITOR = 1700;