Remove isVariantAnalysisReposPanelEnabled feature flag and old flows (#2096)

This commit is contained in:
Charis Kyriakou
2023-02-21 08:52:50 +00:00
committed by GitHub
parent 61f4ce27da
commit 403e893f93
8 changed files with 174 additions and 840 deletions

View File

@@ -579,10 +579,6 @@ export function isVariantAnalysisLiveResultsEnabled(): boolean {
return true;
}
export function isVariantAnalysisReposPanelEnabled(): boolean {
return true;
}
// Settings for mocking the GitHub API.
const MOCK_GH_API_SERVER = new Setting("mockGitHubApiServer", ROOT_SETTING);

View File

@@ -6,7 +6,7 @@ import { DbConfigStore } from "./config/db-config-store";
import { DbManager } from "./db-manager";
import { DbPanel } from "./ui/db-panel";
import { DbSelectionDecorationProvider } from "./ui/db-selection-decoration-provider";
import { isCanary, isVariantAnalysisReposPanelEnabled } from "../config";
import { isCanary } from "../config";
export class DbModule extends DisposableObject {
public readonly dbManager: DbManager;
@@ -36,7 +36,7 @@ export class DbModule extends DisposableObject {
return true;
}
return isCanary() && isVariantAnalysisReposPanelEnabled();
return isCanary();
}
private async initialize(app: App): Promise<void> {

View File

@@ -638,7 +638,7 @@ async function activateWithInstalledDistribution(
cliServer,
variantAnalysisStorageDir,
variantAnalysisResultsManager,
dbModule?.dbManager, // the dbModule is only needed when variantAnalysisReposPanel is enabled
dbModule?.dbManager,
);
ctx.subscriptions.push(variantAnalysisManager);
ctx.subscriptions.push(variantAnalysisResultsManager);

View File

@@ -1,12 +1,3 @@
import { pathExists as fs_pathExists, stat, readFile } from "fs-extra";
import { QuickPickItem, window } from "vscode";
import { extLogger } from "../common";
import {
getRemoteRepositoryLists,
getRemoteRepositoryListsPath,
isVariantAnalysisReposPanelEnabled,
} from "../config";
import { OWNER_REGEX, REPO_REGEX } from "../pure/helpers-pure";
import { UserCancellationException } from "../commandRunner";
import { DbManager } from "../databases/db-manager";
import { DbItemKind } from "../databases/db-item";
@@ -17,18 +8,6 @@ export interface RepositorySelection {
owners?: string[];
}
interface RepoListQuickPickItem extends QuickPickItem {
repositories?: string[];
repositoryList?: string;
useCustomRepo?: boolean;
useAllReposOfOwner?: boolean;
}
interface RepoList {
label: string;
repositories: string[];
}
/**
* Gets the repositories or repository lists to run the query against.
* @returns The user selection.
@@ -36,101 +15,35 @@ interface RepoList {
export async function getRepositorySelection(
dbManager?: DbManager,
): Promise<RepositorySelection> {
if (isVariantAnalysisReposPanelEnabled()) {
const selectedDbItem = dbManager?.getSelectedDbItem();
if (selectedDbItem) {
switch (selectedDbItem.kind) {
case DbItemKind.LocalDatabase || DbItemKind.LocalList:
const selectedDbItem = dbManager?.getSelectedDbItem();
if (selectedDbItem) {
switch (selectedDbItem.kind) {
case DbItemKind.LocalDatabase || DbItemKind.LocalList:
throw new UserCancellationException(
"Local databases and lists are not supported yet.",
);
case DbItemKind.RemoteSystemDefinedList:
return { repositoryLists: [selectedDbItem.listName] };
case DbItemKind.RemoteUserDefinedList:
if (selectedDbItem.repos.length === 0) {
throw new UserCancellationException(
"Local databases and lists are not supported yet.",
"The selected repository list is empty. Please add repositories to it before running a variant analysis.",
);
case DbItemKind.RemoteSystemDefinedList:
return { repositoryLists: [selectedDbItem.listName] };
case DbItemKind.RemoteUserDefinedList:
if (selectedDbItem.repos.length === 0) {
throw new UserCancellationException(
"The selected repository list is empty. Please add repositories to it before running a variant analysis.",
);
} else {
return {
repositories: selectedDbItem.repos.map(
(repo) => repo.repoFullName,
),
};
}
case DbItemKind.RemoteOwner:
return { owners: [selectedDbItem.ownerName] };
case DbItemKind.RemoteRepo:
return { repositories: [selectedDbItem.repoFullName] };
}
} else {
throw new UserCancellationException(
"Please select a remote database to run the query against.",
);
} else {
return {
repositories: selectedDbItem.repos.map((repo) => repo.repoFullName),
};
}
case DbItemKind.RemoteOwner:
return { owners: [selectedDbItem.ownerName] };
case DbItemKind.RemoteRepo:
return { repositories: [selectedDbItem.repoFullName] };
}
}
const quickPickItems = [
createCustomRepoQuickPickItem(),
createAllReposOfOwnerQuickPickItem(),
...createSystemDefinedRepoListsQuickPickItems(),
...(await createUserDefinedRepoListsQuickPickItems()),
];
const options = {
placeHolder:
"Select a repository list. You can define repository lists in the `codeQL.variantAnalysis.repositoryLists` setting.",
ignoreFocusOut: true,
};
const quickpick = await window.showQuickPick<RepoListQuickPickItem>(
quickPickItems,
options,
throw new UserCancellationException(
"Please select a remote database to run the query against.",
);
if (!quickpick) {
// We don't need to display a warning pop-up in this case, since the user just escaped out of the operation.
// We set 'true' to make this a silent exception.
throw new UserCancellationException("No repositories selected", true);
}
if (quickpick.repositories?.length) {
void extLogger.log(
`Selected repositories: ${quickpick.repositories.join(", ")}`,
);
return { repositories: quickpick.repositories };
} else if (quickpick.repositoryList) {
void extLogger.log(`Selected repository list: ${quickpick.repositoryList}`);
return { repositoryLists: [quickpick.repositoryList] };
} else if (quickpick.useCustomRepo) {
const customRepo = await getCustomRepo();
if (customRepo === undefined) {
// The user cancelled, do nothing.
throw new UserCancellationException("No repositories selected", true);
}
if (!customRepo || !REPO_REGEX.test(customRepo)) {
throw new UserCancellationException(
"Invalid repository format. Please enter a valid repository in the format <owner>/<repo> (e.g. github/codeql)",
);
}
void extLogger.log(`Entered repository: ${customRepo}`);
return { repositories: [customRepo] };
} else if (quickpick.useAllReposOfOwner) {
const owner = await getOwner();
if (owner === undefined) {
// The user cancelled, do nothing.
throw new UserCancellationException("No repositories selected", true);
}
if (!owner || !OWNER_REGEX.test(owner)) {
throw new Error(`Invalid user or organization: ${owner}`);
}
void extLogger.log(`Entered owner: ${owner}`);
return { owners: [owner] };
} else {
// This means the user has selected something, but there is nothing actually linked to this item. We want to show
// this to the user.
throw new UserCancellationException("No repositories selected", false);
}
}
/**
@@ -147,133 +60,3 @@ export function isValidSelection(repoSelection: RepositorySelection): boolean {
repositories.length > 0 || repositoryLists.length > 0 || owners.length > 0
);
}
function createSystemDefinedRepoListsQuickPickItems(): RepoListQuickPickItem[] {
const topNs = [10, 100, 1000];
return topNs.map(
(n) =>
({
label: `$(star) Top ${n}`,
repositoryList: `top_${n}`,
alwaysShow: true,
} as RepoListQuickPickItem),
);
}
async function readExternalRepoLists(): Promise<RepoList[]> {
const repoLists: RepoList[] = [];
const path = getRemoteRepositoryListsPath();
if (!path) {
return repoLists;
}
await validateExternalRepoListsFile(path);
const json = await readExternalRepoListsJson(path);
for (const [repoListName, repositories] of Object.entries(json)) {
if (!Array.isArray(repositories)) {
throw Error(
"Invalid repository lists file. It should contain an array of repositories for each list.",
);
}
repoLists.push({
label: repoListName,
repositories,
});
}
return repoLists;
}
async function validateExternalRepoListsFile(path: string): Promise<void> {
const pathExists = await fs_pathExists(path);
if (!pathExists) {
throw Error(`External repository lists file does not exist at ${path}`);
}
const pathStat = await stat(path);
if (pathStat.isDirectory()) {
throw Error(
"External repository lists path should not point to a directory",
);
}
}
async function readExternalRepoListsJson(
path: string,
): Promise<Record<string, unknown>> {
let json;
try {
const fileContents = await readFile(path, "utf8");
json = await JSON.parse(fileContents);
} catch (error) {
throw Error("Invalid repository lists file. It should contain valid JSON.");
}
if (Array.isArray(json)) {
throw Error(
"Invalid repository lists file. It should be an object mapping names to a list of repositories.",
);
}
return json;
}
function readRepoListsFromSettings(): RepoList[] {
const repoLists = getRemoteRepositoryLists();
if (!repoLists) {
return [];
}
return Object.entries(repoLists).map<RepoList>(([label, repositories]) => ({
label,
repositories,
}));
}
async function createUserDefinedRepoListsQuickPickItems(): Promise<
RepoListQuickPickItem[]
> {
const repoListsFromSetings = readRepoListsFromSettings();
const repoListsFromExternalFile = await readExternalRepoLists();
return [...repoListsFromSetings, ...repoListsFromExternalFile];
}
function createCustomRepoQuickPickItem(): RepoListQuickPickItem {
return {
label: "$(edit) Enter a GitHub repository",
useCustomRepo: true,
alwaysShow: true,
};
}
function createAllReposOfOwnerQuickPickItem(): RepoListQuickPickItem {
return {
label: "$(edit) Enter a GitHub user or organization",
useAllReposOfOwner: true,
alwaysShow: true,
};
}
async function getCustomRepo(): Promise<string | undefined> {
return await window.showInputBox({
title:
"Enter a GitHub repository in the format <owner>/<repo> (e.g. github/codeql)",
placeHolder: "<owner>/<repo>",
prompt:
"Tip: you can save frequently used repositories in the `codeQL.variantAnalysis.repositoryLists` setting",
ignoreFocusOut: true,
});
}
async function getOwner(): Promise<string | undefined> {
return await window.showInputBox({
title: "Enter a GitHub user or organization",
ignoreFocusOut: true,
});
}

View File

@@ -223,7 +223,7 @@ export async function prepareRemoteQueryRun(
uri: Uri | undefined,
progress: ProgressCallback,
token: CancellationToken,
dbManager?: DbManager, // the dbManager is only needed when variantAnalysisReposPanel is enabled
dbManager?: DbManager,
): Promise<PreparedRemoteQuery> {
if (!uri?.fsPath.endsWith(".ql")) {
throw new UserCancellationException("Not a CodeQL query file.");

View File

@@ -60,7 +60,6 @@ import {
} from "../pure/variant-analysis-filter-sort";
import { URLSearchParams } from "url";
import { DbManager } from "../databases/db-manager";
import { isVariantAnalysisReposPanelEnabled } from "../config";
import { App } from "../common/app";
import { redactableError } from "../pure/errors";
@@ -106,7 +105,7 @@ export class VariantAnalysisManager
private readonly cliServer: CodeQLCliServer,
private readonly storagePath: string,
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager,
private readonly dbManager?: DbManager, // the dbManager is only needed when variantAnalysisReposPanel is enabled
private readonly dbManager?: DbManager,
) {
super();
this.variantAnalysisMonitor = this.push(
@@ -635,25 +634,15 @@ export class VariantAnalysisManager
return;
}
let text: string[];
if (isVariantAnalysisReposPanelEnabled()) {
text = [
"{",
` "name": "new-repo-list",`,
` "repositories": [`,
...fullNames.slice(0, -1).map((repo) => ` "${repo}",`),
` "${fullNames[fullNames.length - 1]}"`,
` ]`,
"}",
];
} else {
text = [
'"new-repo-list": [',
...fullNames.slice(0, -1).map((repo) => ` "${repo}",`),
` "${fullNames[fullNames.length - 1]}"`,
"]",
];
}
const text = [
"{",
` "name": "new-repo-list",`,
` "repositories": [`,
...fullNames.slice(0, -1).map((repo) => ` "${repo}",`),
` "${fullNames[fullNames.length - 1]}"`,
` ]`,
"}",
];
await env.clipboard.writeText(text.join(EOL));
}

View File

@@ -12,7 +12,6 @@ import {
} from "vscode";
import { CodeQLExtensionInterface } from "../../../../src/extension";
import { extLogger } from "../../../../src/common";
import * as config from "../../../../src/config";
import {
setRemoteControllerRepo,
setRemoteRepositoryLists,
@@ -915,137 +914,64 @@ describe("Variant Analysis Manager", () => {
expect(writeTextStub).toBeCalledTimes(1);
});
describe("variantAnalysisReposPanel true", () => {
beforeEach(() => {
jest
.spyOn(config, "isVariantAnalysisReposPanelEnabled")
.mockReturnValue(true);
});
it("should be valid JSON when put in object", async () => {
await variantAnalysisManager.copyRepoListToClipboard(
variantAnalysis.id,
);
it("should be valid JSON when put in object", async () => {
await variantAnalysisManager.copyRepoListToClipboard(
variantAnalysis.id,
);
const text = writeTextStub.mock.calls[0][0];
const text = writeTextStub.mock.calls[0][0];
const parsed = JSON.parse(`${text}`);
const parsed = JSON.parse(`${text}`);
expect(parsed).toEqual({
name: "new-repo-list",
repositories: [
scannedRepos[4].repository.fullName,
scannedRepos[2].repository.fullName,
scannedRepos[0].repository.fullName,
],
});
});
it("should use the sort key", async () => {
await variantAnalysisManager.copyRepoListToClipboard(
variantAnalysis.id,
{
...defaultFilterSortState,
sortKey: SortKey.ResultsCount,
},
);
const text = writeTextStub.mock.calls[0][0];
const parsed = JSON.parse(`${text}`);
expect(parsed).toEqual({
name: "new-repo-list",
repositories: [
scannedRepos[2].repository.fullName,
scannedRepos[0].repository.fullName,
scannedRepos[4].repository.fullName,
],
});
});
it("should use the search value", async () => {
await variantAnalysisManager.copyRepoListToClipboard(
variantAnalysis.id,
{
...defaultFilterSortState,
searchValue: "ban",
},
);
const text = writeTextStub.mock.calls[0][0];
const parsed = JSON.parse(`${text}`);
expect(parsed).toEqual({
name: "new-repo-list",
repositories: [scannedRepos[4].repository.fullName],
});
expect(parsed).toEqual({
name: "new-repo-list",
repositories: [
scannedRepos[4].repository.fullName,
scannedRepos[2].repository.fullName,
scannedRepos[0].repository.fullName,
],
});
});
describe("variantAnalysisReposPanel false", () => {
beforeEach(() => {
jest
.spyOn(config, "isVariantAnalysisReposPanelEnabled")
.mockReturnValue(false);
it("should use the sort key", async () => {
await variantAnalysisManager.copyRepoListToClipboard(
variantAnalysis.id,
{
...defaultFilterSortState,
sortKey: SortKey.ResultsCount,
},
);
const text = writeTextStub.mock.calls[0][0];
const parsed = JSON.parse(`${text}`);
expect(parsed).toEqual({
name: "new-repo-list",
repositories: [
scannedRepos[2].repository.fullName,
scannedRepos[0].repository.fullName,
scannedRepos[4].repository.fullName,
],
});
});
it("should be valid JSON when put in object", async () => {
await variantAnalysisManager.copyRepoListToClipboard(
variantAnalysis.id,
);
it("should use the search value", async () => {
await variantAnalysisManager.copyRepoListToClipboard(
variantAnalysis.id,
{
...defaultFilterSortState,
searchValue: "ban",
},
);
const text = writeTextStub.mock.calls[0][0];
const text = writeTextStub.mock.calls[0][0];
const parsed = JSON.parse(`{${text}}`);
const parsed = JSON.parse(`${text}`);
expect(parsed).toEqual({
"new-repo-list": [
scannedRepos[4].repository.fullName,
scannedRepos[2].repository.fullName,
scannedRepos[0].repository.fullName,
],
});
});
it("should use the sort key", async () => {
await variantAnalysisManager.copyRepoListToClipboard(
variantAnalysis.id,
{
...defaultFilterSortState,
sortKey: SortKey.ResultsCount,
},
);
const text = writeTextStub.mock.calls[0][0];
const parsed = JSON.parse(`{${text}}`);
expect(parsed).toEqual({
"new-repo-list": [
scannedRepos[2].repository.fullName,
scannedRepos[0].repository.fullName,
scannedRepos[4].repository.fullName,
],
});
});
it("should use the search value", async () => {
await variantAnalysisManager.copyRepoListToClipboard(
variantAnalysis.id,
{
...defaultFilterSortState,
searchValue: "ban",
},
);
const text = writeTextStub.mock.calls[0][0];
const parsed = JSON.parse(`{${text}}`);
expect(parsed).toEqual({
"new-repo-list": [scannedRepos[4].repository.fullName],
});
expect(parsed).toEqual({
name: "new-repo-list",
repositories: [scannedRepos[4].repository.fullName],
});
});
});

View File

@@ -1,11 +1,3 @@
import { QuickPickItem, window } from "vscode";
import { join } from "path";
import { DirectoryResult } from "tmp-promise";
import * as tmp from "tmp-promise";
import { ensureDir, writeFile, writeJson } from "fs-extra";
import { UserCancellationException } from "../../../../src/commandRunner";
import * as config from "../../../../src/config";
import { getRepositorySelection } from "../../../../src/variant-analysis/repository-selection";
import { DbManager } from "../../../../src/databases/db-manager";
import {
@@ -15,450 +7,98 @@ import {
} from "../../../../src/databases/db-item";
describe("repository selection", () => {
describe("variantAnalysisReposPanel true", () => {
beforeEach(() => {
jest
.spyOn(config, "isVariantAnalysisReposPanelEnabled")
.mockReturnValue(true);
});
it("should throw error when no database item is selected", async () => {
const dbManager = setUpDbManager(undefined);
it("should throw error when no database item is selected", async () => {
const dbManager = setUpDbManager(undefined);
await expect(getRepositorySelection(dbManager)).rejects.toThrow(
"Please select a remote database to run the query against.",
);
});
it("should log error when local database item is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.LocalDatabase,
} as DbItem);
await expect(getRepositorySelection(dbManager)).rejects.toThrow(
"Local databases and lists are not supported yet.",
);
});
it("should log an error when an empty remote user defined list is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteUserDefinedList,
repos: [] as RemoteRepoDbItem[],
} as DbItem);
await expect(getRepositorySelection(dbManager)).rejects.toThrow(
"The selected repository list is empty. Please add repositories to it before running a variant analysis.",
);
});
it("should return correct selection when remote system defined list is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteSystemDefinedList,
listName: "top_10",
} as DbItem);
const repoSelection = await getRepositorySelection(dbManager);
expect(repoSelection.repositoryLists).toEqual(["top_10"]);
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toBeUndefined();
});
it("should return correct selection when remote user defined list is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteUserDefinedList,
repos: [
{ repoFullName: "owner1/repo1" },
{ repoFullName: "owner1/repo2" },
],
} as DbItem);
const repoSelection = await getRepositorySelection(dbManager);
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toEqual([
"owner1/repo1",
"owner1/repo2",
]);
});
it("should return correct selection when remote owner is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteOwner,
ownerName: "owner2",
} as DbItem);
const repoSelection = await getRepositorySelection(dbManager);
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toEqual(["owner2"]);
expect(repoSelection.repositories).toBeUndefined();
});
it("should return correct selection when remote repo is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteRepo,
repoFullName: "owner1/repo2",
} as DbItem);
const repoSelection = await getRepositorySelection(dbManager);
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toEqual(["owner1/repo2"]);
});
function setUpDbManager(response: DbItem | undefined): DbManager {
return {
getSelectedDbItem: jest.fn(() => {
return response;
}),
} as any as DbManager;
}
await expect(getRepositorySelection(dbManager)).rejects.toThrow(
"Please select a remote database to run the query against.",
);
});
describe("variantAnalysisReposPanel false", () => {
let quickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
let showInputBoxSpy: jest.SpiedFunction<typeof window.showInputBox>;
it("should log error when local database item is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.LocalDatabase,
} as DbItem);
let getRemoteRepositoryListsSpy: jest.SpiedFunction<
typeof config.getRemoteRepositoryLists
>;
let getRemoteRepositoryListsPathSpy: jest.SpiedFunction<
typeof config.getRemoteRepositoryListsPath
>;
beforeEach(() => {
quickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockResolvedValue(undefined);
showInputBoxSpy = jest
.spyOn(window, "showInputBox")
.mockResolvedValue(undefined);
getRemoteRepositoryListsSpy = jest
.spyOn(config, "getRemoteRepositoryLists")
.mockReturnValue(undefined);
getRemoteRepositoryListsPathSpy = jest
.spyOn(config, "getRemoteRepositoryListsPath")
.mockReturnValue(undefined);
jest
.spyOn(config, "isVariantAnalysisReposPanelEnabled")
.mockReturnValue(false);
});
describe("repo lists from settings", () => {
it("should allow selection from repo lists from your pre-defined config", async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
repositories: ["foo/bar", "foo/baz"],
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({
list1: ["foo/bar", "foo/baz"],
list2: [],
});
// Make the function call
const repoSelection = await getRepositorySelection();
// Check that the return value is correct
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toEqual(["foo/bar", "foo/baz"]);
});
it("should return an error for an empty repository list", async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
repositories: [],
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({
list1: ["foo/bar", "foo/baz"],
list2: [],
});
await expect(getRepositorySelection()).rejects.toThrow(
"No repositories selected",
);
await expect(getRepositorySelection()).rejects.toThrow(
UserCancellationException,
);
await expect(getRepositorySelection()).rejects.toHaveProperty(
"silent",
false,
);
});
});
describe("system level repo lists", () => {
it("should allow selection from repo lists defined at the system level", async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
repositoryList: "top_100",
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({
list1: ["foo/bar", "foo/baz"],
list2: [],
});
// Make the function call
const repoSelection = await getRepositorySelection();
// Check that the return value is correct
expect(repoSelection.repositories).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositoryLists).toEqual(["top_100"]);
});
});
describe("custom owner", () => {
// Test the owner regex in various "good" cases
const goodOwners = [
"owner",
"owner-with-hyphens",
"ownerWithNumbers58",
"owner_with_underscores",
"owner.with.periods.",
];
goodOwners.forEach((owner) => {
it(`should run on a valid owner that you enter in the text box: ${owner}`, async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
useAllReposOfOwner: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({}); // no pre-defined repo lists
showInputBoxSpy.mockResolvedValue(owner);
// Make the function call
const repoSelection = await getRepositorySelection();
// Check that the return value is correct
expect(repoSelection.repositories).toBeUndefined();
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toEqual([owner]);
});
});
// Test the owner regex in various "bad" cases
const badOwners = ["invalid&owner", "owner-with-repo/repo"];
badOwners.forEach((owner) => {
it(`should show an error message if you enter an invalid owner in the text box: ${owner}`, async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
useAllReposOfOwner: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({}); // no pre-defined repo lists
showInputBoxSpy.mockResolvedValue(owner);
// Function call should throw a UserCancellationException
await expect(getRepositorySelection()).rejects.toThrow(
`Invalid user or organization: ${owner}`,
);
});
});
it("should be ok for the user to change their mind", async () => {
quickPickSpy.mockResolvedValue({
useAllReposOfOwner: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({});
// The user pressed escape to cancel the operation
showInputBoxSpy.mockResolvedValue(undefined);
await expect(getRepositorySelection()).rejects.toThrow(
"No repositories selected",
);
await expect(getRepositorySelection()).rejects.toThrow(
UserCancellationException,
);
await expect(getRepositorySelection()).rejects.toHaveProperty(
"silent",
true,
);
});
});
describe("custom repo", () => {
// Test the repo regex in various "good" cases
const goodRepos = [
"owner/repo",
"owner_with.symbols-/repo.with-symbols_",
"ownerWithNumbers58/repoWithNumbers37",
];
goodRepos.forEach((repo) => {
it(`should run on a valid repo that you enter in the text box: ${repo}`, async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
useCustomRepo: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({}); // no pre-defined repo lists
showInputBoxSpy.mockResolvedValue(repo);
// Make the function call
const repoSelection = await getRepositorySelection();
// Check that the return value is correct
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toEqual([repo]);
});
});
// Test the repo regex in various "bad" cases
const badRepos = [
"invalid*owner/repo",
"owner/repo+some&invalid&stuff",
"owner-with-no-repo/",
"/repo-with-no-owner",
];
badRepos.forEach((repo) => {
it(`should show an error message if you enter an invalid repo in the text box: ${repo}`, async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
useCustomRepo: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({}); // no pre-defined repo lists
showInputBoxSpy.mockResolvedValue(repo);
// Function call should throw a UserCancellationException
await expect(getRepositorySelection()).rejects.toThrow(
"Invalid repository format",
);
await expect(getRepositorySelection()).rejects.toThrow(
UserCancellationException,
);
});
});
it("should be ok for the user to change their mind", async () => {
quickPickSpy.mockResolvedValue({
useCustomRepo: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({});
// The user pressed escape to cancel the operation
showInputBoxSpy.mockResolvedValue(undefined);
await expect(getRepositorySelection()).rejects.toThrow(
"No repositories selected",
);
await expect(getRepositorySelection()).rejects.toThrow(
UserCancellationException,
);
await expect(getRepositorySelection()).rejects.toHaveProperty(
"silent",
true,
);
});
});
describe("external repository lists file", () => {
let directory: DirectoryResult;
beforeEach(async () => {
directory = await tmp.dir({
unsafeCleanup: true,
});
});
afterEach(async () => {
await directory.cleanup();
});
it("should fail if path does not exist", async () => {
const nonExistingFile = join(directory.path, "non-existing-file.json");
getRemoteRepositoryListsPathSpy.mockReturnValue(nonExistingFile);
await expect(getRepositorySelection()).rejects.toThrow(
`External repository lists file does not exist at ${nonExistingFile}`,
);
});
it("should fail if path points to directory", async () => {
const existingDirectory = join(directory.path, "directory");
await ensureDir(existingDirectory);
getRemoteRepositoryListsPathSpy.mockReturnValue(existingDirectory);
await expect(getRepositorySelection()).rejects.toThrow(
"External repository lists path should not point to a directory",
);
});
it("should fail if file does not have valid JSON", async () => {
const existingFile = join(directory.path, "repository-lists.json");
await writeFile(existingFile, "not-json");
getRemoteRepositoryListsPathSpy.mockReturnValue(existingFile);
await expect(getRepositorySelection()).rejects.toThrow(
"Invalid repository lists file. It should contain valid JSON.",
);
});
it("should fail if file contains array", async () => {
const existingFile = join(directory.path, "repository-lists.json");
await writeJson(existingFile, []);
getRemoteRepositoryListsPathSpy.mockReturnValue(existingFile);
await expect(getRepositorySelection()).rejects.toThrow(
"Invalid repository lists file. It should be an object mapping names to a list of repositories.",
);
});
it("should fail if file does not contain repo lists in the right format", async () => {
const existingFile = join(directory.path, "repository-lists.json");
const repoLists = {
list1: "owner1/repo1",
};
await writeJson(existingFile, repoLists);
getRemoteRepositoryListsPathSpy.mockReturnValue(existingFile);
await expect(getRepositorySelection()).rejects.toThrow(
"Invalid repository lists file. It should contain an array of repositories for each list.",
);
});
it("should get repo lists from file", async () => {
const existingFile = join(directory.path, "repository-lists.json");
const repoLists = {
list1: ["owner1/repo1", "owner2/repo2"],
list2: ["owner3/repo3"],
};
await writeJson(existingFile, repoLists);
getRemoteRepositoryListsPathSpy.mockReturnValue(existingFile);
getRemoteRepositoryListsSpy.mockReturnValue({
list3: ["onwer4/repo4"],
list4: [],
});
quickPickSpy.mockResolvedValue({
repositories: ["owner3/repo3"],
} as unknown as QuickPickItem);
const repoSelection = await getRepositorySelection();
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toEqual(["owner3/repo3"]);
});
});
it("should allow the user to cancel", async () => {
// Fake return values
quickPickSpy.mockResolvedValue(undefined);
await expect(getRepositorySelection()).rejects.toThrow(
"No repositories selected",
);
await expect(getRepositorySelection()).rejects.toThrow(
UserCancellationException,
);
await expect(getRepositorySelection()).rejects.toHaveProperty(
"silent",
true,
);
});
await expect(getRepositorySelection(dbManager)).rejects.toThrow(
"Local databases and lists are not supported yet.",
);
});
it("should log an error when an empty remote user defined list is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteUserDefinedList,
repos: [] as RemoteRepoDbItem[],
} as DbItem);
await expect(getRepositorySelection(dbManager)).rejects.toThrow(
"The selected repository list is empty. Please add repositories to it before running a variant analysis.",
);
});
it("should return correct selection when remote system defined list is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteSystemDefinedList,
listName: "top_10",
} as DbItem);
const repoSelection = await getRepositorySelection(dbManager);
expect(repoSelection.repositoryLists).toEqual(["top_10"]);
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toBeUndefined();
});
it("should return correct selection when remote user defined list is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteUserDefinedList,
repos: [
{ repoFullName: "owner1/repo1" },
{ repoFullName: "owner1/repo2" },
],
} as DbItem);
const repoSelection = await getRepositorySelection(dbManager);
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toEqual([
"owner1/repo1",
"owner1/repo2",
]);
});
it("should return correct selection when remote owner is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteOwner,
ownerName: "owner2",
} as DbItem);
const repoSelection = await getRepositorySelection(dbManager);
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toEqual(["owner2"]);
expect(repoSelection.repositories).toBeUndefined();
});
it("should return correct selection when remote repo is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteRepo,
repoFullName: "owner1/repo2",
} as DbItem);
const repoSelection = await getRepositorySelection(dbManager);
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toEqual(["owner1/repo2"]);
});
function setUpDbManager(response: DbItem | undefined): DbManager {
return {
getSelectedDbItem: jest.fn(() => {
return response;
}),
} as any as DbManager;
}
});