Merge remote-tracking branch 'origin/main' into koesie10/variant-analysis-remove-item-tests
This commit is contained in:
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
cp dist/*.vsix artifacts
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
with:
|
||||
name: vscode-codeql-extension
|
||||
|
||||
29
.vscode/settings.json
vendored
29
.vscode/settings.json
vendored
@@ -42,13 +42,28 @@
|
||||
"LANG": "en-US",
|
||||
"TZ": "UTC"
|
||||
},
|
||||
// Uncomment to debug integration tests
|
||||
// "jestrunner.debugOptions": {
|
||||
// "attachSimplePort": 9223,
|
||||
// "env": {
|
||||
// "VSCODE_WAIT_FOR_DEBUGGER": "true",
|
||||
// }
|
||||
// },
|
||||
"jestrunner.debugOptions": {
|
||||
// Uncomment to debug integration tests
|
||||
// "attachSimplePort": 9223,
|
||||
"env": {
|
||||
"LANG": "en-US",
|
||||
"TZ": "UTC",
|
||||
// Uncomment to debug integration tests
|
||||
// "VSCODE_WAIT_FOR_DEBUGGER": "true",
|
||||
}
|
||||
},
|
||||
"terminal.integrated.env.linux": {
|
||||
"LANG": "en-US",
|
||||
"TZ": "UTC"
|
||||
},
|
||||
"terminal.integrated.env.osx": {
|
||||
"LANG": "en-US",
|
||||
"TZ": "UTC"
|
||||
},
|
||||
"terminal.integrated.env.windows": {
|
||||
"LANG": "en-US",
|
||||
"TZ": "UTC"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
|
||||
130
extensions/ql-vscode/package-lock.json
generated
130
extensions/ql-vscode/package-lock.json
generated
@@ -105,6 +105,7 @@
|
||||
"ansi-colors": "^4.1.1",
|
||||
"applicationinsights": "^2.3.5",
|
||||
"babel-loader": "^8.2.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "~3.1.0",
|
||||
"del": "^6.0.0",
|
||||
"eslint": "^8.23.1",
|
||||
@@ -18780,6 +18781,83 @@
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "src/bin/cross-env.js",
|
||||
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-env/node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-env/node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-env/node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-env/node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-env/node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",
|
||||
@@ -55164,6 +55242,58 @@
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",
|
||||
|
||||
@@ -1286,7 +1286,7 @@
|
||||
"watch:webpack": "gulp watchView",
|
||||
"watch:files": "gulp watchTestData",
|
||||
"test": "npm-run-all -p test:*",
|
||||
"test:unit": "jest --projects test",
|
||||
"test:unit": "cross-env TZ=UTC LANG=en-US jest --projects test",
|
||||
"test:view": "jest --projects src/view",
|
||||
"integration": "npm-run-all integration:*",
|
||||
"integration:no-workspace": "jest --projects src/vscode-tests/no-workspace",
|
||||
@@ -1397,6 +1397,7 @@
|
||||
"ansi-colors": "^4.1.1",
|
||||
"applicationinsights": "^2.3.5",
|
||||
"babel-loader": "^8.2.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "~3.1.0",
|
||||
"del": "^6.0.0",
|
||||
"eslint": "^8.23.1",
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
diff --git a/node_modules/jest-runner-vscode/dist/child/environment.js b/node_modules/jest-runner-vscode/dist/child/environment.js
|
||||
index 1ac28d5..f91f216 100644
|
||||
--- a/node_modules/jest-runner-vscode/dist/child/environment.js
|
||||
+++ b/node_modules/jest-runner-vscode/dist/child/environment.js
|
||||
@@ -10,6 +10,21 @@ const wrap_io_1 = __importDefault(require("./wrap-io"));
|
||||
const load_pnp_1 = __importDefault(require("./load-pnp"));
|
||||
const ipc = new ipc_client_1.default('env');
|
||||
class VSCodeEnvironment extends jest_environment_node_1.default {
|
||||
+ constructor(config, context) {
|
||||
+ super(config, context);
|
||||
+ // The _VSCODE_NODE_MODULES is a proxy which will require a module if any property
|
||||
+ // on it is accessed. This is a workaround for the fact that jest will call
|
||||
+ // _isMockFunction on the module, which will cause that function to be required.
|
||||
+ this.global._VSCODE_NODE_MODULES = new Proxy(this.global._VSCODE_NODE_MODULES, {
|
||||
+ get(target, prop) {
|
||||
+ if (prop === '_isMockFunction') {
|
||||
+ return undefined;
|
||||
+ }
|
||||
+ return target[prop];
|
||||
+ },
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
async setup() {
|
||||
await super.setup();
|
||||
await (0, load_pnp_1.default)();
|
||||
diff --git a/node_modules/jest-runner-vscode/dist/child/runner.js b/node_modules/jest-runner-vscode/dist/child/runner.js
|
||||
index 0663c5c..4991663 100644
|
||||
--- a/node_modules/jest-runner-vscode/dist/child/runner.js
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { pathExists, writeJSON, readJSON, readJSONSync } from "fs-extra";
|
||||
import { join } from "path";
|
||||
import { cloneDbConfig, DbConfig, SelectedDbItem } from "./db-config";
|
||||
import {
|
||||
cloneDbConfig,
|
||||
DbConfig,
|
||||
ExpandedDbItem,
|
||||
SelectedDbItem,
|
||||
} from "./db-config";
|
||||
import * as chokidar from "chokidar";
|
||||
import { DisposableObject, DisposeHandler } from "../../pure/disposable-object";
|
||||
import { DbConfigValidator } from "./db-config-validator";
|
||||
@@ -72,6 +77,19 @@ export class DbConfigStore extends DisposableObject {
|
||||
await this.writeConfig(config);
|
||||
}
|
||||
|
||||
public async updateExpandedState(expandedItems: ExpandedDbItem[]) {
|
||||
if (!this.config) {
|
||||
throw Error("Cannot update expansion state if config is not loaded");
|
||||
}
|
||||
|
||||
const config: DbConfig = {
|
||||
...this.config,
|
||||
expanded: expandedItems,
|
||||
};
|
||||
|
||||
await this.writeConfig(config);
|
||||
}
|
||||
|
||||
private async writeConfig(config: DbConfig): Promise<void> {
|
||||
await writeJSON(this.configPath, config, {
|
||||
spaces: 2,
|
||||
@@ -137,6 +155,7 @@ export class DbConfigStore extends DisposableObject {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
export interface DbConfig {
|
||||
databases: DbConfigDatabases;
|
||||
expanded: ExpandedDbItem[];
|
||||
selected?: SelectedDbItem;
|
||||
}
|
||||
|
||||
@@ -87,6 +88,37 @@ export interface LocalDatabase {
|
||||
storagePath: string;
|
||||
}
|
||||
|
||||
export type ExpandedDbItem =
|
||||
| RootLocalExpandedDbItem
|
||||
| LocalUserDefinedListExpandedDbItem
|
||||
| RootRemoteExpandedDbItem
|
||||
| RemoteUserDefinedListExpandedDbItem;
|
||||
|
||||
export enum ExpandedDbItemKind {
|
||||
RootLocal = "rootLocal",
|
||||
LocalUserDefinedList = "localUserDefinedList",
|
||||
RootRemote = "rootRemote",
|
||||
RemoteUserDefinedList = "remoteUserDefinedList",
|
||||
}
|
||||
|
||||
export interface RootLocalExpandedDbItem {
|
||||
kind: ExpandedDbItemKind.RootLocal;
|
||||
}
|
||||
|
||||
export interface LocalUserDefinedListExpandedDbItem {
|
||||
kind: ExpandedDbItemKind.LocalUserDefinedList;
|
||||
listName: string;
|
||||
}
|
||||
|
||||
export interface RootRemoteExpandedDbItem {
|
||||
kind: ExpandedDbItemKind.RootRemote;
|
||||
}
|
||||
|
||||
export interface RemoteUserDefinedListExpandedDbItem {
|
||||
kind: ExpandedDbItemKind.RemoteUserDefinedList;
|
||||
listName: string;
|
||||
}
|
||||
|
||||
export function cloneDbConfig(config: DbConfig): DbConfig {
|
||||
return {
|
||||
databases: {
|
||||
@@ -108,6 +140,7 @@ export function cloneDbConfig(config: DbConfig): DbConfig {
|
||||
databases: config.databases.local.databases.map((db) => ({ ...db })),
|
||||
},
|
||||
},
|
||||
expanded: config.expanded.map(cloneDbConfigExpandedItem),
|
||||
selected: config.selected
|
||||
? cloneDbConfigSelectedItem(config.selected)
|
||||
: undefined,
|
||||
@@ -150,3 +183,17 @@ function cloneDbConfigSelectedItem(selected: SelectedDbItem): SelectedDbItem {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function cloneDbConfigExpandedItem(item: ExpandedDbItem): ExpandedDbItem {
|
||||
switch (item.kind) {
|
||||
case ExpandedDbItemKind.RootLocal:
|
||||
case ExpandedDbItemKind.RootRemote:
|
||||
return { kind: item.kind };
|
||||
case ExpandedDbItemKind.LocalUserDefinedList:
|
||||
case ExpandedDbItemKind.RemoteUserDefinedList:
|
||||
return {
|
||||
kind: item.kind,
|
||||
listName: item.listName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
66
extensions/ql-vscode/src/databases/db-item-expansion.ts
Normal file
66
extensions/ql-vscode/src/databases/db-item-expansion.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ExpandedDbItem, ExpandedDbItemKind } from "./config/db-config";
|
||||
import { DbItem, DbItemKind } from "./db-item";
|
||||
|
||||
export function calculateNewExpandedState(
|
||||
currentExpandedItems: ExpandedDbItem[],
|
||||
dbItem: DbItem,
|
||||
itemExpanded: boolean,
|
||||
): ExpandedDbItem[] {
|
||||
if (itemExpanded) {
|
||||
const expandedDbItem = mapDbItemToExpandedDbItem(dbItem);
|
||||
const expandedItems = [...currentExpandedItems];
|
||||
if (!expandedItems.some((i) => isDbItemEqualToExpandedDbItem(dbItem, i))) {
|
||||
expandedItems.push(expandedDbItem);
|
||||
}
|
||||
return expandedItems;
|
||||
} else {
|
||||
return currentExpandedItems.filter(
|
||||
(i) => !isDbItemEqualToExpandedDbItem(dbItem, i),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapDbItemToExpandedDbItem(dbItem: DbItem): ExpandedDbItem {
|
||||
switch (dbItem.kind) {
|
||||
case DbItemKind.RootLocal:
|
||||
return { kind: ExpandedDbItemKind.RootLocal };
|
||||
case DbItemKind.LocalList:
|
||||
return {
|
||||
kind: ExpandedDbItemKind.LocalUserDefinedList,
|
||||
listName: dbItem.listName,
|
||||
};
|
||||
case DbItemKind.RootRemote:
|
||||
return { kind: ExpandedDbItemKind.RootRemote };
|
||||
case DbItemKind.RemoteUserDefinedList:
|
||||
return {
|
||||
kind: ExpandedDbItemKind.RemoteUserDefinedList,
|
||||
listName: dbItem.listName,
|
||||
};
|
||||
default:
|
||||
throw Error(`Unknown db item kind ${dbItem.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isDbItemEqualToExpandedDbItem(
|
||||
dbItem: DbItem,
|
||||
expandedDbItem: ExpandedDbItem,
|
||||
) {
|
||||
switch (dbItem.kind) {
|
||||
case DbItemKind.RootLocal:
|
||||
return expandedDbItem.kind === ExpandedDbItemKind.RootLocal;
|
||||
case DbItemKind.LocalList:
|
||||
return (
|
||||
expandedDbItem.kind === ExpandedDbItemKind.LocalUserDefinedList &&
|
||||
expandedDbItem.listName === dbItem.listName
|
||||
);
|
||||
case DbItemKind.RootRemote:
|
||||
return expandedDbItem.kind === ExpandedDbItemKind.RootRemote;
|
||||
case DbItemKind.RemoteUserDefinedList:
|
||||
return (
|
||||
expandedDbItem.kind === ExpandedDbItemKind.RemoteUserDefinedList &&
|
||||
expandedDbItem.listName === dbItem.listName
|
||||
);
|
||||
default:
|
||||
throw Error(`Unknown db item kind ${dbItem.kind}`);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export enum DbItemKind {
|
||||
|
||||
export interface RootLocalDbItem {
|
||||
kind: DbItemKind.RootLocal;
|
||||
expanded: boolean;
|
||||
children: LocalDbItem[];
|
||||
}
|
||||
|
||||
@@ -20,6 +21,7 @@ export type LocalDbItem = LocalListDbItem | LocalDatabaseDbItem;
|
||||
|
||||
export interface LocalListDbItem {
|
||||
kind: DbItemKind.LocalList;
|
||||
expanded: boolean;
|
||||
selected: boolean;
|
||||
listName: string;
|
||||
databases: LocalDatabaseDbItem[];
|
||||
@@ -37,6 +39,7 @@ export interface LocalDatabaseDbItem {
|
||||
|
||||
export interface RootRemoteDbItem {
|
||||
kind: DbItemKind.RootRemote;
|
||||
expanded: boolean;
|
||||
children: RemoteDbItem[];
|
||||
}
|
||||
|
||||
@@ -62,6 +65,7 @@ export interface RemoteSystemDefinedListDbItem {
|
||||
|
||||
export interface RemoteUserDefinedListDbItem {
|
||||
kind: DbItemKind.RemoteUserDefinedList;
|
||||
expanded: boolean;
|
||||
selected: boolean;
|
||||
listName: string;
|
||||
repos: RemoteRepoDbItem[];
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AppEvent, AppEventEmitter } from "../common/events";
|
||||
import { ValueResult } from "../common/value-result";
|
||||
import { DbConfigStore } from "./config/db-config-store";
|
||||
import { DbItem } from "./db-item";
|
||||
import { calculateNewExpandedState } from "./db-item-expansion";
|
||||
import {
|
||||
getSelectedDbItem,
|
||||
mapDbItemToSelectedDbItem,
|
||||
@@ -54,4 +55,22 @@ export class DbManager {
|
||||
await this.dbConfigStore.setSelectedDbItem(selectedDbItem);
|
||||
}
|
||||
}
|
||||
|
||||
public async updateDbItemExpandedState(
|
||||
dbItem: DbItem,
|
||||
itemExpanded: boolean,
|
||||
): Promise<void> {
|
||||
const configResult = this.dbConfigStore.getConfig();
|
||||
if (configResult.isFailure) {
|
||||
throw Error("Cannot update expanded state if config is not loaded");
|
||||
}
|
||||
|
||||
const newExpandedItems = calculateNewExpandedState(
|
||||
configResult.value.expanded,
|
||||
dbItem,
|
||||
itemExpanded,
|
||||
);
|
||||
|
||||
await this.dbConfigStore.updateExpandedState(newExpandedItems);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,16 @@ import { DbPanel } from "./ui/db-panel";
|
||||
import { DbSelectionDecorationProvider } from "./ui/db-selection-decoration-provider";
|
||||
|
||||
export class DbModule extends DisposableObject {
|
||||
public readonly dbManager: DbManager;
|
||||
private readonly dbConfigStore: DbConfigStore;
|
||||
|
||||
constructor(app: App) {
|
||||
super();
|
||||
|
||||
this.dbConfigStore = new DbConfigStore(app);
|
||||
this.dbManager = new DbManager(app, this.dbConfigStore);
|
||||
}
|
||||
|
||||
public async initialize(app: App): Promise<void> {
|
||||
if (
|
||||
app.mode !== AppMode.Development ||
|
||||
@@ -23,15 +33,13 @@ export class DbModule extends DisposableObject {
|
||||
|
||||
void extLogger.log("Initializing database module");
|
||||
|
||||
const dbConfigStore = new DbConfigStore(app);
|
||||
await dbConfigStore.initialize();
|
||||
await this.dbConfigStore.initialize();
|
||||
|
||||
const dbManager = new DbManager(app, dbConfigStore);
|
||||
const dbPanel = new DbPanel(dbManager);
|
||||
const dbPanel = new DbPanel(this.dbManager);
|
||||
await dbPanel.initialize();
|
||||
|
||||
this.push(dbPanel);
|
||||
this.push(dbConfigStore);
|
||||
this.push(this.dbConfigStore);
|
||||
|
||||
const dbSelectionDecorationProvider = new DbSelectionDecorationProvider();
|
||||
|
||||
@@ -40,7 +48,7 @@ export class DbModule extends DisposableObject {
|
||||
}
|
||||
|
||||
export async function initializeDbModule(app: App): Promise<DbModule> {
|
||||
const dbModule = new DbModule();
|
||||
const dbModule = new DbModule(app);
|
||||
await dbModule.initialize(app);
|
||||
return dbModule;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
DbConfig,
|
||||
ExpandedDbItemKind,
|
||||
LocalDatabase,
|
||||
LocalList,
|
||||
RemoteRepositoryList,
|
||||
@@ -34,6 +35,10 @@ export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem {
|
||||
createRepoItem(r, dbConfig),
|
||||
);
|
||||
|
||||
const expanded =
|
||||
dbConfig.expanded &&
|
||||
dbConfig.expanded.some((e) => e.kind === ExpandedDbItemKind.RootRemote);
|
||||
|
||||
return {
|
||||
kind: DbItemKind.RootRemote,
|
||||
children: [
|
||||
@@ -42,6 +47,7 @@ export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem {
|
||||
...userDefinedRepoLists,
|
||||
...repos,
|
||||
],
|
||||
expanded: !!expanded,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,9 +59,14 @@ export function createLocalTree(dbConfig: DbConfig): RootLocalDbItem {
|
||||
createLocalDb(l, dbConfig),
|
||||
);
|
||||
|
||||
const expanded =
|
||||
dbConfig.expanded &&
|
||||
dbConfig.expanded.some((e) => e.kind === ExpandedDbItemKind.RootLocal);
|
||||
|
||||
return {
|
||||
kind: DbItemKind.RootLocal,
|
||||
children: [...localLists, ...localDbs],
|
||||
expanded: !!expanded,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,11 +99,20 @@ function createRemoteUserDefinedList(
|
||||
dbConfig.selected.kind === SelectedDbItemKind.RemoteUserDefinedList &&
|
||||
dbConfig.selected.listName === list.name;
|
||||
|
||||
const expanded =
|
||||
dbConfig.expanded &&
|
||||
dbConfig.expanded.some(
|
||||
(e) =>
|
||||
e.kind === ExpandedDbItemKind.RemoteUserDefinedList &&
|
||||
e.listName === list.name,
|
||||
);
|
||||
|
||||
return {
|
||||
kind: DbItemKind.RemoteUserDefinedList,
|
||||
listName: list.name,
|
||||
repos: list.repositories.map((r) => createRepoItem(r, dbConfig, list.name)),
|
||||
selected: !!selected,
|
||||
expanded: !!expanded,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -134,11 +154,20 @@ function createLocalList(list: LocalList, dbConfig: DbConfig): LocalListDbItem {
|
||||
dbConfig.selected.kind === SelectedDbItemKind.LocalUserDefinedList &&
|
||||
dbConfig.selected.listName === list.name;
|
||||
|
||||
const expanded =
|
||||
dbConfig.expanded &&
|
||||
dbConfig.expanded.some(
|
||||
(e) =>
|
||||
e.kind === ExpandedDbItemKind.LocalUserDefinedList &&
|
||||
e.listName === list.name,
|
||||
);
|
||||
|
||||
return {
|
||||
kind: DbItemKind.LocalList,
|
||||
listName: list.name,
|
||||
databases: list.databases.map((d) => createLocalDb(d, dbConfig, list.name)),
|
||||
selected: !!selected,
|
||||
expanded: !!expanded,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { window, workspace } from "vscode";
|
||||
import { TreeViewExpansionEvent, window, workspace } from "vscode";
|
||||
import { commandRunner } from "../../commandRunner";
|
||||
import { DisposableObject } from "../../pure/disposable-object";
|
||||
import { DbManager } from "../db-manager";
|
||||
@@ -18,6 +18,9 @@ export class DbPanel extends DisposableObject {
|
||||
canSelectMany: false,
|
||||
});
|
||||
|
||||
treeView.onDidCollapseElement.bind(this.onDidCollapseElement);
|
||||
treeView.onDidExpandElement.bind(this.onDidExpandElement);
|
||||
|
||||
this.push(treeView);
|
||||
}
|
||||
|
||||
@@ -49,4 +52,26 @@ export class DbPanel extends DisposableObject {
|
||||
}
|
||||
await this.dbManager.setSelectedDbItem(treeViewItem.dbItem);
|
||||
}
|
||||
|
||||
private async onDidCollapseElement(
|
||||
event: TreeViewExpansionEvent<DbTreeViewItem>,
|
||||
): Promise<void> {
|
||||
const dbItem = event.element.dbItem;
|
||||
if (!dbItem) {
|
||||
throw Error("Expected a database item.");
|
||||
}
|
||||
|
||||
await this.dbManager.updateDbItemExpandedState(event.element.dbItem, false);
|
||||
}
|
||||
|
||||
private async onDidExpandElement(
|
||||
event: TreeViewExpansionEvent<DbTreeViewItem>,
|
||||
): Promise<void> {
|
||||
const dbItem = event.element.dbItem;
|
||||
if (!dbItem) {
|
||||
throw Error("Expected a database item.");
|
||||
}
|
||||
|
||||
await this.dbManager.updateDbItemExpandedState(event.element.dbItem, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export function createDbTreeViewItemRoot(
|
||||
undefined,
|
||||
label,
|
||||
tooltip,
|
||||
vscode.TreeItemCollapsibleState.Collapsed,
|
||||
getCollapsibleState(dbItem.expanded),
|
||||
children,
|
||||
);
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export function createDbTreeViewItemUserDefinedList(
|
||||
undefined,
|
||||
listName,
|
||||
undefined,
|
||||
vscode.TreeItemCollapsibleState.Collapsed,
|
||||
getCollapsibleState(dbItem.expanded),
|
||||
children,
|
||||
);
|
||||
}
|
||||
@@ -147,3 +147,11 @@ export function createDbTreeViewItemLocalDatabase(
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
function getCollapsibleState(
|
||||
expanded: boolean,
|
||||
): vscode.TreeItemCollapsibleState {
|
||||
return expanded
|
||||
? vscode.TreeItemCollapsibleState.Expanded
|
||||
: vscode.TreeItemCollapsibleState.Collapsed;
|
||||
}
|
||||
|
||||
@@ -622,6 +622,11 @@ async function activateWithInstalledDistribution(
|
||||
ctx.subscriptions.push(localQueryResultsView);
|
||||
|
||||
void extLogger.log("Initializing variant analysis manager.");
|
||||
|
||||
const app = new ExtensionApp(ctx);
|
||||
const dbModule = await initializeDbModule(app);
|
||||
ctx.subscriptions.push(dbModule);
|
||||
|
||||
const variantAnalysisStorageDir = join(
|
||||
ctx.globalStorageUri.fsPath,
|
||||
"variant-analyses",
|
||||
@@ -636,6 +641,7 @@ async function activateWithInstalledDistribution(
|
||||
cliServer,
|
||||
variantAnalysisStorageDir,
|
||||
variantAnalysisResultsManager,
|
||||
dbModule.dbManager,
|
||||
);
|
||||
ctx.subscriptions.push(variantAnalysisManager);
|
||||
ctx.subscriptions.push(variantAnalysisResultsManager);
|
||||
@@ -1580,10 +1586,6 @@ async function activateWithInstalledDistribution(
|
||||
void extLogger.log("Reading query history");
|
||||
await qhm.readQueryHistory();
|
||||
|
||||
const app = new ExtensionApp(ctx);
|
||||
const dbModule = await initializeDbModule(app);
|
||||
ctx.subscriptions.push(dbModule);
|
||||
|
||||
void extLogger.log("Successfully finished extension initialization.");
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,9 +4,12 @@ import { extLogger } from "../common";
|
||||
import {
|
||||
getRemoteRepositoryLists,
|
||||
getRemoteRepositoryListsPath,
|
||||
isNewQueryRunExperienceEnabled,
|
||||
} 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";
|
||||
|
||||
export interface RepositorySelection {
|
||||
repositories?: string[];
|
||||
@@ -30,7 +33,33 @@ interface RepoList {
|
||||
* Gets the repositories or repository lists to run the query against.
|
||||
* @returns The user selection.
|
||||
*/
|
||||
export async function getRepositorySelection(): Promise<RepositorySelection> {
|
||||
export async function getRepositorySelection(
|
||||
dbManager?: DbManager,
|
||||
): Promise<RepositorySelection> {
|
||||
if (isNewQueryRunExperienceEnabled()) {
|
||||
const selectedDbItem = dbManager?.getSelectedDbItem();
|
||||
if (selectedDbItem) {
|
||||
switch (selectedDbItem.kind) {
|
||||
case DbItemKind.LocalDatabase || DbItemKind.LocalList:
|
||||
throw new Error("Local databases and lists are not supported yet.");
|
||||
case DbItemKind.RemoteSystemDefinedList:
|
||||
return { repositoryLists: [selectedDbItem.listName] };
|
||||
case DbItemKind.RemoteUserDefinedList:
|
||||
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 Error(
|
||||
"Please select a remote database to run the query against.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const quickPickItems = [
|
||||
createCustomRepoQuickPickItem(),
|
||||
createAllReposOfOwnerQuickPickItem(),
|
||||
@@ -49,15 +78,21 @@ export async function getRepositorySelection(): Promise<RepositorySelection> {
|
||||
options,
|
||||
);
|
||||
|
||||
if (quickpick?.repositories?.length) {
|
||||
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) {
|
||||
} else if (quickpick.repositoryList) {
|
||||
void extLogger.log(`Selected repository list: ${quickpick.repositoryList}`);
|
||||
return { repositoryLists: [quickpick.repositoryList] };
|
||||
} else if (quickpick?.useCustomRepo) {
|
||||
} else if (quickpick.useCustomRepo) {
|
||||
const customRepo = await getCustomRepo();
|
||||
if (customRepo === undefined) {
|
||||
// The user cancelled, do nothing.
|
||||
@@ -70,7 +105,7 @@ export async function getRepositorySelection(): Promise<RepositorySelection> {
|
||||
}
|
||||
void extLogger.log(`Entered repository: ${customRepo}`);
|
||||
return { repositories: [customRepo] };
|
||||
} else if (quickpick?.useAllReposOfOwner) {
|
||||
} else if (quickpick.useAllReposOfOwner) {
|
||||
const owner = await getOwner();
|
||||
if (owner === undefined) {
|
||||
// The user cancelled, do nothing.
|
||||
@@ -82,9 +117,9 @@ export async function getRepositorySelection(): Promise<RepositorySelection> {
|
||||
void extLogger.log(`Entered owner: ${owner}`);
|
||||
return { owners: [owner] };
|
||||
} else {
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
RepositorySelection,
|
||||
} from "./repository-selection";
|
||||
import { Repository } from "./shared/repository";
|
||||
import { DbManager } from "../databases/db-manager";
|
||||
|
||||
export interface QlPack {
|
||||
name: string;
|
||||
@@ -213,6 +214,7 @@ export async function prepareRemoteQueryRun(
|
||||
uri: Uri | undefined,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
dbManager?: DbManager, // the dbManager is only needed when the newQueryRunExperience is enabled
|
||||
): Promise<PreparedRemoteQuery> {
|
||||
if (!(await cliServer.cliConstraints.supportsRemoteQueries())) {
|
||||
throw new Error(
|
||||
@@ -232,7 +234,7 @@ export async function prepareRemoteQueryRun(
|
||||
message: "Determining query target language",
|
||||
});
|
||||
|
||||
const repoSelection = await getRepositorySelection();
|
||||
const repoSelection = await getRepositorySelection(dbManager);
|
||||
if (!isValidSelection(repoSelection)) {
|
||||
throw new UserCancellationException("No repositories to query.");
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
RepositoriesFilterSortStateWithIds,
|
||||
} from "../pure/variant-analysis-filter-sort";
|
||||
import { URLSearchParams } from "url";
|
||||
import { DbManager } from "../databases/db-manager";
|
||||
|
||||
export class VariantAnalysisManager
|
||||
extends DisposableObject
|
||||
@@ -100,6 +101,7 @@ export class VariantAnalysisManager
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly storagePath: string,
|
||||
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager,
|
||||
private readonly dbManager: DbManager,
|
||||
) {
|
||||
super();
|
||||
this.variantAnalysisMonitor = this.push(
|
||||
@@ -140,6 +142,7 @@ export class VariantAnalysisManager
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
this.dbManager,
|
||||
);
|
||||
|
||||
const queryName = getQueryName(queryMetadata, queryFile);
|
||||
|
||||
@@ -227,7 +227,7 @@ export const RepoRow = ({
|
||||
[onSelectedChange, repository],
|
||||
);
|
||||
|
||||
const disabled = !canExpand(status, downloadStatus);
|
||||
const disabled = !canExpand(status, downloadStatus) || resultsLoading;
|
||||
const expandableContentLoaded = isExpandableContentLoaded(
|
||||
status,
|
||||
downloadStatus,
|
||||
@@ -247,11 +247,13 @@ export const RepoRow = ({
|
||||
checked={selected}
|
||||
disabled={!repository.id || !canSelect(status, downloadStatus)}
|
||||
/>
|
||||
{isExpanded ? (
|
||||
{isExpanded && (
|
||||
<ExpandCollapseCodicon name="chevron-down" label="Collapse" />
|
||||
) : (
|
||||
)}
|
||||
{!isExpanded && !resultsLoading && (
|
||||
<ExpandCollapseCodicon name="chevron-right" label="Expand" />
|
||||
)}
|
||||
{resultsLoading && <LoadingIcon label="Results are loading" />}
|
||||
<VSCodeBadge>
|
||||
{resultCount === undefined ? "-" : formatDecimal(resultCount)}
|
||||
</VSCodeBadge>
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepository,
|
||||
VariantAnalysisScannedRepositoryDownloadStatus,
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
VariantAnalysisStatus,
|
||||
} from "../../../remote-queries/shared/variant-analysis";
|
||||
import { createTimestampFile } from "../../../helpers";
|
||||
@@ -56,6 +57,7 @@ import {
|
||||
defaultFilterSortState,
|
||||
SortKey,
|
||||
} from "../../../pure/variant-analysis-filter-sort";
|
||||
import { DbManager } from "../../../databases/db-manager";
|
||||
|
||||
// up to 3 minutes per test
|
||||
jest.setTimeout(3 * 60 * 1000);
|
||||
@@ -69,6 +71,7 @@ describe("Variant Analysis Manager", () => {
|
||||
let cancellationTokenSource: CancellationTokenSource;
|
||||
let variantAnalysisManager: VariantAnalysisManager;
|
||||
let variantAnalysisResultsManager: VariantAnalysisResultsManager;
|
||||
let dbManager: DbManager;
|
||||
let variantAnalysis: VariantAnalysis;
|
||||
let scannedRepos: VariantAnalysisScannedRepository[];
|
||||
|
||||
@@ -107,6 +110,7 @@ describe("Variant Analysis Manager", () => {
|
||||
cli,
|
||||
storagePath,
|
||||
variantAnalysisResultsManager,
|
||||
dbManager,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -362,92 +366,94 @@ describe("Variant Analysis Manager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("when credentials are invalid", () => {
|
||||
beforeEach(async () => {
|
||||
jest
|
||||
.spyOn(Credentials, "initialize")
|
||||
.mockResolvedValue(undefined as unknown as Credentials);
|
||||
});
|
||||
|
||||
it("should return early if credentials are wrong", async () => {
|
||||
try {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
} catch (error: any) {
|
||||
expect(error.message).toBe("Error authenticating with GitHub");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("when credentials are valid", () => {
|
||||
let arrayBuffer: ArrayBuffer;
|
||||
|
||||
let getVariantAnalysisRepoStub: jest.SpiedFunction<
|
||||
typeof ghApiClient.getVariantAnalysisRepo
|
||||
>;
|
||||
let getVariantAnalysisRepoResultStub: jest.SpiedFunction<
|
||||
typeof ghApiClient.getVariantAnalysisRepoResult
|
||||
>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockCredentials = {
|
||||
getOctokit: () =>
|
||||
Promise.resolve({
|
||||
request: jest.fn(),
|
||||
}),
|
||||
} as unknown as Credentials;
|
||||
jest.spyOn(Credentials, "initialize").mockResolvedValue(mockCredentials);
|
||||
|
||||
const sourceFilePath = join(
|
||||
__dirname,
|
||||
"../../../../src/vscode-tests/cli-integration/data/variant-analysis-results.zip",
|
||||
);
|
||||
arrayBuffer = fs.readFileSync(sourceFilePath).buffer;
|
||||
|
||||
getVariantAnalysisRepoStub = jest.spyOn(
|
||||
ghApiClient,
|
||||
"getVariantAnalysisRepo",
|
||||
);
|
||||
getVariantAnalysisRepoResultStub = jest.spyOn(
|
||||
ghApiClient,
|
||||
"getVariantAnalysisRepoResult",
|
||||
);
|
||||
});
|
||||
|
||||
describe("when the artifact_url is missing", () => {
|
||||
describe("autoDownloadVariantAnalysisResult", () => {
|
||||
describe("when credentials are invalid", () => {
|
||||
beforeEach(async () => {
|
||||
const dummyRepoTask = createMockVariantAnalysisRepoTask();
|
||||
delete dummyRepoTask.artifact_url;
|
||||
|
||||
getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask);
|
||||
getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer);
|
||||
jest
|
||||
.spyOn(Credentials, "initialize")
|
||||
.mockResolvedValue(undefined as unknown as Credentials);
|
||||
});
|
||||
|
||||
it("should not try to download the result", async () => {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(getVariantAnalysisRepoResultStub).not.toHaveBeenCalled();
|
||||
it("should return early if credentials are wrong", async () => {
|
||||
try {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
} catch (error: any) {
|
||||
expect(error.message).toBe("Error authenticating with GitHub");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the artifact_url is present", () => {
|
||||
let dummyRepoTask: VariantAnalysisRepoTask;
|
||||
describe("when credentials are valid", () => {
|
||||
let arrayBuffer: ArrayBuffer;
|
||||
|
||||
let getVariantAnalysisRepoStub: jest.SpiedFunction<
|
||||
typeof ghApiClient.getVariantAnalysisRepo
|
||||
>;
|
||||
let getVariantAnalysisRepoResultStub: jest.SpiedFunction<
|
||||
typeof ghApiClient.getVariantAnalysisRepoResult
|
||||
>;
|
||||
|
||||
beforeEach(async () => {
|
||||
dummyRepoTask = createMockVariantAnalysisRepoTask();
|
||||
const mockCredentials = {
|
||||
getOctokit: () =>
|
||||
Promise.resolve({
|
||||
request: jest.fn(),
|
||||
}),
|
||||
} as unknown as Credentials;
|
||||
jest
|
||||
.spyOn(Credentials, "initialize")
|
||||
.mockResolvedValue(mockCredentials);
|
||||
|
||||
getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask);
|
||||
getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer);
|
||||
const sourceFilePath = join(
|
||||
__dirname,
|
||||
"../../../../src/vscode-tests/cli-integration/data/variant-analysis-results.zip",
|
||||
);
|
||||
arrayBuffer = fs.readFileSync(sourceFilePath).buffer;
|
||||
|
||||
getVariantAnalysisRepoStub = jest.spyOn(
|
||||
ghApiClient,
|
||||
"getVariantAnalysisRepo",
|
||||
);
|
||||
getVariantAnalysisRepoResultStub = jest.spyOn(
|
||||
ghApiClient,
|
||||
"getVariantAnalysisRepoResult",
|
||||
);
|
||||
});
|
||||
|
||||
describe("autoDownloadVariantAnalysisResult", () => {
|
||||
describe("when the artifact_url is missing", () => {
|
||||
beforeEach(async () => {
|
||||
const dummyRepoTask = createMockVariantAnalysisRepoTask();
|
||||
delete dummyRepoTask.artifact_url;
|
||||
|
||||
getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask);
|
||||
getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer);
|
||||
});
|
||||
|
||||
it("should not try to download the result", async () => {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(getVariantAnalysisRepoResultStub).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the artifact_url is present", () => {
|
||||
let dummyRepoTask: VariantAnalysisRepoTask;
|
||||
|
||||
beforeEach(async () => {
|
||||
dummyRepoTask = createMockVariantAnalysisRepoTask();
|
||||
|
||||
getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask);
|
||||
getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer);
|
||||
});
|
||||
|
||||
it("should return early if variant analysis is cancelled", async () => {
|
||||
cancellationTokenSource.cancel();
|
||||
|
||||
@@ -623,26 +629,18 @@ describe("Variant Analysis Manager", () => {
|
||||
});
|
||||
|
||||
it("should update the repo state correctly", async () => {
|
||||
// To set some initial repo states, we need to mock the correct methods so that the repo states are read in.
|
||||
// The actual tests for these are in rehydrateVariantAnalysis, so we can just mock them here and test that
|
||||
// the methods are called.
|
||||
|
||||
pathExistsStub.mockImplementation(() => true);
|
||||
// This will read in the correct repo states
|
||||
readJsonStub.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
[scannedRepos[1].repository.id]: {
|
||||
repositoryId: scannedRepos[1].repository.id,
|
||||
downloadStatus:
|
||||
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
|
||||
},
|
||||
[scannedRepos[2].repository.id]: {
|
||||
repositoryId: scannedRepos[2].repository.id,
|
||||
downloadStatus:
|
||||
VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
|
||||
},
|
||||
}),
|
||||
);
|
||||
mockRepoStates({
|
||||
[scannedRepos[1].repository.id]: {
|
||||
repositoryId: scannedRepos[1].repository.id,
|
||||
downloadStatus:
|
||||
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
|
||||
},
|
||||
[scannedRepos[2].repository.id]: {
|
||||
repositoryId: scannedRepos[2].repository.id,
|
||||
downloadStatus:
|
||||
VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
|
||||
},
|
||||
});
|
||||
|
||||
await variantAnalysisManager.rehydrateVariantAnalysis(
|
||||
variantAnalysis,
|
||||
@@ -693,78 +691,91 @@ describe("Variant Analysis Manager", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("enqueueDownload", () => {
|
||||
it("should pop download tasks off the queue", async () => {
|
||||
const getResultsSpy = jest.spyOn(
|
||||
variantAnalysisManager,
|
||||
"autoDownloadVariantAnalysisResult",
|
||||
);
|
||||
|
||||
await variantAnalysisManager.enqueueDownload(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
await variantAnalysisManager.enqueueDownload(
|
||||
scannedRepos[1],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
await variantAnalysisManager.enqueueDownload(
|
||||
scannedRepos[2],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(variantAnalysisManager.downloadsQueueSize()).toBe(0);
|
||||
expect(getResultsSpy).toBeCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeVariantAnalysis", () => {
|
||||
let removeAnalysisResultsStub: jest.SpiedFunction<
|
||||
typeof variantAnalysisResultsManager.removeAnalysisResults
|
||||
>;
|
||||
let removeStorageStub: jest.SpiedFunction<typeof fs.remove>;
|
||||
let dummyVariantAnalysis: VariantAnalysis;
|
||||
|
||||
beforeEach(async () => {
|
||||
dummyVariantAnalysis = createMockVariantAnalysis({});
|
||||
|
||||
removeAnalysisResultsStub = jest
|
||||
.spyOn(variantAnalysisResultsManager, "removeAnalysisResults")
|
||||
.mockReturnValue(undefined);
|
||||
|
||||
removeStorageStub = jest
|
||||
.spyOn(fs, "remove")
|
||||
.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it("should remove variant analysis", async () => {
|
||||
function mockRepoStates(
|
||||
repoStates: Record<number, VariantAnalysisScannedRepositoryState>,
|
||||
) {
|
||||
pathExistsStub.mockImplementation(() => true);
|
||||
await variantAnalysisManager.rehydrateVariantAnalysis(
|
||||
dummyVariantAnalysis,
|
||||
);
|
||||
expect(pathExistsStub).toBeCalledWith(
|
||||
join(storagePath, dummyVariantAnalysis.id.toString()),
|
||||
);
|
||||
expect(variantAnalysisManager.variantAnalysesSize).toBe(1);
|
||||
|
||||
await variantAnalysisManager.removeVariantAnalysis(
|
||||
dummyVariantAnalysis,
|
||||
);
|
||||
|
||||
expect(removeAnalysisResultsStub).toBeCalledTimes(1);
|
||||
expect(removeStorageStub).toBeCalledTimes(1);
|
||||
expect(variantAnalysisManager.variantAnalysesSize).toBe(0);
|
||||
});
|
||||
// This will read in the correct repo states
|
||||
readJsonStub.mockImplementation(() => Promise.resolve(repoStates));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rehydrating a query", () => {
|
||||
describe("enqueueDownload", () => {
|
||||
beforeEach(async () => {
|
||||
const mockCredentials = {
|
||||
getOctokit: () =>
|
||||
Promise.resolve({
|
||||
request: jest.fn(),
|
||||
}),
|
||||
} as unknown as Credentials;
|
||||
jest.spyOn(Credentials, "initialize").mockResolvedValue(mockCredentials);
|
||||
});
|
||||
|
||||
it("should pop download tasks off the queue", async () => {
|
||||
const getResultsSpy = jest
|
||||
.spyOn(variantAnalysisManager, "autoDownloadVariantAnalysisResult")
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
await variantAnalysisManager.enqueueDownload(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
await variantAnalysisManager.enqueueDownload(
|
||||
scannedRepos[1],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
await variantAnalysisManager.enqueueDownload(
|
||||
scannedRepos[2],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(variantAnalysisManager.downloadsQueueSize()).toBe(0);
|
||||
expect(getResultsSpy).toBeCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeVariantAnalysis", () => {
|
||||
let removeAnalysisResultsStub: jest.SpiedFunction<
|
||||
typeof variantAnalysisResultsManager.removeAnalysisResults
|
||||
>;
|
||||
let removeStorageStub: jest.SpiedFunction<typeof fs.remove>;
|
||||
let dummyVariantAnalysis: VariantAnalysis;
|
||||
|
||||
beforeEach(async () => {
|
||||
dummyVariantAnalysis = createMockVariantAnalysis({});
|
||||
|
||||
removeAnalysisResultsStub = jest
|
||||
.spyOn(variantAnalysisResultsManager, "removeAnalysisResults")
|
||||
.mockReturnValue(undefined);
|
||||
|
||||
removeStorageStub = jest.spyOn(fs, "remove").mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it("should remove variant analysis", async () => {
|
||||
pathExistsStub.mockImplementation(() => true);
|
||||
await variantAnalysisManager.rehydrateVariantAnalysis(
|
||||
dummyVariantAnalysis,
|
||||
);
|
||||
expect(pathExistsStub).toBeCalledWith(
|
||||
join(storagePath, dummyVariantAnalysis.id.toString()),
|
||||
);
|
||||
expect(variantAnalysisManager.variantAnalysesSize).toBe(1);
|
||||
|
||||
await variantAnalysisManager.removeVariantAnalysis(dummyVariantAnalysis);
|
||||
|
||||
expect(removeAnalysisResultsStub).toBeCalledTimes(1);
|
||||
expect(removeStorageStub).toBeCalledTimes(1);
|
||||
expect(variantAnalysisManager.variantAnalysesSize).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rehydrateVariantAnalysis", () => {
|
||||
let variantAnalysis: VariantAnalysis;
|
||||
const variantAnalysisRemovedSpy = jest.fn();
|
||||
let executeCommandSpy: jest.SpiedFunction<typeof commands.executeCommand>;
|
||||
|
||||
@@ -257,6 +257,65 @@ describe("Variant Analysis Monitor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the responses change", () => {
|
||||
let scannedRepos: ApiVariantAnalysisScannedRepository[];
|
||||
|
||||
beforeEach(async () => {
|
||||
scannedRepos = createMockScannedRepos([
|
||||
"pending",
|
||||
"in_progress",
|
||||
"in_progress",
|
||||
"in_progress",
|
||||
"pending",
|
||||
"pending",
|
||||
]);
|
||||
mockApiResponse = createMockApiResponse("in_progress", scannedRepos);
|
||||
mockGetVariantAnalysis.mockResolvedValueOnce(mockApiResponse);
|
||||
|
||||
let nextApiResponse = {
|
||||
...mockApiResponse,
|
||||
scanned_repositories: [...scannedRepos.map((r) => ({ ...r }))],
|
||||
};
|
||||
nextApiResponse.scanned_repositories[0].analysis_status = "succeeded";
|
||||
nextApiResponse.scanned_repositories[1].analysis_status = "succeeded";
|
||||
mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
|
||||
|
||||
nextApiResponse = {
|
||||
...mockApiResponse,
|
||||
scanned_repositories: [
|
||||
...nextApiResponse.scanned_repositories.map((r) => ({ ...r })),
|
||||
],
|
||||
};
|
||||
nextApiResponse.scanned_repositories[2].analysis_status = "succeeded";
|
||||
nextApiResponse.scanned_repositories[5].analysis_status = "succeeded";
|
||||
mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
|
||||
|
||||
nextApiResponse = {
|
||||
...mockApiResponse,
|
||||
scanned_repositories: [
|
||||
...nextApiResponse.scanned_repositories.map((r) => ({ ...r })),
|
||||
],
|
||||
};
|
||||
nextApiResponse.scanned_repositories[3].analysis_status = "succeeded";
|
||||
nextApiResponse.scanned_repositories[4].analysis_status = "failed";
|
||||
mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
|
||||
});
|
||||
|
||||
it("should trigger a download extension command for each repo", async () => {
|
||||
const commandSpy = jest
|
||||
.spyOn(commands, "executeCommand")
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(mockGetVariantAnalysis).toBeCalledTimes(4);
|
||||
expect(commandSpy).toBeCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there are no repos to scan", () => {
|
||||
beforeEach(async () => {
|
||||
scannedRepos = [];
|
||||
|
||||
@@ -60,6 +60,7 @@ describe("db panel", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
@@ -123,6 +124,7 @@ describe("db panel", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
@@ -174,6 +176,7 @@ describe("db panel", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
@@ -213,6 +216,7 @@ describe("db panel", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
@@ -281,6 +285,7 @@ describe("db panel", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
@@ -359,6 +364,7 @@ describe("db panel", () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
@@ -421,6 +427,7 @@ describe("db panel", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
selected: {
|
||||
kind: SelectedDbItemKind.RemoteUserDefinedList,
|
||||
listName: "my-list-2",
|
||||
@@ -477,6 +484,7 @@ describe("db panel", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
selected: {
|
||||
kind: SelectedDbItemKind.RemoteRepository,
|
||||
repositoryName: "owner1/repo1",
|
||||
|
||||
@@ -4,169 +4,152 @@ import { UserCancellationException } from "../../../commandRunner";
|
||||
|
||||
import * as config from "../../../config";
|
||||
import { getRepositorySelection } from "../../../remote-queries/repository-selection";
|
||||
import { DbManager } from "../../../databases/db-manager";
|
||||
import { DbItem, DbItemKind } from "../../../databases/db-item";
|
||||
|
||||
describe("repository selection", () => {
|
||||
let quickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
|
||||
let showInputBoxSpy: jest.SpiedFunction<typeof window.showInputBox>;
|
||||
describe("newQueryRunExperience true", () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(config, "isNewQueryRunExperienceEnabled")
|
||||
.mockReturnValue(true);
|
||||
});
|
||||
|
||||
let getRemoteRepositoryListsSpy: jest.SpiedFunction<
|
||||
typeof config.getRemoteRepositoryLists
|
||||
>;
|
||||
let getRemoteRepositoryListsPathSpy: jest.SpiedFunction<
|
||||
typeof config.getRemoteRepositoryListsPath
|
||||
>;
|
||||
it("should throw error when no database item is selected", async () => {
|
||||
const dbManager = setUpDbManager(undefined);
|
||||
|
||||
let pathExistsStub: jest.SpiedFunction<typeof fs.pathExists>;
|
||||
let fsStatStub: jest.SpiedFunction<typeof fs.stat>;
|
||||
let fsReadFileStub: jest.SpiedFunction<typeof fs.readFile>;
|
||||
await expect(getRepositorySelection(dbManager)).rejects.toThrow(
|
||||
Error("Please select a remote database to run the query against."),
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
quickPickSpy = jest
|
||||
.spyOn(window, "showQuickPick")
|
||||
.mockResolvedValue(undefined);
|
||||
showInputBoxSpy = jest
|
||||
.spyOn(window, "showInputBox")
|
||||
.mockResolvedValue(undefined);
|
||||
it("should throw error when local database item is selected", async () => {
|
||||
const dbManager = setUpDbManager({
|
||||
kind: DbItemKind.LocalDatabase,
|
||||
} as DbItem);
|
||||
|
||||
getRemoteRepositoryListsSpy = jest
|
||||
.spyOn(config, "getRemoteRepositoryLists")
|
||||
.mockReturnValue(undefined);
|
||||
getRemoteRepositoryListsPathSpy = jest
|
||||
.spyOn(config, "getRemoteRepositoryListsPath")
|
||||
.mockReturnValue(undefined);
|
||||
await expect(getRepositorySelection(dbManager)).rejects.toThrow(
|
||||
Error("Local databases and lists are not supported yet."),
|
||||
);
|
||||
});
|
||||
|
||||
pathExistsStub = jest
|
||||
.spyOn(fs, "pathExists")
|
||||
.mockImplementation(() => false);
|
||||
fsStatStub = jest
|
||||
.spyOn(fs, "stat")
|
||||
.mockRejectedValue(new Error("not found"));
|
||||
fsReadFileStub = jest
|
||||
.spyOn(fs, "readFile")
|
||||
.mockRejectedValue(new Error("not found"));
|
||||
});
|
||||
it("should return correct selection when remote system defined list is selected", async () => {
|
||||
const dbManager = setUpDbManager({
|
||||
kind: DbItemKind.RemoteSystemDefinedList,
|
||||
listName: "top_10",
|
||||
} as DbItem);
|
||||
|
||||
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: [],
|
||||
});
|
||||
const repoSelection = await getRepositorySelection(dbManager);
|
||||
|
||||
// Make the function call
|
||||
const repoSelection = await getRepositorySelection();
|
||||
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);
|
||||
|
||||
// Check that the return value is correct
|
||||
expect(repoSelection.repositoryLists).toBeUndefined();
|
||||
expect(repoSelection.owners).toBeUndefined();
|
||||
expect(repoSelection.repositories).toEqual(["foo/bar", "foo/baz"]);
|
||||
expect(repoSelection.repositories).toEqual([
|
||||
"owner1/repo1",
|
||||
"owner1/repo2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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: [],
|
||||
});
|
||||
it("should return correct selection when remote owner is selected", async () => {
|
||||
const dbManager = setUpDbManager({
|
||||
kind: DbItemKind.RemoteOwner,
|
||||
ownerName: "owner2",
|
||||
} as DbItem);
|
||||
|
||||
// Make the function call
|
||||
const repoSelection = await getRepositorySelection();
|
||||
const repoSelection = await getRepositorySelection(dbManager);
|
||||
|
||||
// Check that the return value is correct
|
||||
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.repositoryLists).toEqual(["top_100"]);
|
||||
expect(repoSelection.repositories).toEqual(["owner1/repo2"]);
|
||||
});
|
||||
|
||||
function setUpDbManager(response: DbItem | undefined): DbManager {
|
||||
return {
|
||||
getSelectedDbItem: jest.fn(() => {
|
||||
return response;
|
||||
}),
|
||||
} as any as DbManager;
|
||||
}
|
||||
});
|
||||
|
||||
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 () => {
|
||||
describe("newQueryRunExperience false", () => {
|
||||
let quickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
|
||||
let showInputBoxSpy: jest.SpiedFunction<typeof window.showInputBox>;
|
||||
|
||||
let getRemoteRepositoryListsSpy: jest.SpiedFunction<
|
||||
typeof config.getRemoteRepositoryLists
|
||||
>;
|
||||
let getRemoteRepositoryListsPathSpy: jest.SpiedFunction<
|
||||
typeof config.getRemoteRepositoryListsPath
|
||||
>;
|
||||
|
||||
let pathExistsStub: jest.SpiedFunction<typeof fs.pathExists>;
|
||||
let fsStatStub: jest.SpiedFunction<typeof fs.stat>;
|
||||
let fsReadFileStub: jest.SpiedFunction<typeof fs.readFile>;
|
||||
|
||||
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);
|
||||
|
||||
pathExistsStub = jest
|
||||
.spyOn(fs, "pathExists")
|
||||
.mockImplementation(() => false);
|
||||
fsStatStub = jest
|
||||
.spyOn(fs, "stat")
|
||||
.mockRejectedValue(new Error("not found"));
|
||||
fsReadFileStub = jest
|
||||
.spyOn(fs, "readFile")
|
||||
.mockRejectedValue(new Error("not found"));
|
||||
});
|
||||
describe("repo lists from settings", () => {
|
||||
it("should allow selection from repo lists from your pre-defined config", async () => {
|
||||
// Fake return values
|
||||
quickPickSpy.mockResolvedValue({
|
||||
useAllReposOfOwner: true,
|
||||
repositories: ["foo/bar", "foo/baz"],
|
||||
} 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
getRemoteRepositoryListsSpy.mockReturnValue({
|
||||
list1: ["foo/bar", "foo/baz"],
|
||||
list2: [],
|
||||
});
|
||||
|
||||
// Make the function call
|
||||
const repoSelection = await getRepositorySelection();
|
||||
@@ -174,44 +157,290 @@ describe("repository selection", () => {
|
||||
// Check that the return value is correct
|
||||
expect(repoSelection.repositoryLists).toBeUndefined();
|
||||
expect(repoSelection.owners).toBeUndefined();
|
||||
expect(repoSelection.repositories).toEqual([repo]);
|
||||
expect(repoSelection.repositories).toEqual(["foo/bar", "foo/baz"]);
|
||||
});
|
||||
});
|
||||
|
||||
// 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 () => {
|
||||
it("should return an error for an empty repository list", async () => {
|
||||
// Fake return values
|
||||
quickPickSpy.mockResolvedValue({
|
||||
useCustomRepo: true,
|
||||
repositories: [],
|
||||
} as unknown as QuickPickItem);
|
||||
getRemoteRepositoryListsSpy.mockReturnValue({}); // no pre-defined repo lists
|
||||
showInputBoxSpy.mockResolvedValue(repo);
|
||||
getRemoteRepositoryListsSpy.mockReturnValue({
|
||||
list1: ["foo/bar", "foo/baz"],
|
||||
list2: [],
|
||||
});
|
||||
|
||||
// Function call should throw a UserCancellationException
|
||||
await expect(getRepositorySelection()).rejects.toThrow(
|
||||
"Invalid repository format",
|
||||
"No repositories selected",
|
||||
);
|
||||
await expect(getRepositorySelection()).rejects.toThrow(
|
||||
UserCancellationException,
|
||||
);
|
||||
await expect(getRepositorySelection()).rejects.toHaveProperty(
|
||||
"silent",
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should be ok for the user to change their mind", async () => {
|
||||
quickPickSpy.mockResolvedValue({
|
||||
useCustomRepo: true,
|
||||
} as unknown as QuickPickItem);
|
||||
getRemoteRepositoryListsSpy.mockReturnValue({});
|
||||
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: [],
|
||||
});
|
||||
|
||||
// The user pressed escape to cancel the operation
|
||||
showInputBoxSpy.mockResolvedValue(undefined);
|
||||
// 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", () => {
|
||||
it("should fail if path does not exist", async () => {
|
||||
const fakeFilePath = "/path/that/does/not/exist.json";
|
||||
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
|
||||
pathExistsStub.mockImplementation(() => false);
|
||||
|
||||
await expect(getRepositorySelection()).rejects.toThrow(
|
||||
`External repository lists file does not exist at ${fakeFilePath}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail if path points to directory", async () => {
|
||||
const fakeFilePath = "/path/to/dir";
|
||||
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
|
||||
pathExistsStub.mockImplementation(() => true);
|
||||
fsStatStub.mockResolvedValue({ isDirectory: () => true } as any);
|
||||
|
||||
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 fakeFilePath = "/path/to/file.json";
|
||||
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
|
||||
pathExistsStub.mockImplementation(() => true);
|
||||
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
|
||||
fsReadFileStub.mockResolvedValue("not-json" as any as Buffer);
|
||||
|
||||
await expect(getRepositorySelection()).rejects.toThrow(
|
||||
"Invalid repository lists file. It should contain valid JSON.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail if file contains array", async () => {
|
||||
const fakeFilePath = "/path/to/file.json";
|
||||
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
|
||||
pathExistsStub.mockImplementation(() => true);
|
||||
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
|
||||
fsReadFileStub.mockResolvedValue("[]" as any as Buffer);
|
||||
|
||||
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 fakeFilePath = "/path/to/file.json";
|
||||
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
|
||||
pathExistsStub.mockImplementation(() => true);
|
||||
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
|
||||
const repoLists = {
|
||||
list1: "owner1/repo1",
|
||||
};
|
||||
fsReadFileStub.mockResolvedValue(
|
||||
JSON.stringify(repoLists) as any as Buffer,
|
||||
);
|
||||
|
||||
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 fakeFilePath = "/path/to/file.json";
|
||||
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
|
||||
pathExistsStub.mockImplementation(() => true);
|
||||
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
|
||||
const repoLists = {
|
||||
list1: ["owner1/repo1", "owner2/repo2"],
|
||||
list2: ["owner3/repo3"],
|
||||
};
|
||||
fsReadFileStub.mockResolvedValue(
|
||||
JSON.stringify(repoLists) as any as Buffer,
|
||||
);
|
||||
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",
|
||||
@@ -219,98 +448,10 @@ describe("repository selection", () => {
|
||||
await expect(getRepositorySelection()).rejects.toThrow(
|
||||
UserCancellationException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("external repository lists file", () => {
|
||||
it("should fail if path does not exist", async () => {
|
||||
const fakeFilePath = "/path/that/does/not/exist.json";
|
||||
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
|
||||
pathExistsStub.mockImplementation(() => false);
|
||||
|
||||
await expect(getRepositorySelection()).rejects.toThrow(
|
||||
`External repository lists file does not exist at ${fakeFilePath}`,
|
||||
await expect(getRepositorySelection()).rejects.toHaveProperty(
|
||||
"silent",
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail if path points to directory", async () => {
|
||||
const fakeFilePath = "/path/to/dir";
|
||||
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
|
||||
pathExistsStub.mockImplementation(() => true);
|
||||
fsStatStub.mockResolvedValue({ isDirectory: () => true } as any);
|
||||
|
||||
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 fakeFilePath = "/path/to/file.json";
|
||||
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
|
||||
pathExistsStub.mockImplementation(() => true);
|
||||
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
|
||||
fsReadFileStub.mockResolvedValue("not-json" as any as Buffer);
|
||||
|
||||
await expect(getRepositorySelection()).rejects.toThrow(
|
||||
"Invalid repository lists file. It should contain valid JSON.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail if file contains array", async () => {
|
||||
const fakeFilePath = "/path/to/file.json";
|
||||
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
|
||||
pathExistsStub.mockImplementation(() => true);
|
||||
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
|
||||
fsReadFileStub.mockResolvedValue("[]" as any as Buffer);
|
||||
|
||||
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 fakeFilePath = "/path/to/file.json";
|
||||
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
|
||||
pathExistsStub.mockImplementation(() => true);
|
||||
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
|
||||
const repoLists = {
|
||||
list1: "owner1/repo1",
|
||||
};
|
||||
fsReadFileStub.mockResolvedValue(
|
||||
JSON.stringify(repoLists) as any as Buffer,
|
||||
);
|
||||
|
||||
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 fakeFilePath = "/path/to/file.json";
|
||||
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
|
||||
pathExistsStub.mockImplementation(() => true);
|
||||
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
|
||||
const repoLists = {
|
||||
list1: ["owner1/repo1", "owner2/repo2"],
|
||||
list2: ["owner3/repo3"],
|
||||
};
|
||||
fsReadFileStub.mockResolvedValue(
|
||||
JSON.stringify(repoLists) as any as Buffer,
|
||||
);
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
34
extensions/ql-vscode/test/factories/db-item-factories.ts
Normal file
34
extensions/ql-vscode/test/factories/db-item-factories.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
DbItemKind,
|
||||
RemoteUserDefinedListDbItem,
|
||||
RootRemoteDbItem,
|
||||
} from "../../src/databases/db-item";
|
||||
|
||||
export function createRootRemoteDbItem(): RootRemoteDbItem {
|
||||
return {
|
||||
kind: DbItemKind.RootRemote,
|
||||
children: [],
|
||||
expanded: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function createRemoteUserDefinedListDbItem({
|
||||
name = "list1",
|
||||
}: {
|
||||
name: string;
|
||||
}): RemoteUserDefinedListDbItem {
|
||||
return {
|
||||
kind: DbItemKind.RemoteUserDefinedList,
|
||||
selected: false,
|
||||
expanded: false,
|
||||
listName: name,
|
||||
repos: [
|
||||
{
|
||||
kind: DbItemKind.RemoteRepo,
|
||||
selected: false,
|
||||
repoFullName: "repo1",
|
||||
parentListName: name,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -9,5 +9,6 @@
|
||||
"lists": [],
|
||||
"databases": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"expanded": []
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"expanded": [],
|
||||
"selected": {
|
||||
"kind": "remoteUserDefinedList",
|
||||
"listName": "repoList1"
|
||||
|
||||
@@ -22,6 +22,7 @@ describe("db config validation", () => {
|
||||
somethingElse: "bar",
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
} as any as DbConfig;
|
||||
|
||||
const validationOutput = configValidator.validate(dbConfig);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
ExpandedDbItem,
|
||||
ExpandedDbItemKind,
|
||||
} from "../../../src/databases/config/db-config";
|
||||
import {
|
||||
RemoteUserDefinedListDbItem,
|
||||
RootRemoteDbItem,
|
||||
} from "../../../src/databases/db-item";
|
||||
import { calculateNewExpandedState } from "../../../src/databases/db-item-expansion";
|
||||
import {
|
||||
createRemoteUserDefinedListDbItem,
|
||||
createRootRemoteDbItem,
|
||||
} from "../../factories/db-item-factories";
|
||||
|
||||
describe("db item expansion", () => {
|
||||
it("should add an expanded item to an existing list", () => {
|
||||
const currentExpandedItems: ExpandedDbItem[] = [
|
||||
{
|
||||
kind: ExpandedDbItemKind.RootRemote,
|
||||
},
|
||||
{
|
||||
kind: ExpandedDbItemKind.RemoteUserDefinedList,
|
||||
listName: "list1",
|
||||
},
|
||||
];
|
||||
|
||||
const dbItem: RemoteUserDefinedListDbItem =
|
||||
createRemoteUserDefinedListDbItem({
|
||||
name: "list2",
|
||||
});
|
||||
|
||||
const newExpandedItems = calculateNewExpandedState(
|
||||
currentExpandedItems,
|
||||
dbItem,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(newExpandedItems).toEqual([
|
||||
...currentExpandedItems,
|
||||
{
|
||||
kind: ExpandedDbItemKind.RemoteUserDefinedList,
|
||||
listName: "list2",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should add an expanded item to an empty list", () => {
|
||||
const dbItem: RemoteUserDefinedListDbItem =
|
||||
createRemoteUserDefinedListDbItem({
|
||||
name: "list2",
|
||||
});
|
||||
|
||||
const newExpandedItems = calculateNewExpandedState([], dbItem, true);
|
||||
|
||||
expect(newExpandedItems).toEqual([
|
||||
{
|
||||
kind: ExpandedDbItemKind.RemoteUserDefinedList,
|
||||
listName: "list2",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should remove a collapsed item from a list", () => {
|
||||
const currentExpandedItems: ExpandedDbItem[] = [
|
||||
{
|
||||
kind: ExpandedDbItemKind.RootRemote,
|
||||
},
|
||||
{
|
||||
kind: ExpandedDbItemKind.RemoteUserDefinedList,
|
||||
listName: "list1",
|
||||
},
|
||||
];
|
||||
|
||||
const dbItem: RemoteUserDefinedListDbItem =
|
||||
createRemoteUserDefinedListDbItem({
|
||||
name: "list1",
|
||||
});
|
||||
|
||||
const newExpandedItems = calculateNewExpandedState(
|
||||
currentExpandedItems,
|
||||
dbItem,
|
||||
false,
|
||||
);
|
||||
|
||||
expect(newExpandedItems).toEqual([
|
||||
{
|
||||
kind: ExpandedDbItemKind.RootRemote,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should remove a collapsed item from a list that becomes empty", () => {
|
||||
const currentExpandedItems: ExpandedDbItem[] = [
|
||||
{
|
||||
kind: ExpandedDbItemKind.RootRemote,
|
||||
},
|
||||
];
|
||||
|
||||
const dbItem: RootRemoteDbItem = createRootRemoteDbItem();
|
||||
|
||||
const newExpandedItems = calculateNewExpandedState(
|
||||
currentExpandedItems,
|
||||
dbItem,
|
||||
false,
|
||||
);
|
||||
|
||||
expect(newExpandedItems).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,12 @@
|
||||
import { DbItem, DbItemKind } from "../../../src/databases/db-item";
|
||||
import { getSelectedDbItem } from "../../../src/databases/db-item-selection";
|
||||
|
||||
describe("db item selection", () => {
|
||||
it("should return undefined if no item is selected", () => {
|
||||
const dbItems: DbItem[] = [
|
||||
{
|
||||
kind: DbItemKind.RootRemote,
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
kind: DbItemKind.RemoteSystemDefinedList,
|
||||
@@ -27,6 +29,7 @@ describe("db item selection", () => {
|
||||
},
|
||||
{
|
||||
kind: DbItemKind.RemoteUserDefinedList,
|
||||
expanded: false,
|
||||
listName: "my list",
|
||||
repos: [
|
||||
{
|
||||
@@ -46,9 +49,11 @@ describe("db item selection", () => {
|
||||
},
|
||||
{
|
||||
kind: DbItemKind.RootLocal,
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
kind: DbItemKind.LocalList,
|
||||
expanded: false,
|
||||
listName: "list-1",
|
||||
databases: [
|
||||
{
|
||||
@@ -89,9 +94,11 @@ describe("db item selection", () => {
|
||||
const dbItems: DbItem[] = [
|
||||
{
|
||||
kind: DbItemKind.RootLocal,
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
kind: DbItemKind.LocalList,
|
||||
expanded: false,
|
||||
listName: "list-1",
|
||||
databases: [
|
||||
{
|
||||
@@ -139,6 +146,7 @@ describe("db item selection", () => {
|
||||
const dbItems: DbItem[] = [
|
||||
{
|
||||
kind: DbItemKind.RootRemote,
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
kind: DbItemKind.RemoteSystemDefinedList,
|
||||
@@ -154,6 +162,7 @@ describe("db item selection", () => {
|
||||
},
|
||||
{
|
||||
kind: DbItemKind.RemoteUserDefinedList,
|
||||
expanded: false,
|
||||
listName: "my list",
|
||||
repos: [
|
||||
{
|
||||
@@ -175,6 +184,7 @@ describe("db item selection", () => {
|
||||
|
||||
expect(getSelectedDbItem(dbItems)).toEqual({
|
||||
kind: DbItemKind.RemoteUserDefinedList,
|
||||
expanded: false,
|
||||
listName: "my list",
|
||||
repos: [
|
||||
{
|
||||
@@ -196,6 +206,7 @@ describe("db item selection", () => {
|
||||
const dbItems: DbItem[] = [
|
||||
{
|
||||
kind: DbItemKind.RootRemote,
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
kind: DbItemKind.RemoteSystemDefinedList,
|
||||
@@ -211,6 +222,7 @@ describe("db item selection", () => {
|
||||
},
|
||||
{
|
||||
kind: DbItemKind.RemoteUserDefinedList,
|
||||
expanded: false,
|
||||
listName: "my list",
|
||||
repos: [
|
||||
{
|
||||
@@ -250,6 +262,7 @@ describe("db item selection", () => {
|
||||
const dbItems: DbItem[] = [
|
||||
{
|
||||
kind: DbItemKind.RootRemote,
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
kind: DbItemKind.RemoteSystemDefinedList,
|
||||
@@ -265,6 +278,7 @@ describe("db item selection", () => {
|
||||
},
|
||||
{
|
||||
kind: DbItemKind.RemoteUserDefinedList,
|
||||
expanded: false,
|
||||
listName: "my list",
|
||||
repos: [],
|
||||
selected: false,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
DbConfig,
|
||||
ExpandedDbItemKind,
|
||||
SelectedDbItemKind,
|
||||
} from "../../../src/databases/config/db-config";
|
||||
import {
|
||||
@@ -28,12 +29,14 @@ describe("db tree creator", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
|
||||
const dbTreeRoot = createRemoteTree(dbConfig);
|
||||
|
||||
expect(dbTreeRoot).toBeTruthy();
|
||||
expect(dbTreeRoot.kind).toBe(DbItemKind.RootRemote);
|
||||
expect(dbTreeRoot.expanded).toBe(false);
|
||||
expect(dbTreeRoot.children.length).toBe(3);
|
||||
expect(dbTreeRoot.children[0]).toEqual({
|
||||
kind: DbItemKind.RemoteSystemDefinedList,
|
||||
@@ -80,6 +83,7 @@ describe("db tree creator", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
|
||||
const dbTreeRoot = createRemoteTree(dbConfig);
|
||||
@@ -94,6 +98,7 @@ describe("db tree creator", () => {
|
||||
expect(repositoryListNodes[0]).toEqual({
|
||||
kind: DbItemKind.RemoteUserDefinedList,
|
||||
selected: false,
|
||||
expanded: false,
|
||||
listName: dbConfig.databases.remote.repositoryLists[0].name,
|
||||
repos: dbConfig.databases.remote.repositoryLists[0].repositories.map(
|
||||
(repo) => ({
|
||||
@@ -107,6 +112,7 @@ describe("db tree creator", () => {
|
||||
expect(repositoryListNodes[1]).toEqual({
|
||||
kind: DbItemKind.RemoteUserDefinedList,
|
||||
selected: false,
|
||||
expanded: false,
|
||||
listName: dbConfig.databases.remote.repositoryLists[1].name,
|
||||
repos: dbConfig.databases.remote.repositoryLists[1].repositories.map(
|
||||
(repo) => ({
|
||||
@@ -132,6 +138,7 @@ describe("db tree creator", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
|
||||
const dbTreeRoot = createRemoteTree(dbConfig);
|
||||
@@ -166,6 +173,7 @@ describe("db tree creator", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
|
||||
const dbTreeRoot = createRemoteTree(dbConfig);
|
||||
@@ -215,6 +223,7 @@ describe("db tree creator", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
selected: {
|
||||
kind: SelectedDbItemKind.RemoteUserDefinedList,
|
||||
listName: "my-list-1",
|
||||
@@ -246,6 +255,7 @@ describe("db tree creator", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
selected: {
|
||||
kind: SelectedDbItemKind.RemoteOwner,
|
||||
ownerName: "owner1",
|
||||
@@ -278,6 +288,7 @@ describe("db tree creator", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
selected: {
|
||||
kind: SelectedDbItemKind.RemoteRepository,
|
||||
repositoryName: "owner1/repo2",
|
||||
@@ -313,6 +324,7 @@ describe("db tree creator", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
selected: {
|
||||
kind: SelectedDbItemKind.RemoteRepository,
|
||||
listName: "my-list-1",
|
||||
@@ -335,6 +347,81 @@ describe("db tree creator", () => {
|
||||
expect(listNodes[0].repos[0].selected).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("expanded db items", () => {
|
||||
it("should allow expanding the root remote list node", () => {
|
||||
const dbConfig: DbConfig = {
|
||||
databases: {
|
||||
remote: {
|
||||
repositoryLists: [],
|
||||
owners: [],
|
||||
repositories: [],
|
||||
},
|
||||
local: {
|
||||
lists: [],
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [
|
||||
{
|
||||
kind: ExpandedDbItemKind.RootRemote,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const dbTreeRoot = createRemoteTree(dbConfig);
|
||||
|
||||
expect(dbTreeRoot).toBeTruthy();
|
||||
expect(dbTreeRoot.kind).toBe(DbItemKind.RootRemote);
|
||||
expect(dbTreeRoot.expanded).toBe(true);
|
||||
});
|
||||
|
||||
it("should allow expanding a remote user defined list node", () => {
|
||||
const dbConfig: DbConfig = {
|
||||
databases: {
|
||||
remote: {
|
||||
repositoryLists: [
|
||||
{
|
||||
name: "my-list-1",
|
||||
repositories: [
|
||||
"owner1/repo1",
|
||||
"owner1/repo2",
|
||||
"owner2/repo1",
|
||||
],
|
||||
},
|
||||
],
|
||||
owners: [],
|
||||
repositories: [],
|
||||
},
|
||||
local: {
|
||||
lists: [],
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [
|
||||
{
|
||||
kind: ExpandedDbItemKind.RootRemote,
|
||||
},
|
||||
{
|
||||
kind: ExpandedDbItemKind.RemoteUserDefinedList,
|
||||
listName: "my-list-1",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const dbTreeRoot = createRemoteTree(dbConfig);
|
||||
|
||||
expect(dbTreeRoot).toBeTruthy();
|
||||
expect(dbTreeRoot.kind).toBe(DbItemKind.RootRemote);
|
||||
expect(dbTreeRoot.expanded).toBe(true);
|
||||
const repositoryListNodes = dbTreeRoot.children.filter(
|
||||
isRemoteUserDefinedListDbItem,
|
||||
);
|
||||
|
||||
expect(repositoryListNodes.length).toBe(1);
|
||||
expect(repositoryListNodes[0].expanded).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createLocalTree", () => {
|
||||
@@ -351,12 +438,14 @@ describe("db tree creator", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
|
||||
const dbTreeRoot = createLocalTree(dbConfig);
|
||||
|
||||
expect(dbTreeRoot).toBeTruthy();
|
||||
expect(dbTreeRoot.kind).toBe(DbItemKind.RootLocal);
|
||||
expect(dbTreeRoot.expanded).toBe(false);
|
||||
expect(dbTreeRoot.children.length).toBe(0);
|
||||
});
|
||||
|
||||
@@ -402,6 +491,7 @@ describe("db tree creator", () => {
|
||||
databases: [],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
|
||||
const dbTreeRoot = createLocalTree(dbConfig);
|
||||
@@ -415,6 +505,7 @@ describe("db tree creator", () => {
|
||||
expect(localListNodes.length).toBe(2);
|
||||
expect(localListNodes[0]).toEqual({
|
||||
kind: DbItemKind.LocalList,
|
||||
expanded: false,
|
||||
selected: false,
|
||||
listName: dbConfig.databases.local.lists[0].name,
|
||||
databases: dbConfig.databases.local.lists[0].databases.map((db) => ({
|
||||
@@ -429,6 +520,7 @@ describe("db tree creator", () => {
|
||||
});
|
||||
expect(localListNodes[1]).toEqual({
|
||||
kind: DbItemKind.LocalList,
|
||||
expanded: false,
|
||||
selected: false,
|
||||
listName: dbConfig.databases.local.lists[1].name,
|
||||
databases: dbConfig.databases.local.lists[1].databases.map((db) => ({
|
||||
@@ -469,6 +561,7 @@ describe("db tree creator", () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
expanded: [],
|
||||
};
|
||||
|
||||
const dbTreeRoot = createLocalTree(dbConfig);
|
||||
|
||||
@@ -121,6 +121,60 @@
|
||||
"required": ["remote", "local"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"expanded": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["rootLocal"]
|
||||
}
|
||||
},
|
||||
"required": ["kind"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["localUserDefinedList"]
|
||||
},
|
||||
"listName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["kind", "listName"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["rootRemote"]
|
||||
}
|
||||
},
|
||||
"required": ["kind"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["remoteUserDefinedList"]
|
||||
},
|
||||
"listName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["kind", "listName"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"selected": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
@@ -211,6 +265,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["databases"],
|
||||
"required": ["databases", "expanded"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user