diff --git a/src/bazel/bazel_workspace_info.ts b/src/bazel/bazel_workspace_info.ts index 7fde6e7e..dfa86a96 100644 --- a/src/bazel/bazel_workspace_info.ts +++ b/src/bazel/bazel_workspace_info.ts @@ -100,8 +100,8 @@ export class BazelWorkspaceInfo { * belong to a workspace folder (for example, a standalone file loaded * into the editor). */ - private constructor( + constructor( public readonly bazelWorkspacePath: string, - public readonly workspaceFolder: vscode.WorkspaceFolder | undefined, + public readonly workspaceFolder?: vscode.WorkspaceFolder, ) {} } diff --git a/src/extension/extension.ts b/src/extension/extension.ts index 1c4becd2..50f019b2 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -41,7 +41,7 @@ import { activateWrapperCommands } from "./bazel_wrapper_commands"; * @param context The extension context. */ export async function activate(context: vscode.ExtensionContext) { - const workspaceTreeProvider = new BazelWorkspaceTreeProvider(); + const workspaceTreeProvider = await BazelWorkspaceTreeProvider.forExtension(); context.subscriptions.push(workspaceTreeProvider); const codeLensProvider = new BazelBuildCodeLensProvider(context); @@ -95,11 +95,15 @@ export async function activate(context: vscode.ExtensionContext) { ), // Commands ...activateWrapperCommands(), - vscode.commands.registerCommand("bazel.refreshBazelBuildTargets", () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - completionItemProvider.refresh(); - workspaceTreeProvider.refresh(); - }), + vscode.commands.registerCommand( + "bazel.refreshBazelBuildTargets", + async () => { + await Promise.allSettled([ + completionItemProvider.refresh(), + workspaceTreeProvider.refresh(vscode.workspace.workspaceFolders), + ]); + }, + ), vscode.commands.registerCommand( "bazel.copyTargetToClipboard", bazelCopyTargetToClipboard, diff --git a/src/workspace-tree/bazel_package_tree_item.ts b/src/workspace-tree/bazel_package_tree_item.ts index 4b118033..ff63b1fb 100644 --- a/src/workspace-tree/bazel_package_tree_item.ts +++ b/src/workspace-tree/bazel_package_tree_item.ts @@ -33,6 +33,7 @@ export class BazelPackageTreeItem /** * Initializes a new tree item with the given workspace path and package path. * + * @param querier Querier for getting information inside a Bazel workspace. * @param workspacePath The path to the VS Code workspace folder. * @param packagePath The path to the build package that this item represents. * @param parentPackagePath The path to the build package of the tree item @@ -43,7 +44,7 @@ export class BazelPackageTreeItem private readonly querier: IBazelQuerier, private readonly workspaceInfo: BazelWorkspaceInfo, private readonly packagePath: string, - private readonly parentPackagePath: string, + private readonly parentPackagePath?: string, ) {} public mightHaveChildren(): boolean { @@ -62,14 +63,23 @@ export class BazelPackageTreeItem } public getLabel(): string { - // If this is a top-level package, include the leading double-slash on the - // label. - if (this.parentPackagePath.length === 0) { - return `//${this.packagePath}`; + if (this.parentPackagePath === undefined) { + return this.packagePath; } - // Otherwise, strip off the part of the package path that came from the - // parent item (along with the slash). - return this.packagePath.substring(this.parentPackagePath.length + 1); + // Strip off the part of the package path that came from the + // parent item. + const parentLength = this.parentPackagePath.length; + // (null) + // //a + // + // @repo//foo + // @repo//foo/bar + // + // @repo// + // @repo//foo + const diffIsLeadingSlash = this.packagePath[parentLength] === "/"; + const prefixLength = diffIsLeadingSlash ? parentLength + 1 : parentLength; + return this.packagePath.substring(prefixLength); } public getIcon(): vscode.ThemeIcon { @@ -77,11 +87,11 @@ export class BazelPackageTreeItem } public getTooltip(): string { - return `//${this.packagePath}`; + return this.packagePath; } - public getCommand(): vscode.Command | undefined { - return undefined; + public getCommand(): Thenable { + return Promise.resolve(undefined); } public getContextValue(): string { @@ -91,7 +101,7 @@ export class BazelPackageTreeItem public getBazelCommandOptions(): IBazelCommandOptions { return { options: [], - targets: [`//${this.packagePath}`], + targets: [this.packagePath], workspaceInfo: this.workspaceInfo, }; } diff --git a/src/workspace-tree/bazel_target_tree_item.ts b/src/workspace-tree/bazel_target_tree_item.ts index ad5d0069..f1a8e444 100644 --- a/src/workspace-tree/bazel_target_tree_item.ts +++ b/src/workspace-tree/bazel_target_tree_item.ts @@ -13,11 +13,14 @@ // limitations under the License. import * as vscode from "vscode"; +import * as fs from "fs/promises"; import { BazelWorkspaceInfo, QueryLocation } from "../bazel"; import { IBazelCommandAdapter, IBazelCommandOptions } from "../bazel"; import { blaze_query } from "../protos"; import { IBazelTreeItem } from "./bazel_tree_item"; import { getBazelRuleIcon } from "./icons"; +import { BazelInfo } from "../bazel/bazel_info"; +import { getDefaultBazelExecutablePath } from "../extension/configuration"; /** A tree item representing a build target. */ export class BazelTargetTreeItem @@ -27,6 +30,7 @@ export class BazelTargetTreeItem * Initializes a new tree item with the given query result representing a * build target. * + * @param querier Querier for getting information inside a Bazel workspace. * @param target An object representing a build target that was produced by a * query. */ @@ -55,16 +59,36 @@ export class BazelTargetTreeItem } public getTooltip(): string { - return `${this.target.rule.name}`; + return this.target.rule.name; } - public getCommand(): vscode.Command | undefined { + public async getCommand(): Promise { + // Resolve the prefix if prefix is + // $(./prebuilts/bazel info output_base)/external/ const location = new QueryLocation(this.target.rule.location); + // Maybe we should cache this to prevent the repeating invocations. + const outputBase = await new BazelInfo( + getDefaultBazelExecutablePath(), + this.workspaceInfo.workspaceFolder.uri.fsPath, + ).getOne("output_base"); + let locationPath = location.path; + // If location is in pattern `${execRoot}/external//...`, then it + // should be a file in local_repository(). Trying to remapping it back to + // the origin source folder by resolve the symlink + // ${execRoot}/external/. + const outputBaseExternalPath = `${outputBase}/external/`; + if (location.path.startsWith(outputBaseExternalPath)) { + const repoPath = location.path.substring(outputBaseExternalPath.length); + const repoPathMatch = repoPath.match(/^([^/]+)\/(.*)$/); + if (repoPathMatch.length === 3) { + const repo = repoPathMatch[1]; + const rest = repoPathMatch[2]; + const realRepo = await fs.realpath(`${outputBaseExternalPath}${repo}`); + locationPath = `${realRepo}/${rest}`; + } + } return { - arguments: [ - vscode.Uri.file(location.path), - { selection: location.range }, - ], + arguments: [vscode.Uri.file(locationPath), { selection: location.range }], command: "vscode.open", title: "Jump to Build Target", }; @@ -81,7 +105,7 @@ export class BazelTargetTreeItem public getBazelCommandOptions(): IBazelCommandOptions { return { options: [], - targets: [`${this.target.rule.name}`], + targets: [this.target.rule.name], workspaceInfo: this.workspaceInfo, }; } diff --git a/src/workspace-tree/bazel_tree_item.ts b/src/workspace-tree/bazel_tree_item.ts index 436d5392..aab2cb4a 100644 --- a/src/workspace-tree/bazel_tree_item.ts +++ b/src/workspace-tree/bazel_tree_item.ts @@ -46,7 +46,7 @@ export interface IBazelTreeItem { getTooltip(): string | undefined; /** Returns the command that should be executed when the item is selected. */ - getCommand(): vscode.Command | undefined; + getCommand(): Thenable; /** * Returns an identifying string that is used to filter which commands are diff --git a/src/workspace-tree/bazel_workspace_folder_tree_item.ts b/src/workspace-tree/bazel_workspace_folder_tree_item.ts index a27f4e23..ce5e6510 100644 --- a/src/workspace-tree/bazel_workspace_folder_tree_item.ts +++ b/src/workspace-tree/bazel_workspace_folder_tree_item.ts @@ -25,6 +25,7 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { /** * Initializes a new tree item with the given workspace folder. * + * @param querier Querier for getting information inside a Bazel workspace. * @param workspaceFolder The workspace folder that the tree item represents. */ constructor( @@ -52,8 +53,8 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { return this.workspaceInfo.workspaceFolder.uri.fsPath; } - public getCommand(): vscode.Command | undefined { - return undefined; + public getCommand(): Thenable { + return Promise.resolve(undefined); } public getContextValue(): string { @@ -81,7 +82,7 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { startIndex: number, endIndex: number, treeItems: IBazelTreeItem[], - parentPackagePath: string, + parentPackagePath?: string, ) { // We can assume that the caller has sorted the packages, so we scan them to // find groupings into which we should traverse more deeply. For example, if @@ -115,7 +116,9 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { // erroneously collapse something like "foo" and "foobar". while ( groupEnd < endIndex && - packagePaths[groupEnd].startsWith(packagePath + "/") + (packagePaths[groupEnd].startsWith(packagePath + "/") || + (packagePaths[groupEnd].startsWith(packagePath) && + packagePath.endsWith("//"))) ) { groupEnd++; } @@ -160,14 +163,8 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { return Promise.resolve([] as IBazelTreeItem[]); } const packagePaths = await this.querier.queryPackages(this.workspaceInfo); - const topLevelItems: BazelPackageTreeItem[] = []; - this.buildPackageTree( - packagePaths, - 0, - packagePaths.length, - topLevelItems, - "", - ); + const topLevelItems: IBazelTreeItem[] = []; + this.buildPackageTree(packagePaths, 0, packagePaths.length, topLevelItems); // Now collect any targets in the directory also (this can fail since // there might not be a BUILD files at this level (but down levels)). @@ -179,6 +176,6 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { return new BazelTargetTreeItem(this.workspaceInfo, target); }); - return Promise.resolve((topLevelItems as IBazelTreeItem[]).concat(targets)); + return Promise.resolve(topLevelItems.concat(targets)); } } diff --git a/src/workspace-tree/bazel_workspace_tree_provider.ts b/src/workspace-tree/bazel_workspace_tree_provider.ts index daf79751..e8266678 100644 --- a/src/workspace-tree/bazel_workspace_tree_provider.ts +++ b/src/workspace-tree/bazel_workspace_tree_provider.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as vscode from "vscode"; -import { BazelWorkspaceInfo } from "../bazel"; +import { assert } from "../assert"; import { IBazelTreeItem } from "./bazel_tree_item"; import { BazelWorkspaceFolderTreeItem } from "./bazel_workspace_folder_tree_item"; import { IBazelQuerier, ProcessBazelQuerier } from "./querier"; @@ -31,67 +31,70 @@ export class BazelWorkspaceTreeProvider public readonly onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event; /** The cached toplevel items. */ - private workspaceFolderTreeItems: BazelWorkspaceFolderTreeItem[] | undefined; + private workspaceFolderTreeItems?: IBazelTreeItem[]; private disposables: vscode.Disposable[] = []; - /** - * Initializes a new tree provider with the given extension context. - * - * @param context The VS Code extension context. - * @param querier The interface providing the `bazel query` results. - */ - constructor( - private readonly querier: IBazelQuerier = new ProcessBazelQuerier(), - ) { + public static async forExtension(): Promise { + const workspaceTreeProvider = new BazelWorkspaceTreeProvider( + new ProcessBazelQuerier(), + ); + + const refreshWorkspaceFolders = () => + workspaceTreeProvider.refresh(vscode.workspace.workspaceFolders); + + workspaceTreeProvider.disposables.push( + vscode.workspace.onDidChangeWorkspaceFolders(refreshWorkspaceFolders), + ); + const buildFilesWatcher = vscode.workspace.createFileSystemWatcher( "**/{BUILD,BUILD.bazel}", false, false, false, ); - this.disposables.push( + workspaceTreeProvider.disposables.push( buildFilesWatcher, - buildFilesWatcher.onDidChange(() => this.onBuildFilesChanged()), - buildFilesWatcher.onDidCreate(() => this.onBuildFilesChanged()), - buildFilesWatcher.onDidDelete(() => this.onBuildFilesChanged()), - vscode.workspace.onDidChangeWorkspaceFolders(() => this.refresh()), + buildFilesWatcher.onDidChange(refreshWorkspaceFolders), + buildFilesWatcher.onDidCreate(refreshWorkspaceFolders), + buildFilesWatcher.onDidDelete(refreshWorkspaceFolders), ); - this.updateWorkspaceFolderTreeItems(); + await refreshWorkspaceFolders(); + + return workspaceTreeProvider; } - public getChildren(element?: IBazelTreeItem): Thenable { + /** + * @param querier The interface providing the `bazel query` results. + */ + constructor(private readonly querier: IBazelQuerier) {} + + public async getChildren( + element?: IBazelTreeItem, + ): Promise { // If we're given an element, we're not asking for the top-level elements, // so just delegate to that element to get its children. if (element) { return element.getChildren(); } - if (this.workspaceFolderTreeItems === undefined) { - this.updateWorkspaceFolderTreeItems(); - } + // Assuming the extension or test cases should call refresh at least once. + assert(this.workspaceFolderTreeItems !== undefined); - if (this.workspaceFolderTreeItems && vscode.workspace.workspaceFolders) { - // If the user has a workspace open and there's only one folder in it, - // then don't show the workspace folder; just show its packages at the top - // level. - if (vscode.workspace.workspaceFolders.length === 1) { - const folderItem = this.workspaceFolderTreeItems[0]; - return folderItem.getChildren(); - } - - // If the user has multiple workspace folders open, then show them as - // individual top level items. - return Promise.resolve(this.workspaceFolderTreeItems); + // If the user has a workspace open and there's only one folder in it, then + // don't show the workspace folder; just show its packages at the top level. + if (this.workspaceFolderTreeItems.length === 1) { + const folderItem = this.workspaceFolderTreeItems[0]; + return folderItem.getChildren(); } - // If the user doesn't have a folder open in the workspace, or none of them - // have Bazel workspaces, don't show anything. - return Promise.resolve([]); + // If the user has multiple or no workspace folders open, then show them as + // individual top level items. + return this.workspaceFolderTreeItems; } - public getTreeItem(element: IBazelTreeItem): vscode.TreeItem { + public async getTreeItem(element: IBazelTreeItem): Promise { const label = element.getLabel(); const collapsibleState = element.mightHaveChildren() ? vscode.TreeItemCollapsibleState.Collapsed @@ -101,51 +104,60 @@ export class BazelWorkspaceTreeProvider treeItem.contextValue = element.getContextValue(); treeItem.iconPath = element.getIcon(); treeItem.tooltip = element.getTooltip(); - treeItem.command = element.getCommand(); + treeItem.command = await element.getCommand(); return treeItem; } - /** Forces a re-query and refresh of the tree's contents. */ - public refresh() { - this.updateWorkspaceFolderTreeItems(); - this.onDidChangeTreeDataEmitter.fire(); - } - - /** - * Called to update the tree when a BUILD file is created, deleted, or - * changed. + /** Forces a re-query and refresh of the tree's contents. * - * @param uri The file system URI of the file that changed. + * Also for initialize or to update the tree when a BUILD file is created, + * deleted, or changed. */ - private onBuildFilesChanged() { - // TODO(allevato): Look into firing the event only for tree items that are - // affected by the change. - this.refresh(); + public async refresh( + workspaceFolders: readonly vscode.WorkspaceFolder[], + ): Promise { + await this.updateWorkspaceFolderTreeItems(workspaceFolders); + this.onDidChangeTreeDataEmitter.fire(); } - /** Refresh the cached BazelWorkspaceFolderTreeItems. */ - private updateWorkspaceFolderTreeItems() { - if (vscode.workspace.workspaceFolders) { - this.workspaceFolderTreeItems = vscode.workspace.workspaceFolders - .map((folder) => { - const workspaceInfo = BazelWorkspaceInfo.fromWorkspaceFolder(folder); - if (workspaceInfo) { - return new BazelWorkspaceFolderTreeItem( - this.querier, - workspaceInfo, - ); - } - return undefined; - }) - .filter((folder) => folder !== undefined); - } else { - this.workspaceFolderTreeItems = []; + private async createWorkspaceFolderTreeItem( + workspaceFolder: vscode.WorkspaceFolder, + ): Promise { + const workspaceInfo = await this.querier.queryWorkspace(workspaceFolder); + if (workspaceInfo === undefined) { + return undefined; } + return new BazelWorkspaceFolderTreeItem(this.querier, workspaceInfo); + } + + private async createWorkspaceFolderTreeItems( + workspaceFolders: readonly vscode.WorkspaceFolder[], + ): Promise { + const maybeWorkspaceFolderTreeItems = await Promise.all( + workspaceFolders.map((workspaceFolder) => + this.createWorkspaceFolderTreeItem(workspaceFolder), + ), + ); + return maybeWorkspaceFolderTreeItems.filter( + (folder) => folder !== undefined, + ); + } + + /** + * Update the cached BazelWorkspaceFolderTreeItems and other UI components + * interested in. + */ + private async updateWorkspaceFolderTreeItems( + workspaceFolders?: readonly vscode.WorkspaceFolder[], + ): Promise { + this.workspaceFolderTreeItems = await this.createWorkspaceFolderTreeItems( + workspaceFolders ?? [], + ); - // All the UI to update based on having items. + // Updates other UI components based on the context value for Bazel + // workspace. const haveBazelWorkspace = this.workspaceFolderTreeItems.length !== 0; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - vscode.commands.executeCommand( + void vscode.commands.executeCommand( "setContext", "bazel.haveWorkspace", haveBazelWorkspace, diff --git a/test/workspace-tree.test.ts b/test/workspace-tree.test.ts new file mode 100644 index 00000000..40a3d451 --- /dev/null +++ b/test/workspace-tree.test.ts @@ -0,0 +1,206 @@ +import * as assert from "assert"; +import * as vscode from "vscode"; +import { BazelWorkspaceInfo } from "../src/bazel"; +import { blaze_query } from "../src/protos"; +import { + BazelWorkspaceTreeProvider, + IBazelQuerier, +} from "../src/workspace-tree"; + +class FakeBazelQuerier implements IBazelQuerier { + constructor( + private readonly packages: string[], + private readonly targets: Map, + ) {} + + queryWorkspace( + workspaceFolder: vscode.WorkspaceFolder, + ): Thenable { + // Assuming query from root for simplest test case. (single root) + return Promise.resolve( + new BazelWorkspaceInfo(workspaceFolder.uri.fsPath, workspaceFolder), + ); + } + + queryPackages(workspaceInfo: BazelWorkspaceInfo): Thenable { + void workspaceInfo; + return Promise.resolve(this.packages); + } + + queryChildrenTargets( + workspaceInfo: BazelWorkspaceInfo, + packagePath: string, + ): Thenable { + void workspaceInfo; + return Promise.resolve(this.targets.get(packagePath)); + } +} + +function fakeWorkspaceFolder(path: string): vscode.WorkspaceFolder { + const uri = vscode.Uri.file(path); + return { + uri, + name: path, + index: 0, + }; +} + +async function workspaceTreeProviderForTest( + querier: IBazelQuerier, + workspaceFolders: vscode.WorkspaceFolder[], +): Promise { + const provider = new BazelWorkspaceTreeProvider(querier); + await provider.refresh(workspaceFolders); + return provider; +} + +describe("The Bazel workspace tree provider", () => { + it("Returns nothing on empty workspace folders", async () => { + const querier = new FakeBazelQuerier([], new Map()); + const workspaceFolders: vscode.WorkspaceFolder[] = []; + const provider = await workspaceTreeProviderForTest( + querier, + workspaceFolders, + ); + + const topChildren = await provider.getChildren(); + assert.deepStrictEqual(topChildren, []); + }); + + it("Flatten on single workspace folder", async () => { + const querier = new FakeBazelQuerier( + ["//a"], + new Map([ + ["", { target: [] }], + ["//a", { target: [] }], + ]), + ); + const workspaceFolders: vscode.WorkspaceFolder[] = [ + fakeWorkspaceFolder("fake/path"), + ]; + const provider = await workspaceTreeProviderForTest( + querier, + workspaceFolders, + ); + + const topChildren = await provider.getChildren(); + assert.equal(topChildren[0].getLabel(), "//a"); + }); + + it("Not flatten on 2 workspace folders", async () => { + const querier = new FakeBazelQuerier([], new Map([["", { target: [] }]])); + const workspaceFolders: vscode.WorkspaceFolder[] = [ + fakeWorkspaceFolder("fake/path0"), + fakeWorkspaceFolder("fake/path1"), + ]; + const provider = await workspaceTreeProviderForTest( + querier, + workspaceFolders, + ); + + const topChildren = await provider.getChildren(); + assert.equal(topChildren[0].getLabel(), "fake/path0"); + assert.equal(topChildren[1].getLabel(), "fake/path1"); + }); + + it("Can handle root package", async () => { + const querier = new FakeBazelQuerier( + ["//", "//a"], + new Map([ + ["", { target: [] }], + ["//", { target: [] }], + ["//a", { target: [] }], + ]), + ); + const workspaceFolders: vscode.WorkspaceFolder[] = [ + fakeWorkspaceFolder("fake/path"), + ]; + const provider = await workspaceTreeProviderForTest( + querier, + workspaceFolders, + ); + + const topChildren = await provider.getChildren(); + const root = topChildren[0]; + assert.equal(root.getLabel(), "//"); + const rootChildren = await root.getChildren(); + const a = rootChildren[0]; + assert.equal(a.getLabel(), "a"); + }); + + it("Skips non-package folders", async () => { + const querier = new FakeBazelQuerier( + ["//a", "//a/b/c"], + new Map([ + ["", { target: [] }], + ["//a", { target: [] }], + ["//a/b/c", { target: [] }], + ]), + ); + const workspaceFolders: vscode.WorkspaceFolder[] = [ + fakeWorkspaceFolder("fake/path"), + ]; + const provider = await workspaceTreeProviderForTest( + querier, + workspaceFolders, + ); + + const topChildren = await provider.getChildren(); + const a = topChildren[0]; + assert.equal(a.getLabel(), "//a"); + const aChildren = await topChildren[0].getChildren(); + const bc = aChildren[0]; + assert.equal(bc.getLabel(), "b/c"); + }); + + it("Handles external dependencies (single workspace)", async () => { + const querier = new FakeBazelQuerier( + [ + "@repo//", + "@repo2//a", + "@repo2//a/b", + "@repo2//c", + "@repo2//c/d/e", + "@repo3//f", + ], + new Map([ + ["", { target: [] }], + ["@repo//", { target: [] }], + ["@repo2//a", { target: [] }], + ["@repo2//a/b", { target: [] }], + ["@repo2//c", { target: [] }], + ["@repo2//c/d/e", { target: [] }], + ["@repo3//f", { target: [] }], + ]), + ); + const workspaceFolders: vscode.WorkspaceFolder[] = [ + fakeWorkspaceFolder("fake/path"), + ]; + const provider = await workspaceTreeProviderForTest( + querier, + workspaceFolders, + ); + + const topChildren = await provider.getChildren(); + const repo = topChildren[0]; + assert.strictEqual(repo.getLabel(), "@repo//"); + assert.strictEqual((await repo.getChildren()).length, 0); + + const repo2a = topChildren[1]; + assert.strictEqual(repo2a.getLabel(), "@repo2//a"); + const repo2aChildren = await repo2a.getChildren(); + const repo2ab = repo2aChildren[0]; + assert.strictEqual(repo2ab.getLabel(), "b"); + + const repo2c = topChildren[2]; + assert.strictEqual(repo2c.getLabel(), "@repo2//c"); + const repo2cChildren = await repo2c.getChildren(); + const repo2cde = repo2cChildren[0]; + assert.strictEqual(repo2cde.getLabel(), "d/e"); + + const repo3f = topChildren[3]; + assert.strictEqual(repo3f.getLabel(), "@repo3//f"); + }); + + // TODO query target test cases. +});