Use Node qltest discovery in test manager

This commit is contained in:
Koen Vlaswinkel 2024-01-12 11:54:16 +01:00
Родитель 2c1606e9f5
Коммит d05446da32
5 изменённых файлов: 81 добавлений и 134 удалений

Просмотреть файл

@ -131,6 +131,7 @@ import { OpenReferencedFileCodeLensProvider } from "./local-queries/open-referen
import { LanguageContextStore } from "./language-context-store";
import { LanguageSelectionPanel } from "./language-selection-panel/language-selection-panel";
import { GitHubDatabasesModule } from "./databases/github-databases";
import { QLTestFileDiscovery } from "./query-testing/qltest-file-discovery";
/**
* extension.ts
@ -971,7 +972,13 @@ async function activateWithInstalledDistribution(
const testRunner = new TestRunner(dbm, cliServer);
ctx.subscriptions.push(testRunner);
const testManager = new TestManager(app, testRunner, cliServer);
const qlTestDiscovery = new QLTestFileDiscovery(
app,
queriesModule.queryPackDiscovery,
);
void qlTestDiscovery.initialRefresh();
const testManager = new TestManager(app, testRunner, qlTestDiscovery);
ctx.subscriptions.push(testManager);
const testUiCommands = testManager?.getCommands() ?? {};

Просмотреть файл

@ -18,8 +18,12 @@ export class QueriesModule extends DisposableObject {
public readonly onDidChangeSelection = this.onDidChangeSelectionEmitter.event;
public queryPackDiscovery: QueryPackDiscovery;
private constructor(readonly app: App) {
super();
this.queryPackDiscovery = this.push(new QueryPackDiscovery());
}
public static initialize(
@ -36,13 +40,11 @@ export class QueriesModule extends DisposableObject {
private initialize(app: App, langauageContext: LanguageContextStore): void {
void extLogger.log("Initializing queries panel.");
const queryPackDiscovery = new QueryPackDiscovery();
this.push(queryPackDiscovery);
void queryPackDiscovery.initialRefresh();
void this.queryPackDiscovery.initialRefresh();
const queryDiscovery = new QueryDiscovery(
app,
queryPackDiscovery,
this.queryPackDiscovery,
langauageContext,
);
this.push(queryDiscovery);

Просмотреть файл

@ -1,10 +1,21 @@
import { FilePathDiscovery } from "../common/vscode/file-path-discovery";
import type { App } from "../common/app";
import type { AppEvent, AppEventEmitter } from "../common/events";
import { basename, dirname, extname, join, sep } from "path";
import {
basename,
dirname,
extname,
join,
normalize,
relative,
sep,
} from "path";
import type { Event } from "vscode";
import { containsPath } from "../common/files";
import { pathExists } from "fs-extra";
import type { FileTreeNode } from "../common/file-tree-nodes";
import { FileTreeDirectory, FileTreeLeaf } from "../common/file-tree-nodes";
import { getOnDiskWorkspaceFoldersObjects } from "../common/vscode/workspace-folders";
interface QueryPackDiscoverer {
getTestsPathForFile(path: string): string | undefined;
@ -26,7 +37,7 @@ export class QLTestFileDiscovery extends FilePathDiscovery<Test> {
public readonly onDidChangeTests: AppEvent<void>;
constructor(
app: App,
private readonly app: App,
private readonly queryPackDiscovery: QueryPackDiscoverer,
) {
super(
@ -49,6 +60,43 @@ export class QLTestFileDiscovery extends FilePathDiscovery<Test> {
);
}
/**
* Return all known tests, represented as a tree.
*
* Trivial directories where there is only one child will be collapsed into a single node.
*/
public buildTestTree(): FileTreeNode[] | undefined {
const pathData = this.getPathData();
if (pathData === undefined) {
return undefined;
}
const roots = [];
for (const workspaceFolder of getOnDiskWorkspaceFoldersObjects()) {
const queriesInRoot = pathData.filter((query) =>
containsPath(workspaceFolder.uri.fsPath, query.path),
);
if (queriesInRoot.length === 0) {
continue;
}
const root = new FileTreeDirectory(
workspaceFolder.uri.fsPath,
workspaceFolder.name,
this.app.environment,
);
for (const query of queriesInRoot) {
const dirName = dirname(normalize(relative(root.path, query.path)));
const parentDirectory = root.createDirectory(dirName);
parentDirectory.addChild(
new FileTreeLeaf(query.path, basename(query.path)),
);
}
root.finish();
roots.push(root);
}
return roots;
}
protected async getDataForPath(path: string): Promise<Test | undefined> {
const testsPath = this.queryPackDiscovery.getTestsPathForFile(path);

Просмотреть файл

@ -6,31 +6,26 @@ import type {
TestRun,
TestRunRequest,
TextDocumentShowOptions,
WorkspaceFolder,
WorkspaceFoldersChangeEvent,
} from "vscode";
import {
Location,
Range,
TestMessage,
TestRunProfileKind,
Uri,
tests,
Uri,
window,
workspace,
} from "vscode";
import { DisposableObject } from "../common/disposable-object";
import { QLTestDiscovery } from "./qltest-discovery";
import type { CodeQLCliServer } from "../codeql-cli/cli";
import { getErrorMessage } from "../common/helpers-pure";
import type { BaseLogger, LogOptions } from "../common/logging";
import type { TestRunner } from "./test-runner";
import type { App } from "../common/app";
import { isWorkspaceFolderOnDisk } from "../common/vscode/workspace-folders";
import type { FileTreeNode } from "../common/file-tree-nodes";
import { FileTreeDirectory, FileTreeLeaf } from "../common/file-tree-nodes";
import type { TestUICommands } from "../common/commands";
import { basename, extname } from "path";
import type { QLTestFileDiscovery } from "./qltest-file-discovery";
/**
* Get the full path of the `.expected` file for the specified QL test.
@ -49,7 +44,7 @@ function getActualFile(testPath: string): string {
}
/**
* Gets the the full path to a particular output file of the specified QL test.
* Gets the full path to a particular output file of the specified QL test.
* @param testPath The full path to the QL test.
* @param extension The file extension of the output file.
*/
@ -111,54 +106,15 @@ class TestRunLogger implements BaseLogger {
}
}
/**
* Handles test discovery for a specific workspace folder, and reports back to `TestManager`.
*/
class WorkspaceFolderHandler extends DisposableObject {
private readonly testDiscovery: QLTestDiscovery;
public constructor(
private readonly workspaceFolder: WorkspaceFolder,
private readonly testUI: TestManager,
cliServer: CodeQLCliServer,
) {
super();
this.testDiscovery = new QLTestDiscovery(workspaceFolder, cliServer);
this.push(
this.testDiscovery.onDidChangeTests(this.handleDidChangeTests, this),
);
void this.testDiscovery.refresh();
}
private handleDidChangeTests(): void {
const testDirectory = this.testDiscovery.testDirectory;
this.testUI.updateTestsForWorkspaceFolder(
this.workspaceFolder,
testDirectory,
);
}
}
/**
* Service that populates the VS Code "Test Explorer" panel for CodeQL, and handles running and
* debugging of tests.
*/
export class TestManager extends DisposableObject {
/**
* Maps from each workspace folder being tracked to the `WorkspaceFolderHandler` responsible for
* tracking it.
*/
private readonly workspaceFolderHandlers = new Map<
WorkspaceFolder,
WorkspaceFolderHandler
>();
public constructor(
private readonly app: App,
private readonly testRunner: TestRunner,
private readonly cliServer: CodeQLCliServer,
private readonly qlTestDiscovery: QLTestFileDiscovery,
// Having this as a parameter with a default value makes passing in a mock easier.
private readonly testController: TestController = tests.createTestController(
"codeql",
@ -173,19 +129,7 @@ export class TestManager extends DisposableObject {
this.run.bind(this),
);
// Start by tracking whatever folders are currently in the workspace.
this.startTrackingWorkspaceFolders(workspace.workspaceFolders ?? []);
// Listen for changes to the set of workspace folders.
workspace.onDidChangeWorkspaceFolders(
this.handleDidChangeWorkspaceFolders,
this,
);
}
public dispose(): void {
this.workspaceFolderHandlers.clear(); // These will be disposed in the `super.dispose()` call.
super.dispose();
this.qlTestDiscovery.onDidChangeTests(this.handleDidChangeTests.bind(this));
}
public getCommands(): TestUICommands {
@ -246,71 +190,14 @@ export class TestManager extends DisposableObject {
}
}
/** Start tracking tests in the specified workspace folders. */
private startTrackingWorkspaceFolders(
workspaceFolders: readonly WorkspaceFolder[],
): void {
// Only track on-disk workspace folders, to avoid trying to run the CLI test discovery command
// on random URIs.
workspaceFolders
.filter(isWorkspaceFolderOnDisk)
.forEach((workspaceFolder) => {
const workspaceFolderHandler = new WorkspaceFolderHandler(
workspaceFolder,
this,
this.cliServer,
);
this.track(workspaceFolderHandler);
this.workspaceFolderHandlers.set(
workspaceFolder,
workspaceFolderHandler,
);
});
}
/** Stop tracking tests in the specified workspace folders. */
private stopTrackingWorkspaceFolders(
workspaceFolders: readonly WorkspaceFolder[],
): void {
for (const workspaceFolder of workspaceFolders) {
const workspaceFolderHandler =
this.workspaceFolderHandlers.get(workspaceFolder);
if (workspaceFolderHandler !== undefined) {
// Delete the root item for this workspace folder, if any.
this.testController.items.delete(workspaceFolder.uri.toString());
this.disposeAndStopTracking(workspaceFolderHandler);
this.workspaceFolderHandlers.delete(workspaceFolder);
}
}
}
private handleDidChangeWorkspaceFolders(
e: WorkspaceFoldersChangeEvent,
): void {
this.startTrackingWorkspaceFolders(e.added);
this.stopTrackingWorkspaceFolders(e.removed);
}
/**
* Update the test controller when we discover changes to the tests in the workspace folder.
*/
public updateTestsForWorkspaceFolder(
workspaceFolder: WorkspaceFolder,
testDirectory: FileTreeDirectory | undefined,
): void {
if (testDirectory !== undefined) {
// Adding an item with the same ID as an existing item will replace it, which is exactly what
// we want.
// Test discovery returns a root `QLTestDirectory` representing the workspace folder itself,
// named after the `WorkspaceFolder` object's `name` property. We can map this directly to a
// `TestItem`.
this.testController.items.add(
this.createTestItemTree(testDirectory, true),
);
} else {
// No tests, so delete any existing item.
this.testController.items.delete(workspaceFolder.uri.toString());
}
private handleDidChangeTests(): void {
this.testController.items.replace(
this.qlTestDiscovery
.buildTestTree()
?.map((testDirectory) =>
this.createTestItemTree(testDirectory, true),
) ?? [],
);
}
/**

Просмотреть файл

@ -18,6 +18,7 @@ import {
} from "./test-runner-helpers";
import { TestManager } from "../../../../src/query-testing/test-manager";
import { createMockApp } from "../../../__mocks__/appMock";
import type { QLTestFileDiscovery } from "../../../../src/query-testing/qltest-file-discovery";
type IdTestItemPair = [id: string, testItem: TestItem];
@ -55,7 +56,9 @@ describe("test-adapter", () => {
const testManager = new TestManager(
createMockApp({}),
testRunner,
fakeCliServer,
mockedObject<QLTestFileDiscovery>({
onDidChangeTests: jest.fn(),
}),
testController,
);