Merge branch 'github:main' into main
This commit is contained in:
@@ -780,7 +780,7 @@
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabasesExperimental.addNewList",
|
||||
"when": "view == codeQLDatabasesExperimental",
|
||||
"when": "view == codeQLDatabasesExperimental && codeQLDatabasesExperimental.configError == false",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
10
extensions/ql-vscode/src/databases/db-validation-errors.ts
Normal file
10
extensions/ql-vscode/src/databases/db-validation-errors.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export enum DbConfigValidationErrorKind {
|
||||
InvalidJson = "InvalidJson",
|
||||
InvalidConfig = "InvalidConfig",
|
||||
DuplicateNames = "DuplicateNames",
|
||||
}
|
||||
|
||||
export interface DbConfigValidationError {
|
||||
kind: DbConfigValidationErrorKind;
|
||||
message: string;
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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,
|
||||
|
||||
9
extensions/ql-vscode/test/__mocks__/loggerMock.ts
Normal file
9
extensions/ql-vscode/test/__mocks__/loggerMock.ts
Normal 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(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
15
extensions/ql-vscode/test/pure-tests/text-utils.test.ts
Normal file
15
extensions/ql-vscode/test/pure-tests/text-utils.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["**/*.ts"],
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../src/vscode-tests/factories/db-config-factories.ts"
|
||||
],
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
|
||||
Reference in New Issue
Block a user