Merge branch 'github:main' into main

This commit is contained in:
Taj
2022-12-16 17:21:43 +00:00
committed by GitHub
23 changed files with 743 additions and 249 deletions

View File

@@ -780,7 +780,7 @@
},
{
"command": "codeQLDatabasesExperimental.addNewList",
"when": "view == codeQLDatabasesExperimental",
"when": "view == codeQLDatabasesExperimental && codeQLDatabasesExperimental.configError == false",
"group": "navigation"
}
],

View File

@@ -1,10 +1,12 @@
import { Disposable } from "../pure/disposable-object";
import { AppEventEmitter } from "./events";
import { Logger } from "./logging";
export interface App {
createEventEmitter<T>(): AppEventEmitter<T>;
executeCommand(command: string, ...args: any): Thenable<void>;
mode: AppMode;
logger: Logger;
subscriptions: Disposable[];
extensionPath: string;
globalStoragePath: string;

View File

@@ -2,6 +2,7 @@ import * as vscode from "vscode";
import { Disposable } from "../../pure/disposable-object";
import { App, AppMode } from "../app";
import { AppEventEmitter } from "../events";
import { extLogger, Logger } from "../logging";
import { VSCodeAppEventEmitter } from "./events";
export class ExtensionApp implements App {
@@ -36,6 +37,10 @@ export class ExtensionApp implements App {
}
}
public get logger(): Logger {
return extLogger;
}
public createEventEmitter<T>(): AppEventEmitter<T> {
return new VSCodeAppEventEmitter<T>();
}

View File

@@ -1,4 +1,4 @@
import { pathExists, writeJSON, readJSON, readJSONSync } from "fs-extra";
import { pathExists, outputJSON, readJSON, readJSONSync } from "fs-extra";
import { join } from "path";
import {
cloneDbConfig,
@@ -9,9 +9,13 @@ import {
import * as chokidar from "chokidar";
import { DisposableObject, DisposeHandler } from "../../pure/disposable-object";
import { DbConfigValidator } from "./db-config-validator";
import { ValueResult } from "../../common/value-result";
import { App } from "../../common/app";
import { AppEvent, AppEventEmitter } from "../../common/events";
import {
DbConfigValidationError,
DbConfigValidationErrorKind,
} from "../db-validation-errors";
import { ValueResult } from "../../common/value-result";
export class DbConfigStore extends DisposableObject {
public readonly onDidChangeConfig: AppEvent<void>;
@@ -21,10 +25,10 @@ export class DbConfigStore extends DisposableObject {
private readonly configValidator: DbConfigValidator;
private config: DbConfig | undefined;
private configErrors: string[];
private configErrors: DbConfigValidationError[];
private configWatcher: chokidar.FSWatcher | undefined;
public constructor(app: App) {
public constructor(private readonly app: App) {
super();
const storagePath = app.workspaceStoragePath || app.globalStoragePath;
@@ -48,7 +52,7 @@ export class DbConfigStore extends DisposableObject {
this.configWatcher?.unwatch(this.configPath);
}
public getConfig(): ValueResult<DbConfig, string> {
public getConfig(): ValueResult<DbConfig, DbConfigValidationError> {
if (this.config) {
// Clone the config so that it's not modified outside of this class.
return ValueResult.ok(cloneDbConfig(this.config));
@@ -95,28 +99,45 @@ export class DbConfigStore extends DisposableObject {
throw Error("Cannot add remote list if config is not loaded");
}
if (this.doesRemoteListExist(listName)) {
throw Error(`A remote list with the name '${listName}' already exists`);
}
const config: DbConfig = cloneDbConfig(this.config);
config.databases.remote.repositoryLists.push({
name: listName,
repositories: [],
});
// TODO: validate that the name doesn't already exist
await this.writeConfig(config);
}
public doesRemoteListExist(listName: string): boolean {
if (!this.config) {
throw Error("Cannot check remote list existence if config is not loaded");
}
return this.config.databases.remote.repositoryLists.some(
(l) => l.name === listName,
);
}
private async writeConfig(config: DbConfig): Promise<void> {
await writeJSON(this.configPath, config, {
await outputJSON(this.configPath, config, {
spaces: 2,
});
}
private async loadConfig(): Promise<void> {
if (!(await pathExists(this.configPath))) {
void this.app.logger.log(
`Creating new database config file at ${this.configPath}`,
);
await this.writeConfig(this.createEmptyConfig());
}
await this.readConfig();
void this.app.logger.log(`Database config loaded from ${this.configPath}`);
}
private async readConfig(): Promise<void> {
@@ -124,14 +145,33 @@ export class DbConfigStore extends DisposableObject {
try {
newConfig = await readJSON(this.configPath);
} catch (e) {
this.configErrors = [`Failed to read config file: ${this.configPath}`];
this.configErrors = [
{
kind: DbConfigValidationErrorKind.InvalidJson,
message: `Failed to read config file: ${this.configPath}`,
},
];
}
if (newConfig) {
this.configErrors = this.configValidator.validate(newConfig);
}
this.config = this.configErrors.length === 0 ? newConfig : undefined;
if (this.configErrors.length === 0) {
this.config = newConfig;
await this.app.executeCommand(
"setContext",
"codeQLDatabasesExperimental.configError",
false,
);
} else {
this.config = undefined;
await this.app.executeCommand(
"setContext",
"codeQLDatabasesExperimental.configError",
true,
);
}
}
private readConfigSync(): void {
@@ -139,22 +179,51 @@ export class DbConfigStore extends DisposableObject {
try {
newConfig = readJSONSync(this.configPath);
} catch (e) {
this.configErrors = [`Failed to read config file: ${this.configPath}`];
this.configErrors = [
{
kind: DbConfigValidationErrorKind.InvalidJson,
message: `Failed to read config file: ${this.configPath}`,
},
];
}
if (newConfig) {
this.configErrors = this.configValidator.validate(newConfig);
}
this.config = this.configErrors.length === 0 ? newConfig : undefined;
if (this.configErrors.length === 0) {
this.config = newConfig;
void this.app.executeCommand(
"setContext",
"codeQLDatabasesExperimental.configError",
false,
);
} else {
this.config = undefined;
void this.app.executeCommand(
"setContext",
"codeQLDatabasesExperimental.configError",
true,
);
}
this.onDidChangeConfigEventEmitter.fire();
}
private watchConfig(): void {
this.configWatcher = chokidar.watch(this.configPath).on("change", () => {
this.readConfigSync();
});
this.configWatcher = chokidar
.watch(this.configPath, {
// In some cases, change events are emitted while the file is still
// being written. The awaitWriteFinish option tells the watcher to
// poll the file size, holding its add and change events until the size
// does not change for a configurable amount of time. We set that time
// to 1 second, but it may need to be adjusted if there are issues.
awaitWriteFinish: {
stabilityThreshold: 1000,
},
})
.on("change", () => {
this.readConfigSync();
});
}
private createEmptyConfig(): DbConfig {

View File

@@ -2,6 +2,11 @@ import { readJsonSync } from "fs-extra";
import { resolve } from "path";
import Ajv from "ajv";
import { DbConfig } from "./db-config";
import { findDuplicateStrings } from "../../text-utils";
import {
DbConfigValidationError,
DbConfigValidationErrorKind,
} from "../db-validation-errors";
export class DbConfigValidator {
private readonly schema: any;
@@ -14,16 +19,118 @@ export class DbConfigValidator {
this.schema = readJsonSync(schemaPath);
}
public validate(dbConfig: DbConfig): string[] {
public validate(dbConfig: DbConfig): DbConfigValidationError[] {
const ajv = new Ajv({ allErrors: true });
ajv.validate(this.schema, dbConfig);
if (ajv.errors) {
return ajv.errors.map(
(error) => `${error.instancePath} ${error.message}`,
);
return ajv.errors.map((error) => ({
kind: DbConfigValidationErrorKind.InvalidConfig,
message: `${error.instancePath} ${error.message}`,
}));
}
return [];
return [
...this.validateDbListNames(dbConfig),
...this.validateDbNames(dbConfig),
...this.validateDbNamesInLists(dbConfig),
...this.validateOwners(dbConfig),
];
}
private validateDbListNames(dbConfig: DbConfig): DbConfigValidationError[] {
const errors: DbConfigValidationError[] = [];
const buildError = (dups: string[]) => ({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: `There are database lists with the same name: ${dups.join(
", ",
)}`,
});
const duplicateLocalDbLists = findDuplicateStrings(
dbConfig.databases.local.lists.map((n) => n.name),
);
if (duplicateLocalDbLists.length > 0) {
errors.push(buildError(duplicateLocalDbLists));
}
const duplicateRemoteDbLists = findDuplicateStrings(
dbConfig.databases.remote.repositoryLists.map((n) => n.name),
);
if (duplicateRemoteDbLists.length > 0) {
errors.push(buildError(duplicateRemoteDbLists));
}
return errors;
}
private validateDbNames(dbConfig: DbConfig): DbConfigValidationError[] {
const errors: DbConfigValidationError[] = [];
const buildError = (dups: string[]) => ({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: `There are databases with the same name: ${dups.join(", ")}`,
});
const duplicateLocalDbs = findDuplicateStrings(
dbConfig.databases.local.databases.map((d) => d.name),
);
if (duplicateLocalDbs.length > 0) {
errors.push(buildError(duplicateLocalDbs));
}
const duplicateRemoteDbs = findDuplicateStrings(
dbConfig.databases.remote.repositories,
);
if (duplicateRemoteDbs.length > 0) {
errors.push(buildError(duplicateRemoteDbs));
}
return errors;
}
private validateDbNamesInLists(
dbConfig: DbConfig,
): DbConfigValidationError[] {
const errors: DbConfigValidationError[] = [];
const buildError = (listName: string, dups: string[]) => ({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: `There are databases with the same name in the ${listName} list: ${dups.join(
", ",
)}`,
});
for (const list of dbConfig.databases.local.lists) {
const dups = findDuplicateStrings(list.databases.map((d) => d.name));
if (dups.length > 0) {
errors.push(buildError(list.name, dups));
}
}
for (const list of dbConfig.databases.remote.repositoryLists) {
const dups = findDuplicateStrings(list.repositories);
if (dups.length > 0) {
errors.push(buildError(list.name, dups));
}
}
return errors;
}
private validateOwners(dbConfig: DbConfig): DbConfigValidationError[] {
const errors: DbConfigValidationError[] = [];
const dups = findDuplicateStrings(dbConfig.databases.remote.owners);
if (dups.length > 0) {
errors.push({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: `There are owners with the same name: ${dups.join(", ")}`,
});
}
return errors;
}
}

View File

@@ -9,6 +9,7 @@ import {
mapDbItemToSelectedDbItem,
} from "./db-item-selection";
import { createLocalTree, createRemoteTree } from "./db-tree-creator";
import { DbConfigValidationError } from "./db-validation-errors";
export class DbManager {
public readonly onDbItemsChanged: AppEvent<void>;
@@ -24,16 +25,16 @@ export class DbManager {
}
public getSelectedDbItem(): DbItem | undefined {
const dbItems = this.getDbItems();
const dbItemsResult = this.getDbItems();
if (dbItems.isFailure) {
if (dbItemsResult.errors.length > 0) {
return undefined;
}
return getSelectedDbItem(dbItems.value);
return getSelectedDbItem(dbItemsResult.value);
}
public getDbItems(): ValueResult<DbItem[], string> {
public getDbItems(): ValueResult<DbItem[], DbConfigValidationError> {
const configResult = this.dbConfigStore.getConfig();
if (configResult.isFailure) {
return ValueResult.fail(configResult.errors);
@@ -75,6 +76,14 @@ export class DbManager {
}
public async addNewRemoteList(listName: string): Promise<void> {
if (this.dbConfigStore.doesRemoteListExist(listName)) {
throw Error(`A list with the name '${listName}' already exists`);
}
await this.dbConfigStore.addRemoteList(listName);
}
public doesRemoteListExist(listName: string): boolean {
return this.dbConfigStore.doesRemoteListExist(listName);
}
}

View File

@@ -20,22 +20,25 @@ export class DbModule extends DisposableObject {
}
public static async initialize(app: App): Promise<DbModule | undefined> {
if (
isCanary() &&
isNewQueryRunExperienceEnabled() &&
app.mode === AppMode.Development
) {
if (DbModule.shouldEnableModule(app.mode)) {
const dbModule = new DbModule(app);
app.subscriptions.push(dbModule);
await dbModule.initialize();
return dbModule;
}
return undefined;
}
private static shouldEnableModule(app: AppMode): boolean {
if (app === AppMode.Development || app === AppMode.Test) {
return true;
}
return isCanary() && isNewQueryRunExperienceEnabled();
}
private async initialize(): Promise<void> {
void extLogger.log("Initializing database module");

View File

@@ -0,0 +1,10 @@
export enum DbConfigValidationErrorKind {
InvalidJson = "InvalidJson",
InvalidConfig = "InvalidConfig",
DuplicateNames = "DuplicateNames",
}
export interface DbConfigValidationError {
kind: DbConfigValidationErrorKind;
message: string;
}

View File

@@ -1,5 +1,6 @@
import { TreeViewExpansionEvent, window, workspace } from "vscode";
import { commandRunner } from "../../commandRunner";
import { showAndLogErrorMessage } from "../../helpers";
import { DisposableObject } from "../../pure/disposable-object";
import { DbManager } from "../db-manager";
import { DbTreeDataProvider } from "./db-tree-data-provider";
@@ -58,7 +59,6 @@ export class DbPanel extends DisposableObject {
}
private async addNewRemoteList(): Promise<void> {
// TODO: check that config exists *before* showing the input box
const listName = await window.showInputBox({
prompt: "Enter a name for the new list",
placeHolder: "example-list",
@@ -66,7 +66,14 @@ export class DbPanel extends DisposableObject {
if (listName === undefined) {
return;
}
await this.dbManager.addNewRemoteList(listName);
if (this.dbManager.doesRemoteListExist(listName)) {
void showAndLogErrorMessage(
`A list with the name '${listName}' already exists`,
);
} else {
await this.dbManager.addNewRemoteList(listName);
}
}
private async setSelectedItem(treeViewItem: DbTreeViewItem): Promise<void> {

View File

@@ -9,6 +9,10 @@ import { createDbTreeViewItemError, DbTreeViewItem } from "./db-tree-view-item";
import { DbManager } from "../db-manager";
import { mapDbItemToTreeViewItem } from "./db-item-mapper";
import { DisposableObject } from "../../pure/disposable-object";
import {
DbConfigValidationError,
DbConfigValidationErrorKind,
} from "../db-validation-errors";
export class DbTreeDataProvider
extends DisposableObject
@@ -61,14 +65,34 @@ export class DbTreeDataProvider
const dbItemsResult = this.dbManager.getDbItems();
if (dbItemsResult.isFailure) {
return this.createErrorItems(dbItemsResult.errors);
}
return dbItemsResult.value.map(mapDbItemToTreeViewItem);
}
private createErrorItems(
errors: DbConfigValidationError[],
): DbTreeViewItem[] {
if (
errors.some(
(e) =>
e.kind === DbConfigValidationErrorKind.InvalidJson ||
e.kind === DbConfigValidationErrorKind.InvalidConfig,
)
) {
const errorTreeViewItem = createDbTreeViewItemError(
"Error when reading databases config",
"Please open your databases config and address errors",
);
return [errorTreeViewItem];
} else {
return errors
.filter((e) => e.kind === DbConfigValidationErrorKind.DuplicateNames)
.map((e) =>
createDbTreeViewItemError(e.message, "Please remove duplicates"),
);
}
return dbItemsResult.value.map(mapDbItemToTreeViewItem);
}
}

View File

@@ -8,6 +8,7 @@ import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAna
import { VariantAnalysisAnalyzedRepos } from "../../view/variant-analysis/VariantAnalysisAnalyzedRepos";
import {
VariantAnalysisRepoStatus,
VariantAnalysisScannedRepositoryDownloadStatus,
VariantAnalysisStatus,
} from "../../remote-queries/shared/variant-analysis";
import { AnalysisAlert } from "../../remote-queries/shared/analysis-result";
@@ -148,8 +149,8 @@ const manyScannedRepos = Array.from({ length: 1000 }, (_, i) => {
};
});
export const PerformanceExample = Template.bind({});
PerformanceExample.args = {
export const ManyRepositoriesPerformanceExample = Template.bind({});
ManyRepositoriesPerformanceExample.args = {
variantAnalysis: {
...createMockVariantAnalysis({
status: VariantAnalysisStatus.Succeeded,
@@ -163,3 +164,39 @@ PerformanceExample.args = {
interpretedResults: interpretedResultsForRepo("facebook/create-react-app"),
})),
};
const mockAnalysisAlert = interpretedResultsForRepo(
"facebook/create-react-app",
)![0];
const performanceNumbers = [10, 50, 100, 500, 1000, 2000, 5000, 10_000];
export const ManyResultsPerformanceExample = Template.bind({});
ManyResultsPerformanceExample.args = {
variantAnalysis: {
...createMockVariantAnalysis({
status: VariantAnalysisStatus.Succeeded,
scannedRepos: performanceNumbers.map((resultCount, i) => ({
repository: {
...createMockRepositoryWithMetadata(),
id: resultCount,
fullName: `octodemo/${i}-${resultCount}-results`,
},
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
resultCount,
})),
}),
id: 1,
},
repositoryStates: performanceNumbers.map((resultCount) => ({
repositoryId: resultCount,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
})),
repositoryResults: performanceNumbers.map((resultCount) => ({
variantAnalysisId: 1,
repositoryId: resultCount,
interpretedResults: Array.from({ length: resultCount }, (_, i) => ({
...mockAnalysisAlert,
})),
})),
};

View File

@@ -31,3 +31,11 @@ export function convertNonPrintableChars(label: string | undefined) {
return convertedLabelArray.join("");
}
}
export function findDuplicateStrings(strings: string[]): string[] {
const dups = strings.filter(
(string, index, strings) => strings.indexOf(string) !== index,
);
return [...new Set(dups)];
}

View File

@@ -0,0 +1,36 @@
import { commands, extensions, window } from "vscode";
import { CodeQLExtensionInterface } from "../../../extension";
import { readJson } from "fs-extra";
import * as path from "path";
import { DbConfig } from "../../../databases/config/db-config";
jest.setTimeout(60_000);
describe("Db panel UI commands", () => {
let extension: CodeQLExtensionInterface | Record<string, never>;
let storagePath: string;
beforeEach(async () => {
extension = await extensions
.getExtension<CodeQLExtensionInterface | Record<string, never>>(
"GitHub.vscode-codeql",
)!
.activate();
storagePath =
extension.ctx.storageUri?.fsPath || extension.ctx.globalStorageUri.fsPath;
});
it("should add new remote db list", async () => {
// Add db list
jest.spyOn(window, "showInputBox").mockResolvedValue("my-list-1");
await commands.executeCommand("codeQLDatabasesExperimental.addNewList");
// Check db config
const dbConfigFilePath = path.join(storagePath, "workspace-databases.json");
const dbConfig: DbConfig = await readJson(dbConfigFilePath);
expect(dbConfig.databases.remote.repositoryLists).toHaveLength(1);
expect(dbConfig.databases.remote.repositoryLists[0].name).toBe("my-list-1");
});
});

View File

@@ -1,3 +1,4 @@
import { faker } from "@faker-js/faker";
import {
DbConfig,
ExpandedDbItem,
@@ -5,7 +6,7 @@ import {
LocalList,
RemoteRepositoryList,
SelectedDbItem,
} from "../../src/databases/config/db-config";
} from "../../databases/config/db-config";
export function createDbConfig({
remoteLists = [],
@@ -40,3 +41,22 @@ export function createDbConfig({
selected,
};
}
export function createLocalDbConfigItem({
name = `database${faker.datatype.number()}`,
dateAdded = faker.date.past().getTime(),
language = `language${faker.datatype.number()}`,
storagePath = `storagePath${faker.datatype.number()}`,
}: {
name?: string;
dateAdded?: number;
language?: string;
storagePath?: string;
} = {}): LocalDatabase {
return {
name,
dateAdded,
language,
storagePath,
};
}

View File

@@ -1,4 +1,4 @@
import { TreeItemCollapsibleState, ThemeIcon } from "vscode";
import { TreeItemCollapsibleState, ThemeIcon, ThemeColor } from "vscode";
import { join } from "path";
import { ensureDir, readJSON, remove, writeJson } from "fs-extra";
import {
@@ -12,6 +12,7 @@ import { DbItemKind, LocalDatabaseDbItem } from "../../../databases/db-item";
import { DbTreeViewItem } from "../../../databases/ui/db-tree-view-item";
import { ExtensionApp } from "../../../common/vscode/vscode-app";
import { createMockExtensionContext } from "../../factories/extension-context";
import { createDbConfig } from "../../factories/db-config-factories";
describe("db panel", () => {
const workspaceStoragePath = join(__dirname, "test-workspace-storage");
@@ -48,20 +49,7 @@ describe("db panel", () => {
});
it("should render default local and remote nodes when the config is empty", async () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [],
owners: [],
repositories: [],
},
local: {
lists: [],
databases: [],
},
},
expanded: [],
};
const dbConfig: DbConfig = createDbConfig();
await saveDbConfig(dbConfig);
@@ -103,29 +91,18 @@ describe("db panel", () => {
});
it("should render remote repository list nodes", async () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-2",
repositories: ["owner1/repo1", "owner2/repo1", "owner2/repo2"],
},
],
owners: [],
repositories: [],
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
local: {
lists: [],
databases: [],
{
name: "my-list-2",
repositories: ["owner1/repo1", "owner2/repo1", "owner2/repo2"],
},
},
expanded: [],
};
],
});
await saveDbConfig(dbConfig);
@@ -164,20 +141,9 @@ describe("db panel", () => {
});
it("should render owner list nodes", async () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [],
owners: ["owner1", "owner2"],
repositories: [],
},
local: {
lists: [],
databases: [],
},
},
expanded: [],
};
const dbConfig: DbConfig = createDbConfig({
remoteOwners: ["owner1", "owner2"],
});
await saveDbConfig(dbConfig);
@@ -204,20 +170,9 @@ describe("db panel", () => {
});
it("should render repository nodes", async () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [],
owners: [],
repositories: ["owner1/repo1", "owner1/repo2"],
},
local: {
lists: [],
databases: [],
},
},
expanded: [],
};
const dbConfig: DbConfig = createDbConfig({
remoteRepos: ["owner1/repo1", "owner1/repo2"],
});
await saveDbConfig(dbConfig);
@@ -244,49 +199,38 @@ describe("db panel", () => {
});
it("should render local list nodes", async () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [],
owners: [],
repositories: [],
},
local: {
lists: [
const dbConfig: DbConfig = createDbConfig({
localLists: [
{
name: "my-list-1",
databases: [
{
name: "my-list-1",
databases: [
{
name: "db1",
dateAdded: 1668428293677,
language: "cpp",
storagePath: "/path/to/db1/",
},
{
name: "db2",
dateAdded: 1668428472731,
language: "cpp",
storagePath: "/path/to/db2/",
},
],
name: "db1",
dateAdded: 1668428293677,
language: "cpp",
storagePath: "/path/to/db1/",
},
{
name: "my-list-2",
databases: [
{
name: "db3",
dateAdded: 1668428472731,
language: "ruby",
storagePath: "/path/to/db3/",
},
],
name: "db2",
dateAdded: 1668428472731,
language: "cpp",
storagePath: "/path/to/db2/",
},
],
databases: [],
},
},
expanded: [],
};
{
name: "my-list-2",
databases: [
{
name: "db3",
dateAdded: 1668428472731,
language: "ruby",
storagePath: "/path/to/db3/",
},
],
},
],
});
await saveDbConfig(dbConfig);
@@ -339,33 +283,22 @@ describe("db panel", () => {
});
it("should render local database nodes", async () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [],
owners: [],
repositories: [],
const dbConfig: DbConfig = createDbConfig({
localDbs: [
{
name: "db1",
dateAdded: 1668428293677,
language: "csharp",
storagePath: "/path/to/db1/",
},
local: {
lists: [],
databases: [
{
name: "db1",
dateAdded: 1668428293677,
language: "csharp",
storagePath: "/path/to/db1/",
},
{
name: "db2",
dateAdded: 1668428472731,
language: "go",
storagePath: "/path/to/db2/",
},
],
{
name: "db2",
dateAdded: 1668428472731,
language: "go",
storagePath: "/path/to/db2/",
},
},
expanded: [],
};
],
});
await saveDbConfig(dbConfig);
@@ -406,33 +339,22 @@ describe("db panel", () => {
});
it("should mark selected remote db list as selected", async () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-2",
repositories: ["owner2/repo1", "owner2/repo2"],
},
],
owners: [],
repositories: [],
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
local: {
lists: [],
databases: [],
{
name: "my-list-2",
repositories: ["owner2/repo1", "owner2/repo2"],
},
},
expanded: [],
],
selected: {
kind: SelectedDbItemKind.RemoteUserDefinedList,
listName: "my-list-2",
},
};
});
await saveDbConfig(dbConfig);
@@ -463,34 +385,24 @@ describe("db panel", () => {
});
it("should mark selected remote db inside list as selected", async () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-2",
repositories: ["owner1/repo1", "owner2/repo2"],
},
],
owners: [],
repositories: ["owner1/repo1"],
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
local: {
lists: [],
databases: [],
{
name: "my-list-2",
repositories: ["owner1/repo1", "owner2/repo2"],
},
},
expanded: [],
],
remoteRepos: ["owner1/repo1"],
selected: {
kind: SelectedDbItemKind.RemoteRepository,
repositoryName: "owner1/repo1",
listName: "my-list-2",
},
};
});
await saveDbConfig(dbConfig);
@@ -532,29 +444,18 @@ describe("db panel", () => {
});
it("should add a new list to the remote db list", async () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
],
owners: [],
repositories: [],
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
local: {
lists: [],
databases: [],
},
},
expanded: [],
],
selected: {
kind: SelectedDbItemKind.RemoteUserDefinedList,
listName: "my-list-1",
},
};
});
await saveDbConfig(dbConfig);
@@ -591,6 +492,63 @@ describe("db panel", () => {
});
});
it("should show error for invalid config", async () => {
// We're intentionally bypassing the type check because we'd
// like to make sure validation errors are highlighted.
const dbConfig = {
databases: {},
} as any as DbConfig;
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
const items = dbTreeItems!;
expect(items.length).toBe(1);
checkErrorItem(
items[0],
"Error when reading databases config",
"Please open your databases config and address errors",
);
});
it("should show errors for duplicate names", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner2/repo2"],
},
],
remoteRepos: ["owner1/repo1", "owner1/repo1"],
});
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
const items = dbTreeItems!;
expect(items.length).toBe(2);
checkErrorItem(
items[0],
"There are database lists with the same name: my-list-1",
"Please remove duplicates",
);
checkErrorItem(
items[1],
"There are databases with the same name: owner1/repo1",
"Please remove duplicates",
);
});
async function saveDbConfig(dbConfig: DbConfig): Promise<void> {
await writeJson(dbConfigFilePath, dbConfig);
@@ -672,6 +630,21 @@ describe("db panel", () => {
expect(item.collapsibleState).toBe(TreeItemCollapsibleState.None);
}
function checkErrorItem(
item: DbTreeViewItem,
label: string,
tooltip: string,
): void {
expect(item.dbItem).toBe(undefined);
expect(item.iconPath).toEqual(
new ThemeIcon("error", new ThemeColor("problemsErrorIcon.foreground")),
);
expect(item.label).toBe(label);
expect(item.tooltip).toBe(tooltip);
expect(item.collapsibleState).toBe(TreeItemCollapsibleState.None);
expect(item.children.length).toBe(0);
}
function isTreeViewItemSelectable(treeViewItem: DbTreeViewItem) {
return (
treeViewItem.resourceUri === undefined &&

View File

@@ -1,6 +1,7 @@
import { App, AppMode } from "../../src/common/app";
import { AppEvent, AppEventEmitter } from "../../src/common/events";
import { Disposable } from "../../src/pure/disposable-object";
import { createMockLogger } from "./loggerMock";
export function createMockApp({
extensionPath = "/mock/extension/path",
@@ -17,6 +18,7 @@ export function createMockApp({
}): App {
return {
mode: AppMode.Test,
logger: createMockLogger(),
subscriptions: [],
extensionPath,
workspaceStoragePath,

View File

@@ -0,0 +1,9 @@
import { Logger } from "../../src/common";
export function createMockLogger(): Logger {
return {
log: jest.fn(() => Promise.resolve()),
show: jest.fn(),
removeAdditionalLogLocation: jest.fn(),
};
}

View File

@@ -128,4 +128,39 @@ describe("db config store", () => {
configStore.dispose();
});
it("should set codeQLDatabasesExperimental.configError to true when config has error", async () => {
const testDataStoragePathInvalid = join(__dirname, "data", "invalid");
const app = createMockApp({
extensionPath,
workspaceStoragePath: testDataStoragePathInvalid,
});
const configStore = new DbConfigStore(app);
await configStore.initialize();
expect(app.executeCommand).toBeCalledWith(
"setContext",
"codeQLDatabasesExperimental.configError",
true,
);
configStore.dispose();
});
it("should set codeQLDatabasesExperimental.configError to false when config is valid", async () => {
const app = createMockApp({
extensionPath,
workspaceStoragePath: testDataStoragePath,
});
const configStore = new DbConfigStore(app);
await configStore.initialize();
expect(app.executeCommand).toBeCalledWith(
"setContext",
"codeQLDatabasesExperimental.configError",
false,
);
configStore.dispose();
});
});

View File

@@ -1,6 +1,11 @@
import { join } from "path";
import { DbConfig } from "../../../../src/databases/config/db-config";
import { DbConfigValidator } from "../../../../src/databases/config/db-config-validator";
import { DbConfigValidationErrorKind } from "../../../../src/databases/db-validation-errors";
import {
createDbConfig,
createLocalDbConfigItem,
} from "../../../../src/vscode-tests/factories/db-config-factories";
describe("db config validation", () => {
const extensionPath = join(__dirname, "../../../..");
@@ -29,14 +34,139 @@ describe("db config validation", () => {
expect(validationOutput).toHaveLength(3);
expect(validationOutput[0]).toEqual(
"/databases must have required property 'local'",
);
expect(validationOutput[1]).toEqual(
"/databases/remote must have required property 'owners'",
);
expect(validationOutput[2]).toEqual(
"/databases/remote must NOT have additional properties",
);
expect(validationOutput[0]).toEqual({
kind: DbConfigValidationErrorKind.InvalidConfig,
message: "/databases must have required property 'local'",
});
expect(validationOutput[1]).toEqual({
kind: DbConfigValidationErrorKind.InvalidConfig,
message: "/databases/remote must have required property 'owners'",
});
expect(validationOutput[2]).toEqual({
kind: DbConfigValidationErrorKind.InvalidConfig,
message: "/databases/remote must NOT have additional properties",
});
});
it("should return error when there are multiple remote db lists with the same name", async () => {
const dbConfig = createDbConfig({
remoteLists: [
{
name: "repoList1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "repoList1",
repositories: ["owner2/repo1", "owner2/repo2"],
},
],
});
const validationOutput = configValidator.validate(dbConfig);
expect(validationOutput).toHaveLength(1);
expect(validationOutput[0]).toEqual({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: "There are database lists with the same name: repoList1",
});
});
it("should return error when there are multiple remote dbs with the same name", async () => {
const dbConfig = createDbConfig({
remoteRepos: ["owner1/repo1", "owner1/repo2", "owner1/repo2"],
});
const validationOutput = configValidator.validate(dbConfig);
expect(validationOutput).toHaveLength(1);
expect(validationOutput[0]).toEqual({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: "There are databases with the same name: owner1/repo2",
});
});
it("should return error when there are multiple remote dbs with the same name in the same list", async () => {
const dbConfig = createDbConfig({
remoteLists: [
{
name: "repoList1",
repositories: ["owner1/repo1", "owner1/repo2", "owner1/repo2"],
},
],
});
const validationOutput = configValidator.validate(dbConfig);
expect(validationOutput).toHaveLength(1);
expect(validationOutput[0]).toEqual({
kind: DbConfigValidationErrorKind.DuplicateNames,
message:
"There are databases with the same name in the repoList1 list: owner1/repo2",
});
});
it("should return error when there are multiple local db lists with the same name", async () => {
const dbConfig = createDbConfig({
localLists: [
{
name: "dbList1",
databases: [createLocalDbConfigItem()],
},
{
name: "dbList1",
databases: [createLocalDbConfigItem()],
},
],
});
const validationOutput = configValidator.validate(dbConfig);
expect(validationOutput).toHaveLength(1);
expect(validationOutput[0]).toEqual({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: "There are database lists with the same name: dbList1",
});
});
it("should return error when there are multiple local dbs with the same name", async () => {
const dbConfig = createDbConfig({
localDbs: [
createLocalDbConfigItem({ name: "db1" }),
createLocalDbConfigItem({ name: "db2" }),
createLocalDbConfigItem({ name: "db1" }),
],
});
const validationOutput = configValidator.validate(dbConfig);
expect(validationOutput).toHaveLength(1);
expect(validationOutput[0]).toEqual({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: "There are databases with the same name: db1",
});
});
it("should return error when there are multiple local dbs with the same name in the same list", async () => {
const dbConfig = createDbConfig({
localLists: [
{
name: "dbList1",
databases: [
createLocalDbConfigItem({ name: "db1" }),
createLocalDbConfigItem({ name: "db2" }),
createLocalDbConfigItem({ name: "db1" }),
],
},
],
});
const validationOutput = configValidator.validate(dbConfig);
expect(validationOutput).toHaveLength(1);
expect(validationOutput[0]).toEqual({
kind: DbConfigValidationErrorKind.DuplicateNames,
message:
"There are databases with the same name in the dbList1 list: db1",
});
});
});

View File

@@ -13,7 +13,7 @@ import {
createLocalTree,
createRemoteTree,
} from "../../../src/databases/db-tree-creator";
import { createDbConfig } from "../../factories/db-config-factories";
import { createDbConfig } from "../../../src/vscode-tests/factories/db-config-factories";
describe("db tree creator", () => {
describe("createRemoteTree", () => {
@@ -103,20 +103,9 @@ describe("db tree creator", () => {
});
it("should create remote owner nodes", () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [],
owners: ["owner1", "owner2"],
repositories: [],
},
local: {
lists: [],
databases: [],
},
},
expanded: [],
};
const dbConfig: DbConfig = createDbConfig({
remoteOwners: ["owner1", "owner2"],
});
const dbTreeRoot = createRemoteTree(dbConfig);

View File

@@ -0,0 +1,15 @@
import { findDuplicateStrings } from "../../src/text-utils";
describe("findDuplicateStrings", () => {
it("should find duplicates strings in an array of strings", () => {
const strings = ["a", "b", "c", "a", "aa", "bb"];
const duplicates = findDuplicateStrings(strings);
expect(duplicates).toEqual(["a"]);
});
it("should not find duplicates strings if there aren't any", () => {
const strings = ["a", "b", "c", "aa", "bb"];
const duplicates = findDuplicateStrings(strings);
expect(duplicates).toEqual([]);
});
});

View File

@@ -1,6 +1,9 @@
{
"extends": "../tsconfig.json",
"include": ["**/*.ts"],
"include": [
"**/*.ts",
"../src/vscode-tests/factories/db-config-factories.ts"
],
"exclude": [],
"compilerOptions": {
"noEmit": true,