Merge remote-tracking branch 'origin/main' into koesie10/new-variant-analysis-statuses
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
## 1.7.5 - 8 November 2022
|
||||
|
||||
- Fix a bug where the AST Viewer was not working unless the associated CodeQL library pack is in the workspace. [#1735](https://github.com/github/vscode-codeql/pull/1735)
|
||||
|
||||
## 1.7.4 - 29 October 2022
|
||||
|
||||
No user facing changes.
|
||||
|
||||
30
extensions/ql-vscode/package-lock.json
generated
30
extensions/ql-vscode/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "vscode-codeql",
|
||||
"version": "1.7.5",
|
||||
"version": "1.7.6",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vscode-codeql",
|
||||
"version": "1.7.5",
|
||||
"version": "1.7.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/plugin-retry": "^3.0.9",
|
||||
@@ -15,6 +15,7 @@
|
||||
"@primer/react": "^35.0.0",
|
||||
"@vscode/codicons": "^0.0.31",
|
||||
"@vscode/webview-ui-toolkit": "^1.0.1",
|
||||
"ajv": "^8.11.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"classnames": "~2.2.6",
|
||||
@@ -105,7 +106,6 @@
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||
"@typescript-eslint/parser": "^4.26.0",
|
||||
"@vscode/test-electron": "^2.2.0",
|
||||
"ajv": "^8.11.0",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"applicationinsights": "^2.3.5",
|
||||
"babel-loader": "^8.2.5",
|
||||
@@ -14650,7 +14650,6 @@
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
||||
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
@@ -21602,8 +21601,7 @@
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.2.11",
|
||||
@@ -29613,8 +29611,7 @@
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
"version": "1.0.1",
|
||||
@@ -33897,7 +33894,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -34880,7 +34876,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -38512,7 +38507,6 @@
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
@@ -51543,7 +51537,6 @@
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
||||
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
@@ -57032,8 +57025,7 @@
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.11",
|
||||
@@ -63145,8 +63137,7 @@
|
||||
"json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
|
||||
},
|
||||
"json-stable-stringify-without-jsonify": {
|
||||
"version": "1.0.1",
|
||||
@@ -66535,8 +66526,7 @@
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.10.3",
|
||||
@@ -67289,8 +67279,7 @@
|
||||
"require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
|
||||
},
|
||||
"require-main-filename": {
|
||||
"version": "1.0.1",
|
||||
@@ -70084,7 +70073,6 @@
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.7.5",
|
||||
"version": "1.7.6",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -1299,6 +1299,7 @@
|
||||
"@primer/react": "^35.0.0",
|
||||
"@vscode/codicons": "^0.0.31",
|
||||
"@vscode/webview-ui-toolkit": "^1.0.1",
|
||||
"ajv": "^8.11.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"classnames": "~2.2.6",
|
||||
@@ -1389,7 +1390,6 @@
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||
"@typescript-eslint/parser": "^4.26.0",
|
||||
"@vscode/test-electron": "^2.2.0",
|
||||
"ajv": "^8.11.0",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"applicationinsights": "^2.3.5",
|
||||
"babel-loader": "^8.2.5",
|
||||
|
||||
@@ -970,6 +970,12 @@ export class CodeQLCliServer implements Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
async packResolveDependencies(dir: string): Promise<{ [pack: string]: string }> {
|
||||
// Uses the default `--mode use-lock`, which creates the lock file if it doesn't exist.
|
||||
const results: { [pack: string]: string } = await this.runJsonCodeQlCliCommand(['pack', 'resolve-dependencies'], [dir], 'Resolving pack dependencies');
|
||||
return results;
|
||||
}
|
||||
|
||||
async generateDil(qloFile: string, outFile: string): Promise<void> {
|
||||
const extraArgs = await this.cliConstraints.supportsDecompileDil()
|
||||
? ['--kind', 'dil', '-o', outFile, qloFile]
|
||||
|
||||
50
extensions/ql-vscode/src/common/value-result.ts
Normal file
50
extensions/ql-vscode/src/common/value-result.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Represents a result that can be either a value or some errors.
|
||||
*/
|
||||
export class ValueResult<TValue> {
|
||||
private constructor(
|
||||
private readonly errorMsgs: string[],
|
||||
private readonly val?: TValue,
|
||||
) {
|
||||
}
|
||||
|
||||
public static ok<TValue>(value: TValue): ValueResult<TValue> {
|
||||
if (value === undefined) {
|
||||
throw new Error('Value must be set for successful result');
|
||||
}
|
||||
|
||||
return new ValueResult([], value);
|
||||
}
|
||||
|
||||
public static fail<TValue>(errorMsgs: string[]): ValueResult<TValue> {
|
||||
if (errorMsgs.length === 0) {
|
||||
throw new Error('At least one error message must be set for a failed result');
|
||||
}
|
||||
|
||||
return new ValueResult<TValue>(errorMsgs, undefined);
|
||||
}
|
||||
|
||||
public get isOk(): boolean {
|
||||
return this.errorMsgs.length === 0;
|
||||
}
|
||||
|
||||
public get isFailure(): boolean {
|
||||
return this.errorMsgs.length > 0;
|
||||
}
|
||||
|
||||
public get errors(): string[] {
|
||||
if (!this.errorMsgs) {
|
||||
throw new Error('Cannot get error for successful result');
|
||||
}
|
||||
|
||||
return this.errorMsgs;
|
||||
}
|
||||
|
||||
public get value(): TValue {
|
||||
if (this.val === undefined) {
|
||||
throw new Error('Cannot get value for unsuccessful result');
|
||||
}
|
||||
|
||||
return this.val;
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import { DatabaseManager, DatabaseItem } from '../databases';
|
||||
import fileRangeFromURI from './fileRangeFromURI';
|
||||
import { ProgressCallback } from '../commandRunner';
|
||||
import { KeyType } from './keyType';
|
||||
import { qlpackOfDatabase, resolveQueries } from './queryResolver';
|
||||
import { qlpackOfDatabase, resolveQueries, runContextualQuery } from './queryResolver';
|
||||
import { CancellationToken, LocationLink, Uri } from 'vscode';
|
||||
import { createInitialQueryInfo, QueryWithResults } from '../run-queries-shared';
|
||||
import { QueryWithResults } from '../run-queries-shared';
|
||||
import { QueryRunner } from '../queryRunner';
|
||||
|
||||
export const SELECT_QUERY_NAME = '#select';
|
||||
@@ -56,15 +56,7 @@ export async function getLocationsForUriString(
|
||||
|
||||
const links: FullLocationLink[] = [];
|
||||
for (const query of await resolveQueries(cli, qlpack, keyType)) {
|
||||
const initialInfo = await createInitialQueryInfo(
|
||||
Uri.file(query),
|
||||
{
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(),
|
||||
},
|
||||
false
|
||||
);
|
||||
const results = await qs.compileAndRunQueryAgainstDatabase(db, initialInfo, queryStorageDir, progress, token, templates);
|
||||
const results = await runContextualQuery(query, db, queryStorageDir, qs, cli, progress, token, templates);
|
||||
if (results.successful) {
|
||||
links.push(...await getLinksFromResults(results, cli, db, filter));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as tmp from 'tmp-promise';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as helpers from '../helpers';
|
||||
import {
|
||||
@@ -12,6 +13,11 @@ import {
|
||||
import { CodeQLCliServer } from '../cli';
|
||||
import { DatabaseItem } from '../databases';
|
||||
import { QlPacksForLanguage } from '../helpers';
|
||||
import { logger } from '../logging';
|
||||
import { createInitialQueryInfo } from '../run-queries-shared';
|
||||
import { CancellationToken, Uri } from 'vscode';
|
||||
import { ProgressCallback } from '../commandRunner';
|
||||
import { QueryRunner } from '../queryRunner';
|
||||
|
||||
export async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem): Promise<QlPacksForLanguage> {
|
||||
if (db.contents === undefined) {
|
||||
@@ -104,3 +110,69 @@ export async function resolveQueries(cli: CodeQLCliServer, qlpacks: QlPacksForLa
|
||||
void helpers.showAndLogErrorMessage(errorMessage);
|
||||
throw new Error(`Couldn't find any queries tagged ${tagOfKeyType(keyType)} in any of the following packs: ${packsToSearch.join(', ')}.`);
|
||||
}
|
||||
|
||||
async function resolveContextualQuery(cli: CodeQLCliServer, query: string): Promise<{ packPath: string, createdTempLockFile: boolean }> {
|
||||
// Contextual queries now live within the standard library packs.
|
||||
// This simplifies distribution (you don't need the standard query pack to use the AST viewer),
|
||||
// but if the library pack doesn't have a lockfile, we won't be able to find
|
||||
// other pack dependencies of the library pack.
|
||||
|
||||
// Work out the enclosing pack.
|
||||
const packContents = await cli.packPacklist(query, false);
|
||||
const packFilePath = packContents.find((p) => ['codeql-pack.yml', 'qlpack.yml'].includes(path.basename(p)));
|
||||
if (packFilePath === undefined) {
|
||||
// Should not happen; we already resolved this query.
|
||||
throw new Error(`Could not find a CodeQL pack file for the pack enclosing the contextual query ${query}`);
|
||||
}
|
||||
const packPath = path.dirname(packFilePath);
|
||||
const lockFilePath = packContents.find((p) => ['codeql-pack.lock.yml', 'qlpack.lock.yml'].includes(path.basename(p)));
|
||||
let createdTempLockFile = false;
|
||||
if (!lockFilePath) {
|
||||
// No lock file, likely because this library pack is in the package cache.
|
||||
// Create a lock file so that we can resolve dependencies and library path
|
||||
// for the contextual query.
|
||||
void logger.log(`Library pack ${packPath} is missing a lock file; creating a temporary lock file`);
|
||||
await cli.packResolveDependencies(packPath);
|
||||
createdTempLockFile = true;
|
||||
// Clear CLI server pack cache before installing dependencies,
|
||||
// so that it picks up the new lock file, not the previously cached pack.
|
||||
void logger.log('Clearing the CodeQL CLI server\'s pack cache');
|
||||
await cli.clearCache();
|
||||
// Install dependencies.
|
||||
void logger.log(`Installing package dependencies for library pack ${packPath}`);
|
||||
await cli.packInstall(packPath);
|
||||
}
|
||||
return { packPath, createdTempLockFile };
|
||||
}
|
||||
|
||||
async function removeTemporaryLockFile(packPath: string) {
|
||||
const tempLockFilePath = path.resolve(packPath, 'codeql-pack.lock.yml');
|
||||
void logger.log(`Deleting temporary package lock file at ${tempLockFilePath}`);
|
||||
// It's fine if the file doesn't exist.
|
||||
await fs.promises.rm(path.resolve(packPath, 'codeql-pack.lock.yml'), { force: true });
|
||||
}
|
||||
|
||||
export async function runContextualQuery(query: string, db: DatabaseItem, queryStorageDir: string, qs: QueryRunner, cli: CodeQLCliServer, progress: ProgressCallback, token: CancellationToken, templates: Record<string, string>) {
|
||||
const { packPath, createdTempLockFile } = await resolveContextualQuery(cli, query);
|
||||
const initialInfo = await createInitialQueryInfo(
|
||||
Uri.file(query),
|
||||
{
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(),
|
||||
},
|
||||
false
|
||||
);
|
||||
void logger.log(`Running contextual query ${query}; results will be stored in ${queryStorageDir}`);
|
||||
const queryResult = await qs.compileAndRunQueryAgainstDatabase(
|
||||
db,
|
||||
initialInfo,
|
||||
queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
templates
|
||||
);
|
||||
if (createdTempLockFile) {
|
||||
await removeTemporaryLockFile(packPath);
|
||||
}
|
||||
return queryResult;
|
||||
}
|
||||
|
||||
@@ -21,18 +21,17 @@ import {
|
||||
KeyType,
|
||||
} from './keyType';
|
||||
import { FullLocationLink, getLocationsForUriString, TEMPLATE_NAME } from './locationFinder';
|
||||
import { qlpackOfDatabase, resolveQueries } from './queryResolver';
|
||||
import { qlpackOfDatabase, resolveQueries, runContextualQuery } from './queryResolver';
|
||||
import { isCanary, NO_CACHE_AST_VIEWER } from '../config';
|
||||
import { createInitialQueryInfo, QueryWithResults } from '../run-queries-shared';
|
||||
import { QueryWithResults } from '../run-queries-shared';
|
||||
import { QueryRunner } from '../queryRunner';
|
||||
|
||||
/**
|
||||
* Run templated CodeQL queries to find definitions and references in
|
||||
* Runs templated CodeQL queries to find definitions in
|
||||
* source-language files. We may eventually want to find a way to
|
||||
* generalize this to other custom queries, e.g. showing dataflow to
|
||||
* or from a selected identifier.
|
||||
*/
|
||||
|
||||
export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
private cache: CachedOperation<LocationLink[]>;
|
||||
|
||||
@@ -77,6 +76,12 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs templated CodeQL queries to find references in
|
||||
* source-language files. We may eventually want to find a way to
|
||||
* generalize this to other custom queries, e.g. showing dataflow to
|
||||
* or from a selected identifier.
|
||||
*/
|
||||
export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
||||
private cache: CachedOperation<FullLocationLink[]>;
|
||||
|
||||
@@ -131,6 +136,10 @@ type QueryWithDb = {
|
||||
dbUri: Uri
|
||||
};
|
||||
|
||||
/**
|
||||
* Run templated CodeQL queries to produce AST information for
|
||||
* source-language files.
|
||||
*/
|
||||
export class TemplatePrintAstProvider {
|
||||
private cache: CachedOperation<QueryWithDb>;
|
||||
|
||||
@@ -199,29 +208,18 @@ export class TemplatePrintAstProvider {
|
||||
zippedArchive.pathWithinSourceArchive
|
||||
};
|
||||
|
||||
const initialInfo = await createInitialQueryInfo(
|
||||
Uri.file(query),
|
||||
{
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(),
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const queryResult = await runContextualQuery(query, db, this.queryStorageDir, this.qs, this.cli, progress, token, templates);
|
||||
return {
|
||||
query: await this.qs.compileAndRunQueryAgainstDatabase(
|
||||
db,
|
||||
initialInfo,
|
||||
this.queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
templates
|
||||
),
|
||||
query: queryResult,
|
||||
dbUri: db.databaseUri
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run templated CodeQL queries to produce CFG information for
|
||||
* source-language files.
|
||||
*/
|
||||
export class TemplatePrintCfgProvider {
|
||||
private cache: CachedOperation<[Uri, Record<string, string>] | undefined>;
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ async function findDataset(parentDirectory: string): Promise<vscode.Uri> {
|
||||
|
||||
// exported for testing
|
||||
export async function findSourceArchive(
|
||||
databasePath: string, silent = false
|
||||
databasePath: string
|
||||
): Promise<vscode.Uri | undefined> {
|
||||
const relativePaths = ['src', 'output/src_archive'];
|
||||
|
||||
@@ -138,11 +138,10 @@ export async function findSourceArchive(
|
||||
return vscode.Uri.file(basePath);
|
||||
}
|
||||
}
|
||||
if (!silent) {
|
||||
void showAndLogInformationMessage(
|
||||
`Could not find source archive for database '${databasePath}'. Assuming paths are absolute.`
|
||||
);
|
||||
}
|
||||
|
||||
void showAndLogInformationMessage(
|
||||
`Could not find source archive for database '${databasePath}'. Assuming paths are absolute.`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,20 +3,28 @@ import * as path from 'path';
|
||||
import { cloneDbConfig, DbConfig } from './db-config';
|
||||
import * as chokidar from 'chokidar';
|
||||
import { DisposableObject } from '../pure/disposable-object';
|
||||
import { DbConfigValidator } from './db-config-validator';
|
||||
import { ValueResult } from '../common/value-result';
|
||||
|
||||
export class DbConfigStore extends DisposableObject {
|
||||
private readonly configPath: string;
|
||||
private readonly configValidator: DbConfigValidator;
|
||||
|
||||
private config: DbConfig;
|
||||
private config: DbConfig | undefined;
|
||||
private configErrors: string[];
|
||||
private configWatcher: chokidar.FSWatcher | undefined;
|
||||
|
||||
public constructor(workspaceStoragePath: string) {
|
||||
public constructor(
|
||||
workspaceStoragePath: string,
|
||||
extensionPath: string) {
|
||||
super();
|
||||
|
||||
this.configPath = path.join(workspaceStoragePath, 'workspace-databases.json');
|
||||
|
||||
this.config = this.createEmptyConfig();
|
||||
this.configErrors = [];
|
||||
this.configWatcher = undefined;
|
||||
this.configValidator = new DbConfigValidator(extensionPath);
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
@@ -28,9 +36,13 @@ export class DbConfigStore extends DisposableObject {
|
||||
this.configWatcher?.unwatch(this.configPath);
|
||||
}
|
||||
|
||||
public getConfig(): DbConfig {
|
||||
// Clone the config so that it's not modified outside of this class.
|
||||
return cloneDbConfig(this.config);
|
||||
public getConfig(): ValueResult<DbConfig> {
|
||||
if (this.config) {
|
||||
// Clone the config so that it's not modified outside of this class.
|
||||
return ValueResult.ok(cloneDbConfig(this.config));
|
||||
} else {
|
||||
return ValueResult.fail(this.configErrors);
|
||||
}
|
||||
}
|
||||
|
||||
public getConfigPath(): string {
|
||||
@@ -46,11 +58,33 @@ export class DbConfigStore extends DisposableObject {
|
||||
}
|
||||
|
||||
private async readConfig(): Promise<void> {
|
||||
this.config = await fs.readJSON(this.configPath);
|
||||
let newConfig: DbConfig | undefined = undefined;
|
||||
try {
|
||||
newConfig = await fs.readJSON(this.configPath);
|
||||
} catch (e) {
|
||||
this.configErrors = [`Failed to read config file: ${this.configPath}`];
|
||||
}
|
||||
|
||||
if (newConfig) {
|
||||
this.configErrors = this.configValidator.validate(newConfig);
|
||||
}
|
||||
|
||||
this.config = this.configErrors.length === 0 ? newConfig : undefined;
|
||||
}
|
||||
|
||||
private readConfigSync(): void {
|
||||
this.config = fs.readJSONSync(this.configPath);
|
||||
let newConfig: DbConfig | undefined = undefined;
|
||||
try {
|
||||
newConfig = fs.readJSONSync(this.configPath);
|
||||
} catch (e) {
|
||||
this.configErrors = [`Failed to read config file: ${this.configPath}`];
|
||||
}
|
||||
|
||||
if (newConfig) {
|
||||
this.configErrors = this.configValidator.validate(newConfig);
|
||||
}
|
||||
|
||||
this.config = this.configErrors.length === 0 ? newConfig : undefined;
|
||||
}
|
||||
|
||||
private watchConfig(): void {
|
||||
|
||||
24
extensions/ql-vscode/src/databases/db-config-validator.ts
Normal file
24
extensions/ql-vscode/src/databases/db-config-validator.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import Ajv from 'ajv';
|
||||
import { DbConfig } from './db-config';
|
||||
|
||||
export class DbConfigValidator {
|
||||
private readonly schema: any;
|
||||
|
||||
constructor(extensionPath: string) {
|
||||
const schemaPath = path.resolve(extensionPath, 'workspace-databases-schema.json');
|
||||
this.schema = fs.readJsonSync(schemaPath);
|
||||
}
|
||||
|
||||
public validate(dbConfig: DbConfig): string[] {
|
||||
const ajv = new Ajv({ allErrors: true });
|
||||
ajv.validate(this.schema, dbConfig);
|
||||
|
||||
if (ajv.errors) {
|
||||
return ajv.errors.map((error) => `${error.instancePath} ${error.message}`);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ValueResult } from '../common/value-result';
|
||||
import { DbConfigStore } from './db-config-store';
|
||||
import { DbItem } from './db-item';
|
||||
import { createLocalTree, createRemoteTree } from './db-tree-creator';
|
||||
@@ -8,13 +9,16 @@ export class DbManager {
|
||||
) {
|
||||
}
|
||||
|
||||
public getDbItems(): DbItem[] {
|
||||
const config = this.dbConfigStore.getConfig();
|
||||
public getDbItems(): ValueResult<DbItem[]> {
|
||||
const configResult = this.dbConfigStore.getConfig();
|
||||
if (configResult.isFailure) {
|
||||
return ValueResult.fail(configResult.errors);
|
||||
}
|
||||
|
||||
return [
|
||||
createRemoteTree(config),
|
||||
return ValueResult.ok([
|
||||
createRemoteTree(configResult.value),
|
||||
createLocalTree()
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
public getConfigPath(): string {
|
||||
|
||||
@@ -22,7 +22,8 @@ export class DbModule extends DisposableObject {
|
||||
void logger.log('Initializing database module');
|
||||
|
||||
const storagePath = extensionContext.storageUri?.fsPath || extensionContext.globalStorageUri.fsPath;
|
||||
const dbConfigStore = new DbConfigStore(storagePath);
|
||||
const extensionPath = extensionContext.extensionPath;
|
||||
const dbConfigStore = new DbConfigStore(storagePath, extensionPath);
|
||||
await dbConfigStore.initialize();
|
||||
|
||||
const dbManager = new DbManager(dbConfigStore);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ProviderResult, TreeDataProvider, TreeItem } from 'vscode';
|
||||
import { createDbTreeViewItemWarning, DbTreeViewItem } from './db-tree-view-item';
|
||||
import { createDbTreeViewItemError, DbTreeViewItem } from './db-tree-view-item';
|
||||
import { DbManager } from '../db-manager';
|
||||
import { mapDbItemToTreeViewItem } from './db-item-mapper';
|
||||
|
||||
@@ -36,14 +36,17 @@ export class DbTreeDataProvider implements TreeDataProvider<DbTreeViewItem> {
|
||||
}
|
||||
|
||||
private createTree(): DbTreeViewItem[] {
|
||||
const dbItems = this.dbManager.getDbItems();
|
||||
const dbItemsResult = this.dbManager.getDbItems();
|
||||
|
||||
// Add a sample warning as a proof of concept.
|
||||
const warningTreeViewItem = createDbTreeViewItemWarning(
|
||||
'There was an error',
|
||||
'Fix it'
|
||||
);
|
||||
if (dbItemsResult.isFailure) {
|
||||
const errorTreeViewItem = createDbTreeViewItemError(
|
||||
'Error when reading databases config',
|
||||
'Please open your databases config and address errors'
|
||||
);
|
||||
|
||||
return [...dbItems.map(mapDbItemToTreeViewItem), warningTreeViewItem];
|
||||
return [errorTreeViewItem];
|
||||
}
|
||||
|
||||
return dbItemsResult.value.map(mapDbItemToTreeViewItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ export class DbTreeViewItem extends vscode.TreeItem {
|
||||
}
|
||||
}
|
||||
|
||||
export function createDbTreeViewItemWarning(label: string, tooltip: string): DbTreeViewItem {
|
||||
export function createDbTreeViewItemError(label: string, tooltip: string): DbTreeViewItem {
|
||||
return new DbTreeViewItem(
|
||||
undefined,
|
||||
new vscode.ThemeIcon('warning', new vscode.ThemeColor('problemsWarningIcon.foreground')),
|
||||
new vscode.ThemeIcon('error', new vscode.ThemeColor('problemsErrorIcon.foreground')),
|
||||
label,
|
||||
tooltip,
|
||||
vscode.TreeItemCollapsibleState.None,
|
||||
|
||||
@@ -77,6 +77,22 @@ SucceededDownloading.args = {
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
|
||||
};
|
||||
|
||||
export const SucceededSuccessfulDownload = Template.bind({});
|
||||
SucceededSuccessfulDownload.args = {
|
||||
...Pending.args,
|
||||
status: VariantAnalysisRepoStatus.Succeeded,
|
||||
resultCount: 198,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
|
||||
};
|
||||
|
||||
export const SucceededFailedDownload = Template.bind({});
|
||||
SucceededFailedDownload.args = {
|
||||
...Pending.args,
|
||||
status: VariantAnalysisRepoStatus.Succeeded,
|
||||
resultCount: 198,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
|
||||
};
|
||||
|
||||
export const InterpretedResults = Template.bind({});
|
||||
InterpretedResults.args = {
|
||||
...Pending.args,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { VariantAnalysis as VariantAnalysisComponent } from '../../view/variant-analysis/VariantAnalysis';
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisDomainModel,
|
||||
VariantAnalysisFailureReason,
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisScannedRepositoryDownloadStatus,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
@@ -256,3 +257,129 @@ FullExampleWithoutSkipped.args = {
|
||||
repoStates,
|
||||
repoResults,
|
||||
};
|
||||
|
||||
export const Canceled = Template.bind({});
|
||||
Canceled.args = {
|
||||
variantAnalysis: {
|
||||
...variantAnalysis,
|
||||
status: VariantAnalysisStatus.Canceled,
|
||||
completedAt: new Date(new Date(variantAnalysis.createdAt).getTime() + 100_000).toISOString(),
|
||||
scannedRepos: [
|
||||
{
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 1,
|
||||
fullName: 'octodemo/hello-world-1',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
resultCount: 200,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 2,
|
||||
fullName: 'octodemo/hello-world-2',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
resultCount: 10_000,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 3,
|
||||
fullName: 'octodemo/hello-world-3',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
resultCount: 500,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 4,
|
||||
fullName: 'octodemo/hello-world-4',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Canceled,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 5,
|
||||
fullName: 'octodemo/hello-world-5',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Failed,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 6,
|
||||
fullName: 'octodemo/hello-world-6',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Canceled,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 7,
|
||||
fullName: 'octodemo/hello-world-7',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Canceled,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 8,
|
||||
fullName: 'octodemo/hello-world-8',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Canceled,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 9,
|
||||
fullName: 'octodemo/hello-world-9',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Canceled,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 10,
|
||||
fullName: 'octodemo/hello-world-10',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Canceled,
|
||||
},
|
||||
],
|
||||
},
|
||||
repoStates,
|
||||
repoResults,
|
||||
};
|
||||
|
||||
export const Failed = Template.bind({});
|
||||
Failed.args = {
|
||||
variantAnalysis: {
|
||||
...variantAnalysis,
|
||||
status: VariantAnalysisStatus.Failed,
|
||||
failureReason: VariantAnalysisFailureReason.NoReposQueried,
|
||||
completedAt: new Date(new Date(variantAnalysis.createdAt).getTime() + 100_000).toISOString(),
|
||||
scannedRepos: [],
|
||||
skippedRepos: {
|
||||
...variantAnalysis.skippedRepos,
|
||||
overLimitRepos: {
|
||||
repositoryCount: 0,
|
||||
repositories: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
repoStates,
|
||||
repoResults,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,10 @@ import styled from 'styled-components';
|
||||
import { AnalysisAlert, AnalysisRawResults } from '../../remote-queries/shared/analysis-result';
|
||||
import AnalysisAlertResult from '../remote-queries/AnalysisAlertResult';
|
||||
import RawResultsTable from '../remote-queries/RawResultsTable';
|
||||
import { VariantAnalysisRepoStatus } from '../../remote-queries/shared/variant-analysis';
|
||||
import {
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisScannedRepositoryDownloadStatus,
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
import { Alert } from '../common';
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
@@ -32,7 +35,8 @@ const RawResultsContainer = styled.div`
|
||||
`;
|
||||
|
||||
export type AnalyzedRepoItemContentProps = {
|
||||
status: VariantAnalysisRepoStatus;
|
||||
status?: VariantAnalysisRepoStatus;
|
||||
downloadStatus?: VariantAnalysisScannedRepositoryDownloadStatus;
|
||||
|
||||
interpretedResults?: AnalysisAlert[];
|
||||
rawResults?: AnalysisRawResults;
|
||||
@@ -40,6 +44,7 @@ export type AnalyzedRepoItemContentProps = {
|
||||
|
||||
export const AnalyzedRepoItemContent = ({
|
||||
status,
|
||||
downloadStatus,
|
||||
interpretedResults,
|
||||
rawResults,
|
||||
}: AnalyzedRepoItemContentProps) => {
|
||||
@@ -66,6 +71,13 @@ export const AnalyzedRepoItemContent = ({
|
||||
message="The variant analysis or this repository was canceled."
|
||||
/>
|
||||
</AlertContainer>}
|
||||
{downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.Failed && <AlertContainer>
|
||||
<Alert
|
||||
type="error"
|
||||
title="Download failed"
|
||||
message="The query was successful on this repository, but the extension failed to download the results for this repository."
|
||||
/>
|
||||
</AlertContainer>}
|
||||
{interpretedResults && (
|
||||
<InterpretedResultsContainer>
|
||||
{interpretedResults.map((r, i) =>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as React from 'react';
|
||||
import { VSCodeButton } from '@vscode/webview-ui-toolkit/react';
|
||||
import { Alert } from '../common';
|
||||
import { vscode } from '../vscode-api';
|
||||
import { VariantAnalysisFailureReason } from '../../remote-queries/shared/variant-analysis';
|
||||
|
||||
type Props = {
|
||||
failureReason: VariantAnalysisFailureReason;
|
||||
showLogsButton: boolean;
|
||||
};
|
||||
|
||||
const getTitle = (failureReason: VariantAnalysisFailureReason): string => {
|
||||
switch (failureReason) {
|
||||
case VariantAnalysisFailureReason.NoReposQueried:
|
||||
return 'No repositories to analyze';
|
||||
case VariantAnalysisFailureReason.InternalError:
|
||||
return 'Something unexpected happened';
|
||||
}
|
||||
};
|
||||
|
||||
const getMessage = (failureReason: VariantAnalysisFailureReason): string => {
|
||||
switch (failureReason) {
|
||||
case VariantAnalysisFailureReason.NoReposQueried:
|
||||
return 'No repositories available after processing. No repositories were analyzed.';
|
||||
case VariantAnalysisFailureReason.InternalError:
|
||||
return 'An internal error occurred while running this variant analysis. Please try again later.';
|
||||
}
|
||||
};
|
||||
|
||||
const openLogs = () => {
|
||||
vscode.postMessage({
|
||||
t: 'openLogs',
|
||||
});
|
||||
};
|
||||
|
||||
export const FailureReasonAlert = ({
|
||||
failureReason,
|
||||
showLogsButton,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Alert
|
||||
type="error"
|
||||
title={getTitle(failureReason)}
|
||||
message={getMessage(failureReason)}
|
||||
actions={showLogsButton && <VSCodeButton appearance="secondary" onClick={openLogs}>View logs</VSCodeButton>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -82,6 +82,50 @@ export type RepoRowProps = {
|
||||
rawResults?: AnalysisRawResults;
|
||||
}
|
||||
|
||||
const canExpand = (
|
||||
status: VariantAnalysisRepoStatus | undefined,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus | undefined,
|
||||
): boolean => {
|
||||
if (!status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isCompletedAnalysisRepoStatus(status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status !== VariantAnalysisRepoStatus.Succeeded) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.Succeeded || downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.Failed;
|
||||
};
|
||||
|
||||
const isExpandableContentLoaded = (
|
||||
status: VariantAnalysisRepoStatus | undefined,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus | undefined,
|
||||
resultsLoaded: boolean,
|
||||
): boolean => {
|
||||
if (!canExpand(status, downloadStatus)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status !== VariantAnalysisRepoStatus.Succeeded) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.Failed) {
|
||||
// If the download has failed, we allow expansion to show the error
|
||||
return true;
|
||||
}
|
||||
|
||||
return resultsLoaded;
|
||||
};
|
||||
|
||||
export const RepoRow = ({
|
||||
repository,
|
||||
status,
|
||||
@@ -99,7 +143,7 @@ export const RepoRow = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (resultsLoaded || status !== VariantAnalysisRepoStatus.Succeeded) {
|
||||
if (resultsLoaded || status !== VariantAnalysisRepoStatus.Succeeded || downloadStatus !== VariantAnalysisScannedRepositoryDownloadStatus.Succeeded) {
|
||||
setExpanded(oldIsExpanded => !oldIsExpanded);
|
||||
return;
|
||||
}
|
||||
@@ -110,7 +154,7 @@ export const RepoRow = ({
|
||||
});
|
||||
|
||||
setResultsLoading(true);
|
||||
}, [resultsLoading, resultsLoaded, repository.fullName, status]);
|
||||
}, [resultsLoading, resultsLoaded, repository.fullName, status, downloadStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resultsLoaded && resultsLoading) {
|
||||
@@ -119,8 +163,8 @@ export const RepoRow = ({
|
||||
}
|
||||
}, [resultsLoaded, resultsLoading]);
|
||||
|
||||
const disabled = !status || !isCompletedAnalysisRepoStatus(status) || (status === VariantAnalysisRepoStatus.Succeeded && downloadStatus !== VariantAnalysisScannedRepositoryDownloadStatus.Succeeded);
|
||||
const expandableContentLoaded = status && (status !== VariantAnalysisRepoStatus.Succeeded || resultsLoaded);
|
||||
const disabled = !canExpand(status, downloadStatus);
|
||||
const expandableContentLoaded = isExpandableContentLoaded(status, downloadStatus, resultsLoaded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -139,13 +183,20 @@ export const RepoRow = ({
|
||||
{!status && <WarningIcon />}
|
||||
</span>
|
||||
{downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.InProgress && <LoadingIcon label="Downloading" />}
|
||||
{downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.Failed && <WarningIcon label="Failed to download the results" />}
|
||||
<MetadataContainer>
|
||||
<div><StarCount starCount={repository.stargazersCount} /></div>
|
||||
<LastUpdated lastUpdated={repository.updatedAt} />
|
||||
</MetadataContainer>
|
||||
</TitleContainer>
|
||||
{isExpanded && expandableContentLoaded &&
|
||||
<AnalyzedRepoItemContent status={status} interpretedResults={interpretedResults} rawResults={rawResults} />}
|
||||
{isExpanded && expandableContentLoaded && (
|
||||
<AnalyzedRepoItemContent
|
||||
status={status}
|
||||
downloadStatus={downloadStatus}
|
||||
interpretedResults={interpretedResults}
|
||||
rawResults={rawResults}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,13 +6,15 @@ import { formatDecimal } from '../../pure/number';
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisScannedRepositoryState
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
VariantAnalysisStatus
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
import { VariantAnalysisAnalyzedRepos } from './VariantAnalysisAnalyzedRepos';
|
||||
import { Alert } from '../common';
|
||||
import { VariantAnalysisSkippedRepositoriesTab } from './VariantAnalysisSkippedRepositoriesTab';
|
||||
import { defaultFilterSortState, RepositoriesFilterSortState } from './filterSort';
|
||||
import { RepositoriesSearchSortRow } from './RepositoriesSearchSortRow';
|
||||
import { FailureReasonAlert } from './FailureReasonAlert';
|
||||
|
||||
export type VariantAnalysisOutcomePanelProps = {
|
||||
variantAnalysis: VariantAnalysis;
|
||||
@@ -47,6 +49,7 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
}: VariantAnalysisOutcomePanelProps) => {
|
||||
const [filterSortState, setFilterSortState] = useState<RepositoriesFilterSortState>(defaultFilterSortState);
|
||||
|
||||
const scannedReposCount = variantAnalysis.scannedRepos?.length ?? 0;
|
||||
const noCodeqlDbRepos = variantAnalysis.skippedRepos?.noCodeqlDbRepos;
|
||||
const notFoundRepos = variantAnalysis.skippedRepos?.notFoundRepos;
|
||||
const overLimitRepositoryCount = variantAnalysis.skippedRepos?.overLimitRepos?.repositoryCount ?? 0;
|
||||
@@ -54,18 +57,28 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
|
||||
const warnings = (
|
||||
<WarningsContainer>
|
||||
{variantAnalysis.status === VariantAnalysisStatus.Canceled && (
|
||||
<Alert
|
||||
type="warning"
|
||||
title="Variant analysis canceled"
|
||||
message="Variant analysis canceled before all queries were complete. Some repositories were not analyzed."
|
||||
/>
|
||||
)}
|
||||
{variantAnalysis.status === VariantAnalysisStatus.Failed && variantAnalysis.failureReason && (
|
||||
<FailureReasonAlert failureReason={variantAnalysis.failureReason} showLogsButton={!!variantAnalysis.actionsWorkflowRunId} />
|
||||
)}
|
||||
{overLimitRepositoryCount > 0 && (
|
||||
<Alert
|
||||
type="warning"
|
||||
title="Repository limit exceeded"
|
||||
message={`The number of requested repositories exceeds the maximum number of repositories supported by multi-repository variant analysis. ${overLimitRepositoryCount} ${overLimitRepositoryCount === 1 ? 'repository was' : 'repositories were'} skipped.`}
|
||||
title="Repository list too large"
|
||||
message={`Repository list contains more than ${formatDecimal(scannedReposCount)} entries. Only the first ${formatDecimal(scannedReposCount)} repositories were processed.`}
|
||||
/>
|
||||
)}
|
||||
{accessMismatchRepositoryCount > 0 && (
|
||||
<Alert
|
||||
type="warning"
|
||||
title="Access mismatch"
|
||||
message={`${accessMismatchRepositoryCount} ${accessMismatchRepositoryCount === 1 ? 'repository is' : 'repositories are'} private, while the controller repository is public. ${accessMismatchRepositoryCount === 1 ? 'This repository was' : 'These repositories were'} skipped.`}
|
||||
title="Problem with controller repository"
|
||||
message={`Publicly visible controller repository can't be used to analyze private repositories. ${formatDecimal(accessMismatchRepositoryCount)} ${accessMismatchRepositoryCount === 1 ? 'private repository was' : 'private repositories were'} not analyzed.`}
|
||||
/>
|
||||
)}
|
||||
</WarningsContainer>
|
||||
@@ -118,8 +131,8 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
{notFoundRepos?.repositoryCount &&
|
||||
<VSCodePanelView>
|
||||
<VariantAnalysisSkippedRepositoriesTab
|
||||
alertTitle='No access'
|
||||
alertMessage='The following repositories could not be scanned because you do not have read access.'
|
||||
alertTitle="No access"
|
||||
alertMessage="The following repositories can't be analyzed because they don’t exist or you don’t have access."
|
||||
skippedRepositoryGroup={notFoundRepos}
|
||||
filterSortState={filterSortState}
|
||||
/>
|
||||
@@ -127,8 +140,8 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
{noCodeqlDbRepos?.repositoryCount &&
|
||||
<VSCodePanelView>
|
||||
<VariantAnalysisSkippedRepositoriesTab
|
||||
alertTitle='No database'
|
||||
alertMessage='The following repositories could not be scanned because they do not have an available CodeQL database.'
|
||||
alertTitle="No CodeQL database"
|
||||
alertMessage="The following repositories can't be analyzed because they don't currently have a CodeQL database available for the selected language."
|
||||
skippedRepositoryGroup={noCodeqlDbRepos}
|
||||
filterSortState={filterSortState}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { render as reactRender, screen } from '@testing-library/react';
|
||||
import { VariantAnalysisRepoStatus } from '../../../remote-queries/shared/variant-analysis';
|
||||
import {
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisScannedRepositoryDownloadStatus
|
||||
} from '../../../remote-queries/shared/variant-analysis';
|
||||
import { AnalyzedRepoItemContent, AnalyzedRepoItemContentProps } from '../AnalyzedRepoItemContent';
|
||||
|
||||
describe(AnalyzedRepoItemContent.name, () => {
|
||||
@@ -112,4 +115,13 @@ describe(AnalyzedRepoItemContent.name, () => {
|
||||
|
||||
expect(screen.getByText('Error: Canceled')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the failed download state', () => {
|
||||
render({
|
||||
status: VariantAnalysisRepoStatus.Succeeded,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
|
||||
});
|
||||
|
||||
expect(screen.getByText('Error: Download failed')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,6 +104,21 @@ describe(RepoRow.name, () => {
|
||||
})).toBeEnabled();
|
||||
});
|
||||
|
||||
it('renders the succeeded state with failed download status', () => {
|
||||
render({
|
||||
status: VariantAnalysisRepoStatus.Succeeded,
|
||||
resultCount: 178,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
|
||||
});
|
||||
|
||||
expect(screen.getByRole<HTMLButtonElement>('button', {
|
||||
expanded: false
|
||||
})).toBeEnabled();
|
||||
expect(screen.getByRole('img', {
|
||||
name: 'Failed to download the results',
|
||||
})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the failed state', () => {
|
||||
render({
|
||||
status: VariantAnalysisRepoStatus.Failed,
|
||||
|
||||
@@ -8,7 +8,10 @@ import {
|
||||
import { VariantAnalysisOutcomePanelProps, VariantAnalysisOutcomePanels } from '../VariantAnalysisOutcomePanels';
|
||||
import { createMockVariantAnalysis } from '../../../vscode-tests/factories/remote-queries/shared/variant-analysis';
|
||||
import { createMockRepositoryWithMetadata } from '../../../vscode-tests/factories/remote-queries/shared/repository';
|
||||
import { createMockScannedRepo } from '../../../vscode-tests/factories/remote-queries/shared/scanned-repositories';
|
||||
import {
|
||||
createMockScannedRepo,
|
||||
createMockScannedRepos
|
||||
} from '../../../vscode-tests/factories/remote-queries/shared/scanned-repositories';
|
||||
|
||||
describe(VariantAnalysisOutcomePanels.name, () => {
|
||||
const defaultVariantAnalysis = {
|
||||
@@ -136,6 +139,14 @@ describe(VariantAnalysisOutcomePanels.name, () => {
|
||||
expect(screen.getByText('No database')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders warning with canceled variant analysis', () => {
|
||||
render({
|
||||
status: VariantAnalysisStatus.Canceled,
|
||||
});
|
||||
|
||||
expect(screen.getByText('Warning: Variant analysis canceled')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders warning with access mismatch repos', () => {
|
||||
render({
|
||||
skippedRepos: {
|
||||
@@ -144,7 +155,7 @@ describe(VariantAnalysisOutcomePanels.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByText('Warning: Access mismatch')).toBeInTheDocument();
|
||||
expect(screen.getByText('Warning: Problem with controller repository')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders warning with over limit repos', () => {
|
||||
@@ -154,7 +165,7 @@ describe(VariantAnalysisOutcomePanels.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByText('Warning: Repository limit exceeded')).toBeInTheDocument();
|
||||
expect(screen.getByText('Warning: Repository list too large')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders singulars in warnings', () => {
|
||||
@@ -171,12 +182,12 @@ describe(VariantAnalysisOutcomePanels.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByText('The number of requested repositories exceeds the maximum number of repositories supported by multi-repository variant analysis. 1 repository was skipped.')).toBeInTheDocument();
|
||||
expect(screen.getByText('1 repository is private, while the controller repository is public. This repository was skipped.')).toBeInTheDocument();
|
||||
expect(screen.getByText('Publicly visible controller repository can\'t be used to analyze private repositories. 1 private repository was not analyzed.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders plurals in warnings', () => {
|
||||
render({
|
||||
scannedRepos: createMockScannedRepos(),
|
||||
skippedRepos: {
|
||||
overLimitRepos: {
|
||||
repositoryCount: 2,
|
||||
@@ -189,7 +200,7 @@ describe(VariantAnalysisOutcomePanels.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByText('The number of requested repositories exceeds the maximum number of repositories supported by multi-repository variant analysis. 2 repositories were skipped.')).toBeInTheDocument();
|
||||
expect(screen.getByText('2 repositories are private, while the controller repository is public. These repositories were skipped.')).toBeInTheDocument();
|
||||
expect(screen.getByText('Repository list contains more than 3 entries. Only the first 3 repositories were processed.')).toBeInTheDocument();
|
||||
expect(screen.getByText('Publicly visible controller repository can\'t be used to analyze private repositories. 2 private repositories were not analyzed.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ import { QueryRunner } from '../../queryRunner';
|
||||
* Integration tests for queries
|
||||
*/
|
||||
describe('Queries', function() {
|
||||
this.timeout(20000);
|
||||
this.timeout(20_000);
|
||||
|
||||
before(function() {
|
||||
skipIfNoCodeQL(this);
|
||||
@@ -42,7 +42,9 @@ describe('Queries', function() {
|
||||
let qlFile: string;
|
||||
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(async function() {
|
||||
this.timeout(20_000);
|
||||
|
||||
sandbox = sinon.createSandbox();
|
||||
|
||||
try {
|
||||
@@ -89,7 +91,8 @@ describe('Queries', function() {
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
afterEach(async function() {
|
||||
this.timeout(20_000);
|
||||
try {
|
||||
sandbox.restore();
|
||||
safeDel(qlpackFile);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
### Databases
|
||||
|
||||
This folder contains tests for the new experimental databases panel and new query run experience.
|
||||
@@ -0,0 +1,259 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as pq from 'proxyquire';
|
||||
import { DbConfig } from '../../../databases/db-config';
|
||||
import { DbManager } from '../../../databases/db-manager';
|
||||
import { DbConfigStore } from '../../../databases/db-config-store';
|
||||
import { DbTreeDataProvider } from '../../../databases/ui/db-tree-data-provider';
|
||||
import { DbPanel } from '../../../databases/ui/db-panel';
|
||||
import { DbItemKind } from '../../../databases/db-item';
|
||||
import { DbTreeViewItem } from '../../../databases/ui/db-tree-view-item';
|
||||
|
||||
const proxyquire = pq.noPreserveCache();
|
||||
|
||||
describe('db panel', async () => {
|
||||
const workspaceStoragePath = path.join(__dirname, 'test-workspace');
|
||||
const extensionPath = path.join(__dirname, '../../../../');
|
||||
const dbConfigFilePath = path.join(workspaceStoragePath, 'workspace-databases.json');
|
||||
let dbTreeDataProvider: DbTreeDataProvider;
|
||||
let dbManager: DbManager;
|
||||
let dbConfigStore: DbConfigStore;
|
||||
let dbPanel: DbPanel;
|
||||
|
||||
before(async () => {
|
||||
dbConfigStore = new DbConfigStore(workspaceStoragePath, extensionPath);
|
||||
dbManager = new DbManager(dbConfigStore);
|
||||
|
||||
// Create a modified version of the DbPanel module that allows
|
||||
// us to override the creation of the DbTreeDataProvider
|
||||
const mod = proxyquire('../../../databases/ui/db-panel', {
|
||||
'./db-tree-data-provider': {
|
||||
DbTreeDataProvider: class {
|
||||
constructor() {
|
||||
return dbTreeDataProvider;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize the panel using the modified module
|
||||
dbPanel = new mod.DbPanel(dbManager) as DbPanel;
|
||||
await dbPanel.initialize();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.ensureDir(workspaceStoragePath);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.remove(workspaceStoragePath);
|
||||
});
|
||||
|
||||
it('should render default local and remote nodes when the config is empty', async () => {
|
||||
const dbConfig: DbConfig = {
|
||||
remote: {
|
||||
repositoryLists: [],
|
||||
owners: [],
|
||||
repositories: []
|
||||
},
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
|
||||
const dbTreeItems = await dbTreeDataProvider.getChildren();
|
||||
|
||||
expect(dbTreeItems).to.be.ok;
|
||||
const items = dbTreeItems!;
|
||||
expect(items.length).to.equal(2);
|
||||
|
||||
const remoteRootNode = items[0];
|
||||
expect(remoteRootNode.dbItem).to.be.ok;
|
||||
expect(remoteRootNode.dbItem?.kind).to.equal(DbItemKind.RootRemote);
|
||||
expect(remoteRootNode.label).to.equal('remote');
|
||||
expect(remoteRootNode.tooltip).to.equal('Remote databases');
|
||||
expect(remoteRootNode.collapsibleState).to.equal(vscode.TreeItemCollapsibleState.Collapsed);
|
||||
expect(remoteRootNode.children).to.be.ok;
|
||||
expect(remoteRootNode.children.length).to.equal(3);
|
||||
|
||||
const systemDefinedListItems = remoteRootNode.children.filter(item => item.dbItem?.kind === DbItemKind.RemoteSystemDefinedList);
|
||||
expect(systemDefinedListItems.length).to.equal(3);
|
||||
checkRemoteSystemDefinedListItem(systemDefinedListItems[0], 10);
|
||||
checkRemoteSystemDefinedListItem(systemDefinedListItems[1], 100);
|
||||
checkRemoteSystemDefinedListItem(systemDefinedListItems[2], 1000);
|
||||
|
||||
const localRootNode = items[1];
|
||||
expect(localRootNode.dbItem).to.be.ok;
|
||||
expect(localRootNode.dbItem?.kind).to.equal(DbItemKind.RootLocal);
|
||||
expect(localRootNode.label).to.equal('local');
|
||||
expect(localRootNode.tooltip).to.equal('Local databases');
|
||||
expect(localRootNode.collapsibleState).to.equal(vscode.TreeItemCollapsibleState.Collapsed);
|
||||
expect(localRootNode.children).to.be.ok;
|
||||
expect(localRootNode.children.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('should render remote repository list nodes', async () => {
|
||||
const dbConfig: DbConfig = {
|
||||
remote: {
|
||||
repositoryLists: [
|
||||
{
|
||||
name: 'my-list-1',
|
||||
repositories: [
|
||||
'owner1/repo1',
|
||||
'owner1/repo2'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'my-list-2',
|
||||
repositories: [
|
||||
'owner1/repo1',
|
||||
'owner2/repo1',
|
||||
'owner2/repo2'
|
||||
]
|
||||
},
|
||||
],
|
||||
owners: [],
|
||||
repositories: []
|
||||
},
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
|
||||
const dbTreeItems = await dbTreeDataProvider.getChildren();
|
||||
|
||||
expect(dbTreeItems).to.be.ok;
|
||||
const items = dbTreeItems!;
|
||||
expect(items.length).to.equal(2);
|
||||
|
||||
const remoteRootNode = items[0];
|
||||
expect(remoteRootNode.dbItem).to.be.ok;
|
||||
expect(remoteRootNode.collapsibleState).to.equal(vscode.TreeItemCollapsibleState.Collapsed);
|
||||
expect(remoteRootNode.children).to.be.ok;
|
||||
expect(remoteRootNode.children.length).to.equal(5);
|
||||
|
||||
const systemDefinedListItems = remoteRootNode.children.filter(item => item.dbItem?.kind === DbItemKind.RemoteSystemDefinedList);
|
||||
expect(systemDefinedListItems.length).to.equal(3);
|
||||
|
||||
const userDefinedListItems = remoteRootNode.children.filter(item => item.dbItem?.kind === DbItemKind.RemoteUserDefinedList);
|
||||
expect(userDefinedListItems.length).to.equal(2);
|
||||
checkUserDefinedListItem(userDefinedListItems[0], 'my-list-1', ['owner1/repo1', 'owner1/repo2']);
|
||||
checkUserDefinedListItem(userDefinedListItems[1], 'my-list-2', ['owner1/repo1', 'owner2/repo1', 'owner2/repo2']);
|
||||
});
|
||||
|
||||
it('should render owner list nodes', async () => {
|
||||
const dbConfig: DbConfig = {
|
||||
remote: {
|
||||
repositoryLists: [],
|
||||
owners: ['owner1', 'owner2'],
|
||||
repositories: []
|
||||
},
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
|
||||
const dbTreeItems = await dbTreeDataProvider.getChildren();
|
||||
|
||||
expect(dbTreeItems).to.be.ok;
|
||||
const items = dbTreeItems!;
|
||||
expect(items.length).to.equal(2);
|
||||
|
||||
const remoteRootNode = items[0];
|
||||
expect(remoteRootNode.dbItem).to.be.ok;
|
||||
expect(remoteRootNode.collapsibleState).to.equal(vscode.TreeItemCollapsibleState.Collapsed);
|
||||
expect(remoteRootNode.children).to.be.ok;
|
||||
expect(remoteRootNode.children.length).to.equal(5);
|
||||
|
||||
const ownerListItems = remoteRootNode.children.filter(item => item.dbItem?.kind === DbItemKind.RemoteOwner);
|
||||
expect(ownerListItems.length).to.equal(2);
|
||||
checkOwnerItem(ownerListItems[0], 'owner1');
|
||||
checkOwnerItem(ownerListItems[1], 'owner2');
|
||||
});
|
||||
|
||||
it('should render repository nodes', async () => {
|
||||
const dbConfig: DbConfig = {
|
||||
remote: {
|
||||
repositoryLists: [],
|
||||
owners: [],
|
||||
repositories: ['owner1/repo1', 'owner1/repo2']
|
||||
},
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
|
||||
const dbTreeItems = await dbTreeDataProvider.getChildren();
|
||||
|
||||
expect(dbTreeItems).to.be.ok;
|
||||
const items = dbTreeItems!;
|
||||
expect(items.length).to.equal(2);
|
||||
|
||||
const remoteRootNode = items[0];
|
||||
expect(remoteRootNode.dbItem).to.be.ok;
|
||||
expect(remoteRootNode.collapsibleState).to.equal(vscode.TreeItemCollapsibleState.Collapsed);
|
||||
expect(remoteRootNode.children).to.be.ok;
|
||||
expect(remoteRootNode.children.length).to.equal(5);
|
||||
|
||||
const repoItems = remoteRootNode.children.filter(item => item.dbItem?.kind === DbItemKind.RemoteRepo);
|
||||
expect(repoItems.length).to.equal(2);
|
||||
checkRemoteRepoItem(repoItems[0], 'owner1/repo1');
|
||||
checkRemoteRepoItem(repoItems[1], 'owner1/repo2');
|
||||
});
|
||||
|
||||
async function saveDbConfig(dbConfig: DbConfig): Promise<void> {
|
||||
await fs.writeJson(dbConfigFilePath, dbConfig);
|
||||
|
||||
// Once we have watching of the db config, this can happen
|
||||
// at the start of the test.
|
||||
await dbConfigStore.initialize();
|
||||
dbTreeDataProvider = new DbTreeDataProvider(dbManager);
|
||||
}
|
||||
|
||||
function checkRemoteSystemDefinedListItem(
|
||||
item: DbTreeViewItem,
|
||||
n: number
|
||||
): void {
|
||||
expect(item.label).to.equal(`Top ${n} repositories`);
|
||||
expect(item.tooltip).to.equal(`Top ${n} repositories of a language`);
|
||||
expect(item.iconPath).to.deep.equal(new vscode.ThemeIcon('github'));
|
||||
expect(item.collapsibleState).to.equal(vscode.TreeItemCollapsibleState.None);
|
||||
}
|
||||
|
||||
function checkUserDefinedListItem(
|
||||
item: DbTreeViewItem,
|
||||
listName: string,
|
||||
repos: string[]
|
||||
): void {
|
||||
expect(item.label).to.equal(listName);
|
||||
expect(item.tooltip).to.be.undefined;
|
||||
expect(item.iconPath).to.be.undefined;
|
||||
expect(item.collapsibleState).to.equal(vscode.TreeItemCollapsibleState.Collapsed);
|
||||
expect(item.children).to.be.ok;
|
||||
expect(item.children.length).to.equal(repos.length);
|
||||
|
||||
for (let i = 0; i < repos.length; i++) {
|
||||
checkRemoteRepoItem(item.children[i], repos[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function checkOwnerItem(
|
||||
item: DbTreeViewItem,
|
||||
ownerName: string
|
||||
): void {
|
||||
expect(item.label).to.equal(ownerName);
|
||||
expect(item.tooltip).to.be.undefined;
|
||||
expect(item.iconPath).to.deep.equal(new vscode.ThemeIcon('organization'));
|
||||
expect(item.collapsibleState).to.equal(vscode.TreeItemCollapsibleState.None);
|
||||
expect(item.children).to.be.ok;
|
||||
expect(item.children.length).to.equal(0);
|
||||
}
|
||||
|
||||
function checkRemoteRepoItem(
|
||||
item: DbTreeViewItem,
|
||||
repoName: string
|
||||
): void {
|
||||
expect(item.label).to.equal(repoName);
|
||||
expect(item.tooltip).to.be.undefined;
|
||||
expect(item.iconPath).to.deep.equal(new vscode.ThemeIcon('database'));
|
||||
expect(item.collapsibleState).to.equal(vscode.TreeItemCollapsibleState.None);
|
||||
}
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { DbConfigStore } from '../../../src/databases/db-config-store';
|
||||
import { expect } from 'chai';
|
||||
|
||||
describe('db config store', async () => {
|
||||
const extensionPath = path.join(__dirname, '../../..');
|
||||
const tempWorkspaceStoragePath = path.join(__dirname, 'test-workspace');
|
||||
const testDataStoragePath = path.join(__dirname, 'data');
|
||||
|
||||
@@ -18,21 +19,21 @@ describe('db config store', async () => {
|
||||
it('should create a new config if one does not exist', async () => {
|
||||
const configPath = path.join(tempWorkspaceStoragePath, 'workspace-databases.json');
|
||||
|
||||
const configStore = new DbConfigStore(tempWorkspaceStoragePath);
|
||||
const configStore = new DbConfigStore(tempWorkspaceStoragePath, extensionPath);
|
||||
await configStore.initialize();
|
||||
|
||||
expect(await fs.pathExists(configPath)).to.be.true;
|
||||
const config = configStore.getConfig();
|
||||
const config = configStore.getConfig().value;
|
||||
expect(config.remote.repositoryLists).to.be.empty;
|
||||
expect(config.remote.owners).to.be.empty;
|
||||
expect(config.remote.repositories).to.be.empty;
|
||||
});
|
||||
|
||||
it('should load an existing config', async () => {
|
||||
const configStore = new DbConfigStore(testDataStoragePath);
|
||||
const configStore = new DbConfigStore(testDataStoragePath, extensionPath);
|
||||
await configStore.initialize();
|
||||
|
||||
const config = configStore.getConfig();
|
||||
const config = configStore.getConfig().value;
|
||||
expect(config.remote.repositoryLists).to.have.length(1);
|
||||
expect(config.remote.repositoryLists[0]).to.deep.equal({
|
||||
'name': 'repoList1',
|
||||
@@ -44,13 +45,13 @@ describe('db config store', async () => {
|
||||
});
|
||||
|
||||
it('should not allow modification of the config', async () => {
|
||||
const configStore = new DbConfigStore(testDataStoragePath);
|
||||
const configStore = new DbConfigStore(testDataStoragePath, extensionPath);
|
||||
await configStore.initialize();
|
||||
|
||||
const config = configStore.getConfig();
|
||||
const config = configStore.getConfig().value;
|
||||
config.remote.repositoryLists = [];
|
||||
|
||||
const reRetrievedConfig = configStore.getConfig();
|
||||
const reRetrievedConfig = configStore.getConfig().value;
|
||||
expect(reRetrievedConfig.remote.repositoryLists).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
import { DbConfig } from '../../../src/databases/db-config';
|
||||
import { DbConfigValidator } from '../../../src/databases/db-config-validator';
|
||||
|
||||
describe('db config validation', async () => {
|
||||
const extensionPath = path.join(__dirname, '../../..');
|
||||
const configValidator = new DbConfigValidator(extensionPath);
|
||||
|
||||
it('should return error when file is not valid', async () => {
|
||||
// We're intentionally bypassing the type check because we'd
|
||||
// like to make sure validation errors are highlighted.
|
||||
const dbConfig = {
|
||||
'remote': {
|
||||
'repositoryLists': [
|
||||
{
|
||||
'name': 'repoList1',
|
||||
'repositories': ['foo/bar', 'foo/baz']
|
||||
}
|
||||
],
|
||||
'repositories': ['owner/repo1', 'owner/repo2', 'owner/repo3'],
|
||||
'somethingElse': 'bar'
|
||||
}
|
||||
} as any as DbConfig;
|
||||
|
||||
const validationOutput = configValidator.validate(dbConfig);
|
||||
|
||||
expect(validationOutput).to.have.length(2);
|
||||
|
||||
expect(validationOutput[0]).to.deep.equal('/remote must have required property \'owners\'');
|
||||
expect(validationOutput[1]).to.deep.equal('/remote must NOT have additional properties');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user