Switch to built-in VS Code test UI unconditionally

This commit is contained in:
Dave Bartolomeo
2024-01-11 10:47:48 -05:00
parent 1b737678ba
commit 1dc6111fba
11 changed files with 11 additions and 407 deletions

View File

@@ -21,13 +21,6 @@ To see what has changed in the last few versions of the extension, see the [Chan
This project will track new feature development in CodeQL and, whenever appropriate, bring that functionality to the Visual Studio Code experience. This project will track new feature development in CodeQL and, whenever appropriate, bring that functionality to the Visual Studio Code experience.
## Dependencies
This extension depends on the following two extensions for required functionality. They will be installed automatically when you install VS Code CodeQL.
- [Test Adapter Converter](https://marketplace.visualstudio.com/items?itemName=ms-vscode.test-adapter-converter)
- [Test Explorer UI](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer)
## Contributing ## Contributing
This project welcomes contributions. See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to build, install, and contribute. This project welcomes contributions. See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to build, install, and contribute.

View File

@@ -17,8 +17,6 @@ For information about other configurations, see the separate [CodeQL help](https
### Quick start: Installing and configuring the extension ### Quick start: Installing and configuring the extension
1. [Install the extension](#installing-the-extension). 1. [Install the extension](#installing-the-extension).
*Note: vscode-codeql installs the following dependencies for required functionality: [Test Adapter Converter](https://marketplace.visualstudio.com/items?itemName=ms-vscode.test-adapter-converter), [Test Explorer UI](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer).*
1. [Check access to the CodeQL CLI](#checking-access-to-the-codeql-cli). 1. [Check access to the CodeQL CLI](#checking-access-to-the-codeql-cli).
1. [Clone the CodeQL starter workspace](#cloning-the-codeql-starter-workspace). 1. [Clone the CodeQL starter workspace](#cloning-the-codeql-starter-workspace).

View File

@@ -40,8 +40,6 @@
"vscode-extension-telemetry": "^0.1.6", "vscode-extension-telemetry": "^0.1.6",
"vscode-jsonrpc": "^8.0.2", "vscode-jsonrpc": "^8.0.2",
"vscode-languageclient": "^8.0.2", "vscode-languageclient": "^8.0.2",
"vscode-test-adapter-api": "^1.7.0",
"vscode-test-adapter-util": "^0.7.0",
"yauzl": "^2.10.0", "yauzl": "^2.10.0",
"zip-a-folder": "^3.1.3" "zip-a-folder": "^3.1.3"
}, },
@@ -31984,31 +31982,6 @@
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz",
"integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA=="
}, },
"node_modules/vscode-test-adapter-api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/vscode-test-adapter-api/-/vscode-test-adapter-api-1.9.0.tgz",
"integrity": "sha512-lltjehUP0J9H3R/HBctjlqeUCwn2t9Lbhj2Y500ib+j5Y4H3hw+hVTzuSsfw16LtxY37knlU39QIlasa7svzOQ==",
"engines": {
"vscode": "^1.23.0"
}
},
"node_modules/vscode-test-adapter-util": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/vscode-test-adapter-util/-/vscode-test-adapter-util-0.7.1.tgz",
"integrity": "sha512-OZZvLDDNhayVVISyTmgUntOhMzl6j9/wVGfNqI2zuR5bQIziTQlDs9W29dFXDTGXZOxazS6uiHkrr86BKDzYUA==",
"dependencies": {
"tslib": "^1.11.1",
"vscode-test-adapter-api": "^1.8.0"
},
"engines": {
"vscode": "^1.24.0"
}
},
"node_modules/vscode-test-adapter-util/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/w3c-xmlserializer": { "node_modules/w3c-xmlserializer": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",

View File

@@ -21,7 +21,6 @@
"Programming Languages" "Programming Languages"
], ],
"extensionDependencies": [ "extensionDependencies": [
"hbenl.vscode-test-explorer",
"vscode.git" "vscode.git"
], ],
"capabilities": { "capabilities": {
@@ -1938,8 +1937,6 @@
"vscode-extension-telemetry": "^0.1.6", "vscode-extension-telemetry": "^0.1.6",
"vscode-jsonrpc": "^8.0.2", "vscode-jsonrpc": "^8.0.2",
"vscode-languageclient": "^8.0.2", "vscode-languageclient": "^8.0.2",
"vscode-test-adapter-api": "^1.7.0",
"vscode-test-adapter-util": "^0.7.0",
"yauzl": "^2.10.0", "yauzl": "^2.10.0",
"zip-a-folder": "^3.1.3" "zip-a-folder": "^3.1.3"
}, },

View File

@@ -1,10 +1,9 @@
import type { CommandManager } from "../packages/commands"; import type { CommandManager } from "../packages/commands";
import type { Uri, Range, TextDocumentShowOptions } from "vscode"; import type { Uri, Range, TextDocumentShowOptions, TestItem } from "vscode";
import type { AstItem } from "../language-support"; import type { AstItem } from "../language-support";
import type { DbTreeViewItem } from "../databases/ui/db-tree-view-item"; import type { DbTreeViewItem } from "../databases/ui/db-tree-view-item";
import type { DatabaseItem } from "../databases/local-databases"; import type { DatabaseItem } from "../databases/local-databases";
import type { QueryHistoryInfo } from "../query-history/query-history-info"; import type { QueryHistoryInfo } from "../query-history/query-history-info";
import type { TestTreeNode } from "../query-testing/test-tree-node";
import type { import type {
VariantAnalysis, VariantAnalysis,
VariantAnalysisScannedRepository, VariantAnalysisScannedRepository,
@@ -334,11 +333,9 @@ export type SummaryLanguageSupportCommands = {
}; };
export type TestUICommands = { export type TestUICommands = {
"codeQLTests.showOutputDifferences": (node: TestTreeNode) => Promise<void>; "codeQLTests.showOutputDifferences": (node: TestItem) => Promise<void>;
"codeQLTests.acceptOutput": (node: TestTreeNode) => Promise<void>; "codeQLTests.acceptOutput": (node: TestItem) => Promise<void>;
"codeQLTests.acceptOutputContextTestItem": ( "codeQLTests.acceptOutputContextTestItem": (node: TestItem) => Promise<void>;
node: TestTreeNode,
) => Promise<void>;
}; };
export type MockGitHubApiServerCommands = { export type MockGitHubApiServerCommands = {

View File

@@ -15,8 +15,6 @@ import { arch, homedir, platform } from "os";
import { ensureDir } from "fs-extra"; import { ensureDir } from "fs-extra";
import { join } from "path"; import { join } from "path";
import { dirSync } from "tmp-promise"; import { dirSync } from "tmp-promise";
import type { TestHub } from "vscode-test-adapter-api";
import { testExplorerExtensionId } from "vscode-test-adapter-api";
import { lt, parse } from "semver"; import { lt, parse } from "semver";
import { watch } from "chokidar"; import { watch } from "chokidar";
import { import {
@@ -28,7 +26,6 @@ import {
CliConfigListener, CliConfigListener,
DistributionConfigListener, DistributionConfigListener,
GitHubDatabaseConfigListener, GitHubDatabaseConfigListener,
isCanary,
joinOrderWarningThreshold, joinOrderWarningThreshold,
QueryHistoryConfigListener, QueryHistoryConfigListener,
QueryServerConfigListener, QueryServerConfigListener,
@@ -90,8 +87,6 @@ import {
} from "./common/logging/vscode"; } from "./common/logging/vscode";
import { QueryHistoryManager } from "./query-history/query-history-manager"; import { QueryHistoryManager } from "./query-history/query-history-manager";
import type { CompletedLocalQueryInfo } from "./query-results"; import type { CompletedLocalQueryInfo } from "./query-results";
import { QLTestAdapterFactory } from "./query-testing/test-adapter";
import { TestUIService } from "./query-testing/test-ui";
import { CompareView } from "./compare/compare-view"; import { CompareView } from "./compare/compare-view";
import { import {
initializeTelemetry, initializeTelemetry,
@@ -130,7 +125,6 @@ import { DebuggerUI } from "./debugger/debugger-ui";
import { ModelEditorModule } from "./model-editor/model-editor-module"; import { ModelEditorModule } from "./model-editor/model-editor-module";
import { TestManager } from "./query-testing/test-manager"; import { TestManager } from "./query-testing/test-manager";
import { TestRunner } from "./query-testing/test-runner"; import { TestRunner } from "./query-testing/test-runner";
import type { TestManagerBase } from "./query-testing/test-manager-base";
import { QueryRunner, QueryServerClient } from "./query-server"; import { QueryRunner, QueryServerClient } from "./query-server";
import { QueriesModule } from "./queries-panel/queries-module"; import { QueriesModule } from "./queries-panel/queries-module";
import { OpenReferencedFileCodeLensProvider } from "./local-queries/open-referenced-file-code-lens-provider"; import { OpenReferencedFileCodeLensProvider } from "./local-queries/open-referenced-file-code-lens-provider";
@@ -977,27 +971,8 @@ async function activateWithInstalledDistribution(
const testRunner = new TestRunner(dbm, cliServer); const testRunner = new TestRunner(dbm, cliServer);
ctx.subscriptions.push(testRunner); ctx.subscriptions.push(testRunner);
let testManager: TestManagerBase | undefined = undefined; const testManager = new TestManager(app, testRunner, cliServer);
if (isCanary()) { ctx.subscriptions.push(testManager);
testManager = new TestManager(app, testRunner, cliServer);
ctx.subscriptions.push(testManager);
} else {
const testExplorerExtension = extensions.getExtension<TestHub>(
testExplorerExtensionId,
);
if (testExplorerExtension) {
const testHub = testExplorerExtension.exports;
const testAdapterFactory = new QLTestAdapterFactory(
testHub,
testRunner,
cliServer,
);
ctx.subscriptions.push(testAdapterFactory);
testManager = new TestUIService(app, testHub);
ctx.subscriptions.push(testManager);
}
}
const testUiCommands = testManager?.getCommands() ?? {}; const testUiCommands = testManager?.getCommands() ?? {};

View File

@@ -1,26 +1,4 @@
import { extname } from "path"; import { extname } from "path";
import type { Event, WorkspaceFolder } from "vscode";
import { CancellationTokenSource, EventEmitter } from "vscode";
import type {
TestAdapter,
TestEvent,
TestHub,
TestInfo,
TestLoadFinishedEvent,
TestLoadStartedEvent,
TestRunFinishedEvent,
TestRunStartedEvent,
TestSuiteEvent,
TestSuiteInfo,
} from "vscode-test-adapter-api";
import { TestAdapterRegistrar } from "vscode-test-adapter-util";
import { QLTestDiscovery } from "./qltest-discovery";
import { DisposableObject } from "../common/disposable-object";
import type { CodeQLCliServer, TestCompleted } from "../codeql-cli/cli";
import { testLogger } from "../common/logging/vscode";
import type { TestRunner } from "./test-runner";
import type { FileTreeNode } from "../common/file-tree-nodes";
import { FileTreeDirectory, FileTreeLeaf } from "../common/file-tree-nodes";
/** /**
* Get the full path of the `.expected` file for the specified QL test. * Get the full path of the `.expected` file for the specified QL test.
@@ -47,28 +25,6 @@ function getTestOutputFile(testPath: string, extension: string): string {
return changeExtension(testPath, extension); return changeExtension(testPath, extension);
} }
/**
* A factory service that creates `QLTestAdapter` objects for workspace folders on demand.
*/
export class QLTestAdapterFactory extends DisposableObject {
constructor(
testHub: TestHub,
testRunner: TestRunner,
cliServer: CodeQLCliServer,
) {
super();
// this will register a QLTestAdapter for each WorkspaceFolder
this.push(
new TestAdapterRegistrar(
testHub,
(workspaceFolder) =>
new QLTestAdapter(workspaceFolder, testRunner, cliServer),
),
);
}
}
/** /**
* Change the file extension of the specified path. * Change the file extension of the specified path.
* @param p The original file path. * @param p The original file path.
@@ -77,197 +33,3 @@ export class QLTestAdapterFactory extends DisposableObject {
function changeExtension(p: string, ext: string): string { function changeExtension(p: string, ext: string): string {
return p.slice(0, -extname(p).length) + ext; return p.slice(0, -extname(p).length) + ext;
} }
/**
* Test adapter for QL tests.
*/
export class QLTestAdapter extends DisposableObject implements TestAdapter {
private readonly qlTestDiscovery: QLTestDiscovery;
private readonly _tests = this.push(
new EventEmitter<TestLoadStartedEvent | TestLoadFinishedEvent>(),
);
private readonly _testStates = this.push(
new EventEmitter<
TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent
>(),
);
private readonly _autorun = this.push(new EventEmitter<void>());
private runningTask?: CancellationTokenSource = undefined;
constructor(
public readonly workspaceFolder: WorkspaceFolder,
private readonly testRunner: TestRunner,
cliServer: CodeQLCliServer,
) {
super();
this.qlTestDiscovery = this.push(
new QLTestDiscovery(workspaceFolder, cliServer),
);
void this.qlTestDiscovery.refresh();
this.push(this.qlTestDiscovery.onDidChangeTests(this.discoverTests, this));
}
public get tests(): Event<TestLoadStartedEvent | TestLoadFinishedEvent> {
return this._tests.event;
}
public get testStates(): Event<
TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent
> {
return this._testStates.event;
}
public get autorun(): Event<void> | undefined {
return this._autorun.event;
}
private static createTestOrSuiteInfos(
testNodes: readonly FileTreeNode[],
): Array<TestSuiteInfo | TestInfo> {
return testNodes.map((childNode) => {
return QLTestAdapter.createTestOrSuiteInfo(childNode);
});
}
private static createTestOrSuiteInfo(
testNode: FileTreeNode,
): TestSuiteInfo | TestInfo {
if (testNode instanceof FileTreeLeaf) {
return QLTestAdapter.createTestInfo(testNode);
} else if (testNode instanceof FileTreeDirectory) {
return QLTestAdapter.createTestSuiteInfo(testNode, testNode.name);
} else {
throw new Error("Unexpected test type.");
}
}
private static createTestInfo(testFile: FileTreeLeaf): TestInfo {
return {
type: "test",
id: testFile.path,
label: testFile.name,
tooltip: testFile.path,
file: testFile.path,
};
}
private static createTestSuiteInfo(
testDirectory: FileTreeDirectory,
label: string,
): TestSuiteInfo {
return {
type: "suite",
id: testDirectory.path,
label,
children: QLTestAdapter.createTestOrSuiteInfos(testDirectory.children),
tooltip: testDirectory.path,
};
}
public async load(): Promise<void> {
this.discoverTests();
}
private discoverTests(): void {
this._tests.fire({ type: "started" } as TestLoadStartedEvent);
const testDirectory = this.qlTestDiscovery.testDirectory;
let testSuite: TestSuiteInfo | undefined;
if (testDirectory?.children.length) {
const children = QLTestAdapter.createTestOrSuiteInfos(
testDirectory.children,
);
testSuite = {
type: "suite",
label: "CodeQL",
id: testDirectory.path,
children,
};
}
this._tests.fire({
type: "finished",
suite: testSuite,
} as TestLoadFinishedEvent);
}
public async run(tests: string[]): Promise<void> {
if (this.runningTask !== undefined) {
throw new Error("Tests already running.");
}
testLogger.outputChannel.clear();
testLogger.outputChannel.show(true);
this.runningTask = this.track(new CancellationTokenSource());
const token = this.runningTask.token;
this._testStates.fire({
type: "started",
tests,
} as TestRunStartedEvent);
await this.testRunner.run(tests, testLogger, token, (event) =>
this.processTestEvent(event),
);
this._testStates.fire({ type: "finished" } as TestRunFinishedEvent);
this.clearTask();
}
private clearTask(): void {
if (this.runningTask !== undefined) {
const runningTask = this.runningTask;
this.runningTask = undefined;
this.disposeAndStopTracking(runningTask);
}
}
public cancel(): void {
if (this.runningTask !== undefined) {
void testLogger.log("Cancelling test run...");
this.runningTask.cancel();
this.clearTask();
}
}
private async processTestEvent(event: TestCompleted): Promise<void> {
const state = event.pass
? "passed"
: event.messages?.length
? "errored"
: "failed";
let message: string | undefined;
if (event.failureDescription || event.diff?.length) {
message =
event.failureStage === "RESULT"
? [
"",
`${state}: ${event.test}`,
event.failureDescription || event.diff?.join("\n"),
"",
].join("\n")
: [
"",
`${event.failureStage?.toLowerCase() ?? "unknown stage"} error: ${
event.test
}`,
event.failureDescription ||
`${event.messages[0].severity}: ${event.messages[0].message}`,
"",
].join("\n");
void testLogger.log(message);
}
this._testStates.fire({
type: "test",
state,
test: event.test,
message,
decorations: event.messages?.map((msg) => ({
line: msg.position.line,
message: msg.message,
})),
});
}
}

View File

@@ -6,9 +6,8 @@ import type { TestItem, TextDocumentShowOptions } from "vscode";
import { Uri, window } from "vscode"; import { Uri, window } from "vscode";
import { basename } from "path"; import { basename } from "path";
import type { App } from "../common/app"; import type { App } from "../common/app";
import type { TestTreeNode } from "./test-tree-node";
type TestNode = TestTreeNode | TestItem; type TestNode = TestItem;
/** /**
* Base class for both the legacy and new test services. Implements commands that are common to * Base class for both the legacy and new test services. Implements commands that are common to

View File

@@ -1,9 +0,0 @@
import type { TestSuiteInfo, TestInfo } from "vscode-test-adapter-api";
/**
* Tree view node for a test, suite, or collection. This object is passed as the argument to the
* command handler of a context menu item for a tree view item.
*/
export interface TestTreeNode {
readonly info: TestSuiteInfo | TestInfo;
}

View File

@@ -1,71 +0,0 @@
import type {
TestHub,
TestController,
TestAdapter,
TestRunStartedEvent,
TestRunFinishedEvent,
TestEvent,
TestSuiteEvent,
} from "vscode-test-adapter-api";
import type { TestTreeNode } from "./test-tree-node";
import { DisposableObject } from "../common/disposable-object";
import { QLTestAdapter } from "./test-adapter";
import type { App } from "../common/app";
import { TestManagerBase } from "./test-manager-base";
type VSCodeTestEvent =
| TestRunStartedEvent
| TestRunFinishedEvent
| TestSuiteEvent
| TestEvent;
/**
* Test event listener. Currently unused, but left in to keep the plumbing hooked up for future use.
*/
class QLTestListener extends DisposableObject {
constructor(adapter: TestAdapter) {
super();
this.push(adapter.testStates(this.onTestStatesEvent, this));
}
private onTestStatesEvent(_e: VSCodeTestEvent): void {
/**/
}
}
/**
* Service that implements all UI and commands for QL tests.
*/
export class TestUIService extends TestManagerBase implements TestController {
private readonly listeners: Map<TestAdapter, QLTestListener> = new Map();
public constructor(
app: App,
private readonly testHub: TestHub,
) {
super(app);
testHub.registerTestController(this);
}
public dispose(): void {
this.testHub.unregisterTestController(this);
super.dispose();
}
public registerTestAdapter(adapter: TestAdapter): void {
this.listeners.set(adapter, new QLTestListener(adapter));
}
public unregisterTestAdapter(adapter: TestAdapter): void {
if (adapter instanceof QLTestAdapter) {
this.listeners.delete(adapter);
}
}
protected getTestPath(node: TestTreeNode): string {
return node.info.id;
}
}

View File

@@ -50,20 +50,10 @@ export default class JestRunnerInstalledExtensions extends VSCodeTestRunner {
const [cli, ...args] = const [cli, ...args] =
resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath); resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath);
spawnSync( spawnSync(cli, args, {
cli, encoding: "utf-8",
[ stdio: "inherit",
...args, });
"--install-extension",
"hbenl.vscode-test-explorer",
"--install-extension",
"ms-vscode.test-adapter-converter",
],
{
encoding: "utf-8",
stdio: "inherit",
},
);
installedOnVsCodeVersions.add(versionKey); installedOnVsCodeVersions.add(versionKey);
} }