From bc51e7462b5a971193483b01bdec2ac1ce479dbb Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Wed, 15 Feb 2023 15:41:29 +0100 Subject: [PATCH 01/19] Fix Graphviz WASM module not loading for graph viewer It seems that when we added the CSP policy to the webview, we did not take into account that `d3-graphviz` uses `@hpcc-js/wasm` to load Graphviz as a WASM module. This commit adds `'wasm-unsafe-eval'` to the CSP policy to allow this. --- extensions/ql-vscode/src/interface-utils.ts | 4 ++-- extensions/ql-vscode/src/view/results/graph.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extensions/ql-vscode/src/interface-utils.ts b/extensions/ql-vscode/src/interface-utils.ts index f4b094ab9..779575726 100644 --- a/extensions/ql-vscode/src/interface-utils.ts +++ b/extensions/ql-vscode/src/interface-utils.ts @@ -163,7 +163,7 @@ export function getHtmlForWebview( /* * Content security policy: * default-src: allow nothing by default. - * script-src: allow only the given script, using the nonce. + * script-src: allow the given script, using the nonce. also allow loading WebAssembly modules. * style-src: allow only the given stylesheet, using the nonce. * connect-src: only allow fetch calls to webview resource URIs * (this is used to load BQRS result files). @@ -172,7 +172,7 @@ export function getHtmlForWebview( ${stylesheetsHtmlLines.join(` ${EOL}`)} diff --git a/extensions/ql-vscode/src/view/results/graph.tsx b/extensions/ql-vscode/src/view/results/graph.tsx index e5f9b86a8..ae5722bcc 100644 --- a/extensions/ql-vscode/src/view/results/graph.tsx +++ b/extensions/ql-vscode/src/view/results/graph.tsx @@ -5,7 +5,7 @@ import { InterpretedResultSet, GraphInterpretationData, } from "../../pure/interface-types"; -import { graphviz } from "d3-graphviz"; +import { graphviz, GraphvizOptions } from "d3-graphviz"; import { tryGetLocationFromString } from "../../pure/bqrs-utils"; export type GraphProps = ResultTableProps & { resultSet: InterpretedResultSet; @@ -59,11 +59,12 @@ export class Graph extends React.Component { return; } - const options = { + const options: GraphvizOptions = { fit: true, fade: false, growEnteringEdges: false, zoom: true, + useWorker: false, }; const element = document.querySelector(`#${graphId}`); @@ -77,8 +78,7 @@ export class Graph extends React.Component { const borderColor = getComputedStyle(element).borderColor; let firstPolygon = true; - graphviz(`#${graphId}`) - .options(options) + graphviz(`#${graphId}`, options) .attributer(function (d) { if (d.tag === "a") { const url = d.attributes["xlink:href"] || d.attributes["href"]; From 7b2ef6bf76be4af119caedf94d7769b0960671bb Mon Sep 17 00:00:00 2001 From: Charis Kyriakou Date: Mon, 27 Feb 2023 15:51:55 +0000 Subject: [PATCH 02/19] Pass both local queries and variant analyses dirs in query history scrubber --- extensions/ql-vscode/src/extension.ts | 8 +++++++- .../src/query-history/query-history-dirs.ts | 4 ++++ .../src/query-history/query-history-manager.ts | 5 +++-- .../src/query-history/query-history-scrubber.ts | 17 +++++++++-------- .../query-history/query-history-dirs.ts | 14 ++++++++++++++ .../history-tree-data-provider.test.ts | 3 ++- .../query-history/query-history-manager.test.ts | 3 ++- .../query-history-scrubber.test.ts | 2 +- .../variant-analysis-history.test.ts | 3 ++- 9 files changed, 44 insertions(+), 15 deletions(-) create mode 100644 extensions/ql-vscode/src/query-history/query-history-dirs.ts create mode 100644 extensions/ql-vscode/test/factories/query-history/query-history-dirs.ts diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 5960da4dc..e9143b129 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -131,6 +131,7 @@ import { ExtensionApp } from "./common/vscode/vscode-app"; import { RepositoriesFilterSortStateWithIds } from "./pure/variant-analysis-filter-sort"; import { DbModule } from "./databases/db-module"; import { redactableError } from "./pure/errors"; +import { QueryHistoryDirs } from "./query-history/query-history-dirs"; /** * extension.ts @@ -649,13 +650,18 @@ async function activateWithInstalledDistribution( ); void extLogger.log("Initializing query history."); + const queryHistoryDirs: QueryHistoryDirs = { + localQueriesDirPath: queryStorageDir, + variantAnalysesDirPath: variantAnalysisStorageDir, + }; + const qhm = new QueryHistoryManager( qs, dbm, localQueryResultsView, variantAnalysisManager, evalLogViewer, - queryStorageDir, + queryHistoryDirs, ctx, queryHistoryConfigurationListener, labelProvider, diff --git a/extensions/ql-vscode/src/query-history/query-history-dirs.ts b/extensions/ql-vscode/src/query-history/query-history-dirs.ts new file mode 100644 index 000000000..46a463d7e --- /dev/null +++ b/extensions/ql-vscode/src/query-history/query-history-dirs.ts @@ -0,0 +1,4 @@ +export interface QueryHistoryDirs { + localQueriesDirPath: string; + variantAnalysesDirPath: string; +} diff --git a/extensions/ql-vscode/src/query-history/query-history-manager.ts b/extensions/ql-vscode/src/query-history/query-history-manager.ts index dda9ad1b2..daffbb9d5 100644 --- a/extensions/ql-vscode/src/query-history/query-history-manager.ts +++ b/extensions/ql-vscode/src/query-history/query-history-manager.ts @@ -65,6 +65,7 @@ import { VariantAnalysisHistoryItem } from "./variant-analysis-history-item"; import { getTotalResultCount } from "../variant-analysis/shared/variant-analysis"; import { HistoryTreeDataProvider } from "./history-tree-data-provider"; import { redactableError } from "../pure/errors"; +import { QueryHistoryDirs } from "./query-history-dirs"; /** * query-history-manager.ts @@ -139,7 +140,7 @@ export class QueryHistoryManager extends DisposableObject { private readonly localQueriesResultsView: ResultsView, private readonly variantAnalysisManager: VariantAnalysisManager, private readonly evalLogViewer: EvalLogViewer, - private readonly queryStorageDir: string, + private readonly queryHistoryDirs: QueryHistoryDirs, ctx: ExtensionContext, private readonly queryHistoryConfigListener: QueryHistoryConfig, private readonly labelProvider: HistoryItemLabelProvider, @@ -389,7 +390,7 @@ export class QueryHistoryManager extends DisposableObject { ONE_HOUR_IN_MS, TWO_HOURS_IN_MS, queryHistoryConfigListener.ttlInMillis, - this.queryStorageDir, + this.queryHistoryDirs, qhm, ctx, ), diff --git a/extensions/ql-vscode/src/query-history/query-history-scrubber.ts b/extensions/ql-vscode/src/query-history/query-history-scrubber.ts index cae683d7b..2d900adb0 100644 --- a/extensions/ql-vscode/src/query-history/query-history-scrubber.ts +++ b/extensions/ql-vscode/src/query-history/query-history-scrubber.ts @@ -3,6 +3,7 @@ import { EOL } from "os"; import { join } from "path"; import { Disposable, ExtensionContext } from "vscode"; import { extLogger } from "../common"; +import { QueryHistoryDirs } from "./query-history-dirs"; import { QueryHistoryManager } from "./query-history-manager"; const LAST_SCRUB_TIME_KEY = "lastScrubTime"; @@ -23,14 +24,14 @@ type Counter = { * @param wakeInterval How often to check to see if the job should run. * @param throttleTime How often to actually run the job. * @param maxQueryTime The maximum age of a query before is ready for deletion. - * @param queryDirectory The directory containing all queries. + * @param queryHistoryDirs The directories containing all query history information. * @param ctx The extension context. */ export function registerQueryHistoryScrubber( wakeInterval: number, throttleTime: number, maxQueryTime: number, - queryDirectory: string, + queryHistoryDirs: QueryHistoryDirs, qhm: QueryHistoryManager, ctx: ExtensionContext, @@ -42,7 +43,7 @@ export function registerQueryHistoryScrubber( wakeInterval, throttleTime, maxQueryTime, - queryDirectory, + queryHistoryDirs, qhm, ctx, counter, @@ -58,7 +59,7 @@ export function registerQueryHistoryScrubber( async function scrubQueries( throttleTime: number, maxQueryTime: number, - queryDirectory: string, + queryHistoryDirs: QueryHistoryDirs, qhm: QueryHistoryManager, ctx: ExtensionContext, counter?: Counter, @@ -75,17 +76,17 @@ async function scrubQueries( try { counter?.increment(); void extLogger.log("Scrubbing query directory. Removing old queries."); - if (!(await pathExists(queryDirectory))) { + if (!(await pathExists(queryHistoryDirs.localQueriesDirPath))) { void extLogger.log( - `Cannot scrub. Query directory does not exist: ${queryDirectory}`, + `Cannot scrub. Query directory does not exist: ${queryHistoryDirs.localQueriesDirPath}`, ); return; } - const baseNames = await readdir(queryDirectory); + const baseNames = await readdir(queryHistoryDirs.localQueriesDirPath); const errors: string[] = []; for (const baseName of baseNames) { - const dir = join(queryDirectory, baseName); + const dir = join(queryHistoryDirs.localQueriesDirPath, baseName); const scrubResult = await scrubDirectory(dir, now, maxQueryTime); if (scrubResult.errorMsg) { errors.push(scrubResult.errorMsg); diff --git a/extensions/ql-vscode/test/factories/query-history/query-history-dirs.ts b/extensions/ql-vscode/test/factories/query-history/query-history-dirs.ts new file mode 100644 index 000000000..02f9cdf79 --- /dev/null +++ b/extensions/ql-vscode/test/factories/query-history/query-history-dirs.ts @@ -0,0 +1,14 @@ +import { QueryHistoryDirs } from "../../../src/query-history/query-history-dirs"; + +export function createMockQueryHistoryDirs({ + localQueriesDirPath = "mock-local-queries-dir-path", + variantAnalysesDirPath = "mock-variant-analyses-dir-path", +}: { + localQueriesDirPath?: string; + variantAnalysesDirPath?: string; +} = {}): QueryHistoryDirs { + return { + localQueriesDirPath, + variantAnalysesDirPath, + }; +} diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/history-tree-data-provider.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/history-tree-data-provider.test.ts index f5192f98a..b63448a40 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/history-tree-data-provider.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/history-tree-data-provider.test.ts @@ -26,6 +26,7 @@ import { SortOrder, } from "../../../../src/query-history/history-tree-data-provider"; import { QueryHistoryManager } from "../../../../src/query-history/query-history-manager"; +import { createMockQueryHistoryDirs } from "../../../factories/query-history/query-history-dirs"; describe("HistoryTreeDataProvider", () => { const mockExtensionLocation = join(tmpDir.name, "mock-extension-location"); @@ -425,7 +426,7 @@ describe("HistoryTreeDataProvider", () => { localQueriesResultsViewStub, variantAnalysisManagerStub, {} as EvalLogViewer, - "xxx", + createMockQueryHistoryDirs(), { globalStorageUri: vscode.Uri.file(mockExtensionLocation), extensionPath: vscode.Uri.file("/x/y/z").fsPath, diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/query-history-manager.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/query-history-manager.test.ts index 0c05339a3..78f52c02f 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/query-history-manager.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/query-history-manager.test.ts @@ -26,6 +26,7 @@ import { QuickPickItem, TextEditor } from "vscode"; import { WebviewReveal } from "../../../../src/interface-utils"; import * as helpers from "../../../../src/helpers"; import { mockedObject } from "../../utils/mocking.helpers"; +import { createMockQueryHistoryDirs } from "../../../factories/query-history/query-history-dirs"; describe("QueryHistoryManager", () => { const mockExtensionLocation = join(tmpDir.name, "mock-extension-location"); @@ -1150,7 +1151,7 @@ describe("QueryHistoryManager", () => { localQueriesResultsViewStub, variantAnalysisManagerStub, {} as EvalLogViewer, - "xxx", + createMockQueryHistoryDirs(), { globalStorageUri: vscode.Uri.file(mockExtensionLocation), extensionPath: vscode.Uri.file("/x/y/z").fsPath, diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/query-history-scrubber.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/query-history-scrubber.test.ts index 227ff6eea..427c84de4 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/query-history-scrubber.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/query-history-scrubber.test.ts @@ -181,7 +181,7 @@ describe("query history scrubber", () => { ONE_HOUR_IN_MS, TWO_HOURS_IN_MS, LESS_THAN_ONE_DAY, - dir, + { localQueriesDirPath: dir, variantAnalysesDirPath: dir }, mockedObject({ removeDeletedQueries: () => { return Promise.resolve(); diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/variant-analysis-history.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/variant-analysis-history.test.ts index 9ce8c372f..6441d2fea 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/variant-analysis-history.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/variant-analysis-history.test.ts @@ -20,6 +20,7 @@ import { QueryRunner } from "../../../../src/queryRunner"; import { VariantAnalysisManager } from "../../../../src/variant-analysis/variant-analysis-manager"; import { QueryHistoryManager } from "../../../../src/query-history/query-history-manager"; import { mockedObject } from "../../utils/mocking.helpers"; +import { createMockQueryHistoryDirs } from "../../../factories/query-history/query-history-dirs"; // set a higher timeout since recursive delete may take a while, expecially on Windows. jest.setTimeout(120000); @@ -74,7 +75,7 @@ describe("Variant Analyses and QueryHistoryManager", () => { localQueriesResultsViewStub, variantAnalysisManagerStub, {} as EvalLogViewer, - STORAGE_DIR, + createMockQueryHistoryDirs({ localQueriesDirPath: STORAGE_DIR }), mockedObject({ globalStorageUri: Uri.file(STORAGE_DIR), storageUri: undefined, From 06463a25e69af2b539ee417684640162b6839a6b Mon Sep 17 00:00:00 2001 From: Charis Kyriakou Date: Mon, 27 Feb 2023 16:26:30 +0000 Subject: [PATCH 03/19] Clean up variant analyses directory --- extensions/ql-vscode/src/pure/files.ts | 5 ++++ .../query-history/query-history-scrubber.ts | 28 +++++++++++++++---- .../test/unit-tests/pure/files.test.ts | 15 ++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/extensions/ql-vscode/src/pure/files.ts b/extensions/ql-vscode/src/pure/files.ts index c07e0f26d..e35034ed4 100644 --- a/extensions/ql-vscode/src/pure/files.ts +++ b/extensions/ql-vscode/src/pure/files.ts @@ -67,3 +67,8 @@ export function pathsEqual( } return path1 === path2; } + +export async function readDirFullPaths(path: string): Promise { + const baseNames = await readdir(path); + return baseNames.map((baseName) => join(path, baseName)); +} diff --git a/extensions/ql-vscode/src/query-history/query-history-scrubber.ts b/extensions/ql-vscode/src/query-history/query-history-scrubber.ts index 2d900adb0..878e7c382 100644 --- a/extensions/ql-vscode/src/query-history/query-history-scrubber.ts +++ b/extensions/ql-vscode/src/query-history/query-history-scrubber.ts @@ -1,8 +1,9 @@ -import { pathExists, readdir, stat, remove, readFile } from "fs-extra"; +import { pathExists, stat, remove, readFile } from "fs-extra"; import { EOL } from "os"; import { join } from "path"; import { Disposable, ExtensionContext } from "vscode"; import { extLogger } from "../common"; +import { readDirFullPaths } from "../pure/files"; import { QueryHistoryDirs } from "./query-history-dirs"; import { QueryHistoryManager } from "./query-history-manager"; @@ -75,18 +76,33 @@ async function scrubQueries( let scrubCount = 0; // total number of directories deleted try { counter?.increment(); - void extLogger.log("Scrubbing query directory. Removing old queries."); + void extLogger.log( + "Cleaning up query history directories. Removing old entries.", + ); + if (!(await pathExists(queryHistoryDirs.localQueriesDirPath))) { void extLogger.log( - `Cannot scrub. Query directory does not exist: ${queryHistoryDirs.localQueriesDirPath}`, + `Cannot clean up query history directories. Local queries directory does not exist: ${queryHistoryDirs.localQueriesDirPath}`, + ); + return; + } + if (!(await pathExists(queryHistoryDirs.variantAnalysesDirPath))) { + void extLogger.log( + `Cannot clean up query history directories. Variant analyses directory does not exist: ${queryHistoryDirs.variantAnalysesDirPath}`, ); return; } - const baseNames = await readdir(queryHistoryDirs.localQueriesDirPath); + const localQueryDirPaths = await readDirFullPaths( + queryHistoryDirs.localQueriesDirPath, + ); + const variantAnalysisDirPaths = await readDirFullPaths( + queryHistoryDirs.variantAnalysesDirPath, + ); + const allDirPaths = [...localQueryDirPaths, ...variantAnalysisDirPaths]; + const errors: string[] = []; - for (const baseName of baseNames) { - const dir = join(queryHistoryDirs.localQueriesDirPath, baseName); + for (const dir of allDirPaths) { const scrubResult = await scrubDirectory(dir, now, maxQueryTime); if (scrubResult.errorMsg) { errors.push(scrubResult.errorMsg); diff --git a/extensions/ql-vscode/test/unit-tests/pure/files.test.ts b/extensions/ql-vscode/test/unit-tests/pure/files.test.ts index 8f3ddec60..5bd244086 100644 --- a/extensions/ql-vscode/test/unit-tests/pure/files.test.ts +++ b/extensions/ql-vscode/test/unit-tests/pure/files.test.ts @@ -4,6 +4,7 @@ import { gatherQlFiles, getDirectoryNamesInsidePath, pathsEqual, + readDirFullPaths, } from "../../../src/pure/files"; describe("files", () => { @@ -100,6 +101,20 @@ describe("files", () => { expect(result).toEqual(["sub-folder"]); }); }); + + describe("readDirFullPaths", () => { + it("should return all files with full path", async () => { + const file1 = join(dataDir, "compute-default-strings.ql"); + const file2 = join(dataDir, "multiple-result-sets.ql"); + const file3 = join(dataDir, "query.ql"); + + const paths = await readDirFullPaths(dataDir); + + expect(paths.some((path) => path === file1)).toBe(true); + expect(paths.some((path) => path === file2)).toBe(true); + expect(paths.some((path) => path === file3)).toBe(true); + }); + }); }); describe("pathsEqual", () => { From d3e64539d0680de048b243896f01e33dde4fe8f2 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Tue, 28 Feb 2023 15:22:23 +0100 Subject: [PATCH 04/19] Only allow WASM execution in results view --- extensions/ql-vscode/src/abstract-webview.ts | 2 ++ extensions/ql-vscode/src/interface-utils.ts | 7 ++++++- extensions/ql-vscode/src/interface.ts | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/extensions/ql-vscode/src/abstract-webview.ts b/extensions/ql-vscode/src/abstract-webview.ts index c6ad5ab4c..cf46408ad 100644 --- a/extensions/ql-vscode/src/abstract-webview.ts +++ b/extensions/ql-vscode/src/abstract-webview.ts @@ -24,6 +24,7 @@ export type WebviewPanelConfig = { view: WebviewView; preserveFocus?: boolean; additionalOptions?: WebviewPanelOptions & WebviewOptions; + allowWasmEval?: boolean; }; export abstract class AbstractWebview< @@ -116,6 +117,7 @@ export abstract class AbstractWebview< config.view, { allowInlineStyles: true, + allowWasmEval: config.allowWasmEval ?? false, }, ); this.push( diff --git a/extensions/ql-vscode/src/interface-utils.ts b/extensions/ql-vscode/src/interface-utils.ts index 779575726..a8cf2d90d 100644 --- a/extensions/ql-vscode/src/interface-utils.ts +++ b/extensions/ql-vscode/src/interface-utils.ts @@ -129,10 +129,13 @@ export function getHtmlForWebview( view: WebviewView, { allowInlineStyles, + allowWasmEval, }: { allowInlineStyles?: boolean; + allowWasmEval?: boolean; } = { allowInlineStyles: false, + allowWasmEval: false, }, ): string { const scriptUriOnDisk = Uri.file(ctx.asAbsolutePath("out/webview.js")); @@ -172,7 +175,9 @@ export function getHtmlForWebview( ${stylesheetsHtmlLines.join(` ${EOL}`)} diff --git a/extensions/ql-vscode/src/interface.ts b/extensions/ql-vscode/src/interface.ts index cf27f52db..e6d0c4266 100644 --- a/extensions/ql-vscode/src/interface.ts +++ b/extensions/ql-vscode/src/interface.ts @@ -221,6 +221,8 @@ export class ResultsView extends AbstractWebview< viewColumn: this.chooseColumnForWebview(), preserveFocus: true, view: "results", + // Required for the graph viewer which is using d3-graphviz WASM module + allowWasmEval: true, }; } From b3d980484241f2d1fd768597b6c05b0382ebb85b Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Wed, 1 Mar 2023 09:52:56 +0100 Subject: [PATCH 05/19] Only enable graph viewer in canary mode --- extensions/ql-vscode/src/interface.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/extensions/ql-vscode/src/interface.ts b/extensions/ql-vscode/src/interface.ts index e6d0c4266..a10b9d76e 100644 --- a/extensions/ql-vscode/src/interface.ts +++ b/extensions/ql-vscode/src/interface.ts @@ -64,7 +64,7 @@ import { ResultSetSchema, } from "./pure/bqrs-cli-types"; import { AbstractWebview, WebviewPanelConfig } from "./abstract-webview"; -import { PAGE_SIZE } from "./config"; +import { isCanary, PAGE_SIZE } from "./config"; import { HistoryItemLabelProvider } from "./query-history/history-item-label-provider"; import { telemetryListener } from "./telemetry"; import { redactableError } from "./pure/errors"; @@ -221,8 +221,8 @@ export class ResultsView extends AbstractWebview< viewColumn: this.chooseColumnForWebview(), preserveFocus: true, view: "results", - // Required for the graph viewer which is using d3-graphviz WASM module - allowWasmEval: true, + // Required for the graph viewer which is using d3-graphviz WASM module. Only supported in canary mode. + allowWasmEval: isCanary(), }; } @@ -658,7 +658,8 @@ export class ResultsView extends AbstractWebview< } let data; let numTotalResults; - if (metadata?.kind === GRAPH_TABLE_NAME) { + // Graph results are only supported in canary mode because the graph viewer is unsupported + if (metadata?.kind === GRAPH_TABLE_NAME && isCanary()) { data = await interpretGraphResults( this.cliServer, metadata, From 59cc93f94f624729dc04d33c91ff4d1f30f8dd04 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Wed, 1 Mar 2023 12:29:11 +0100 Subject: [PATCH 06/19] Remove `as unknown as DatabaseItem` --- .../no-workspace/astViewer.test.ts | 6 +- .../contextual/astBuilder.test.ts | 7 ++- .../contextual/fileRangeFromURI.test.ts | 5 +- .../contextual/queryResolver.test.ts | 7 +-- .../no-workspace/interface-utils.test.ts | 62 +++++++------------ .../vscode-tests/utils/mocking.helpers.ts | 16 +++++ 6 files changed, 50 insertions(+), 53 deletions(-) diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/astViewer.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/astViewer.test.ts index f5593b216..09822a296 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/astViewer.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/astViewer.test.ts @@ -3,8 +3,8 @@ import { load } from "js-yaml"; import { AstViewer, AstItem } from "../../../src/astViewer"; import { commands, Range, Uri } from "vscode"; -import { DatabaseItem } from "../../../src/local-databases"; import { testDisposeHandler } from "../test-dispose-handler"; +import { mockDatabaseItem } from "../utils/mocking.helpers"; describe("AstViewer", () => { let astRoots: AstItem[]; @@ -31,7 +31,7 @@ describe("AstViewer", () => { }); it("should update the viewer roots", () => { - const item = {} as DatabaseItem; + const item = mockDatabaseItem(); viewer = new AstViewer(); viewer.updateRoots(astRoots, item, Uri.file("def/abc")); @@ -71,7 +71,7 @@ describe("AstViewer", () => { selectionRange: Range | undefined, fileUri = defaultUri, ) { - const item = {} as DatabaseItem; + const item = mockDatabaseItem(); viewer = new AstViewer(); viewer.updateRoots(astRoots, item, defaultUri); diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/astBuilder.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/astBuilder.test.ts index 4f2c51944..02022aeba 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/astBuilder.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/astBuilder.test.ts @@ -2,10 +2,9 @@ import { readFileSync } from "fs-extra"; import AstBuilder from "../../../../src/contextual/astBuilder"; import { CodeQLCliServer } from "../../../../src/cli"; -import { DatabaseItem } from "../../../../src/local-databases"; import { Uri } from "vscode"; import { QueryWithResults } from "../../../../src/run-queries-shared"; -import { mockedObject } from "../../utils/mocking.helpers"; +import { mockDatabaseItem, mockedObject } from "../../utils/mocking.helpers"; /** * @@ -146,7 +145,9 @@ describe("AstBuilder", () => { }, } as QueryWithResults, mockCli, - {} as DatabaseItem, + mockDatabaseItem({ + resolveSourceFile: undefined, + }), Uri.file(""), ); } diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/fileRangeFromURI.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/fileRangeFromURI.test.ts index afa1adfbd..f69842c42 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/fileRangeFromURI.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/fileRangeFromURI.test.ts @@ -6,6 +6,7 @@ import { WholeFileLocation, LineColumnLocation, } from "../../../../src/pure/bqrs-cli-types"; +import { mockDatabaseItem } from "../../utils/mocking.helpers"; describe("fileRangeFromURI", () => { it("should return undefined when value is not a file URI", () => { @@ -92,8 +93,8 @@ describe("fileRangeFromURI", () => { }); function createMockDatabaseItem(): DatabaseItem { - return { + return mockDatabaseItem({ resolveSourceFile: (file: string) => Uri.parse(file), - } as DatabaseItem; + }); } }); diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/queryResolver.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/queryResolver.test.ts index 83a18d361..ed4fd5129 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/queryResolver.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/contextual/queryResolver.test.ts @@ -10,8 +10,7 @@ import { resolveQueries, } from "../../../../src/contextual/queryResolver"; import { CodeQLCliServer } from "../../../../src/cli"; -import { DatabaseItem } from "../../../../src/local-databases"; -import { mockedObject } from "../../utils/mocking.helpers"; +import { mockDatabaseItem, mockedObject } from "../../utils/mocking.helpers"; describe("queryResolver", () => { let getQlPackForDbschemeSpy: jest.SpiedFunction< @@ -96,13 +95,13 @@ describe("queryResolver", () => { dbschemePack: "my-qlpack", dbschemePackIsLibraryPack: false, }); - const db = { + const db = mockDatabaseItem({ contents: { datasetUri: { fsPath: "/path/to/database", }, }, - } as unknown as DatabaseItem; + }); const result = await qlpackOfDatabase(mockCli, db); expect(result).toEqual({ dbschemePack: "my-qlpack", diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/interface-utils.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/interface-utils.test.ts index 6abee13c4..ff0c0fb1f 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/interface-utils.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/interface-utils.test.ts @@ -14,7 +14,7 @@ import { tryResolveLocation, } from "../../../src/interface-utils"; import { getDefaultResultSetName } from "../../../src/pure/interface-types"; -import { DatabaseItem } from "../../../src/local-databases"; +import { mockDatabaseItem } from "../utils/mocking.helpers"; describe("interface-utils", () => { describe("webview uri conversion", () => { @@ -84,27 +84,21 @@ describe("interface-utils", () => { describe("resolveWholeFileLocation", () => { it("should resolve a whole file location", () => { - const mockDatabaseItem: DatabaseItem = { - resolveSourceFile: jest.fn().mockReturnValue(Uri.file("abc")), - } as unknown as DatabaseItem; + const databaseItem = mockDatabaseItem(); expect( - tryResolveLocation("file://hucairz:0:0:0:0", mockDatabaseItem), + tryResolveLocation("file://hucairz:0:0:0:0", databaseItem), ).toEqual(new Location(Uri.file("abc"), new Range(0, 0, 0, 0))); }); it("should resolve a five-part location edge case", () => { - const mockDatabaseItem: DatabaseItem = { - resolveSourceFile: jest.fn().mockReturnValue(Uri.file("abc")), - } as unknown as DatabaseItem; + const databaseItem = mockDatabaseItem(); expect( - tryResolveLocation("file://hucairz:1:1:1:1", mockDatabaseItem), + tryResolveLocation("file://hucairz:1:1:1:1", databaseItem), ).toEqual(new Location(Uri.file("abc"), new Range(0, 0, 0, 1))); }); it("should resolve a five-part location", () => { - const mockDatabaseItem: DatabaseItem = { - resolveSourceFile: jest.fn().mockReturnValue(Uri.parse("abc")), - } as unknown as DatabaseItem; + const databaseItem = mockDatabaseItem(); expect( tryResolveLocation( @@ -115,7 +109,7 @@ describe("interface-utils", () => { endLine: 5, uri: "hucairz", }, - mockDatabaseItem, + databaseItem, ), ).toEqual( new Location( @@ -123,16 +117,12 @@ describe("interface-utils", () => { new Range(new Position(4, 3), new Position(3, 0)), ), ); - expect(mockDatabaseItem.resolveSourceFile).toHaveBeenCalledTimes(1); - expect(mockDatabaseItem.resolveSourceFile).toHaveBeenCalledWith( - "hucairz", - ); + expect(databaseItem.resolveSourceFile).toHaveBeenCalledTimes(1); + expect(databaseItem.resolveSourceFile).toHaveBeenCalledWith("hucairz"); }); it("should resolve a five-part location with an empty path", () => { - const mockDatabaseItem: DatabaseItem = { - resolveSourceFile: jest.fn().mockReturnValue(Uri.parse("abc")), - } as unknown as DatabaseItem; + const databaseItem = mockDatabaseItem(); expect( tryResolveLocation( @@ -143,51 +133,41 @@ describe("interface-utils", () => { endLine: 5, uri: "", }, - mockDatabaseItem, + databaseItem, ), ).toBeUndefined(); }); it("should resolve a string location for whole file", () => { - const mockDatabaseItem: DatabaseItem = { - resolveSourceFile: jest.fn().mockReturnValue(Uri.parse("abc")), - } as unknown as DatabaseItem; + const databaseItem = mockDatabaseItem(); expect( - tryResolveLocation("file://hucairz:0:0:0:0", mockDatabaseItem), + tryResolveLocation("file://hucairz:0:0:0:0", databaseItem), ).toEqual(new Location(Uri.parse("abc"), new Range(0, 0, 0, 0))); - expect(mockDatabaseItem.resolveSourceFile).toHaveBeenCalledTimes(1); - expect(mockDatabaseItem.resolveSourceFile).toHaveBeenCalledWith( - "hucairz", - ); + expect(databaseItem.resolveSourceFile).toHaveBeenCalledTimes(1); + expect(databaseItem.resolveSourceFile).toHaveBeenCalledWith("hucairz"); }); it("should resolve a string location for five-part location", () => { - const mockDatabaseItem: DatabaseItem = { - resolveSourceFile: jest.fn().mockReturnValue(Uri.parse("abc")), - } as unknown as DatabaseItem; + const databaseItem = mockDatabaseItem(); expect( - tryResolveLocation("file://hucairz:5:4:3:2", mockDatabaseItem), + tryResolveLocation("file://hucairz:5:4:3:2", databaseItem), ).toEqual( new Location( Uri.parse("abc"), new Range(new Position(4, 3), new Position(2, 2)), ), ); - expect(mockDatabaseItem.resolveSourceFile).toHaveBeenCalledTimes(1); - expect(mockDatabaseItem.resolveSourceFile).toHaveBeenCalledWith( - "hucairz", - ); + expect(databaseItem.resolveSourceFile).toHaveBeenCalledTimes(1); + expect(databaseItem.resolveSourceFile).toHaveBeenCalledWith("hucairz"); }); it("should resolve a string location for invalid string", () => { - const mockDatabaseItem: DatabaseItem = { - resolveSourceFile: jest.fn().mockReturnValue(Uri.parse("abc")), - } as unknown as DatabaseItem; + const databaseItem = mockDatabaseItem(); expect( - tryResolveLocation("file://hucairz:x:y:z:a", mockDatabaseItem), + tryResolveLocation("file://hucairz:x:y:z:a", databaseItem), ).toBeUndefined(); }); }); diff --git a/extensions/ql-vscode/test/vscode-tests/utils/mocking.helpers.ts b/extensions/ql-vscode/test/vscode-tests/utils/mocking.helpers.ts index 6d552029e..70b58e7b7 100644 --- a/extensions/ql-vscode/test/vscode-tests/utils/mocking.helpers.ts +++ b/extensions/ql-vscode/test/vscode-tests/utils/mocking.helpers.ts @@ -1,3 +1,6 @@ +import { DatabaseItem } from "../../../src/local-databases"; +import { Uri } from "vscode"; + export type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial; @@ -41,3 +44,16 @@ export function mockedObject( }, }); } + +export function mockDatabaseItem( + props: DeepPartial = {}, +): DatabaseItem { + return mockedObject({ + databaseUri: Uri.file("abc"), + name: "github/codeql", + language: "javascript", + sourceArchive: undefined, + resolveSourceFile: jest.fn().mockReturnValue(Uri.file("abc")), + ...props, + }); +} From 1c6ecf4a5c8f7e0c1b4ebe10cb72b0526af78d40 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Wed, 1 Mar 2023 12:30:11 +0100 Subject: [PATCH 07/19] Remove `as unknown as FullDatabaseOptions` --- .../test/vscode-tests/no-workspace/test-adapter.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/test-adapter.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/test-adapter.test.ts index 357bec8df..129efca7a 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/test-adapter.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/test-adapter.test.ts @@ -37,7 +37,7 @@ describe("test-adapter", () => { const preTestDatabaseItem = new DatabaseItemImpl( Uri.file("/path/to/test/dir/dir.testproj"), undefined, - { displayName: "custom display name" } as unknown as FullDatabaseOptions, + mockedObject({ displayName: "custom display name" }), (_) => { /* no change event listener */ }, @@ -45,7 +45,7 @@ describe("test-adapter", () => { const postTestDatabaseItem = new DatabaseItemImpl( Uri.file("/path/to/test/dir/dir.testproj"), undefined, - { displayName: "default name" } as unknown as FullDatabaseOptions, + mockedObject({ displayName: "default name" }), (_) => { /* no change event listener */ }, From 2b346b68732c06e380119f4357b02c2ee26de938 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Wed, 1 Mar 2023 13:54:54 +0100 Subject: [PATCH 08/19] Move variant analysis monitor tests to activated extension suite --- .../variant-analysis/variant-analysis-monitor.test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename extensions/ql-vscode/test/vscode-tests/{cli-integration => activated-extension}/variant-analysis/variant-analysis-monitor.test.ts (100%) diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-monitor.test.ts b/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-monitor.test.ts similarity index 100% rename from extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-monitor.test.ts rename to extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-monitor.test.ts From daaeb5be3f9fc2ed7677056adeae2b0ab57831ad Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Wed, 1 Mar 2023 14:03:00 +0100 Subject: [PATCH 09/19] Move some variant analysis manager test to activated extension suite --- .../activated-extension/jest.setup.ts | 5 + .../variant-analysis-manager.test.ts | 910 ++++++++++++++++++ .../cli-integration/jest.setup.ts | 36 +- .../variant-analysis-manager.test.ts | 868 +---------------- .../jest.activated-extension.setup.ts | 29 + 5 files changed, 953 insertions(+), 895 deletions(-) create mode 100644 extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-manager.test.ts diff --git a/extensions/ql-vscode/test/vscode-tests/activated-extension/jest.setup.ts b/extensions/ql-vscode/test/vscode-tests/activated-extension/jest.setup.ts index 69c4f39f1..6ffa38a39 100644 --- a/extensions/ql-vscode/test/vscode-tests/activated-extension/jest.setup.ts +++ b/extensions/ql-vscode/test/vscode-tests/activated-extension/jest.setup.ts @@ -1,4 +1,5 @@ import { + afterAllAction, beforeAllAction, beforeEachAction, } from "../jest.activated-extension.setup"; @@ -10,3 +11,7 @@ beforeAll(async () => { beforeEach(async () => { await beforeEachAction(); }); + +afterAll(async () => { + await afterAllAction(); +}); diff --git a/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-manager.test.ts b/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-manager.test.ts new file mode 100644 index 000000000..04389f6c9 --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-manager.test.ts @@ -0,0 +1,910 @@ +import { + CancellationTokenSource, + commands, + env, + extensions, + TextDocument, + TextEditor, + Uri, + window, + workspace, +} from "vscode"; +import { CodeQLExtensionInterface } from "../../../../src/extension"; +import { extLogger } from "../../../../src/common"; +import * as ghApiClient from "../../../../src/variant-analysis/gh-api/gh-api-client"; +import * as ghActionsApiClient from "../../../../src/variant-analysis/gh-api/gh-actions-api-client"; +import * as fs from "fs-extra"; +import { join } from "path"; +import { Readable } from "stream"; +import * as fetchModule from "node-fetch"; +import { Response } from "node-fetch"; + +import { VariantAnalysisManager } from "../../../../src/variant-analysis/variant-analysis-manager"; +import { CodeQLCliServer } from "../../../../src/cli"; +import { storagePath } from "../../global.helper"; +import { VariantAnalysisResultsManager } from "../../../../src/variant-analysis/variant-analysis-results-manager"; +import { createMockVariantAnalysis } from "../../../factories/variant-analysis/shared/variant-analysis"; +import * as VariantAnalysisModule from "../../../../src/variant-analysis/shared/variant-analysis"; +import { + VariantAnalysis, + VariantAnalysisScannedRepository, + VariantAnalysisScannedRepositoryDownloadStatus, + VariantAnalysisScannedRepositoryState, + VariantAnalysisStatus, +} from "../../../../src/variant-analysis/shared/variant-analysis"; +import { + createMockScannedRepo, + createMockScannedRepos, +} from "../../../factories/variant-analysis/shared/scanned-repositories"; +import { createTimestampFile } from "../../../../src/helpers"; +import { createMockVariantAnalysisRepoTask } from "../../../factories/variant-analysis/gh-api/variant-analysis-repo-task"; +import { VariantAnalysisRepoTask } from "../../../../src/variant-analysis/gh-api/variant-analysis"; +import { + defaultFilterSortState, + SortKey, +} from "../../../../src/pure/variant-analysis-filter-sort"; +import { DbManager } from "../../../../src/databases/db-manager"; +import { App } from "../../../../src/common/app"; +import { ExtensionApp } from "../../../../src/common/vscode/vscode-app"; +import { DbConfigStore } from "../../../../src/databases/config/db-config-store"; +import { mockedObject } from "../../utils/mocking.helpers"; + +// up to 3 minutes per test +jest.setTimeout(3 * 60 * 1000); + +describe("Variant Analysis Manager", () => { + let app: App; + let cancellationTokenSource: CancellationTokenSource; + let variantAnalysisManager: VariantAnalysisManager; + let variantAnalysisResultsManager: VariantAnalysisResultsManager; + let variantAnalysis: VariantAnalysis; + let scannedRepos: VariantAnalysisScannedRepository[]; + + beforeEach(async () => { + jest.spyOn(extLogger, "log").mockResolvedValue(undefined); + + cancellationTokenSource = new CancellationTokenSource(); + + scannedRepos = createMockScannedRepos(); + variantAnalysis = createMockVariantAnalysis({ + status: VariantAnalysisStatus.InProgress, + scannedRepos, + }); + + const extension = await extensions + .getExtension>( + "GitHub.vscode-codeql", + )! + .activate(); + const cli = mockedObject({}); + app = new ExtensionApp(extension.ctx); + const dbManager = new DbManager(app, new DbConfigStore(app)); + variantAnalysisResultsManager = new VariantAnalysisResultsManager( + cli, + extLogger, + ); + variantAnalysisManager = new VariantAnalysisManager( + extension.ctx, + app, + cli, + storagePath, + variantAnalysisResultsManager, + dbManager, + ); + }); + + describe("rehydrateVariantAnalysis", () => { + const variantAnalysis = createMockVariantAnalysis({}); + + describe("when the directory does not exist", () => { + it("should fire the removed event if the file does not exist", async () => { + const stub = jest.fn(); + variantAnalysisManager.onVariantAnalysisRemoved(stub); + + await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); + + expect(stub).toBeCalledTimes(1); + }); + }); + + describe("when the directory exists", () => { + beforeEach(async () => { + await fs.ensureDir(join(storagePath, variantAnalysis.id.toString())); + }); + + it("should store the variant analysis", async () => { + await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); + + expect( + await variantAnalysisManager.getVariantAnalysis(variantAnalysis.id), + ).toEqual(variantAnalysis); + }); + + it("should not error if the repo states file does not exist", async () => { + await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); + + expect( + await variantAnalysisManager.getRepoStates(variantAnalysis.id), + ).toEqual([]); + }); + + it("should read in the repo states if it exists", async () => { + await fs.writeJson( + join(storagePath, variantAnalysis.id.toString(), "repo_states.json"), + { + [scannedRepos[0].repository.id]: { + repositoryId: scannedRepos[0].repository.id, + downloadStatus: + VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, + }, + [scannedRepos[1].repository.id]: { + repositoryId: scannedRepos[1].repository.id, + downloadStatus: + VariantAnalysisScannedRepositoryDownloadStatus.InProgress, + }, + }, + ); + + await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); + + expect( + await variantAnalysisManager.getRepoStates(variantAnalysis.id), + ).toEqual( + expect.arrayContaining([ + { + repositoryId: scannedRepos[0].repository.id, + downloadStatus: + VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, + }, + { + repositoryId: scannedRepos[1].repository.id, + downloadStatus: + VariantAnalysisScannedRepositoryDownloadStatus.InProgress, + }, + ]), + ); + }); + }); + }); + + describe("autoDownloadVariantAnalysisResult", () => { + let getVariantAnalysisRepoStub: jest.SpiedFunction< + typeof ghApiClient.getVariantAnalysisRepo + >; + let getVariantAnalysisRepoResultStub: jest.SpiedFunction< + typeof fetchModule.default + >; + + let repoStatesPath: string; + + beforeEach(async () => { + getVariantAnalysisRepoStub = jest.spyOn( + ghApiClient, + "getVariantAnalysisRepo", + ); + getVariantAnalysisRepoResultStub = jest.spyOn(fetchModule, "default"); + + repoStatesPath = join( + storagePath, + variantAnalysis.id.toString(), + "repo_states.json", + ); + }); + + describe("when the artifact_url is missing", () => { + beforeEach(async () => { + const dummyRepoTask = createMockVariantAnalysisRepoTask(); + delete dummyRepoTask.artifact_url; + + getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask); + }); + + 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); + + const sourceFilePath = join( + __dirname, + "../../cli-integration/data/variant-analysis-results.zip", + ); + const fileContents = fs.readFileSync(sourceFilePath); + const response = new Response(Readable.from(fileContents)); + response.size = fileContents.length; + getVariantAnalysisRepoResultStub.mockResolvedValue(response); + }); + + it("should return early if variant analysis is cancelled", async () => { + cancellationTokenSource.cancel(); + + await variantAnalysisManager.autoDownloadVariantAnalysisResult( + scannedRepos[0], + variantAnalysis, + cancellationTokenSource.token, + ); + + expect(getVariantAnalysisRepoStub).not.toHaveBeenCalled(); + }); + + it("should fetch a repo task", async () => { + await variantAnalysisManager.autoDownloadVariantAnalysisResult( + scannedRepos[0], + variantAnalysis, + cancellationTokenSource.token, + ); + + expect(getVariantAnalysisRepoStub).toHaveBeenCalled(); + }); + + it("should fetch a repo result", async () => { + await variantAnalysisManager.autoDownloadVariantAnalysisResult( + scannedRepos[0], + variantAnalysis, + cancellationTokenSource.token, + ); + + expect(getVariantAnalysisRepoResultStub).toHaveBeenCalled(); + }); + + it("should skip the download if the repository has already been downloaded", async () => { + // First, do a download so it is downloaded. This avoids having to mock the repo states. + await variantAnalysisManager.autoDownloadVariantAnalysisResult( + scannedRepos[0], + variantAnalysis, + cancellationTokenSource.token, + ); + + getVariantAnalysisRepoStub.mockClear(); + + await variantAnalysisManager.autoDownloadVariantAnalysisResult( + scannedRepos[0], + variantAnalysis, + cancellationTokenSource.token, + ); + + expect(getVariantAnalysisRepoStub).not.toHaveBeenCalled(); + }); + + it("should write the repo state when the download is successful", async () => { + await variantAnalysisManager.autoDownloadVariantAnalysisResult( + scannedRepos[0], + variantAnalysis, + cancellationTokenSource.token, + ); + + await expect(fs.readJson(repoStatesPath)).resolves.toEqual({ + [scannedRepos[0].repository.id]: { + repositoryId: scannedRepos[0].repository.id, + downloadStatus: + VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, + }, + }); + }); + + it("should not write the repo state when the download fails", async () => { + getVariantAnalysisRepoResultStub.mockRejectedValue( + new Error("Failed to download"), + ); + + await expect( + variantAnalysisManager.autoDownloadVariantAnalysisResult( + scannedRepos[0], + variantAnalysis, + cancellationTokenSource.token, + ), + ).rejects.toThrow(); + + await expect(fs.pathExists(repoStatesPath)).resolves.toBe(false); + }); + + it("should have a failed repo state when the repo task API fails", async () => { + getVariantAnalysisRepoStub.mockRejectedValueOnce( + new Error("Failed to download"), + ); + + await expect( + variantAnalysisManager.autoDownloadVariantAnalysisResult( + scannedRepos[0], + variantAnalysis, + cancellationTokenSource.token, + ), + ).rejects.toThrow(); + + await expect(fs.pathExists(repoStatesPath)).resolves.toBe(false); + + await variantAnalysisManager.autoDownloadVariantAnalysisResult( + scannedRepos[1], + variantAnalysis, + cancellationTokenSource.token, + ); + + await expect(fs.readJson(repoStatesPath)).resolves.toEqual({ + [scannedRepos[0].repository.id]: { + repositoryId: scannedRepos[0].repository.id, + downloadStatus: + VariantAnalysisScannedRepositoryDownloadStatus.Failed, + }, + [scannedRepos[1].repository.id]: { + repositoryId: scannedRepos[1].repository.id, + downloadStatus: + VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, + }, + }); + }); + + it("should have a failed repo state when the download fails", async () => { + getVariantAnalysisRepoResultStub.mockRejectedValueOnce( + new Error("Failed to download"), + ); + + await expect( + variantAnalysisManager.autoDownloadVariantAnalysisResult( + scannedRepos[0], + variantAnalysis, + cancellationTokenSource.token, + ), + ).rejects.toThrow(); + + await expect(fs.pathExists(repoStatesPath)).resolves.toBe(false); + + await variantAnalysisManager.autoDownloadVariantAnalysisResult( + scannedRepos[1], + variantAnalysis, + cancellationTokenSource.token, + ); + + await expect(fs.readJson(repoStatesPath)).resolves.toEqual({ + [scannedRepos[0].repository.id]: { + repositoryId: scannedRepos[0].repository.id, + downloadStatus: + VariantAnalysisScannedRepositoryDownloadStatus.Failed, + }, + [scannedRepos[1].repository.id]: { + repositoryId: scannedRepos[1].repository.id, + downloadStatus: + VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, + }, + }); + }); + + it("should update the repo state correctly", async () => { + await 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); + + await variantAnalysisManager.autoDownloadVariantAnalysisResult( + scannedRepos[0], + variantAnalysis, + cancellationTokenSource.token, + ); + + await expect(fs.readJson(repoStatesPath)).resolves.toEqual({ + [scannedRepos[1].repository.id]: { + repositoryId: scannedRepos[1].repository.id, + downloadStatus: + VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, + }, + [scannedRepos[2].repository.id]: { + repositoryId: scannedRepos[2].repository.id, + downloadStatus: + VariantAnalysisScannedRepositoryDownloadStatus.InProgress, + }, + [scannedRepos[0].repository.id]: { + repositoryId: scannedRepos[0].repository.id, + downloadStatus: + VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, + }, + }); + }); + + async function mockRepoStates( + repoStates: Record, + ) { + await fs.outputJson(repoStatesPath, repoStates); + } + }); + }); + + describe("enqueueDownload", () => { + 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 dummyVariantAnalysis: VariantAnalysis; + + beforeEach(async () => { + dummyVariantAnalysis = createMockVariantAnalysis({}); + + removeAnalysisResultsStub = jest + .spyOn(variantAnalysisResultsManager, "removeAnalysisResults") + .mockReturnValue(undefined); + }); + + it("should remove variant analysis", async () => { + await fs.ensureDir(join(storagePath, dummyVariantAnalysis.id.toString())); + + await variantAnalysisManager.rehydrateVariantAnalysis( + dummyVariantAnalysis, + ); + expect(variantAnalysisManager.variantAnalysesSize).toBe(1); + + await variantAnalysisManager.removeVariantAnalysis(dummyVariantAnalysis); + + expect(removeAnalysisResultsStub).toBeCalledTimes(1); + expect(variantAnalysisManager.variantAnalysesSize).toBe(0); + + await expect( + fs.pathExists(join(storagePath, dummyVariantAnalysis.id.toString())), + ).resolves.toBe(false); + }); + }); + + describe("rehydrateVariantAnalysis", () => { + let variantAnalysis: VariantAnalysis; + const variantAnalysisRemovedSpy = jest.fn(); + let executeCommandSpy: jest.SpiedFunction; + + beforeEach(() => { + variantAnalysis = createMockVariantAnalysis({}); + + variantAnalysisManager.onVariantAnalysisRemoved( + variantAnalysisRemovedSpy, + ); + + executeCommandSpy = jest + .spyOn(commands, "executeCommand") + .mockResolvedValue(undefined); + }); + + describe("when variant analysis record doesn't exist", () => { + it("should remove the variant analysis", async () => { + await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); + expect(variantAnalysisRemovedSpy).toHaveBeenCalledTimes(1); + }); + + it("should not trigger a monitoring command", async () => { + await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); + expect(executeCommandSpy).not.toHaveBeenCalled(); + }); + }); + + describe("when variant analysis record does exist", () => { + let variantAnalysisStorageLocation: string; + + beforeEach(async () => { + variantAnalysisStorageLocation = + variantAnalysisManager.getVariantAnalysisStorageLocation( + variantAnalysis.id, + ); + await createTimestampFile(variantAnalysisStorageLocation); + }); + + afterEach(() => { + fs.rmSync(variantAnalysisStorageLocation, { recursive: true }); + }); + + describe("when the variant analysis is not complete", () => { + beforeEach(() => { + jest + .spyOn(VariantAnalysisModule, "isVariantAnalysisComplete") + .mockResolvedValue(false); + }); + + it("should not remove the variant analysis", async () => { + await variantAnalysisManager.rehydrateVariantAnalysis( + variantAnalysis, + ); + expect(variantAnalysisRemovedSpy).not.toHaveBeenCalled(); + }); + + it("should trigger a monitoring command", async () => { + await variantAnalysisManager.rehydrateVariantAnalysis( + variantAnalysis, + ); + expect(executeCommandSpy).toHaveBeenCalledWith( + "codeQL.monitorVariantAnalysis", + expect.anything(), + ); + }); + }); + + describe("when the variant analysis is complete", () => { + beforeEach(() => { + jest + .spyOn(VariantAnalysisModule, "isVariantAnalysisComplete") + .mockResolvedValue(true); + }); + + it("should not remove the variant analysis", async () => { + await variantAnalysisManager.rehydrateVariantAnalysis( + variantAnalysis, + ); + expect(variantAnalysisRemovedSpy).not.toHaveBeenCalled(); + }); + + it("should not trigger a monitoring command", async () => { + await variantAnalysisManager.rehydrateVariantAnalysis( + variantAnalysis, + ); + expect(executeCommandSpy).not.toHaveBeenCalled(); + }); + }); + }); + }); + + describe("cancelVariantAnalysis", () => { + let variantAnalysis: VariantAnalysis; + let mockCancelVariantAnalysis: jest.SpiedFunction< + typeof ghActionsApiClient.cancelVariantAnalysis + >; + + let variantAnalysisStorageLocation: string; + + beforeEach(async () => { + variantAnalysis = createMockVariantAnalysis({}); + + mockCancelVariantAnalysis = jest + .spyOn(ghActionsApiClient, "cancelVariantAnalysis") + .mockResolvedValue(undefined); + + variantAnalysisStorageLocation = + variantAnalysisManager.getVariantAnalysisStorageLocation( + variantAnalysis.id, + ); + await createTimestampFile(variantAnalysisStorageLocation); + await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); + }); + + afterEach(() => { + fs.rmSync(variantAnalysisStorageLocation, { recursive: true }); + }); + + it("should return early if the variant analysis is not found", async () => { + try { + await variantAnalysisManager.cancelVariantAnalysis( + variantAnalysis.id + 100, + ); + } catch (error: any) { + expect(error.message).toBe( + `No variant analysis with id: ${variantAnalysis.id + 100}`, + ); + } + }); + + it("should return early if the variant analysis does not have an actions workflow run id", async () => { + await variantAnalysisManager.onVariantAnalysisUpdated({ + ...variantAnalysis, + actionsWorkflowRunId: undefined, + }); + + try { + await variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id); + } catch (error: any) { + expect(error.message).toBe( + `No workflow run id for variant analysis with id: ${variantAnalysis.id}`, + ); + } + }); + + it("should return cancel if valid", async () => { + await variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id); + + expect(mockCancelVariantAnalysis).toBeCalledWith( + app.credentials, + variantAnalysis, + ); + }); + }); + + describe("copyRepoListToClipboard", () => { + let variantAnalysis: VariantAnalysis; + let variantAnalysisStorageLocation: string; + + const writeTextStub = jest.fn(); + + beforeEach(async () => { + variantAnalysis = createMockVariantAnalysis({}); + + variantAnalysisStorageLocation = + variantAnalysisManager.getVariantAnalysisStorageLocation( + variantAnalysis.id, + ); + await createTimestampFile(variantAnalysisStorageLocation); + await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); + + jest.spyOn(env, "clipboard", "get").mockReturnValue({ + readText: jest.fn(), + writeText: writeTextStub, + }); + }); + + afterEach(() => { + fs.rmSync(variantAnalysisStorageLocation, { recursive: true }); + }); + + describe("when the variant analysis does not have any repositories", () => { + beforeEach(async () => { + await variantAnalysisManager.rehydrateVariantAnalysis({ + ...variantAnalysis, + scannedRepos: [], + }); + }); + + it("should not copy any text", async () => { + await variantAnalysisManager.copyRepoListToClipboard( + variantAnalysis.id, + ); + + expect(writeTextStub).not.toBeCalled(); + }); + }); + + describe("when the variant analysis does not have any repositories with results", () => { + beforeEach(async () => { + await variantAnalysisManager.rehydrateVariantAnalysis({ + ...variantAnalysis, + scannedRepos: [ + { + ...createMockScannedRepo(), + resultCount: 0, + }, + { + ...createMockScannedRepo(), + resultCount: undefined, + }, + ], + }); + }); + + it("should not copy any text", async () => { + await variantAnalysisManager.copyRepoListToClipboard( + variantAnalysis.id, + ); + + expect(writeTextStub).not.toBeCalled(); + }); + }); + + describe("when the variant analysis has repositories with results", () => { + const scannedRepos = [ + { + ...createMockScannedRepo("pear"), + resultCount: 100, + }, + { + ...createMockScannedRepo("apple"), + resultCount: 0, + }, + { + ...createMockScannedRepo("citrus"), + resultCount: 200, + }, + { + ...createMockScannedRepo("sky"), + resultCount: undefined, + }, + { + ...createMockScannedRepo("banana"), + resultCount: 5, + }, + ]; + + beforeEach(async () => { + await variantAnalysisManager.rehydrateVariantAnalysis({ + ...variantAnalysis, + scannedRepos, + }); + }); + + it("should copy text", async () => { + await variantAnalysisManager.copyRepoListToClipboard( + variantAnalysis.id, + ); + + expect(writeTextStub).toBeCalledTimes(1); + }); + + it("should be valid JSON when put in object", async () => { + await variantAnalysisManager.copyRepoListToClipboard( + variantAnalysis.id, + ); + + const text = writeTextStub.mock.calls[0][0]; + + const parsed = JSON.parse(`${text}`); + + expect(parsed).toEqual({ + name: "new-repo-list", + repositories: [ + scannedRepos[4].repository.fullName, + scannedRepos[2].repository.fullName, + scannedRepos[0].repository.fullName, + ], + }); + }); + + it("should use the sort key", async () => { + await variantAnalysisManager.copyRepoListToClipboard( + variantAnalysis.id, + { + ...defaultFilterSortState, + sortKey: SortKey.ResultsCount, + }, + ); + + const text = writeTextStub.mock.calls[0][0]; + + const parsed = JSON.parse(`${text}`); + + expect(parsed).toEqual({ + name: "new-repo-list", + repositories: [ + scannedRepos[2].repository.fullName, + scannedRepos[0].repository.fullName, + scannedRepos[4].repository.fullName, + ], + }); + }); + + it("should use the search value", async () => { + await variantAnalysisManager.copyRepoListToClipboard( + variantAnalysis.id, + { + ...defaultFilterSortState, + searchValue: "ban", + }, + ); + + const text = writeTextStub.mock.calls[0][0]; + + const parsed = JSON.parse(`${text}`); + + expect(parsed).toEqual({ + name: "new-repo-list", + repositories: [scannedRepos[4].repository.fullName], + }); + }); + }); + }); + + describe("openQueryText", () => { + let variantAnalysis: VariantAnalysis; + let variantAnalysisStorageLocation: string; + + let showTextDocumentSpy: jest.SpiedFunction; + let openTextDocumentSpy: jest.SpiedFunction< + typeof workspace.openTextDocument + >; + + beforeEach(async () => { + variantAnalysis = createMockVariantAnalysis({}); + + variantAnalysisStorageLocation = + variantAnalysisManager.getVariantAnalysisStorageLocation( + variantAnalysis.id, + ); + await createTimestampFile(variantAnalysisStorageLocation); + await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); + + showTextDocumentSpy = jest + .spyOn(window, "showTextDocument") + .mockResolvedValue(mockedObject({})); + openTextDocumentSpy = jest + .spyOn(workspace, "openTextDocument") + .mockResolvedValue(mockedObject({})); + }); + + afterEach(() => { + fs.rmSync(variantAnalysisStorageLocation, { recursive: true }); + }); + + it("opens the query text", async () => { + await variantAnalysisManager.openQueryText(variantAnalysis.id); + + expect(openTextDocumentSpy).toHaveBeenCalledTimes(1); + expect(showTextDocumentSpy).toHaveBeenCalledTimes(1); + + const uri: Uri = openTextDocumentSpy.mock.calls[0][0] as Uri; + expect(uri.scheme).toEqual("codeql-variant-analysis"); + expect(uri.path).toEqual(variantAnalysis.query.filePath); + const params = new URLSearchParams(uri.query); + expect(Array.from(params.keys())).toEqual(["variantAnalysisId"]); + expect(params.get("variantAnalysisId")).toEqual( + variantAnalysis.id.toString(), + ); + }); + }); + + describe("openQueryFile", () => { + let variantAnalysis: VariantAnalysis; + let variantAnalysisStorageLocation: string; + + let showTextDocumentSpy: jest.SpiedFunction; + let openTextDocumentSpy: jest.SpiedFunction< + typeof workspace.openTextDocument + >; + + beforeEach(async () => { + variantAnalysis = createMockVariantAnalysis({}); + + variantAnalysisStorageLocation = + variantAnalysisManager.getVariantAnalysisStorageLocation( + variantAnalysis.id, + ); + await createTimestampFile(variantAnalysisStorageLocation); + await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); + + showTextDocumentSpy = jest + .spyOn(window, "showTextDocument") + .mockResolvedValue(mockedObject({})); + openTextDocumentSpy = jest + .spyOn(workspace, "openTextDocument") + .mockResolvedValue(mockedObject({})); + }); + + afterEach(() => { + fs.rmSync(variantAnalysisStorageLocation, { recursive: true }); + }); + + it("opens the query file", async () => { + await variantAnalysisManager.openQueryFile(variantAnalysis.id); + + expect(showTextDocumentSpy).toHaveBeenCalledTimes(1); + expect(openTextDocumentSpy).toHaveBeenCalledTimes(1); + + const filename: string = openTextDocumentSpy.mock.calls[0][0] as string; + expect(filename).toEqual(variantAnalysis.query.filePath); + }); + }); +}); diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/jest.setup.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/jest.setup.ts index 416ca1ea9..4fcbfe775 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/jest.setup.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/jest.setup.ts @@ -1,23 +1,15 @@ import { workspace } from "vscode"; import { + afterAllAction, beforeAllAction, beforeEachAction, } from "../jest.activated-extension.setup"; -import * as tmp from "tmp"; -import { - createWriteStream, - existsSync, - mkdirpSync, - realpathSync, -} from "fs-extra"; +import { createWriteStream, existsSync, mkdirpSync } from "fs-extra"; import { dirname } from "path"; -import { DB_URL, dbLoc, setStoragePath, storagePath } from "../global.helper"; +import { DB_URL, dbLoc } from "../global.helper"; import fetch from "node-fetch"; -// create an extension storage location -let removeStorage: tmp.DirResult["removeCallback"] | undefined; - beforeAll(async () => { // ensure the test database is downloaded mkdirpSync(dirname(dbLoc)); @@ -38,18 +30,6 @@ beforeAll(async () => { }); } - // Create the temp directory to be used as extension local storage. - const dir = tmp.dirSync(); - let storagePath = realpathSync(dir.name); - if (storagePath.substring(0, 2).match(/[A-Z]:/)) { - storagePath = - storagePath.substring(0, 1).toLocaleLowerCase() + - storagePath.substring(1); - } - setStoragePath(storagePath); - - removeStorage = dir.removeCallback; - await beforeAllAction(); }); @@ -76,14 +56,6 @@ beforeAll(() => { } }); -// ensure extension is cleaned up. afterAll(async () => { - // ensure temp directory is cleaned up. - try { - removeStorage?.(); - } catch (e) { - // we are exiting anyway so don't worry about it. - // most likely the directory this is a test on Windows and some files are locked. - console.warn(`Failed to remove storage directory '${storagePath}': ${e}`); - } + await afterAllAction(); }); diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-manager.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-manager.test.ts index 27c056402..a90616c74 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-manager.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-manager.test.ts @@ -1,25 +1,16 @@ import { CancellationTokenSource, commands, - env, extensions, QuickPickItem, - TextDocument, - TextEditor, Uri, window, - workspace, } from "vscode"; import { CodeQLExtensionInterface } from "../../../../src/extension"; import { extLogger } from "../../../../src/common"; import { setRemoteControllerRepo } from "../../../../src/config"; import * as ghApiClient from "../../../../src/variant-analysis/gh-api/gh-api-client"; -import * as ghActionsApiClient from "../../../../src/variant-analysis/gh-api/gh-actions-api-client"; -import * as fs from "fs-extra"; import { join } from "path"; -import { Readable } from "stream"; -import { Response } from "node-fetch"; -import * as fetchModule from "node-fetch"; import { VariantAnalysisManager } from "../../../../src/variant-analysis/variant-analysis-manager"; import { CodeQLCliServer } from "../../../../src/cli"; @@ -29,71 +20,37 @@ import { storagePath, } from "../../global.helper"; import { VariantAnalysisResultsManager } from "../../../../src/variant-analysis/variant-analysis-results-manager"; -import { createMockVariantAnalysis } from "../../../factories/variant-analysis/shared/variant-analysis"; -import * as VariantAnalysisModule from "../../../../src/variant-analysis/shared/variant-analysis"; -import { - createMockScannedRepo, - createMockScannedRepos, -} from "../../../factories/variant-analysis/shared/scanned-repositories"; -import { - VariantAnalysis, - VariantAnalysisScannedRepository, - VariantAnalysisScannedRepositoryDownloadStatus, - VariantAnalysisScannedRepositoryState, - VariantAnalysisStatus, -} from "../../../../src/variant-analysis/shared/variant-analysis"; -import { createTimestampFile } from "../../../../src/helpers"; -import { createMockVariantAnalysisRepoTask } from "../../../factories/variant-analysis/gh-api/variant-analysis-repo-task"; -import { - VariantAnalysis as VariantAnalysisApiResponse, - VariantAnalysisRepoTask, -} from "../../../../src/variant-analysis/gh-api/variant-analysis"; +import { VariantAnalysisStatus } from "../../../../src/variant-analysis/shared/variant-analysis"; +import { VariantAnalysis as VariantAnalysisApiResponse } from "../../../../src/variant-analysis/gh-api/variant-analysis"; import { createMockApiResponse } from "../../../factories/variant-analysis/gh-api/variant-analysis-api-response"; import { UserCancellationException } from "../../../../src/commandRunner"; import { Repository } from "../../../../src/variant-analysis/gh-api/repository"; -import { - defaultFilterSortState, - SortKey, -} from "../../../../src/pure/variant-analysis-filter-sort"; import { DbManager } from "../../../../src/databases/db-manager"; -import { App } from "../../../../src/common/app"; import { ExtensionApp } from "../../../../src/common/vscode/vscode-app"; import { DbConfigStore } from "../../../../src/databases/config/db-config-store"; -import { mockedObject } from "../../utils/mocking.helpers"; // up to 3 minutes per test jest.setTimeout(3 * 60 * 1000); describe("Variant Analysis Manager", () => { let cli: CodeQLCliServer; - let app: App; let cancellationTokenSource: CancellationTokenSource; let variantAnalysisManager: VariantAnalysisManager; - let variantAnalysisResultsManager: VariantAnalysisResultsManager; - let dbManager: DbManager; - let variantAnalysis: VariantAnalysis; - let scannedRepos: VariantAnalysisScannedRepository[]; beforeEach(async () => { jest.spyOn(extLogger, "log").mockResolvedValue(undefined); cancellationTokenSource = new CancellationTokenSource(); - scannedRepos = createMockScannedRepos(); - variantAnalysis = createMockVariantAnalysis({ - status: VariantAnalysisStatus.InProgress, - scannedRepos, - }); - const extension = await extensions .getExtension>( "GitHub.vscode-codeql", )! .activate(); cli = extension.cliServer; - app = new ExtensionApp(extension.ctx); - dbManager = new DbManager(app, new DbConfigStore(app)); - variantAnalysisResultsManager = new VariantAnalysisResultsManager( + const app = new ExtensionApp(extension.ctx); + const dbManager = new DbManager(app, new DbConfigStore(app)); + const variantAnalysisResultsManager = new VariantAnalysisResultsManager( cli, extLogger, ); @@ -246,819 +203,4 @@ describe("Variant Analysis Manager", () => { await expect(promise).rejects.toThrow(UserCancellationException); }); }); - - describe("rehydrateVariantAnalysis", () => { - const variantAnalysis = createMockVariantAnalysis({}); - - describe("when the directory does not exist", () => { - it("should fire the removed event if the file does not exist", async () => { - const stub = jest.fn(); - variantAnalysisManager.onVariantAnalysisRemoved(stub); - - await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); - - expect(stub).toBeCalledTimes(1); - }); - }); - - describe("when the directory exists", () => { - beforeEach(async () => { - await fs.ensureDir(join(storagePath, variantAnalysis.id.toString())); - }); - - it("should store the variant analysis", async () => { - await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); - - expect( - await variantAnalysisManager.getVariantAnalysis(variantAnalysis.id), - ).toEqual(variantAnalysis); - }); - - it("should not error if the repo states file does not exist", async () => { - await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); - - expect( - await variantAnalysisManager.getRepoStates(variantAnalysis.id), - ).toEqual([]); - }); - - it("should read in the repo states if it exists", async () => { - await fs.writeJson( - join(storagePath, variantAnalysis.id.toString(), "repo_states.json"), - { - [scannedRepos[0].repository.id]: { - repositoryId: scannedRepos[0].repository.id, - downloadStatus: - VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, - }, - [scannedRepos[1].repository.id]: { - repositoryId: scannedRepos[1].repository.id, - downloadStatus: - VariantAnalysisScannedRepositoryDownloadStatus.InProgress, - }, - }, - ); - - await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); - - expect( - await variantAnalysisManager.getRepoStates(variantAnalysis.id), - ).toEqual( - expect.arrayContaining([ - { - repositoryId: scannedRepos[0].repository.id, - downloadStatus: - VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, - }, - { - repositoryId: scannedRepos[1].repository.id, - downloadStatus: - VariantAnalysisScannedRepositoryDownloadStatus.InProgress, - }, - ]), - ); - }); - }); - }); - - describe("autoDownloadVariantAnalysisResult", () => { - let getVariantAnalysisRepoStub: jest.SpiedFunction< - typeof ghApiClient.getVariantAnalysisRepo - >; - let getVariantAnalysisRepoResultStub: jest.SpiedFunction< - typeof fetchModule.default - >; - - let repoStatesPath: string; - - beforeEach(async () => { - getVariantAnalysisRepoStub = jest.spyOn( - ghApiClient, - "getVariantAnalysisRepo", - ); - getVariantAnalysisRepoResultStub = jest.spyOn(fetchModule, "default"); - - repoStatesPath = join( - storagePath, - variantAnalysis.id.toString(), - "repo_states.json", - ); - }); - - describe("when the artifact_url is missing", () => { - beforeEach(async () => { - const dummyRepoTask = createMockVariantAnalysisRepoTask(); - delete dummyRepoTask.artifact_url; - - getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask); - }); - - 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); - - const sourceFilePath = join( - __dirname, - "../data/variant-analysis-results.zip", - ); - const fileContents = fs.readFileSync(sourceFilePath); - const response = new Response(Readable.from(fileContents)); - response.size = fileContents.length; - getVariantAnalysisRepoResultStub.mockResolvedValue(response); - }); - - it("should return early if variant analysis is cancelled", async () => { - cancellationTokenSource.cancel(); - - await variantAnalysisManager.autoDownloadVariantAnalysisResult( - scannedRepos[0], - variantAnalysis, - cancellationTokenSource.token, - ); - - expect(getVariantAnalysisRepoStub).not.toHaveBeenCalled(); - }); - - it("should fetch a repo task", async () => { - await variantAnalysisManager.autoDownloadVariantAnalysisResult( - scannedRepos[0], - variantAnalysis, - cancellationTokenSource.token, - ); - - expect(getVariantAnalysisRepoStub).toHaveBeenCalled(); - }); - - it("should fetch a repo result", async () => { - await variantAnalysisManager.autoDownloadVariantAnalysisResult( - scannedRepos[0], - variantAnalysis, - cancellationTokenSource.token, - ); - - expect(getVariantAnalysisRepoResultStub).toHaveBeenCalled(); - }); - - it("should skip the download if the repository has already been downloaded", async () => { - // First, do a download so it is downloaded. This avoids having to mock the repo states. - await variantAnalysisManager.autoDownloadVariantAnalysisResult( - scannedRepos[0], - variantAnalysis, - cancellationTokenSource.token, - ); - - getVariantAnalysisRepoStub.mockClear(); - - await variantAnalysisManager.autoDownloadVariantAnalysisResult( - scannedRepos[0], - variantAnalysis, - cancellationTokenSource.token, - ); - - expect(getVariantAnalysisRepoStub).not.toHaveBeenCalled(); - }); - - it("should write the repo state when the download is successful", async () => { - await variantAnalysisManager.autoDownloadVariantAnalysisResult( - scannedRepos[0], - variantAnalysis, - cancellationTokenSource.token, - ); - - await expect(fs.readJson(repoStatesPath)).resolves.toEqual({ - [scannedRepos[0].repository.id]: { - repositoryId: scannedRepos[0].repository.id, - downloadStatus: - VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, - }, - }); - }); - - it("should not write the repo state when the download fails", async () => { - getVariantAnalysisRepoResultStub.mockRejectedValue( - new Error("Failed to download"), - ); - - await expect( - variantAnalysisManager.autoDownloadVariantAnalysisResult( - scannedRepos[0], - variantAnalysis, - cancellationTokenSource.token, - ), - ).rejects.toThrow(); - - await expect(fs.pathExists(repoStatesPath)).resolves.toBe(false); - }); - - it("should have a failed repo state when the repo task API fails", async () => { - getVariantAnalysisRepoStub.mockRejectedValueOnce( - new Error("Failed to download"), - ); - - await expect( - variantAnalysisManager.autoDownloadVariantAnalysisResult( - scannedRepos[0], - variantAnalysis, - cancellationTokenSource.token, - ), - ).rejects.toThrow(); - - await expect(fs.pathExists(repoStatesPath)).resolves.toBe(false); - - await variantAnalysisManager.autoDownloadVariantAnalysisResult( - scannedRepos[1], - variantAnalysis, - cancellationTokenSource.token, - ); - - await expect(fs.readJson(repoStatesPath)).resolves.toEqual({ - [scannedRepos[0].repository.id]: { - repositoryId: scannedRepos[0].repository.id, - downloadStatus: - VariantAnalysisScannedRepositoryDownloadStatus.Failed, - }, - [scannedRepos[1].repository.id]: { - repositoryId: scannedRepos[1].repository.id, - downloadStatus: - VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, - }, - }); - }); - - it("should have a failed repo state when the download fails", async () => { - getVariantAnalysisRepoResultStub.mockRejectedValueOnce( - new Error("Failed to download"), - ); - - await expect( - variantAnalysisManager.autoDownloadVariantAnalysisResult( - scannedRepos[0], - variantAnalysis, - cancellationTokenSource.token, - ), - ).rejects.toThrow(); - - await expect(fs.pathExists(repoStatesPath)).resolves.toBe(false); - - await variantAnalysisManager.autoDownloadVariantAnalysisResult( - scannedRepos[1], - variantAnalysis, - cancellationTokenSource.token, - ); - - await expect(fs.readJson(repoStatesPath)).resolves.toEqual({ - [scannedRepos[0].repository.id]: { - repositoryId: scannedRepos[0].repository.id, - downloadStatus: - VariantAnalysisScannedRepositoryDownloadStatus.Failed, - }, - [scannedRepos[1].repository.id]: { - repositoryId: scannedRepos[1].repository.id, - downloadStatus: - VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, - }, - }); - }); - - it("should update the repo state correctly", async () => { - await 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); - - await variantAnalysisManager.autoDownloadVariantAnalysisResult( - scannedRepos[0], - variantAnalysis, - cancellationTokenSource.token, - ); - - await expect(fs.readJson(repoStatesPath)).resolves.toEqual({ - [scannedRepos[1].repository.id]: { - repositoryId: scannedRepos[1].repository.id, - downloadStatus: - VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, - }, - [scannedRepos[2].repository.id]: { - repositoryId: scannedRepos[2].repository.id, - downloadStatus: - VariantAnalysisScannedRepositoryDownloadStatus.InProgress, - }, - [scannedRepos[0].repository.id]: { - repositoryId: scannedRepos[0].repository.id, - downloadStatus: - VariantAnalysisScannedRepositoryDownloadStatus.Succeeded, - }, - }); - }); - - async function mockRepoStates( - repoStates: Record, - ) { - await fs.outputJson(repoStatesPath, repoStates); - } - }); - }); - - describe("enqueueDownload", () => { - 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 dummyVariantAnalysis: VariantAnalysis; - - beforeEach(async () => { - dummyVariantAnalysis = createMockVariantAnalysis({}); - - removeAnalysisResultsStub = jest - .spyOn(variantAnalysisResultsManager, "removeAnalysisResults") - .mockReturnValue(undefined); - }); - - it("should remove variant analysis", async () => { - await fs.ensureDir(join(storagePath, dummyVariantAnalysis.id.toString())); - - await variantAnalysisManager.rehydrateVariantAnalysis( - dummyVariantAnalysis, - ); - expect(variantAnalysisManager.variantAnalysesSize).toBe(1); - - await variantAnalysisManager.removeVariantAnalysis(dummyVariantAnalysis); - - expect(removeAnalysisResultsStub).toBeCalledTimes(1); - expect(variantAnalysisManager.variantAnalysesSize).toBe(0); - - await expect( - fs.pathExists(join(storagePath, dummyVariantAnalysis.id.toString())), - ).resolves.toBe(false); - }); - }); - - describe("rehydrateVariantAnalysis", () => { - let variantAnalysis: VariantAnalysis; - const variantAnalysisRemovedSpy = jest.fn(); - let executeCommandSpy: jest.SpiedFunction; - - beforeEach(() => { - variantAnalysis = createMockVariantAnalysis({}); - - variantAnalysisManager.onVariantAnalysisRemoved( - variantAnalysisRemovedSpy, - ); - - executeCommandSpy = jest - .spyOn(commands, "executeCommand") - .mockResolvedValue(undefined); - }); - - describe("when variant analysis record doesn't exist", () => { - it("should remove the variant analysis", async () => { - await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); - expect(variantAnalysisRemovedSpy).toHaveBeenCalledTimes(1); - }); - - it("should not trigger a monitoring command", async () => { - await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); - expect(executeCommandSpy).not.toHaveBeenCalled(); - }); - }); - - describe("when variant analysis record does exist", () => { - let variantAnalysisStorageLocation: string; - - beforeEach(async () => { - variantAnalysisStorageLocation = - variantAnalysisManager.getVariantAnalysisStorageLocation( - variantAnalysis.id, - ); - await createTimestampFile(variantAnalysisStorageLocation); - }); - - afterEach(() => { - fs.rmSync(variantAnalysisStorageLocation, { recursive: true }); - }); - - describe("when the variant analysis is not complete", () => { - beforeEach(() => { - jest - .spyOn(VariantAnalysisModule, "isVariantAnalysisComplete") - .mockResolvedValue(false); - }); - - it("should not remove the variant analysis", async () => { - await variantAnalysisManager.rehydrateVariantAnalysis( - variantAnalysis, - ); - expect(variantAnalysisRemovedSpy).not.toHaveBeenCalled(); - }); - - it("should trigger a monitoring command", async () => { - await variantAnalysisManager.rehydrateVariantAnalysis( - variantAnalysis, - ); - expect(executeCommandSpy).toHaveBeenCalledWith( - "codeQL.monitorVariantAnalysis", - expect.anything(), - ); - }); - }); - - describe("when the variant analysis is complete", () => { - beforeEach(() => { - jest - .spyOn(VariantAnalysisModule, "isVariantAnalysisComplete") - .mockResolvedValue(true); - }); - - it("should not remove the variant analysis", async () => { - await variantAnalysisManager.rehydrateVariantAnalysis( - variantAnalysis, - ); - expect(variantAnalysisRemovedSpy).not.toHaveBeenCalled(); - }); - - it("should not trigger a monitoring command", async () => { - await variantAnalysisManager.rehydrateVariantAnalysis( - variantAnalysis, - ); - expect(executeCommandSpy).not.toHaveBeenCalled(); - }); - }); - }); - }); - - describe("cancelVariantAnalysis", () => { - let variantAnalysis: VariantAnalysis; - let mockCancelVariantAnalysis: jest.SpiedFunction< - typeof ghActionsApiClient.cancelVariantAnalysis - >; - - let variantAnalysisStorageLocation: string; - - beforeEach(async () => { - variantAnalysis = createMockVariantAnalysis({}); - - mockCancelVariantAnalysis = jest - .spyOn(ghActionsApiClient, "cancelVariantAnalysis") - .mockResolvedValue(undefined); - - variantAnalysisStorageLocation = - variantAnalysisManager.getVariantAnalysisStorageLocation( - variantAnalysis.id, - ); - await createTimestampFile(variantAnalysisStorageLocation); - await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); - }); - - afterEach(() => { - fs.rmSync(variantAnalysisStorageLocation, { recursive: true }); - }); - - it("should return early if the variant analysis is not found", async () => { - try { - await variantAnalysisManager.cancelVariantAnalysis( - variantAnalysis.id + 100, - ); - } catch (error: any) { - expect(error.message).toBe( - `No variant analysis with id: ${variantAnalysis.id + 100}`, - ); - } - }); - - it("should return early if the variant analysis does not have an actions workflow run id", async () => { - await variantAnalysisManager.onVariantAnalysisUpdated({ - ...variantAnalysis, - actionsWorkflowRunId: undefined, - }); - - try { - await variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id); - } catch (error: any) { - expect(error.message).toBe( - `No workflow run id for variant analysis with id: ${variantAnalysis.id}`, - ); - } - }); - - it("should return cancel if valid", async () => { - await variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id); - - expect(mockCancelVariantAnalysis).toBeCalledWith( - app.credentials, - variantAnalysis, - ); - }); - }); - - describe("copyRepoListToClipboard", () => { - let variantAnalysis: VariantAnalysis; - let variantAnalysisStorageLocation: string; - - const writeTextStub = jest.fn(); - - beforeEach(async () => { - variantAnalysis = createMockVariantAnalysis({}); - - variantAnalysisStorageLocation = - variantAnalysisManager.getVariantAnalysisStorageLocation( - variantAnalysis.id, - ); - await createTimestampFile(variantAnalysisStorageLocation); - await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); - - jest.spyOn(env, "clipboard", "get").mockReturnValue({ - readText: jest.fn(), - writeText: writeTextStub, - }); - }); - - afterEach(() => { - fs.rmSync(variantAnalysisStorageLocation, { recursive: true }); - }); - - describe("when the variant analysis does not have any repositories", () => { - beforeEach(async () => { - await variantAnalysisManager.rehydrateVariantAnalysis({ - ...variantAnalysis, - scannedRepos: [], - }); - }); - - it("should not copy any text", async () => { - await variantAnalysisManager.copyRepoListToClipboard( - variantAnalysis.id, - ); - - expect(writeTextStub).not.toBeCalled(); - }); - }); - - describe("when the variant analysis does not have any repositories with results", () => { - beforeEach(async () => { - await variantAnalysisManager.rehydrateVariantAnalysis({ - ...variantAnalysis, - scannedRepos: [ - { - ...createMockScannedRepo(), - resultCount: 0, - }, - { - ...createMockScannedRepo(), - resultCount: undefined, - }, - ], - }); - }); - - it("should not copy any text", async () => { - await variantAnalysisManager.copyRepoListToClipboard( - variantAnalysis.id, - ); - - expect(writeTextStub).not.toBeCalled(); - }); - }); - - describe("when the variant analysis has repositories with results", () => { - const scannedRepos = [ - { - ...createMockScannedRepo("pear"), - resultCount: 100, - }, - { - ...createMockScannedRepo("apple"), - resultCount: 0, - }, - { - ...createMockScannedRepo("citrus"), - resultCount: 200, - }, - { - ...createMockScannedRepo("sky"), - resultCount: undefined, - }, - { - ...createMockScannedRepo("banana"), - resultCount: 5, - }, - ]; - - beforeEach(async () => { - await variantAnalysisManager.rehydrateVariantAnalysis({ - ...variantAnalysis, - scannedRepos, - }); - }); - - it("should copy text", async () => { - await variantAnalysisManager.copyRepoListToClipboard( - variantAnalysis.id, - ); - - expect(writeTextStub).toBeCalledTimes(1); - }); - - it("should be valid JSON when put in object", async () => { - await variantAnalysisManager.copyRepoListToClipboard( - variantAnalysis.id, - ); - - const text = writeTextStub.mock.calls[0][0]; - - const parsed = JSON.parse(`${text}`); - - expect(parsed).toEqual({ - name: "new-repo-list", - repositories: [ - scannedRepos[4].repository.fullName, - scannedRepos[2].repository.fullName, - scannedRepos[0].repository.fullName, - ], - }); - }); - - it("should use the sort key", async () => { - await variantAnalysisManager.copyRepoListToClipboard( - variantAnalysis.id, - { - ...defaultFilterSortState, - sortKey: SortKey.ResultsCount, - }, - ); - - const text = writeTextStub.mock.calls[0][0]; - - const parsed = JSON.parse(`${text}`); - - expect(parsed).toEqual({ - name: "new-repo-list", - repositories: [ - scannedRepos[2].repository.fullName, - scannedRepos[0].repository.fullName, - scannedRepos[4].repository.fullName, - ], - }); - }); - - it("should use the search value", async () => { - await variantAnalysisManager.copyRepoListToClipboard( - variantAnalysis.id, - { - ...defaultFilterSortState, - searchValue: "ban", - }, - ); - - const text = writeTextStub.mock.calls[0][0]; - - const parsed = JSON.parse(`${text}`); - - expect(parsed).toEqual({ - name: "new-repo-list", - repositories: [scannedRepos[4].repository.fullName], - }); - }); - }); - }); - - describe("openQueryText", () => { - let variantAnalysis: VariantAnalysis; - let variantAnalysisStorageLocation: string; - - let showTextDocumentSpy: jest.SpiedFunction; - let openTextDocumentSpy: jest.SpiedFunction< - typeof workspace.openTextDocument - >; - - beforeEach(async () => { - variantAnalysis = createMockVariantAnalysis({}); - - variantAnalysisStorageLocation = - variantAnalysisManager.getVariantAnalysisStorageLocation( - variantAnalysis.id, - ); - await createTimestampFile(variantAnalysisStorageLocation); - await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); - - showTextDocumentSpy = jest - .spyOn(window, "showTextDocument") - .mockResolvedValue(mockedObject({})); - openTextDocumentSpy = jest - .spyOn(workspace, "openTextDocument") - .mockResolvedValue(mockedObject({})); - }); - - afterEach(() => { - fs.rmSync(variantAnalysisStorageLocation, { recursive: true }); - }); - - it("opens the query text", async () => { - await variantAnalysisManager.openQueryText(variantAnalysis.id); - - expect(openTextDocumentSpy).toHaveBeenCalledTimes(1); - expect(showTextDocumentSpy).toHaveBeenCalledTimes(1); - - const uri: Uri = openTextDocumentSpy.mock.calls[0][0] as Uri; - expect(uri.scheme).toEqual("codeql-variant-analysis"); - expect(uri.path).toEqual(variantAnalysis.query.filePath); - const params = new URLSearchParams(uri.query); - expect(Array.from(params.keys())).toEqual(["variantAnalysisId"]); - expect(params.get("variantAnalysisId")).toEqual( - variantAnalysis.id.toString(), - ); - }); - }); - - describe("openQueryFile", () => { - let variantAnalysis: VariantAnalysis; - let variantAnalysisStorageLocation: string; - - let showTextDocumentSpy: jest.SpiedFunction; - let openTextDocumentSpy: jest.SpiedFunction< - typeof workspace.openTextDocument - >; - - beforeEach(async () => { - variantAnalysis = createMockVariantAnalysis({}); - - variantAnalysisStorageLocation = - variantAnalysisManager.getVariantAnalysisStorageLocation( - variantAnalysis.id, - ); - await createTimestampFile(variantAnalysisStorageLocation); - await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); - - showTextDocumentSpy = jest - .spyOn(window, "showTextDocument") - .mockResolvedValue(mockedObject({})); - openTextDocumentSpy = jest - .spyOn(workspace, "openTextDocument") - .mockResolvedValue(mockedObject({})); - }); - - afterEach(() => { - fs.rmSync(variantAnalysisStorageLocation, { recursive: true }); - }); - - it("opens the query file", async () => { - await variantAnalysisManager.openQueryFile(variantAnalysis.id); - - expect(showTextDocumentSpy).toHaveBeenCalledTimes(1); - expect(openTextDocumentSpy).toHaveBeenCalledTimes(1); - - const filename: string = openTextDocumentSpy.mock.calls[0][0] as string; - expect(filename).toEqual(variantAnalysis.query.filePath); - }); - }); }); diff --git a/extensions/ql-vscode/test/vscode-tests/jest.activated-extension.setup.ts b/extensions/ql-vscode/test/vscode-tests/jest.activated-extension.setup.ts index 73886f2af..bf750a9ba 100644 --- a/extensions/ql-vscode/test/vscode-tests/jest.activated-extension.setup.ts +++ b/extensions/ql-vscode/test/vscode-tests/jest.activated-extension.setup.ts @@ -1,11 +1,17 @@ import { CUSTOM_CODEQL_PATH_SETTING } from "../../src/config"; import { ConfigurationTarget, env, extensions } from "vscode"; import { beforeEachAction as testConfigBeforeEachAction } from "./test-config"; +import * as tmp from "tmp"; +import { realpathSync } from "fs-extra"; +import { setStoragePath, storagePath } from "./global.helper"; jest.retryTimes(3, { logErrorsBeforeRetry: true, }); +// create an extension storage location +let removeStorage: tmp.DirResult["removeCallback"] | undefined; + export async function beforeAllAction() { // Set the CLI version here before activation to ensure we don't accidentally try to download a cli await testConfigBeforeEachAction(); @@ -14,6 +20,18 @@ export async function beforeAllAction() { ConfigurationTarget.Workspace, ); + // Create the temp directory to be used as extension local storage. + const dir = tmp.dirSync(); + let storagePath = realpathSync(dir.name); + if (storagePath.substring(0, 2).match(/[A-Z]:/)) { + storagePath = + storagePath.substring(0, 1).toLocaleLowerCase() + + storagePath.substring(1); + } + setStoragePath(storagePath); + + removeStorage = dir.removeCallback; + // Activate the extension await extensions.getExtension("GitHub.vscode-codeql")?.activate(); } @@ -28,3 +46,14 @@ export async function beforeEachAction() { ConfigurationTarget.Workspace, ); } + +export async function afterAllAction() { + // ensure temp directory is cleaned up + try { + removeStorage?.(); + } catch (e) { + // we are exiting anyway so don't worry about it. + // most likely the directory this is a test on Windows and some files are locked. + console.warn(`Failed to remove storage directory '${storagePath}': ${e}`); + } +} From c3799bdb5af987eed474508fa896ed0c71a13c25 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Wed, 1 Mar 2023 14:07:49 +0100 Subject: [PATCH 10/19] Move variant analysis results manager tests to activated extension suite --- .../variant-analysis-results-manager.test.ts | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) rename extensions/ql-vscode/test/vscode-tests/{cli-integration => activated-extension}/variant-analysis/variant-analysis-results-manager.test.ts (93%) diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-results-manager.test.ts b/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-results-manager.test.ts similarity index 93% rename from extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-results-manager.test.ts rename to extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-results-manager.test.ts index 9c2c3590f..50a3cb470 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-results-manager.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-results-manager.test.ts @@ -1,11 +1,9 @@ -import { extensions } from "vscode"; -import { CodeQLExtensionInterface } from "../../../../src/extension"; import { extLogger } from "../../../../src/common"; import * as fs from "fs-extra"; import { join, resolve } from "path"; import { Readable } from "stream"; -import { Response, RequestInfo, RequestInit } from "node-fetch"; import * as fetchModule from "node-fetch"; +import { RequestInfo, RequestInit, Response } from "node-fetch"; import { VariantAnalysisResultsManager } from "../../../../src/variant-analysis/variant-analysis-results-manager"; import { CodeQLCliServer } from "../../../../src/cli"; @@ -16,38 +14,32 @@ import { VariantAnalysisRepositoryTask, VariantAnalysisScannedRepositoryResult, } from "../../../../src/variant-analysis/shared/variant-analysis"; +import { mockedObject } from "../../utils/mocking.helpers"; jest.setTimeout(10_000); describe(VariantAnalysisResultsManager.name, () => { - let cli: CodeQLCliServer; let variantAnalysisId: number; + let variantAnalysisResultsManager: VariantAnalysisResultsManager; beforeEach(async () => { variantAnalysisId = faker.datatype.number(); - const extension = await extensions - .getExtension>( - "GitHub.vscode-codeql", - )! - .activate(); - cli = extension.cliServer; + const cli = mockedObject({}); + variantAnalysisResultsManager = new VariantAnalysisResultsManager( + cli, + extLogger, + ); }); describe("download", () => { let dummyRepoTask: VariantAnalysisRepositoryTask; let variantAnalysisStoragePath: string; let repoTaskStorageDirectory: string; - let variantAnalysisResultsManager: VariantAnalysisResultsManager; beforeEach(async () => { jest.spyOn(extLogger, "log").mockResolvedValue(undefined); - variantAnalysisResultsManager = new VariantAnalysisResultsManager( - cli, - extLogger, - ); - dummyRepoTask = createMockVariantAnalysisRepositoryTask(); variantAnalysisStoragePath = join( @@ -103,7 +95,7 @@ describe(VariantAnalysisResultsManager.name, () => { beforeEach(async () => { const sourceFilePath = join( __dirname, - "../data/variant-analysis-results.zip", + "../../cli-integration/data/variant-analysis-results.zip", ); fileContents = fs.readFileSync(sourceFilePath); @@ -222,17 +214,12 @@ describe(VariantAnalysisResultsManager.name, () => { let dummyRepoTask: VariantAnalysisRepositoryTask; let variantAnalysisStoragePath: string; let repoTaskStorageDirectory: string; - let variantAnalysisResultsManager: VariantAnalysisResultsManager; let onResultLoadedSpy: jest.Mock< void, [VariantAnalysisScannedRepositoryResult] >; beforeEach(() => { - variantAnalysisResultsManager = new VariantAnalysisResultsManager( - cli, - extLogger, - ); onResultLoadedSpy = jest.fn(); variantAnalysisResultsManager.onResultLoaded(onResultLoadedSpy); From b679c18b0b5550115b510f4e70ba6d1ddc77d801 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Wed, 1 Mar 2023 14:09:09 +0100 Subject: [PATCH 11/19] Move variant analysis results zip to be next to tests --- .../data/variant-analysis-results.zip | Bin .../variant-analysis-manager.test.ts | 2 +- .../variant-analysis-results-manager.test.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename extensions/ql-vscode/test/vscode-tests/{cli-integration => activated-extension/variant-analysis}/data/variant-analysis-results.zip (100%) diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/data/variant-analysis-results.zip b/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/data/variant-analysis-results.zip similarity index 100% rename from extensions/ql-vscode/test/vscode-tests/cli-integration/data/variant-analysis-results.zip rename to extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/data/variant-analysis-results.zip diff --git a/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-manager.test.ts b/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-manager.test.ts index 04389f6c9..024a1bc82 100644 --- a/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-manager.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-manager.test.ts @@ -220,7 +220,7 @@ describe("Variant Analysis Manager", () => { const sourceFilePath = join( __dirname, - "../../cli-integration/data/variant-analysis-results.zip", + "data/variant-analysis-results.zip", ); const fileContents = fs.readFileSync(sourceFilePath); const response = new Response(Readable.from(fileContents)); diff --git a/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-results-manager.test.ts b/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-results-manager.test.ts index 50a3cb470..5538fe14b 100644 --- a/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-results-manager.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/activated-extension/variant-analysis/variant-analysis-results-manager.test.ts @@ -95,7 +95,7 @@ describe(VariantAnalysisResultsManager.name, () => { beforeEach(async () => { const sourceFilePath = join( __dirname, - "../../cli-integration/data/variant-analysis-results.zip", + "data/variant-analysis-results.zip", ); fileContents = fs.readFileSync(sourceFilePath); From 074229e2a00715cf2877230954ce3fa6fcd19a88 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Wed, 1 Mar 2023 17:17:06 +0100 Subject: [PATCH 12/19] Clarify some comments --- extensions/ql-vscode/src/interface-utils.ts | 4 +++- extensions/ql-vscode/src/interface.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/ql-vscode/src/interface-utils.ts b/extensions/ql-vscode/src/interface-utils.ts index a8cf2d90d..152cb8d93 100644 --- a/extensions/ql-vscode/src/interface-utils.ts +++ b/extensions/ql-vscode/src/interface-utils.ts @@ -166,7 +166,9 @@ export function getHtmlForWebview( /* * Content security policy: * default-src: allow nothing by default. - * script-src: allow the given script, using the nonce. also allow loading WebAssembly modules. + * script-src: + * - allow the given script, using the nonce. + * - 'wasm-unsafe-eval: allow loading WebAssembly modules if necessary. * style-src: allow only the given stylesheet, using the nonce. * connect-src: only allow fetch calls to webview resource URIs * (this is used to load BQRS result files). diff --git a/extensions/ql-vscode/src/interface.ts b/extensions/ql-vscode/src/interface.ts index a10b9d76e..fc4b3040e 100644 --- a/extensions/ql-vscode/src/interface.ts +++ b/extensions/ql-vscode/src/interface.ts @@ -658,7 +658,7 @@ export class ResultsView extends AbstractWebview< } let data; let numTotalResults; - // Graph results are only supported in canary mode because the graph viewer is unsupported + // Graph results are only supported in canary mode because the graph viewer is not actively supported if (metadata?.kind === GRAPH_TABLE_NAME && isCanary()) { data = await interpretGraphResults( this.cliServer, From 304330074d51d6fe7efbaad732073766ae8a465b Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Wed, 1 Mar 2023 17:17:27 +0100 Subject: [PATCH 13/19] Add some additional safety to `allowWasmEval` --- extensions/ql-vscode/src/abstract-webview.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ql-vscode/src/abstract-webview.ts b/extensions/ql-vscode/src/abstract-webview.ts index cf46408ad..dd50a03f5 100644 --- a/extensions/ql-vscode/src/abstract-webview.ts +++ b/extensions/ql-vscode/src/abstract-webview.ts @@ -117,7 +117,7 @@ export abstract class AbstractWebview< config.view, { allowInlineStyles: true, - allowWasmEval: config.allowWasmEval ?? false, + allowWasmEval: !!config.allowWasmEval, }, ); this.push( From e9787c2702a6a4fc977f730b15d2a700593072ac Mon Sep 17 00:00:00 2001 From: Charis Kyriakou Date: Wed, 1 Mar 2023 16:26:49 +0000 Subject: [PATCH 14/19] Hack to avoid CodeQL CLI v2.12.3 --- extensions/ql-vscode/src/distribution.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/extensions/ql-vscode/src/distribution.ts b/extensions/ql-vscode/src/distribution.ts index e9a84c40c..119c82244 100644 --- a/extensions/ql-vscode/src/distribution.ts +++ b/extensions/ql-vscode/src/distribution.ts @@ -315,6 +315,15 @@ class ExtensionSpecificDistributionManager { const extensionSpecificRelease = this.getInstalledRelease(); const latestRelease = await this.getLatestRelease(); + // v2.12.3 was released with a bug that causes the extension to fail + // so we force the extension to ignore it. + if ( + extensionSpecificRelease && + extensionSpecificRelease.name === "v2.12.3" + ) { + return createUpdateAvailableResult(latestRelease); + } + if ( extensionSpecificRelease !== undefined && codeQlPath !== undefined && @@ -430,6 +439,12 @@ class ExtensionSpecificDistributionManager { this.versionRange, this.config.includePrerelease, (release) => { + // v2.12.3 was released with a bug that causes the extension to fail + // so we force the extension to ignore it. + if (release.name === "v2.12.3") { + return false; + } + const matchingAssets = release.assets.filter( (asset) => asset.name === requiredAssetName, ); From dd2e79477f58399b5487d29c6fc690cce0813c8e Mon Sep 17 00:00:00 2001 From: Charis Kyriakou Date: Wed, 1 Mar 2023 17:02:11 +0000 Subject: [PATCH 15/19] Revert "Move MRVA out of canary " --- README.md | 1 - extensions/ql-vscode/CHANGELOG.md | 1 - extensions/ql-vscode/docs/test-plan.md | 4 +++ extensions/ql-vscode/package.json | 10 ++++--- .../ql-vscode/src/databases/db-module.ts | 25 +++++++++++++---- extensions/ql-vscode/src/extension.ts | 28 +++++++++++-------- .../variant-analysis/repository-selection.ts | 4 +-- .../src/variant-analysis/run-remote-query.ts | 2 +- .../variant-analysis-manager.ts | 2 +- ...nt-analysis-submission-integration.test.ts | 7 ++++- 10 files changed, 56 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 32d2af24c..1fce91247 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ To see what has changed in the last few versions of the extension, see the [Chan * Shows the flow of data through the results of path queries, which is essential for triaging security results. * Provides an easy way to run queries from the large, open source repository of [CodeQL security queries](https://github.com/github/codeql). * Adds IntelliSense to support you writing and editing your own CodeQL query and library files. -* Supports you running CodeQL queries against thousands of repositories on GitHub using multi-repository variant analysis. ## Project goals and scope diff --git a/extensions/ql-vscode/CHANGELOG.md b/extensions/ql-vscode/CHANGELOG.md index 4c39ebd1f..fa07fc515 100644 --- a/extensions/ql-vscode/CHANGELOG.md +++ b/extensions/ql-vscode/CHANGELOG.md @@ -2,7 +2,6 @@ ## [UNRELEASED] -- Enable multi-repository variant analysis. [#2121](https://github.com/github/vscode-codeql/pull/2121) - Enable collection of telemetry concerning interactions with UI elements, including buttons, links, and other inputs. [#2114](https://github.com/github/vscode-codeql/pull/2114) # 1.7.10 - 23 February 2023 diff --git a/extensions/ql-vscode/docs/test-plan.md b/extensions/ql-vscode/docs/test-plan.md index 788dd0e27..c317d4f69 100644 --- a/extensions/ql-vscode/docs/test-plan.md +++ b/extensions/ql-vscode/docs/test-plan.md @@ -16,6 +16,10 @@ choose to go through some of the Optional Test Cases. ## Required Test Cases +### Pre-requisites + +- Flip the `codeQL.canary` flag. This will enable MRVA in the extension. + ### Test Case 1: MRVA - Running a problem path query and viewing results 1. Open the [UnsafeJQueryPlugin query](https://github.com/github/codeql/blob/main/javascript/ql/src/Security/CWE-079/UnsafeJQueryPlugin.ql). diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index dad130243..7401ef7f7 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -978,10 +978,11 @@ }, { "command": "codeQL.runVariantAnalysis", - "when": "editorLangId == ql && resourceExtname == .ql" + "when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql" }, { - "command": "codeQL.exportSelectedVariantAnalysisResults" + "command": "codeQL.exportSelectedVariantAnalysisResults", + "when": "config.codeQL.canary" }, { "command": "codeQL.runQueries", @@ -1235,7 +1236,7 @@ }, { "command": "codeQL.runVariantAnalysis", - "when": "editorLangId == ql && resourceExtname == .ql" + "when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql" }, { "command": "codeQL.viewAst", @@ -1280,7 +1281,8 @@ }, { "id": "codeQLVariantAnalysisRepositories", - "name": "Variant Analysis Repositories" + "name": "Variant Analysis Repositories", + "when": "config.codeQL.canary" }, { "id": "codeQLQueryHistory", diff --git a/extensions/ql-vscode/src/databases/db-module.ts b/extensions/ql-vscode/src/databases/db-module.ts index ceb46779a..f81b42d25 100644 --- a/extensions/ql-vscode/src/databases/db-module.ts +++ b/extensions/ql-vscode/src/databases/db-module.ts @@ -1,11 +1,12 @@ import { window } from "vscode"; -import { App } from "../common/app"; +import { App, AppMode } from "../common/app"; import { extLogger } from "../common"; import { DisposableObject } from "../pure/disposable-object"; import { DbConfigStore } from "./config/db-config-store"; import { DbManager } from "./db-manager"; import { DbPanel } from "./ui/db-panel"; import { DbSelectionDecorationProvider } from "./ui/db-selection-decoration-provider"; +import { isCanary } from "../config"; export class DbModule extends DisposableObject { public readonly dbManager: DbManager; @@ -18,12 +19,24 @@ export class DbModule extends DisposableObject { this.dbManager = new DbManager(app, this.dbConfigStore); } - public static async initialize(app: App): Promise { - const dbModule = new DbModule(app); - app.subscriptions.push(dbModule); + public static async initialize(app: App): Promise { + if (DbModule.shouldEnableModule(app.mode)) { + const dbModule = new DbModule(app); + app.subscriptions.push(dbModule); - await dbModule.initialize(app); - return dbModule; + await dbModule.initialize(app); + return dbModule; + } + + return undefined; + } + + private static shouldEnableModule(app: AppMode): boolean { + if (app === AppMode.Development || app === AppMode.Test) { + return true; + } + + return isCanary(); } private async initialize(app: App): Promise { diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 9be77ab4a..5960da4dc 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -637,7 +637,7 @@ async function activateWithInstalledDistribution( cliServer, variantAnalysisStorageDir, variantAnalysisResultsManager, - dbModule.dbManager, + dbModule?.dbManager, ); ctx.subscriptions.push(variantAnalysisManager); ctx.subscriptions.push(variantAnalysisResultsManager); @@ -1121,17 +1121,23 @@ async function activateWithInstalledDistribution( token: CancellationToken, uri: Uri | undefined, ) => { - progress({ - maxStep: 5, - step: 0, - message: "Getting credentials", - }); + if (isCanary()) { + progress({ + maxStep: 5, + step: 0, + message: "Getting credentials", + }); - await variantAnalysisManager.runVariantAnalysis( - uri || window.activeTextEditor?.document.uri, - progress, - token, - ); + await variantAnalysisManager.runVariantAnalysis( + uri || window.activeTextEditor?.document.uri, + progress, + token, + ); + } else { + throw new Error( + "Variant analysis requires the CodeQL Canary version to run.", + ); + } }, { title: "Run Variant Analysis", diff --git a/extensions/ql-vscode/src/variant-analysis/repository-selection.ts b/extensions/ql-vscode/src/variant-analysis/repository-selection.ts index 59f0b7b7b..c289067de 100644 --- a/extensions/ql-vscode/src/variant-analysis/repository-selection.ts +++ b/extensions/ql-vscode/src/variant-analysis/repository-selection.ts @@ -13,9 +13,9 @@ export interface RepositorySelection { * @returns The user selection. */ export async function getRepositorySelection( - dbManager: DbManager, + dbManager?: DbManager, ): Promise { - const selectedDbItem = dbManager.getSelectedDbItem(); + const selectedDbItem = dbManager?.getSelectedDbItem(); if (selectedDbItem) { switch (selectedDbItem.kind) { case DbItemKind.LocalDatabase || DbItemKind.LocalList: diff --git a/extensions/ql-vscode/src/variant-analysis/run-remote-query.ts b/extensions/ql-vscode/src/variant-analysis/run-remote-query.ts index 8b64aa8c7..ffeb1d89a 100644 --- a/extensions/ql-vscode/src/variant-analysis/run-remote-query.ts +++ b/extensions/ql-vscode/src/variant-analysis/run-remote-query.ts @@ -223,7 +223,7 @@ export async function prepareRemoteQueryRun( uri: Uri | undefined, progress: ProgressCallback, token: CancellationToken, - dbManager: DbManager, + dbManager?: DbManager, ): Promise { if (!uri?.fsPath.endsWith(".ql")) { throw new UserCancellationException("Not a CodeQL query file."); diff --git a/extensions/ql-vscode/src/variant-analysis/variant-analysis-manager.ts b/extensions/ql-vscode/src/variant-analysis/variant-analysis-manager.ts index af42da85c..9156617f4 100644 --- a/extensions/ql-vscode/src/variant-analysis/variant-analysis-manager.ts +++ b/extensions/ql-vscode/src/variant-analysis/variant-analysis-manager.ts @@ -105,7 +105,7 @@ export class VariantAnalysisManager private readonly cliServer: CodeQLCliServer, private readonly storagePath: string, private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager, - private readonly dbManager: DbManager, + private readonly dbManager?: DbManager, ) { super(); this.variantAnalysisMonitor = this.push( diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-submission-integration.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-submission-integration.test.ts index d02b43e41..6fbbb3ca2 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-submission-integration.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-submission-integration.test.ts @@ -3,6 +3,7 @@ import { resolve } from "path"; import { authentication, commands, + ConfigurationTarget, extensions, QuickPickItem, TextDocument, @@ -12,7 +13,10 @@ import { import { CodeQLExtensionInterface } from "../../../../src/extension"; import { MockGitHubApiServer } from "../../../../src/mocks/mock-gh-api-server"; -import { setRemoteControllerRepo } from "../../../../src/config"; +import { + CANARY_FEATURES, + setRemoteControllerRepo, +} from "../../../../src/config"; jest.setTimeout(30_000); @@ -35,6 +39,7 @@ describe("Variant Analysis Submission Integration", () => { let showErrorMessageSpy: jest.SpiedFunction; beforeEach(async () => { + await CANARY_FEATURES.updateValue(true, ConfigurationTarget.Global); await setRemoteControllerRepo("github/vscode-codeql"); jest.spyOn(authentication, "getSession").mockResolvedValue({ From e9062551eea9a7a8a375cf9c5460a5ab7f5da463 Mon Sep 17 00:00:00 2001 From: Charis Kyriakou Date: Wed, 1 Mar 2023 17:03:24 +0000 Subject: [PATCH 16/19] Revert "Add controller repo 'learn more' link (#2120)" This reverts commit fd6cd1f2d2b183c2bd7e5b397b000a66ffbf2304. --- extensions/ql-vscode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index dad130243..7dba46a92 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -1316,7 +1316,7 @@ }, { "view": "codeQLVariantAnalysisRepositories", - "contents": "Set up a controller repository to start using variant analysis. [Learn more](https://codeql.github.com/docs/codeql-for-visual-studio-code/running-codeql-queries-at-scale-with-mrva#controller-repository) about controller repositories. \n[Set up controller repository](command:codeQLVariantAnalysisRepositories.setupControllerRepository)", + "contents": "Set up a controller repository to start using variant analysis.\n[Set up controller repository](command:codeQLVariantAnalysisRepositories.setupControllerRepository)", "when": "!config.codeQL.variantAnalysis.controllerRepo" } ] From 82a2db9fec4ccf8a7f141a1b31ab433abb85127a Mon Sep 17 00:00:00 2001 From: Andrew Eisenberg Date: Wed, 1 Mar 2023 18:33:51 +0000 Subject: [PATCH 17/19] v1.7.11 Release prep and fix markdown linting warnings in test plan. --- extensions/ql-vscode/CHANGELOG.md | 5 +- extensions/ql-vscode/docs/test-plan.md | 129 +++++++++++++++---------- 2 files changed, 79 insertions(+), 55 deletions(-) diff --git a/extensions/ql-vscode/CHANGELOG.md b/extensions/ql-vscode/CHANGELOG.md index fa07fc515..d7d3b0766 100644 --- a/extensions/ql-vscode/CHANGELOG.md +++ b/extensions/ql-vscode/CHANGELOG.md @@ -1,10 +1,11 @@ # CodeQL for Visual Studio Code: Changelog -## [UNRELEASED] +## 1.7.11 - 1 March 2023 - Enable collection of telemetry concerning interactions with UI elements, including buttons, links, and other inputs. [#2114](https://github.com/github/vscode-codeql/pull/2114) +- Prevent the installation of CodeQL CLI version 2.12.3 from being automatically to avoid a bug in the language server. [#2126](https://github.com/github/vscode-codeql/pull/2126) -# 1.7.10 - 23 February 2023 +## 1.7.10 - 23 February 2023 - Fix bug that was causing unwanted error notifications. diff --git a/extensions/ql-vscode/docs/test-plan.md b/extensions/ql-vscode/docs/test-plan.md index c317d4f69..b131580ed 100644 --- a/extensions/ql-vscode/docs/test-plan.md +++ b/extensions/ql-vscode/docs/test-plan.md @@ -2,15 +2,17 @@ This document describes the manual test plan for the QL extension for Visual Studio Code. -The plan will be executed manually to start with but the goal is to eventually automate parts of the process (based on +The plan will be executed manually to start with but the goal is to eventually automate parts of the process (based on effort vs value basis). -#### What this doesn't cover +## What this doesn't cover + We don't need to test features (and permutations of features) that are covered by automated tests. -### Before releasing the VS Code extension +## Before releasing the VS Code extension + - Go through the required test cases listed below -- Check major PRs since the previous release for specific one-off things to test. Based on that, you might want to +- Check major PRs since the previous release for specific one-off things to test. Based on that, you might want to choose to go through some of the Optional Test Cases. - Run a query using the existing version of the extension (to generate an "old" query history item) @@ -24,23 +26,25 @@ choose to go through some of the Optional Test Cases. 1. Open the [UnsafeJQueryPlugin query](https://github.com/github/codeql/blob/main/javascript/ql/src/Security/CWE-079/UnsafeJQueryPlugin.ql). 2. Run a MRVA against the following repo list: -``` -{ - "name": "test-repo-list", - "repositories": [ - "angular-cn/ng-nice", - "apache/hadoop", - "apache/hive" - ] -} -``` + + ```json + { + "name": "test-repo-list", + "repositories": [ + "angular-cn/ng-nice", + "apache/hadoop", + "apache/hive" + ] + } + ``` + 3. Check that a notification message pops up and the results view is opened. 4. Check the query history. It should: - Show that an item has been added to the query history - The item should be marked as "in progress". 5. Once the query starts: - - Check the results view - - Check the code paths view, including the code paths drop down menu. + - Check the results view + - Check the code paths view, including the code paths drop down menu. - Check that the repository filter box works - Click links to files/locations on GitHub - Check that the query history item is updated to show the number of results @@ -74,7 +78,7 @@ choose to go through some of the Optional Test Cases. 1. Click a history item (for MRVA): - Check that exporting results works - Check that sorting results works - - Check that copying repo lists works + - Check that copying repo lists works 2. Open the query results directory: - Check that the correct directory is opened and there are results in it 3. View logs @@ -84,12 +88,12 @@ choose to go through some of the Optional Test Cases. Run one of the above MRVAs, but cancel it from within VS Code: - Check that the query is canceled and the query history item is updated. -- Check that the workflow run is also canceled. +- Check that the workflow run is also canceled. - Check that any available results are visible in VS Code. -### Test Case 6: MRVA - Change to a different colour theme +### Test Case 6: MRVA - Change to a different colour theme -Open one of the above MRVAs, try changing to a different colour theme and check that everything looks sensible. +Open one of the above MRVAs, try changing to a different colour theme and check that everything looks sensible. Are there any components that are not showing up? ## Optional Test Cases @@ -99,9 +103,10 @@ These are mostly aimed at MRVA, but some of them are also applicable to non-MRVA ### Selecting repositories to run on #### Test case 1: Running a query on a single repository -1. When the repository exists and is public - 1. Has a CodeQL database for the correct language - 2. Has a CodeQL database for another language + +1. When the repository exists and is public + 1. Has a CodeQL database for the correct language + 2. Has a CodeQL database for another language 3. Does not have any CodeQL databases 2. When the repository exists and is private 1. Is accessible and has a CodeQL database @@ -109,14 +114,16 @@ These are mostly aimed at MRVA, but some of them are also applicable to non-MRVA 3. When the repository does not exist #### Test case 2: Running a query on a custom repository list + 1. The repository list is non-empty - 1. All repositories in the list have a CodeQL database + 1. All repositories in the list have a CodeQL database 2. Some but not all repositories in the list have a CodeQL database 3. No repositories in the list have a CodeQL database 2. The repository list is empty #### Test case 3: Running a query on all repositories in an organization -1. The org exists + +1. The org exists 1. The org contains repositories that have CodeQL databases 2. The org contains repositories of the right language but without CodeQL databases 3. The org contains repositories not of the right language @@ -126,20 +133,25 @@ These are mostly aimed at MRVA, but some of them are also applicable to non-MRVA ### Using different types of controller repos #### Test case 1: Running a query when the controller repository is public + 1. Can run queries on public repositories 2. Can not run queries on private repositories #### Test case 2: Running a query when the controller repository is private + 1. Can run queries on public repositories 2. Can run queries on private repositories #### Test case 3: Running a query when the controller repo exists but you do not have write access + 1. Cannot run queries #### Test case 4: Running a query when the controller repo doesn’t exist + 1. Cannot run queries #### Test case 5: Running a query when the "config field" for the controller repo is not set + 1. Cannot run queries ### Query History @@ -150,6 +162,7 @@ The first test case specifies actions that you can do when the query is first ru with this since it has quite a limited number of actions you can do. #### Test case 1: When variant analysis state is "pending" + 1. Starts monitoring variant analysis 2. Cannot open query history item 3. Can delete a query history item @@ -160,8 +173,8 @@ with this since it has quite a limited number of actions you can do. 2. By query date 3. By result count 5. Cannot open query directory -6. Can open query that produced these results - 1. When the file still exists and has not moved +6. Can open query that produced these results + 1. When the file still exists and has not moved 2. When the file does not exist 7. Cannot view logs 8. Cannot copy repository list @@ -171,6 +184,7 @@ with this since it has quite a limited number of actions you can do. 12. Cannot cancel analysis #### Test case 2: When the variant analysis state is not "pending" + 1. Query history is loaded when VSCode starts 2. Handles when action workflow was canceled while VSCode was closed 3. Can open query history item @@ -204,12 +218,14 @@ with this since it has quite a limited number of actions you can do. 4. A popup allows you to open the directory #### Test case 3: When variant analysis state is "in_progress" + 1. Starts monitoring variant analysis - 1. Ready results are downloaded -2. Can cancel analysis + 1. Ready results are downloaded +2. Can cancel analysis 1. Causes the actions run to be canceled #### Test case 4: When variant analysis state is in final state ("succeeded"/"failed"/"canceled") + 1. Stops monitoring variant analysis 1. All results are downloaded if state is succeeded 2. Otherwise, ready results are downloaded, if any are available @@ -220,6 +236,7 @@ with this since it has quite a limited number of actions you can do. This requires running a MRVA query and seeing the results view. #### Test case 1: When variant analysis state is "pending" + 1. Can open a results view 2. Results view opens automatically - When starting variant analysis run @@ -227,9 +244,10 @@ This requires running a MRVA query and seeing the results view. 3. Results view is empty #### Test case 2: When variant analysis state is not "pending" + 1. Can open a results view 2. Results view opens automatically - 1. When starting variant analysis run + 1. When starting variant analysis run 2. When VSCode opens (if view was open when VSCode was closed) 3. Can copy repository list 1. Text is copied to clipboard @@ -240,43 +258,45 @@ This requires running a MRVA query and seeing the results view. 6. Can open query file 1. When the file still exists and has not moved 2. When the file does not exist -7. Can open query text -8. Can sort repos - 1. By name - 2. By results - 3. By stars +7. Can open query text +8. Can sort repos + 1. By name + 2. By results + 3. By stars 4. By last updated 9. Can filter repos -10. Shows correct statistics - 1. Total number of results - 2. Total number of repositories +10. Shows correct statistics + 1. Total number of results + 2. Total number of repositories 3. Duration -11. Can see live results +11. Can see live results 1. Results appear in extension as soon as each query is completed 12. Can view interpreted results (i.e. for a "problem" query) - 1. Can view non-path results + 1. Can view non-path results 2. Can view code paths for "path-problem" queries 13. Can view raw results (i.e. for a non "problem" query) 1. Renders a table -14. Can see skipped repositories - 1. Can see repos with no db in a tab - 1. Shown warning that explains the tab +14. Can see skipped repositories + 1. Can see repos with no db in a tab + 1. Shown warning that explains the tab 2. Can see repos with no access in a tab - 1. Shown warning that explains the tab + 1. Shown warning that explains the tab 3. Only shows tab when there are skipped repos -15. Result downloads - 1. All results are downloaded automatically +15. Result downloads + 1. All results are downloaded automatically 2. Download status is indicated by a spinner (Not currently any indication of progress beyond "downloading" and "not downloading") - 3. Only 3 items are downloaded at a time - 4. Results for completed queries are still downloaded when - 1. Some but not all queries failed + 3. Only 3 items are downloaded at a time + 4. Results for completed queries are still downloaded when + 1. Some but not all queries failed 2. The variant analysis was canceled after some queries completed #### Test case 3: When variant analysis state is in "succeeded" state + 1. Can view logs -2. All results are downloaded +2. All results are downloaded #### Test case 4: When variant analysis is in "failed" or "canceled" state + 1. Can view logs 1. Results for finished queries are still downloaded. @@ -305,14 +325,17 @@ This requires running a MRVA query and seeing the results view. 1. Collapse/expand tree nodes Error cases that trigger an error notification: -1. Try to add a list with a name that already exists + +1. Try to add a list with a name that already exists 1. Try to add a top-level database that already exists 1. Try to add a database in a list that already exists in the list Error cases that show an error in the panel (and only the edit button should be visible): + 1. Edit the db config file directly and save invalid JSON 1. Edit the db config file directly and save valid JSON but invalid config (e.g. add an unknown property) -1. Edit the db config file directly and save two lists with the same name +1. Edit the db config file directly and save two lists with the same name Cases where there the welcome view is shown: -1. No controller repo is set in the user's settings JSON. \ No newline at end of file + +1. No controller repo is set in the user's settings JSON. From fb5675a7c564bd8fedb6c17e6a939276a7a2b468 Mon Sep 17 00:00:00 2001 From: Andrew Eisenberg Date: Wed, 1 Mar 2023 10:42:26 -0800 Subject: [PATCH 18/19] Update extensions/ql-vscode/CHANGELOG.md Co-authored-by: Aditya Sharad <6874315+adityasharad@users.noreply.github.com> --- extensions/ql-vscode/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ql-vscode/CHANGELOG.md b/extensions/ql-vscode/CHANGELOG.md index d7d3b0766..9e47897c1 100644 --- a/extensions/ql-vscode/CHANGELOG.md +++ b/extensions/ql-vscode/CHANGELOG.md @@ -3,7 +3,7 @@ ## 1.7.11 - 1 March 2023 - Enable collection of telemetry concerning interactions with UI elements, including buttons, links, and other inputs. [#2114](https://github.com/github/vscode-codeql/pull/2114) -- Prevent the installation of CodeQL CLI version 2.12.3 from being automatically to avoid a bug in the language server. [#2126](https://github.com/github/vscode-codeql/pull/2126) +- Prevent the automatic installation of CodeQL CLI version 2.12.3 to avoid a bug in the language server. CodeQL CLI 2.12.2 will be used instead. [#2126](https://github.com/github/vscode-codeql/pull/2126) ## 1.7.10 - 23 February 2023 From dd19ebdfdb669a2ae544e59038d967d1237e0504 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 1 Mar 2023 19:07:38 +0000 Subject: [PATCH 19/19] Bump version to v1.7.12 --- extensions/ql-vscode/CHANGELOG.md | 2 ++ extensions/ql-vscode/package-lock.json | 4 ++-- extensions/ql-vscode/package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/extensions/ql-vscode/CHANGELOG.md b/extensions/ql-vscode/CHANGELOG.md index 9e47897c1..74f53c129 100644 --- a/extensions/ql-vscode/CHANGELOG.md +++ b/extensions/ql-vscode/CHANGELOG.md @@ -1,5 +1,7 @@ # CodeQL for Visual Studio Code: Changelog +## [UNRELEASED] + ## 1.7.11 - 1 March 2023 - Enable collection of telemetry concerning interactions with UI elements, including buttons, links, and other inputs. [#2114](https://github.com/github/vscode-codeql/pull/2114) diff --git a/extensions/ql-vscode/package-lock.json b/extensions/ql-vscode/package-lock.json index 6669b57ec..0bac771ee 100644 --- a/extensions/ql-vscode/package-lock.json +++ b/extensions/ql-vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-codeql", - "version": "1.7.11", + "version": "1.7.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-codeql", - "version": "1.7.11", + "version": "1.7.12", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index 3beaedd0b..fe8ba4514 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -4,7 +4,7 @@ "description": "CodeQL for Visual Studio Code", "author": "GitHub", "private": true, - "version": "1.7.11", + "version": "1.7.12", "publisher": "GitHub", "license": "MIT", "icon": "media/VS-marketplace-CodeQL-icon.png",