Merge remote-tracking branch 'origin/main' into koesie10/remove-as-unknown-as-quickpickitem
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
## [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.11 - 1 March 2023
|
||||
|
||||
# 1.7.10 - 23 February 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 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
|
||||
|
||||
- Fix bug that was causing unwanted error notifications.
|
||||
|
||||
|
||||
@@ -2,41 +2,49 @@
|
||||
|
||||
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)
|
||||
|
||||
## 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).
|
||||
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
|
||||
@@ -70,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
|
||||
@@ -80,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
|
||||
@@ -95,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
|
||||
@@ -105,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
|
||||
@@ -122,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
|
||||
@@ -146,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
|
||||
@@ -156,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
|
||||
@@ -167,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
|
||||
@@ -200,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
|
||||
@@ -216,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
|
||||
@@ -223,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
|
||||
@@ -236,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.
|
||||
|
||||
@@ -301,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.
|
||||
|
||||
1. No controller repo is set in the user's settings JSON.
|
||||
|
||||
4
extensions/ql-vscode/package-lock.json
generated
4
extensions/ql-vscode/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -1316,7 +1318,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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
this.push(
|
||||
|
||||
@@ -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<DbModule> {
|
||||
const dbModule = new DbModule(app);
|
||||
app.subscriptions.push(dbModule);
|
||||
public static async initialize(app: App): Promise<DbModule | undefined> {
|
||||
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<void> {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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
|
||||
@@ -637,7 +638,7 @@ async function activateWithInstalledDistribution(
|
||||
cliServer,
|
||||
variantAnalysisStorageDir,
|
||||
variantAnalysisResultsManager,
|
||||
dbModule.dbManager,
|
||||
dbModule?.dbManager,
|
||||
);
|
||||
ctx.subscriptions.push(variantAnalysisManager);
|
||||
ctx.subscriptions.push(variantAnalysisResultsManager);
|
||||
@@ -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,
|
||||
@@ -1121,17 +1127,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",
|
||||
|
||||
@@ -125,10 +125,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"));
|
||||
@@ -159,7 +162,9 @@ 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.
|
||||
* - '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).
|
||||
@@ -168,7 +173,9 @@ export function getHtmlForWebview(
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'nonce-${nonce}'; font-src ${fontSrc}; style-src ${styleSrc}; connect-src ${
|
||||
content="default-src 'none'; script-src 'nonce-${nonce}'${
|
||||
allowWasmEval ? " 'wasm-unsafe-eval'" : ""
|
||||
}; font-src ${fontSrc}; style-src ${styleSrc}; connect-src ${
|
||||
webview.cspSource
|
||||
};">
|
||||
${stylesheetsHtmlLines.join(` ${EOL}`)}
|
||||
|
||||
@@ -68,7 +68,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";
|
||||
@@ -225,6 +225,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. Only supported in canary mode.
|
||||
allowWasmEval: isCanary(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -660,7 +662,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 not actively supported
|
||||
if (metadata?.kind === GRAPH_TABLE_NAME && isCanary()) {
|
||||
data = await interpretGraphResults(
|
||||
this.cliServer,
|
||||
metadata,
|
||||
|
||||
@@ -67,3 +67,8 @@ export function pathsEqual(
|
||||
}
|
||||
return path1 === path2;
|
||||
}
|
||||
|
||||
export async function readDirFullPaths(path: string): Promise<string[]> {
|
||||
const baseNames = await readdir(path);
|
||||
return baseNames.map((baseName) => join(path, baseName));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface QueryHistoryDirs {
|
||||
localQueriesDirPath: string;
|
||||
variantAnalysesDirPath: string;
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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";
|
||||
|
||||
const LAST_SCRUB_TIME_KEY = "lastScrubTime";
|
||||
@@ -23,14 +25,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 +44,7 @@ export function registerQueryHistoryScrubber(
|
||||
wakeInterval,
|
||||
throttleTime,
|
||||
maxQueryTime,
|
||||
queryDirectory,
|
||||
queryHistoryDirs,
|
||||
qhm,
|
||||
ctx,
|
||||
counter,
|
||||
@@ -58,7 +60,7 @@ export function registerQueryHistoryScrubber(
|
||||
async function scrubQueries(
|
||||
throttleTime: number,
|
||||
maxQueryTime: number,
|
||||
queryDirectory: string,
|
||||
queryHistoryDirs: QueryHistoryDirs,
|
||||
qhm: QueryHistoryManager,
|
||||
ctx: ExtensionContext,
|
||||
counter?: Counter,
|
||||
@@ -74,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.");
|
||||
if (!(await pathExists(queryDirectory))) {
|
||||
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: ${queryDirectory}`,
|
||||
`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(queryDirectory);
|
||||
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(queryDirectory, baseName);
|
||||
for (const dir of allDirPaths) {
|
||||
const scrubResult = await scrubDirectory(dir, now, maxQueryTime);
|
||||
if (scrubResult.errorMsg) {
|
||||
errors.push(scrubResult.errorMsg);
|
||||
|
||||
@@ -13,9 +13,9 @@ export interface RepositorySelection {
|
||||
* @returns The user selection.
|
||||
*/
|
||||
export async function getRepositorySelection(
|
||||
dbManager: DbManager,
|
||||
dbManager?: DbManager,
|
||||
): Promise<RepositorySelection> {
|
||||
const selectedDbItem = dbManager.getSelectedDbItem();
|
||||
const selectedDbItem = dbManager?.getSelectedDbItem();
|
||||
if (selectedDbItem) {
|
||||
switch (selectedDbItem.kind) {
|
||||
case DbItemKind.LocalDatabase || DbItemKind.LocalList:
|
||||
|
||||
@@ -223,7 +223,7 @@ export async function prepareRemoteQueryRun(
|
||||
uri: Uri | undefined,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
dbManager: DbManager,
|
||||
dbManager?: DbManager,
|
||||
): Promise<PreparedRemoteQuery> {
|
||||
if (!uri?.fsPath.endsWith(".ql")) {
|
||||
throw new UserCancellationException("Not a CodeQL query file.");
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<GraphInterpretationData>;
|
||||
@@ -59,11 +59,12 @@ export class Graph extends React.Component<GraphProps> {
|
||||
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<GraphProps> {
|
||||
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"];
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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<CodeQLExtensionInterface | Record<string, never>>(
|
||||
"GitHub.vscode-codeql",
|
||||
)!
|
||||
.activate();
|
||||
const cli = mockedObject<CodeQLCliServer>({});
|
||||
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,
|
||||
"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<number, VariantAnalysisScannedRepositoryState>,
|
||||
) {
|
||||
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<typeof commands.executeCommand>;
|
||||
|
||||
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<typeof window.showTextDocument>;
|
||||
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<TextEditor>({}));
|
||||
openTextDocumentSpy = jest
|
||||
.spyOn(workspace, "openTextDocument")
|
||||
.mockResolvedValue(mockedObject<TextDocument>({}));
|
||||
});
|
||||
|
||||
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<typeof window.showTextDocument>;
|
||||
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<TextEditor>({}));
|
||||
openTextDocumentSpy = jest
|
||||
.spyOn(workspace, "openTextDocument")
|
||||
.mockResolvedValue(mockedObject<TextDocument>({}));
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<CodeQLExtensionInterface | Record<string, never>>(
|
||||
"GitHub.vscode-codeql",
|
||||
)!
|
||||
.activate();
|
||||
cli = extension.cliServer;
|
||||
const cli = mockedObject<CodeQLCliServer>({});
|
||||
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",
|
||||
"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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
import {
|
||||
CancellationTokenSource,
|
||||
commands,
|
||||
env,
|
||||
extensions,
|
||||
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";
|
||||
@@ -28,71 +19,38 @@ 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, mockedQuickPickItem } from "../../utils/mocking.helpers";
|
||||
import { mockedQuickPickItem } 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<CodeQLExtensionInterface | Record<string, never>>(
|
||||
"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,
|
||||
);
|
||||
@@ -245,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<number, VariantAnalysisScannedRepositoryState>,
|
||||
) {
|
||||
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<typeof commands.executeCommand>;
|
||||
|
||||
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<typeof window.showTextDocument>;
|
||||
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<TextEditor>({}));
|
||||
openTextDocumentSpy = jest
|
||||
.spyOn(workspace, "openTextDocument")
|
||||
.mockResolvedValue(mockedObject<TextDocument>({}));
|
||||
});
|
||||
|
||||
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<typeof window.showTextDocument>;
|
||||
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<TextEditor>({}));
|
||||
openTextDocumentSpy = jest
|
||||
.spyOn(workspace, "openTextDocument")
|
||||
.mockResolvedValue(mockedObject<TextDocument>({}));
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { resolve } from "path";
|
||||
import {
|
||||
authentication,
|
||||
commands,
|
||||
ConfigurationTarget,
|
||||
extensions,
|
||||
TextDocument,
|
||||
window,
|
||||
@@ -11,8 +12,11 @@ import {
|
||||
|
||||
import { CodeQLExtensionInterface } from "../../../../src/extension";
|
||||
import { MockGitHubApiServer } from "../../../../src/mocks/mock-gh-api-server";
|
||||
import { setRemoteControllerRepo } from "../../../../src/config";
|
||||
import { mockedQuickPickItem } from "../../utils/mocking.helpers";
|
||||
import {
|
||||
CANARY_FEATURES,
|
||||
setRemoteControllerRepo,
|
||||
} from "../../../../src/config";
|
||||
|
||||
jest.setTimeout(30_000);
|
||||
|
||||
@@ -35,6 +39,7 @@ describe("Variant Analysis Submission Integration", () => {
|
||||
let showErrorMessageSpy: jest.SpiedFunction<typeof window.showErrorMessage>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await CANARY_FEATURES.updateValue(true, ConfigurationTarget.Global);
|
||||
await setRemoteControllerRepo("github/vscode-codeql");
|
||||
|
||||
jest.spyOn(authentication, "getSession").mockResolvedValue({
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(""),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -26,6 +26,7 @@ import { TextEditor } from "vscode";
|
||||
import { WebviewReveal } from "../../../../src/interface-utils";
|
||||
import * as helpers from "../../../../src/helpers";
|
||||
import { mockedObject, mockedQuickPickItem } from "../../utils/mocking.helpers";
|
||||
import { createMockQueryHistoryDirs } from "../../../factories/query-history/query-history-dirs";
|
||||
|
||||
describe("QueryHistoryManager", () => {
|
||||
const mockExtensionLocation = join(tmpDir.name, "mock-extension-location");
|
||||
@@ -1153,7 +1154,7 @@ describe("QueryHistoryManager", () => {
|
||||
localQueriesResultsViewStub,
|
||||
variantAnalysisManagerStub,
|
||||
{} as EvalLogViewer,
|
||||
"xxx",
|
||||
createMockQueryHistoryDirs(),
|
||||
{
|
||||
globalStorageUri: vscode.Uri.file(mockExtensionLocation),
|
||||
extensionPath: vscode.Uri.file("/x/y/z").fsPath,
|
||||
|
||||
@@ -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<QueryHistoryManager>({
|
||||
removeDeletedQueries: () => {
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -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<ExtensionContext>({
|
||||
globalStorageUri: Uri.file(STORAGE_DIR),
|
||||
storageUri: undefined,
|
||||
|
||||
@@ -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<FullDatabaseOptions>({ 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<FullDatabaseOptions>({ displayName: "default name" }),
|
||||
(_) => {
|
||||
/* no change event listener */
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { QuickPickItem, window } from "vscode";
|
||||
import { QuickPickItem, window, Uri } from "vscode";
|
||||
import { DatabaseItem } from "../../../src/local-databases";
|
||||
|
||||
export type DeepPartial<T> = T extends object
|
||||
? {
|
||||
@@ -44,6 +45,19 @@ export function mockedObject<T extends object>(
|
||||
});
|
||||
}
|
||||
|
||||
export function mockDatabaseItem(
|
||||
props: DeepPartial<DatabaseItem> = {},
|
||||
): DatabaseItem {
|
||||
return mockedObject<DatabaseItem>({
|
||||
databaseUri: Uri.file("abc"),
|
||||
name: "github/codeql",
|
||||
language: "javascript",
|
||||
sourceArchive: undefined,
|
||||
resolveSourceFile: jest.fn().mockReturnValue(Uri.file("abc")),
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
export function mockedQuickPickItem<T extends QuickPickItem | string>(
|
||||
value: T | T[],
|
||||
): Awaited<ReturnType<typeof window.showQuickPick>> {
|
||||
|
||||
Reference in New Issue
Block a user