Merge branch 'main' into robertbrignull/logged_error_telemetry

This commit is contained in:
Robert
2023-02-02 10:46:41 +00:00
43 changed files with 1089 additions and 755 deletions

View File

@@ -122,16 +122,15 @@ jobs:
perl -i -pe 's/^/## \[UNRELEASED\]\n\n/ if($.==3)' CHANGELOG.md
- name: Create version bump PR
uses: peter-evans/create-pull-request@2b011faafdcbc9ceb11414d64d0573f37c774b04 # v4.2.3
uses: ./.github/actions/create-pr
if: success()
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Bump version to ${{ steps.bump-patch-version.outputs.next_version }}
title: Bump version to ${{ steps.bump-patch-version.outputs.next_version }}
body: This PR was automatically generated by the GitHub Actions release workflow in this repository.
branch: ${{ format('version/bump-to-{0}', steps.bump-patch-version.outputs.next_version) }}
base: main
draft: true
head-branch: ${{ format('version/bump-to-{0}', steps.bump-patch-version.outputs.next_version) }}
base-branch: main
vscode-publish:
name: Publish to VS Code Marketplace

View File

@@ -1,6 +1,7 @@
.vscode-test/
node_modules/
out/
build/
# Include the Storybook config
!.storybook

View File

@@ -1,15 +1,20 @@
module.exports = {
const { resolve } = require("path");
const baseConfig = {
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2018,
sourceType: "module",
project: ["tsconfig.json", "./src/**/tsconfig.json", "./test/**/tsconfig.json", "./gulpfile.ts/tsconfig.json", "./scripts/tsconfig.json", "./.storybook/tsconfig.json"],
project: [
resolve(__dirname, "tsconfig.lint.json"),
resolve(__dirname, "src/**/tsconfig.json"),
resolve(__dirname, "test/**/tsconfig.json"),
resolve(__dirname, "gulpfile.ts/tsconfig.json"),
resolve(__dirname, "scripts/tsconfig.json"),
resolve(__dirname, ".storybook/tsconfig.json"),
],
},
plugins: [
"github",
"@typescript-eslint",
"etc"
],
plugins: ["github", "@typescript-eslint", "etc"],
env: {
node: true,
es6: true,
@@ -21,7 +26,7 @@ module.exports = {
"plugin:github/typescript",
"plugin:jest-dom/recommended",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended"
"plugin:@typescript-eslint/recommended",
],
rules: {
"@typescript-eslint/no-use-before-define": 0,
@@ -37,14 +42,14 @@ module.exports = {
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-floating-promises": [ "error", { ignoreVoid: true } ],
"@typescript-eslint/no-floating-promises": ["error", { ignoreVoid: true }],
"@typescript-eslint/no-invalid-this": "off",
"@typescript-eslint/no-shadow": "off",
"prefer-const": ["warn", { destructuring: "all" }],
"@typescript-eslint/no-throw-literal": "error",
"no-useless-escape": 0,
"camelcase": "off",
"eqeqeq": "off",
camelcase: "off",
eqeqeq: "off",
"escompat/no-regexp-lookbehind": "off",
"etc/no-implicit-any-catch": "error",
"filenames/match-regex": "off",
@@ -72,3 +77,102 @@ module.exports = {
"github/no-then": "off",
},
};
module.exports = {
root: true,
...baseConfig,
overrides: [
{
files: ["src/stories/**/*"],
parserOptions: {
project: resolve(__dirname, "src/stories/tsconfig.json"),
},
extends: [
...baseConfig.extends,
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:storybook/recommended",
],
rules: {
...baseConfig.rules,
},
settings: {
react: {
version: "detect",
},
},
},
{
files: ["src/view/**/*"],
parserOptions: {
project: resolve(__dirname, "src/view/tsconfig.json"),
},
extends: [
...baseConfig.extends,
"plugin:react/recommended",
"plugin:react-hooks/recommended",
],
rules: {
...baseConfig.rules,
},
settings: {
react: {
version: "detect",
},
},
},
{
files: ["test/**/*"],
parserOptions: {
project: resolve(__dirname, "test/tsconfig.json"),
},
env: {
jest: true,
},
},
{
files: ["test/vscode-tests/**/*"],
parserOptions: {
project: resolve(__dirname, "test/tsconfig.json"),
},
env: {
jest: true,
},
rules: {
...baseConfig.rules,
"@typescript-eslint/ban-types": [
"error",
{
// For a full list of the default banned types, see:
// https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-types.md
extendDefaults: true,
types: {
// Don't complain about the `Function` type in test files. (Default is `true`.)
Function: false,
},
},
],
},
},
{
files: [
".eslintrc.js",
"test/**/jest-runner-vscode.config.js",
"test/**/jest-runner-vscode.config.base.js",
],
parser: undefined,
plugins: ["github"],
extends: [
"eslint:recommended",
"plugin:github/recommended",
"plugin:prettier/recommended",
],
rules: {
"import/no-commonjs": "off",
"prefer-template": "off",
"filenames/match-regex": "off",
"@typescript-eslint/no-var-requires": "off",
},
},
],
};

View File

@@ -395,7 +395,7 @@
},
{
"command": "codeQLVariantAnalysisRepositories.removeItemContextMenu",
"title": "Remove"
"title": "Delete"
},
{
"command": "codeQLDatabases.chooseDatabaseFolder",
@@ -523,7 +523,7 @@
},
{
"command": "codeQLQueryHistory.openQuery",
"title": "Open Query",
"title": "View Query",
"icon": "$(edit)"
},
{
@@ -533,7 +533,7 @@
},
{
"command": "codeQLQueryHistory.removeHistoryItem",
"title": "Remove from History",
"title": "Delete",
"icon": "$(trash)"
},
{
@@ -550,11 +550,11 @@
},
{
"command": "codeQLQueryHistory.showQueryLog",
"title": "Show Query Log"
"title": "View Query Log"
},
{
"command": "codeQLQueryHistory.openQueryDirectory",
"title": "Open Query Directory"
"title": "Open Results Directory"
},
{
"command": "codeQLQueryHistory.showEvalLog",
@@ -574,7 +574,7 @@
},
{
"command": "codeQLQueryHistory.showQueryText",
"title": "Show Query Text"
"title": "View Query Text"
},
{
"command": "codeQLQueryHistory.exportResults",
@@ -597,8 +597,8 @@
"title": "View DIL"
},
{
"command": "codeQLQueryHistory.setLabel",
"title": "Set Label"
"command": "codeQLQueryHistory.renameItem",
"title": "Rename"
},
{
"command": "codeQLQueryHistory.compareWith",
@@ -606,7 +606,7 @@
},
{
"command": "codeQLQueryHistory.openOnGithub",
"title": "Open Variant Analysis on GitHub"
"title": "View Logs"
},
{
"command": "codeQLQueryHistory.copyRepoList",
@@ -827,57 +827,62 @@
},
{
"command": "codeQLQueryHistory.openQuery",
"group": "9_qlCommands",
"group": "2_queryHistory@0",
"when": "view == codeQLQueryHistory"
},
{
"command": "codeQLQueryHistory.removeHistoryItem",
"group": "9_qlCommands",
"group": "7_queryHistory@0",
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == remoteResultsItem || viewItem == cancelledResultsItem || viewItem == cancelledRemoteResultsItem"
},
{
"command": "codeQLQueryHistory.setLabel",
"group": "9_qlCommands",
"command": "codeQLQueryHistory.removeHistoryItem",
"group": "inline",
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == remoteResultsItem || viewItem == cancelledResultsItem || viewItem == cancelledRemoteResultsItem"
},
{
"command": "codeQLQueryHistory.renameItem",
"group": "6_queryHistory@0",
"when": "view == codeQLQueryHistory"
},
{
"command": "codeQLQueryHistory.compareWith",
"group": "9_qlCommands",
"group": "3_queryHistory@0",
"when": "viewItem == rawResultsItem || viewItem == interpretedResultsItem"
},
{
"command": "codeQLQueryHistory.showQueryLog",
"group": "9_qlCommands",
"group": "4_queryHistory@4",
"when": "viewItem == rawResultsItem || viewItem == interpretedResultsItem"
},
{
"command": "codeQLQueryHistory.openQueryDirectory",
"group": "9_qlCommands",
"group": "2_queryHistory@4",
"when": "view == codeQLQueryHistory && !hasRemoteServer"
},
{
"command": "codeQLQueryHistory.showEvalLog",
"group": "9_qlCommands",
"group": "4_queryHistory@1",
"when": "codeql.supportsEvalLog && viewItem == rawResultsItem || codeql.supportsEvalLog && viewItem == interpretedResultsItem || codeql.supportsEvalLog && viewItem == cancelledResultsItem"
},
{
"command": "codeQLQueryHistory.showEvalLogSummary",
"group": "9_qlCommands",
"group": "4_queryHistory@2",
"when": "codeql.supportsEvalLog && viewItem == rawResultsItem || codeql.supportsEvalLog && viewItem == interpretedResultsItem || codeql.supportsEvalLog && viewItem == cancelledResultsItem"
},
{
"command": "codeQLQueryHistory.showEvalLogViewer",
"group": "9_qlCommands",
"group": "4_queryHistory@3",
"when": "config.codeQL.canary && codeql.supportsEvalLog && viewItem == rawResultsItem || config.codeQL.canary && codeql.supportsEvalLog && viewItem == interpretedResultsItem || config.codeQL.canary && codeql.supportsEvalLog && viewItem == cancelledResultsItem"
},
{
"command": "codeQLQueryHistory.showQueryText",
"group": "9_qlCommands",
"group": "2_queryHistory@2",
"when": "view == codeQLQueryHistory"
},
{
"command": "codeQLQueryHistory.exportResults",
"group": "9_qlCommands",
"group": "1_queryHistory@0",
"when": "view == codeQLQueryHistory && viewItem == remoteResultsItem"
},
{
@@ -887,17 +892,17 @@
},
{
"command": "codeQLQueryHistory.viewCsvAlerts",
"group": "9_qlCommands",
"group": "5_queryHistory@0",
"when": "viewItem == interpretedResultsItem"
},
{
"command": "codeQLQueryHistory.viewSarifAlerts",
"group": "9_qlCommands",
"group": "5_queryHistory@1",
"when": "viewItem == interpretedResultsItem"
},
{
"command": "codeQLQueryHistory.viewDil",
"group": "9_qlCommands",
"group": "5_queryHistory@2",
"when": "viewItem == rawResultsItem || viewItem == interpretedResultsItem"
},
{
@@ -907,12 +912,12 @@
},
{
"command": "codeQLQueryHistory.openOnGithub",
"group": "9_qlCommands",
"group": "2_queryHistory@3",
"when": "viewItem == remoteResultsItem || viewItem == inProgressRemoteResultsItem || viewItem == cancelledRemoteResultsItem"
},
{
"command": "codeQLQueryHistory.copyRepoList",
"group": "9_qlCommands",
"group": "1_queryHistory@1",
"when": "viewItem == remoteResultsItem"
},
{
@@ -1160,7 +1165,7 @@
"when": "false"
},
{
"command": "codeQLQueryHistory.setLabel",
"command": "codeQLQueryHistory.renameItem",
"when": "false"
},
{
@@ -1310,6 +1315,11 @@
{
"view": "codeQLEvalLogViewer",
"contents": "Run the 'Show Evaluator Log (UI)' command on a CodeQL query run in the Query History view."
},
{
"view": "codeQLVariantAnalysisRepositories",
"contents": "Set up a controller repository to start using variant analysis.\n[Set up controller repository](command:codeQLVariantAnalysisRepositories.setupControllerRepository)",
"when": "!config.codeQL.variantAnalysis.controllerRepo"
}
]
},
@@ -1325,7 +1335,7 @@
"cli-integration": "jest --projects test/vscode-tests/cli-integration",
"update-vscode": "node ./node_modules/vscode/bin/install",
"format": "prettier --write **/*.{ts,tsx} && eslint . --ext .ts,.tsx --fix",
"lint": "eslint . --ext .ts,.tsx --max-warnings=0",
"lint": "eslint . --ext .js,.ts,.tsx --max-warnings=0",
"format-staged": "lint-staged",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook",

View File

@@ -50,14 +50,20 @@ function getNwoOrOwnerFromGitHubUrl(
kind: "owner" | "nwo",
): string | undefined {
try {
const uri = new URL(githubUrl);
if (uri.protocol !== "https:") {
return;
let paths: string[];
const urlElements = githubUrl.split("/");
if (
urlElements[0] === "github.com" ||
urlElements[0] === "www.github.com"
) {
paths = githubUrl.split("/").slice(1);
} else {
const uri = new URL(githubUrl);
if (uri.hostname !== "github.com" && uri.hostname !== "www.github.com") {
return;
}
paths = uri.pathname.split("/").filter((segment: string) => segment);
}
if (uri.hostname !== "github.com" && uri.hostname !== "www.github.com") {
return;
}
const paths = uri.pathname.split("/").filter((segment: string) => segment);
const owner = `${paths[0]}`;
if (kind === "owner") {
return owner ? owner : undefined;

View File

@@ -92,6 +92,15 @@ export const GLOBAL_ENABLE_TELEMETRY = new Setting(
GLOBAL_TELEMETRY_SETTING,
);
const ENABLE_NEW_TELEMETRY = new Setting(
"enableNewTelemetry",
TELEMETRY_SETTING,
);
export function newTelemetryEnabled(): boolean {
return ENABLE_NEW_TELEMETRY.getValue<boolean>();
}
// Distribution configuration
const DISTRIBUTION_SETTING = new Setting("cli", ROOT_SETTING);
export const CUSTOM_CODEQL_PATH_SETTING = new Setting(
@@ -539,6 +548,27 @@ export async function setRemoteControllerRepo(repo: string | undefined) {
await REMOTE_CONTROLLER_REPO.updateValue(repo, ConfigurationTarget.Global);
}
export interface VariantAnalysisConfig {
controllerRepo: string | undefined;
onDidChangeConfiguration?: Event<void>;
}
export class VariantAnalysisConfigListener
extends ConfigListener
implements VariantAnalysisConfig
{
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
this.handleDidChangeConfigurationForRelevantSettings(
[VARIANT_ANALYSIS_SETTING],
e,
);
}
public get controllerRepo(): string | undefined {
return getRemoteControllerRepo();
}
}
/**
* The branch of "github/codeql-variant-analysis-action" to use with the "Run Variant Analysis" command.
* Default value is "main".

View File

@@ -24,7 +24,7 @@ export class DbModule extends DisposableObject {
const dbModule = new DbModule(app);
app.subscriptions.push(dbModule);
await dbModule.initialize();
await dbModule.initialize(app);
return dbModule;
}
@@ -39,12 +39,12 @@ export class DbModule extends DisposableObject {
return isCanary() && isVariantAnalysisReposPanelEnabled();
}
private async initialize(): Promise<void> {
private async initialize(app: App): Promise<void> {
void extLogger.log("Initializing database module");
await this.dbConfigStore.initialize();
const dbPanel = new DbPanel(this.dbManager);
const dbPanel = new DbPanel(this.dbManager, app.credentials);
await dbPanel.initialize();
this.push(dbPanel);

View File

@@ -29,6 +29,9 @@ import { DbManager } from "../db-manager";
import { DbTreeDataProvider } from "./db-tree-data-provider";
import { DbTreeViewItem } from "./db-tree-view-item";
import { getGitHubUrl } from "./db-tree-view-item-action";
import { getControllerRepo } from "../../remote-queries/run-remote-query";
import { getErrorMessage } from "../../pure/helpers-pure";
import { Credentials } from "../../common/authentication";
export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
kind: string;
@@ -42,7 +45,10 @@ export class DbPanel extends DisposableObject {
private readonly dataProvider: DbTreeDataProvider;
private readonly treeView: TreeView<DbTreeViewItem>;
public constructor(private readonly dbManager: DbManager) {
public constructor(
private readonly dbManager: DbManager,
private readonly credentials: Credentials,
) {
super();
this.dataProvider = new DbTreeDataProvider(dbManager);
@@ -112,6 +118,12 @@ export class DbPanel extends DisposableObject {
(treeViewItem: DbTreeViewItem) => this.removeItem(treeViewItem),
),
);
this.push(
commandRunner(
"codeQLVariantAnalysisRepositories.setupControllerRepository",
() => this.setupControllerRepository(),
),
);
}
private async openConfigFile(): Promise<void> {
@@ -172,7 +184,7 @@ export class DbPanel extends DisposableObject {
const repoName = await window.showInputBox({
title: "Add a repository",
prompt: "Insert a GitHub repository URL or name with owner",
placeHolder: "github.com/<owner>/<repo> or <owner>/<repo>",
placeHolder: "<owner>/<repo> or https://github.com/<owner>/<repo>",
});
if (!repoName) {
return;
@@ -196,7 +208,7 @@ export class DbPanel extends DisposableObject {
const ownerName = await window.showInputBox({
title: "Add all repositories of a GitHub org or owner",
prompt: "Insert a GitHub organization or owner name",
placeHolder: "github.com/<owner> or <owner>",
placeHolder: "<owner> or https://github.com/<owner>",
});
if (!ownerName) {
@@ -383,4 +395,21 @@ export class DbPanel extends DisposableObject {
await commands.executeCommand("vscode.open", Uri.parse(githubUrl));
}
private async setupControllerRepository(): Promise<void> {
try {
// This will also validate that the controller repository is valid
await getControllerRepo(this.credentials);
} catch (e: unknown) {
if (e instanceof UserCancellationException) {
return;
}
void showAndLogErrorMessage(
`An error occurred while setting up the controller repository: ${getErrorMessage(
e,
)}`,
);
}
}
}

View File

@@ -13,6 +13,7 @@ import {
DbConfigValidationError,
DbConfigValidationErrorKind,
} from "../db-validation-errors";
import { VariantAnalysisConfigListener } from "../../config";
export class DbTreeDataProvider
extends DisposableObject
@@ -27,8 +28,17 @@ export class DbTreeDataProvider
);
private dbTreeItems: DbTreeViewItem[];
private variantAnalysisConfig: VariantAnalysisConfigListener;
public constructor(private readonly dbManager: DbManager) {
super();
this.variantAnalysisConfig = this.push(new VariantAnalysisConfigListener());
this.variantAnalysisConfig.onDidChangeConfiguration(() => {
this.dbTreeItems = this.createTree();
this._onDidChangeTreeData.fire(undefined);
});
this.dbTreeItems = this.createTree();
this.onDidChangeTreeData = this._onDidChangeTreeData.event;
@@ -62,6 +72,11 @@ export class DbTreeDataProvider
}
private createTree(): DbTreeViewItem[] {
// Returning an empty tree here will show the welcome view
if (!this.variantAnalysisConfig.controllerRepo) {
return [];
}
const dbItemsResult = this.dbManager.getDbItems();
if (dbItemsResult.isFailure) {

View File

@@ -635,7 +635,6 @@ async function activateWithInstalledDistribution(
);
await ensureDir(variantAnalysisStorageDir);
const variantAnalysisResultsManager = new VariantAnalysisResultsManager(
app.credentials,
cliServer,
extLogger,
);

View File

@@ -239,8 +239,8 @@ export class QueryHistoryManager extends DisposableObject {
);
this.push(
commandRunner(
"codeQLQueryHistory.setLabel",
this.handleSetLabel.bind(this),
"codeQLQueryHistory.renameItem",
this.handleRenameItem.bind(this),
),
);
this.push(
@@ -752,7 +752,7 @@ export class QueryHistoryManager extends DisposableObject {
}
}
async handleSetLabel(
async handleRenameItem(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
): Promise<void> {

View File

@@ -81,16 +81,6 @@ export async function getVariantAnalysisRepo(
return response.data;
}
export async function getVariantAnalysisRepoResult(
credentials: Credentials,
downloadUrl: string,
): Promise<ArrayBuffer> {
const octokit = await credentials.getOctokit();
const response = await octokit.request(`GET ${downloadUrl}`);
return response.data;
}
export async function getRepositoryFromNwo(
credentials: Credentials,
owner: string,

View File

@@ -376,10 +376,10 @@ export async function getControllerRepo(
);
controllerRepoNwo = await window.showInputBox({
title:
"Controller repository in which to run the GitHub Actions workflow for this variant analysis",
"Controller repository in which to run GitHub Actions workflows for variant analyses",
placeHolder: "<owner>/<repo>",
prompt:
"Enter the name of a GitHub repository in the format <owner>/<repo>",
"Enter the name of a GitHub repository in the format <owner>/<repo>. You can change this in the extension settings.",
ignoreFocusOut: true,
});
if (!controllerRepoNwo) {

View File

@@ -127,6 +127,7 @@ export enum VariantAnalysisScannedRepositoryDownloadStatus {
export interface VariantAnalysisScannedRepositoryState {
repositoryId: number;
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus;
downloadPercentage?: number;
}
export interface VariantAnalysisScannedRepositoryResult {

View File

@@ -69,6 +69,7 @@ export class VariantAnalysisManager
implements VariantAnalysisViewManager<VariantAnalysisView>
{
private static readonly REPO_STATES_FILENAME = "repo_states.json";
private static readonly DOWNLOAD_PERCENTAGE_UPDATE_DELAY_MS = 500;
private readonly _onVariantAnalysisAdded = this.push(
new EventEmitter<VariantAnalysis>(),
@@ -515,10 +516,27 @@ export class VariantAnalysisManager
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
try {
let lastRepoStateUpdate = 0;
const updateRepoStateCallback = async (downloadPercentage: number) => {
const now = new Date().getTime();
if (
lastRepoStateUpdate <
now - VariantAnalysisManager.DOWNLOAD_PERCENTAGE_UPDATE_DELAY_MS
) {
lastRepoStateUpdate = now;
await this.onRepoStateUpdated(variantAnalysis.id, {
repositoryId: scannedRepo.repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
downloadPercentage,
});
}
};
await this.variantAnalysisResultsManager.download(
variantAnalysis.id,
repoTask,
this.getVariantAnalysisStorageLocation(variantAnalysis.id),
updateRepoStateCallback,
);
} catch (e) {
repoState.downloadStatus =

View File

@@ -1,14 +1,8 @@
import {
pathExists,
mkdir,
outputJson,
writeFileSync,
readJson,
} from "fs-extra";
import { appendFile, pathExists, mkdir, outputJson, readJson } from "fs-extra";
import fetch from "node-fetch";
import { EOL } from "os";
import { join } from "path";
import { Credentials } from "../common/authentication";
import { Logger } from "../common";
import { AnalysisAlert, AnalysisRawResults } from "./shared/analysis-result";
import { sarifParser } from "../sarif-parser";
@@ -21,7 +15,6 @@ import {
VariantAnalysisScannedRepositoryResult,
} from "./shared/variant-analysis";
import { DisposableObject, DisposeHandler } from "../pure/disposable-object";
import { getVariantAnalysisRepoResult } from "./gh-api/gh-api-client";
import { EventEmitter } from "vscode";
import { unzipFile } from "../pure/zip";
@@ -63,7 +56,6 @@ export class VariantAnalysisResultsManager extends DisposableObject {
readonly onResultLoaded = this._onResultLoaded.event;
constructor(
private readonly credentials: Credentials,
private readonly cliServer: CodeQLCliServer,
private readonly logger: Logger,
) {
@@ -75,6 +67,7 @@ export class VariantAnalysisResultsManager extends DisposableObject {
variantAnalysisId: number,
repoTask: VariantAnalysisRepositoryTask,
variantAnalysisStoragePath: string,
onDownloadPercentageChanged: (downloadPercentage: number) => Promise<void>,
): Promise<void> {
if (!repoTask.artifactUrl) {
throw new Error("Missing artifact URL");
@@ -85,11 +78,6 @@ export class VariantAnalysisResultsManager extends DisposableObject {
repoTask.repository.fullName,
);
const result = await getVariantAnalysisRepoResult(
this.credentials,
repoTask.artifactUrl,
);
if (!(await pathExists(resultDirectory))) {
await mkdir(resultDirectory, { recursive: true });
}
@@ -100,12 +88,28 @@ export class VariantAnalysisResultsManager extends DisposableObject {
);
const zipFilePath = join(resultDirectory, "results.zip");
const response = await fetch(repoTask.artifactUrl);
let responseSize = parseInt(response.headers.get("content-length") || "0");
if (responseSize === 0 && response.size > 0) {
responseSize = response.size;
}
let amountDownloaded = 0;
for await (const chunk of response.body) {
await appendFile(zipFilePath, Buffer.from(chunk));
amountDownloaded += chunk.length;
await onDownloadPercentageChanged(
Math.floor((amountDownloaded / responseSize) * 100),
);
}
const unzippedFilesDirectory = join(
resultDirectory,
VariantAnalysisResultsManager.RESULTS_DIRECTORY,
);
writeFileSync(zipFilePath, Buffer.from(result));
await unzipFile(zipFilePath, unzippedFilesDirectory);
this._onResultDownloaded.fire({

View File

@@ -52,7 +52,7 @@ export class VariantAnalysisView
});
const panel = await this.getPanel();
panel.title = `${variantAnalysis.query.name} - CodeQL Query Results`;
panel.title = this.getTitle(variantAnalysis);
}
public async updateRepoState(
@@ -88,9 +88,7 @@ export class VariantAnalysisView
return {
viewId: VariantAnalysisView.viewType,
title: variantAnalysis
? `${variantAnalysis.query.name} - CodeQL Query Results`
: `Variant analysis ${this.variantAnalysisId} - CodeQL Query Results`,
title: this.getTitle(variantAnalysis),
viewColumn: ViewColumn.Active,
preserveFocus: true,
view: "variant-analysis",
@@ -189,4 +187,10 @@ export class VariantAnalysisView
repoStates,
});
}
private getTitle(variantAnalysis: VariantAnalysis | undefined): string {
return variantAnalysis
? `${variantAnalysis.query.name} - Variant Analysis Results`
: `Variant Analysis ${this.variantAnalysisId} - Results`;
}
}

View File

@@ -1,26 +0,0 @@
module.exports = {
env: {
browser: true,
},
plugins: ["github"],
extends: [
"plugin:github/react",
"plugin:github/recommended",
"plugin:github/typescript",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:storybook/recommended",
],
rules: {
"filenames/match-regex": "off",
"import/named": "off",
"import/no-namespace": "off",
"import/no-unresolved": "off",
"no-unused-vars": "off",
},
settings: {
react: {
version: "detect",
},
},
};

View File

@@ -15,7 +15,7 @@ import { createMockRepositoryWithMetadata } from "../../../test/factories/remote
import * as analysesResults from "../remote-queries/data/analysesResultsMessage.json";
import * as rawResults from "../remote-queries/data/rawResults.json";
import { RepoRow } from "../../view/variant-analysis/RepoRow";
import { RepoRow, RepoRowProps } from "../../view/variant-analysis/RepoRow";
export default {
title: "Variant Analysis/Repo Row",
@@ -29,7 +29,7 @@ export default {
],
} as ComponentMeta<typeof RepoRow>;
const Template: ComponentStory<typeof RepoRow> = (args) => (
const Template: ComponentStory<typeof RepoRow> = (args: RepoRowProps) => (
<RepoRow {...args} />
);
@@ -77,7 +77,22 @@ SucceededDownloading.args = {
...Pending.args,
status: VariantAnalysisRepoStatus.Succeeded,
resultCount: 198,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
downloadState: {
repositoryId: 63537249,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
},
};
export const SucceededDownloadingWithPercentage = Template.bind({});
SucceededDownloadingWithPercentage.args = {
...Pending.args,
status: VariantAnalysisRepoStatus.Succeeded,
resultCount: 198,
downloadState: {
repositoryId: 63537249,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
downloadPercentage: 42,
},
};
export const SucceededSuccessfulDownload = Template.bind({});
@@ -85,7 +100,10 @@ SucceededSuccessfulDownload.args = {
...Pending.args,
status: VariantAnalysisRepoStatus.Succeeded,
resultCount: 198,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
downloadState: {
repositoryId: 63537249,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
};
export const SucceededFailedDownload = Template.bind({});
@@ -93,7 +111,10 @@ SucceededFailedDownload.args = {
...Pending.args,
status: VariantAnalysisRepoStatus.Succeeded,
resultCount: 198,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
downloadState: {
repositoryId: 63537249,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
},
};
export const InterpretedResults = Template.bind({});

View File

@@ -13,6 +13,7 @@ import {
LOG_TELEMETRY,
isIntegrationTestMode,
isCanary,
newTelemetryEnabled,
} from "./config";
import * as appInsights from "applicationinsights";
import { extLogger } from "./common";
@@ -173,6 +174,10 @@ export class TelemetryListener extends ConfigListener {
return;
}
if (!newTelemetryEnabled()) {
return;
}
this.reporter.sendTelemetryEvent(
"ui-interaction",
{

View File

@@ -1,44 +0,0 @@
module.exports = {
env: {
browser: true
},
plugins: [
"github",
],
extends: [
"plugin:github/react",
"plugin:github/recommended",
"plugin:github/typescript",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-invalid-this": "off",
"@typescript-eslint/no-shadow": "off",
"camelcase": "off",
"eqeqeq": "off",
"filenames/match-regex": "off",
"i18n-text/no-en": "off",
"import/named": "off",
"import/no-dynamic-require": "off",
"import/no-dynamic-required": "off",
"import/no-namespace": "off",
"import/no-unresolved": "off",
"jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/click-events-have-key-events": "off",
"no-console": "off",
"no-invalid-this": "off",
"no-undef": "off",
"no-unused-vars": "off",
"no-shadow": "off",
"github/array-foreach": "off",
},
settings: {
react: {
version: 'detect'
}
}
}

View File

@@ -0,0 +1,60 @@
import * as React from "react";
import styled from "styled-components";
type Props = {
percent: number;
label?: string;
};
const Circle = styled.div`
width: 16px;
height: 16px;
`;
const Background = styled.circle`
stroke: var(--vscode-editorWidget-background);
fill: none;
stroke-width: 2px;
`;
const Determinate = styled.circle`
stroke: var(--vscode-progressBar-background);
fill: none;
stroke-width: 2px;
stroke-linecap: round;
transform-origin: 50% 50%;
transform: rotate(-90deg);
transition: all 0.2s ease-in-out 0s;
`;
const progressSegments = 44;
// This is a re-implementation of the FAST component progress ring
// See https://github.com/microsoft/fast/blob/21c210f2164c5cf285cade1a328460c67e4b97e6/packages/web-components/fast-foundation/src/progress-ring/progress-ring.template.ts
// Once the determinate progress ring is available in the VSCode webview UI toolkit, we should use that instead
export const DeterminateProgressRing = ({
percent,
label = "Loading...",
}: Props) => (
<Circle
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={percent}
>
<svg className="progress" viewBox="0 0 16 16">
<Background cx="8px" cy="8px" r="7px" />
<Determinate
style={{
strokeDasharray: `${
(progressSegments * percent) / 100
}px ${progressSegments}px`,
}}
cx="8px"
cy="8px"
r="7px"
></Determinate>
</svg>
</Circle>
);

View File

@@ -17,6 +17,7 @@ export const Codicon = ({ name, label, className, slot }: Props) => (
<CodiconIcon
role="img"
aria-label={label}
title={label}
className={classNames("codicon", `codicon-${name}`, className)}
slot={slot}
/>

View File

@@ -6,6 +6,7 @@ import {
isCompletedAnalysisRepoStatus,
VariantAnalysisRepoStatus,
VariantAnalysisScannedRepositoryDownloadStatus,
VariantAnalysisScannedRepositoryState,
} from "../../remote-queries/shared/variant-analysis";
import { formatDecimal } from "../../pure/number";
import {
@@ -25,6 +26,7 @@ import { AnalyzedRepoItemContent } from "./AnalyzedRepoItemContent";
import StarCount from "../common/StarCount";
import { LastUpdated } from "../common/LastUpdated";
import { useTelemetryOnChange } from "../common/telemetry";
import { DeterminateProgressRing } from "../common/DeterminateProgressRing";
// This will ensure that these icons have a className which we can use in the TitleContainer
const ExpandCollapseCodicon = styled(Codicon)``;
@@ -91,7 +93,7 @@ export type RepoRowProps = {
repository: Partial<RepositoryWithMetadata> &
Pick<RepositoryWithMetadata, "fullName">;
status?: VariantAnalysisRepoStatus;
downloadStatus?: VariantAnalysisScannedRepositoryDownloadStatus;
downloadState?: VariantAnalysisScannedRepositoryState;
resultCount?: number;
interpretedResults?: AnalysisAlert[];
@@ -163,7 +165,7 @@ const filterRepoRowExpandedTelemetry = (v: boolean) => v;
export const RepoRow = ({
repository,
status,
downloadStatus,
downloadState,
resultCount,
interpretedResults,
rawResults,
@@ -185,7 +187,7 @@ export const RepoRow = ({
if (
resultsLoaded ||
status !== VariantAnalysisRepoStatus.Succeeded ||
downloadStatus !==
downloadState?.downloadStatus !==
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded
) {
setExpanded((oldIsExpanded) => !oldIsExpanded);
@@ -203,7 +205,7 @@ export const RepoRow = ({
resultsLoaded,
repository.fullName,
status,
downloadStatus,
downloadState,
setExpanded,
]);
@@ -234,10 +236,11 @@ export const RepoRow = ({
[onSelectedChange, repository],
);
const disabled = !canExpand(status, downloadStatus) || resultsLoading;
const disabled =
!canExpand(status, downloadState?.downloadStatus) || resultsLoading;
const expandableContentLoaded = isExpandableContentLoaded(
status,
downloadStatus,
downloadState?.downloadStatus,
resultsLoaded,
);
@@ -252,7 +255,9 @@ export const RepoRow = ({
onChange={onChangeCheckbox}
onClick={onClickCheckbox}
checked={selected}
disabled={!repository.id || !canSelect(status, downloadStatus)}
disabled={
!repository.id || !canSelect(status, downloadState?.downloadStatus)
}
/>
{isExpanded && (
<ExpandCollapseCodicon name="chevron-down" label="Collapse" />
@@ -278,11 +283,13 @@ export const RepoRow = ({
)}
{!status && <WarningIcon />}
</span>
{downloadStatus ===
{downloadState?.downloadStatus ===
VariantAnalysisScannedRepositoryDownloadStatus.InProgress && (
<LoadingIcon label="Downloading" />
<DeterminateProgressRing
percent={downloadState.downloadPercentage ?? 0}
/>
)}
{downloadStatus ===
{downloadState?.downloadStatus ===
VariantAnalysisScannedRepositoryDownloadStatus.Failed && (
<WarningIcon label="Failed to download the results" />
)}
@@ -296,7 +303,7 @@ export const RepoRow = ({
{isExpanded && expandableContentLoaded && (
<AnalyzedRepoItemContent
status={status}
downloadStatus={downloadStatus}
downloadStatus={downloadState?.downloadStatus}
interpretedResults={interpretedResults}
rawResults={rawResults}
/>

View File

@@ -89,7 +89,7 @@ export const VariantAnalysisAnalyzedRepos = ({
key={repository.repository.id}
repository={repository.repository}
status={repository.analysisStatus}
downloadStatus={state?.downloadStatus}
downloadState={state}
resultCount={repository.resultCount}
interpretedResults={results?.interpretedResults}
rawResults={results?.rawResults}

View File

@@ -87,7 +87,10 @@ describe(RepoRow.name, () => {
render({
status: VariantAnalysisRepoStatus.Succeeded,
resultCount: 178,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Pending,
downloadState: {
repositoryId: 1,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Pending,
},
});
expect(
@@ -101,7 +104,11 @@ describe(RepoRow.name, () => {
render({
status: VariantAnalysisRepoStatus.Succeeded,
resultCount: 178,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
downloadState: {
repositoryId: 1,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
},
});
expect(
@@ -115,7 +122,11 @@ describe(RepoRow.name, () => {
render({
status: VariantAnalysisRepoStatus.Succeeded,
resultCount: 178,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
downloadState: {
repositoryId: 1,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
});
expect(
@@ -129,7 +140,10 @@ describe(RepoRow.name, () => {
render({
status: VariantAnalysisRepoStatus.Succeeded,
resultCount: 178,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
downloadState: {
repositoryId: 1,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
},
});
expect(
@@ -320,7 +334,11 @@ describe(RepoRow.name, () => {
it("can expand the repo item when succeeded and loaded", async () => {
render({
status: VariantAnalysisRepoStatus.Succeeded,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
downloadState: {
repositoryId: 1,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
interpretedResults: [],
});
@@ -340,7 +358,11 @@ describe(RepoRow.name, () => {
it("can expand the repo item when succeeded and not loaded", async () => {
const { rerender } = render({
status: VariantAnalysisRepoStatus.Succeeded,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
downloadState: {
repositoryId: 1,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
});
await userEvent.click(
@@ -411,7 +433,10 @@ describe(RepoRow.name, () => {
it("does not allow selecting the item if the item has not been downloaded successfully", async () => {
render({
status: VariantAnalysisRepoStatus.Succeeded,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
downloadState: {
repositoryId: 1,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
},
});
// It seems like sometimes the first render doesn't have the checkbox disabled
@@ -424,7 +449,11 @@ describe(RepoRow.name, () => {
it("allows selecting the item if the item has been downloaded", async () => {
render({
status: VariantAnalysisRepoStatus.Succeeded,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
downloadState: {
repositoryId: 1,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
});
expect(screen.getByRole("checkbox")).toBeEnabled();

View File

@@ -1,29 +0,0 @@
module.exports = {
env: {
jest: true,
},
parserOptions: {
project: "./test/tsconfig.json",
},
plugins: [
"github",
],
extends: [
"plugin:github/react",
"plugin:github/recommended",
"plugin:github/typescript",
],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-shadow": "off",
"camelcase": "off",
"filenames/match-regex": "off",
"i18n-text/no-en": "off",
"import/no-namespace": "off",
"import/no-unresolved": "off",
"no-console": "off",
"no-shadow": "off",
"no-undef": "off",
"github/array-foreach": "off",
}
};

View File

@@ -2,7 +2,6 @@ import { expect } from "@jest/globals";
import type { MatcherFunction } from "expect";
import { pathsEqual } from "../../src/pure/files";
// eslint-disable-next-line func-style -- We need to have access to this and specify the type of the function
const toEqualPath: MatcherFunction<[expectedPath: unknown]> = function (
actual,
expectedPath,
@@ -15,20 +14,16 @@ const toEqualPath: MatcherFunction<[expectedPath: unknown]> = function (
if (pass) {
return {
message: () =>
// eslint-disable-next-line @typescript-eslint/no-invalid-this
`expected ${this.utils.printReceived(
actual,
// eslint-disable-next-line @typescript-eslint/no-invalid-this
)} to equal path ${this.utils.printExpected(expectedPath)}`,
pass: true,
};
} else {
return {
message: () =>
// eslint-disable-next-line @typescript-eslint/no-invalid-this
`expected ${this.utils.printReceived(
actual,
// eslint-disable-next-line @typescript-eslint/no-invalid-this
)} to equal path ${this.utils.printExpected(expectedPath)}`,
pass: false,
};

View File

@@ -1,6 +1,6 @@
{
"extends": "../tsconfig.json",
"include": ["**/*.ts", "../src/**/*.ts"],
"include": ["**/*.ts", "../src/**/*.ts", "**/.eslintrc.js", "**/*.config.js"],
"exclude": [],
"compilerOptions": {
"noEmit": true,

View File

@@ -24,7 +24,6 @@ describe("github url identifier helper", () => {
describe("getNwoFromGitHubUrl method", () => {
it("should handle invalid urls", () => {
expect(getNwoFromGitHubUrl("")).toBe(undefined);
expect(getNwoFromGitHubUrl("http://github.com/foo/bar")).toBe(undefined);
expect(getNwoFromGitHubUrl("https://ww.github.com/foo/bar")).toBe(
undefined,
);
@@ -34,7 +33,10 @@ describe("github url identifier helper", () => {
});
it("should handle valid urls", () => {
expect(getNwoFromGitHubUrl("github.com/foo/bar")).toBe("foo/bar");
expect(getNwoFromGitHubUrl("www.github.com/foo/bar")).toBe("foo/bar");
expect(getNwoFromGitHubUrl("https://github.com/foo/bar")).toBe("foo/bar");
expect(getNwoFromGitHubUrl("http://github.com/foo/bar")).toBe("foo/bar");
expect(getNwoFromGitHubUrl("https://www.github.com/foo/bar")).toBe(
"foo/bar",
);
@@ -47,9 +49,6 @@ describe("github url identifier helper", () => {
describe("getOwnerFromGitHubUrl method", () => {
it("should handle invalid urls", () => {
expect(getOwnerFromGitHubUrl("")).toBe(undefined);
expect(getOwnerFromGitHubUrl("http://github.com/foo/bar")).toBe(
undefined,
);
expect(getOwnerFromGitHubUrl("https://ww.github.com/foo/bar")).toBe(
undefined,
);
@@ -58,6 +57,7 @@ describe("github url identifier helper", () => {
});
it("should handle valid urls", () => {
expect(getOwnerFromGitHubUrl("http://github.com/foo/bar")).toBe("foo");
expect(getOwnerFromGitHubUrl("https://github.com/foo/bar")).toBe("foo");
expect(getOwnerFromGitHubUrl("https://www.github.com/foo/bar")).toBe(
"foo",
@@ -66,6 +66,8 @@ describe("github url identifier helper", () => {
getOwnerFromGitHubUrl("https://github.com/foo/bar/sub/pages"),
).toBe("foo");
expect(getOwnerFromGitHubUrl("https://www.github.com/foo")).toBe("foo");
expect(getOwnerFromGitHubUrl("github.com/foo")).toBe("foo");
expect(getOwnerFromGitHubUrl("www.github.com/foo")).toBe("foo");
});
});
});

View File

@@ -1,10 +1,7 @@
import { faker } from "@faker-js/faker";
import {
getRepositoryFromNwo,
getVariantAnalysis,
getVariantAnalysisRepo,
getVariantAnalysisRepoResult,
submitVariantAnalysis,
} from "../../../../src/remote-queries/gh-api/gh-api-client";
import { createMockSubmission } from "../../../factories/remote-queries/shared/variant-analysis-submission";
@@ -69,23 +66,6 @@ describe("getVariantAnalysisRepo", () => {
});
});
describe("getVariantAnalysisRepoResult", () => {
it("returns the variant analysis repo result", async () => {
await mockServer.loadScenario("problem-query-success");
const result = await getVariantAnalysisRepoResult(
testCredentialsWithRealOctokit(),
`https://objects-origin.githubusercontent.com/codeql-query-console/codeql-variant-analysis-repo-tasks/${variantAnalysisId}/${repoTaskId}/${faker.datatype.uuid()}`,
);
expect(result).toBeDefined();
expect(result).toBeInstanceOf(ArrayBuffer);
expect(result.byteLength).toBe(
variantAnalysisRepoJson_response.body.artifact_size_in_bytes,
);
});
});
describe("getRepositoryFromNwo", () => {
it("returns the repository", async () => {
await mockServer.loadScenario("problem-query-success");

View File

@@ -1,45 +0,0 @@
module.exports = {
parserOptions: {
project: ["../../tsconfig.json"],
},
env: {
jest: true,
},
plugins: [
"github",
],
extends: [
"plugin:github/react",
"plugin:github/recommended",
"plugin:github/typescript",
],
rules: {
"@typescript-eslint/ban-types": [
"error",
{
// For a full list of the default banned types, see:
// https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-types.md
extendDefaults: true,
types: {
// Don't complain about the `Function` type in test files. (Default is `true`.)
Function: false,
},
},
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-shadow": "off",
"@typescript-eslint/no-invalid-this": "off",
"eqeqeq": "off",
"filenames/match-regex": "off",
"filenames/match-regexp": "off",
"i18n-text/no-en": "off",
"import/no-anonymous-default-export": "off",
"import/no-dynamic-require": "off",
"import/no-mutable-exports": "off",
"import/no-namespace": "off",
"import/no-unresolved": "off",
"no-console": "off",
"github/array-foreach": "off",
"github/no-then": "off"
}
}

View File

@@ -21,6 +21,9 @@ import * as ghApiClient from "../../../../src/remote-queries/gh-api/gh-api-clien
import * as ghActionsApiClient from "../../../../src/remote-queries/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/remote-queries/variant-analysis-manager";
import { CodeQLCliServer } from "../../../../src/cli";
@@ -95,7 +98,6 @@ describe("Variant Analysis Manager", () => {
cli = extension.cliServer;
app = createMockApp({});
variantAnalysisResultsManager = new VariantAnalysisResultsManager(
app.credentials,
cli,
extLogger,
);
@@ -334,32 +336,21 @@ describe("Variant Analysis Manager", () => {
});
describe("autoDownloadVariantAnalysisResult", () => {
let arrayBuffer: ArrayBuffer;
let getVariantAnalysisRepoStub: jest.SpiedFunction<
typeof ghApiClient.getVariantAnalysisRepo
>;
let getVariantAnalysisRepoResultStub: jest.SpiedFunction<
typeof ghApiClient.getVariantAnalysisRepoResult
typeof fetchModule.default
>;
let repoStatesPath: string;
beforeEach(async () => {
const sourceFilePath = join(
__dirname,
"../data/variant-analysis-results.zip",
);
arrayBuffer = fs.readFileSync(sourceFilePath).buffer;
getVariantAnalysisRepoStub = jest.spyOn(
ghApiClient,
"getVariantAnalysisRepo",
);
getVariantAnalysisRepoResultStub = jest.spyOn(
ghApiClient,
"getVariantAnalysisRepoResult",
);
getVariantAnalysisRepoResultStub = jest.spyOn(fetchModule, "default");
repoStatesPath = join(
storagePath,
@@ -374,7 +365,6 @@ describe("Variant Analysis Manager", () => {
delete dummyRepoTask.artifact_url;
getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask);
getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer);
});
it("should not try to download the result", async () => {
@@ -395,7 +385,15 @@ describe("Variant Analysis Manager", () => {
dummyRepoTask = createMockVariantAnalysisRepoTask();
getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask);
getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer);
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 () => {

View File

@@ -3,19 +3,19 @@ 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 { VariantAnalysisResultsManager } from "../../../../src/remote-queries/variant-analysis-results-manager";
import { CodeQLCliServer } from "../../../../src/cli";
import { storagePath } from "../global.helper";
import { faker } from "@faker-js/faker";
import * as ghApiClient from "../../../../src/remote-queries/gh-api/gh-api-client";
import { createMockVariantAnalysisRepositoryTask } from "../../../factories/remote-queries/shared/variant-analysis-repo-tasks";
import {
VariantAnalysisRepositoryTask,
VariantAnalysisScannedRepositoryResult,
} from "../../../../src/remote-queries/shared/variant-analysis";
import { testCredentialsWithStub } from "../../../factories/authentication";
import { Credentials } from "../../../../src/common/authentication";
jest.setTimeout(10_000);
@@ -44,7 +44,6 @@ describe(VariantAnalysisResultsManager.name, () => {
jest.spyOn(extLogger, "log").mockResolvedValue(undefined);
variantAnalysisResultsManager = new VariantAnalysisResultsManager(
testCredentialsWithStub(),
cli,
extLogger,
);
@@ -89,35 +88,33 @@ describe(VariantAnalysisResultsManager.name, () => {
variantAnalysisId,
dummyRepoTask,
variantAnalysisStoragePath,
() => Promise.resolve(),
),
).rejects.toThrow("Missing artifact URL");
});
});
describe("when the artifact_url is present", () => {
let arrayBuffer: ArrayBuffer;
let getVariantAnalysisRepoResultStub: jest.SpiedFunction<
typeof ghApiClient.getVariantAnalysisRepoResult
typeof fetchModule.default
>;
let fileContents: Buffer;
beforeEach(async () => {
const sourceFilePath = join(
__dirname,
"../data/variant-analysis-results.zip",
);
arrayBuffer = fs.readFileSync(sourceFilePath).buffer;
fileContents = fs.readFileSync(sourceFilePath);
getVariantAnalysisRepoResultStub = jest
.spyOn(ghApiClient, "getVariantAnalysisRepoResult")
.mockImplementation(
(_credentials: Credentials, downloadUrl: string) => {
if (downloadUrl === dummyRepoTask.artifactUrl) {
return Promise.resolve(arrayBuffer);
}
return Promise.reject(new Error("Unexpected artifact URL"));
},
);
.spyOn(fetchModule, "default")
.mockImplementation((url: RequestInfo, _init?: RequestInit) => {
if (url === dummyRepoTask.artifactUrl) {
return Promise.resolve(new Response(Readable.from(fileContents)));
}
return Promise.reject(new Error("Unexpected artifact URL"));
});
});
it("should call the API to download the results", async () => {
@@ -125,6 +122,7 @@ describe(VariantAnalysisResultsManager.name, () => {
variantAnalysisId,
dummyRepoTask,
variantAnalysisStoragePath,
() => Promise.resolve(),
);
expect(getVariantAnalysisRepoResultStub).toHaveBeenCalledTimes(1);
@@ -135,6 +133,7 @@ describe(VariantAnalysisResultsManager.name, () => {
variantAnalysisId,
dummyRepoTask,
variantAnalysisStoragePath,
() => Promise.resolve(),
);
expect(fs.existsSync(`${repoTaskStorageDirectory}/results.zip`)).toBe(
@@ -147,6 +146,7 @@ describe(VariantAnalysisResultsManager.name, () => {
variantAnalysisId,
dummyRepoTask,
variantAnalysisStoragePath,
() => Promise.resolve(),
);
expect(
@@ -154,12 +154,57 @@ describe(VariantAnalysisResultsManager.name, () => {
).toBe(true);
});
it("should report download progress", async () => {
// This generates a "fake" stream which "downloads" the file in 5 chunks,
// rather than in 1 chunk. This is used for testing that we actually get
// multiple progress reports.
async function* generateInParts() {
const partLength = fileContents.length / 5;
for (let i = 0; i < 5; i++) {
yield fileContents.slice(i * partLength, (i + 1) * partLength);
}
}
getVariantAnalysisRepoResultStub.mockImplementation(
(url: RequestInfo, _init?: RequestInit) => {
if (url === dummyRepoTask.artifactUrl) {
const response = new Response(Readable.from(generateInParts()));
response.headers.set(
"Content-Length",
fileContents.length.toString(),
);
return Promise.resolve(response);
}
return Promise.reject(new Error("Unexpected artifact URL"));
},
);
const downloadPercentageChanged = jest
.fn()
.mockResolvedValue(undefined);
await variantAnalysisResultsManager.download(
variantAnalysisId,
dummyRepoTask,
variantAnalysisStoragePath,
downloadPercentageChanged,
);
expect(downloadPercentageChanged).toHaveBeenCalledTimes(5);
expect(downloadPercentageChanged).toHaveBeenCalledWith(20);
expect(downloadPercentageChanged).toHaveBeenCalledWith(40);
expect(downloadPercentageChanged).toHaveBeenCalledWith(60);
expect(downloadPercentageChanged).toHaveBeenCalledWith(80);
expect(downloadPercentageChanged).toHaveBeenCalledWith(100);
});
describe("isVariantAnalysisRepoDownloaded", () => {
it("should return true once results are downloaded", async () => {
await variantAnalysisResultsManager.download(
variantAnalysisId,
dummyRepoTask,
variantAnalysisStoragePath,
() => Promise.resolve(),
);
expect(
@@ -185,7 +230,6 @@ describe(VariantAnalysisResultsManager.name, () => {
beforeEach(() => {
variantAnalysisResultsManager = new VariantAnalysisResultsManager(
testCredentialsWithStub(),
cli,
extLogger,
);

View File

@@ -12,6 +12,7 @@ import {
import { CodeQLExtensionInterface } from "../../../../src/extension";
import { MockGitHubApiServer } from "../../../../src/mocks/mock-gh-api-server";
import { mockConfiguration } from "../../utils/configuration-helpers";
jest.setTimeout(30_000);
@@ -35,50 +36,17 @@ describe("Variant Analysis Submission Integration", () => {
let showErrorMessageSpy: jest.SpiedFunction<typeof window.showErrorMessage>;
beforeEach(async () => {
const originalGetConfiguration = workspace.getConfiguration;
jest
.spyOn(workspace, "getConfiguration")
.mockImplementation((section, scope) => {
const configuration = originalGetConfiguration(section, scope);
return {
get(key: string, defaultValue?: unknown) {
if (section === "codeQL.variantAnalysis" && key === "liveResults") {
return true;
}
if (section === "codeQL" && key == "canary") {
return true;
}
if (
section === "codeQL.variantAnalysis" &&
key === "controllerRepo"
) {
return "github/vscode-codeql";
}
return configuration.get(key, defaultValue);
},
has(key: string) {
return configuration.has(key);
},
inspect(key: string) {
return configuration.inspect(key);
},
update(
key: string,
value: unknown,
configurationTarget?: boolean,
overrideInLanguage?: boolean,
) {
return configuration.update(
key,
value,
configurationTarget,
overrideInLanguage,
);
},
};
});
mockConfiguration({
values: {
codeQL: {
canary: true,
},
"codeQL.variantAnalysis": {
liveResults: true,
controllerRepo: "github/vscode-codeql",
},
},
});
jest.spyOn(authentication, "getSession").mockResolvedValue({
id: "test",

View File

@@ -13,6 +13,7 @@ import { DbTreeViewItem } from "../../../../src/databases/ui/db-tree-view-item";
import { ExtensionApp } from "../../../../src/common/vscode/vscode-app";
import { createMockExtensionContext } from "../../../factories/extension-context";
import { createDbConfig } from "../../../factories/db-config-factories";
import { mockConfiguration } from "../../utils/configuration-helpers";
describe("db panel rendering nodes", () => {
const workspaceStoragePath = join(__dirname, "test-workspace-storage");
@@ -48,238 +49,281 @@ describe("db panel rendering nodes", () => {
await remove(workspaceStoragePath);
});
it("should render default remote nodes when the config is empty", async () => {
const dbConfig: DbConfig = createDbConfig();
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
const items = dbTreeItems!;
expect(items.length).toBe(3);
checkRemoteSystemDefinedListItem(items[0], 10);
checkRemoteSystemDefinedListItem(items[1], 100);
checkRemoteSystemDefinedListItem(items[2], 1000);
});
it("should render remote repository list nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
describe("when controller repo is not set", () => {
mockConfiguration({
values: {
"codeQL.variantAnalysis": {
controllerRepo: undefined,
},
{
name: "my-list-2",
repositories: ["owner1/repo1", "owner2/repo1", "owner2/repo2"],
},
],
},
});
await saveDbConfig(dbConfig);
it("should not have any items", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-2",
repositories: ["owner2/repo1", "owner2/repo2"],
},
],
});
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
await saveDbConfig(dbConfig);
const systemDefinedListItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteSystemDefinedList,
);
expect(systemDefinedListItems.length).toBe(3);
const dbTreeItems = await dbTreeDataProvider.getChildren();
const userDefinedListItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteUserDefinedList,
);
expect(userDefinedListItems.length).toBe(2);
checkUserDefinedListItem(userDefinedListItems[0], "my-list-1", [
"owner1/repo1",
"owner1/repo2",
]);
checkUserDefinedListItem(userDefinedListItems[1], "my-list-2", [
"owner1/repo1",
"owner2/repo1",
"owner2/repo2",
]);
expect(dbTreeItems).toHaveLength(0);
});
});
it("should render owner list nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteOwners: ["owner1", "owner2"],
describe("when controller repo is set", () => {
beforeEach(() => {
mockConfiguration({
values: {
"codeQL.variantAnalysis": {
controllerRepo: "github/codeql",
},
},
});
});
await saveDbConfig(dbConfig);
it("should render default remote nodes when the config is empty", async () => {
const dbConfig: DbConfig = createDbConfig();
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
expect(dbTreeItems?.length).toBe(5);
await saveDbConfig(dbConfig);
const ownerListItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteOwner,
);
expect(ownerListItems.length).toBe(2);
checkOwnerItem(ownerListItems[0], "owner1");
checkOwnerItem(ownerListItems[1], "owner2");
});
const dbTreeItems = await dbTreeDataProvider.getChildren();
it("should render repository nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteRepos: ["owner1/repo1", "owner1/repo2"],
expect(dbTreeItems).toBeTruthy();
const items = dbTreeItems!;
expect(items.length).toBe(3);
checkRemoteSystemDefinedListItem(items[0], 10);
checkRemoteSystemDefinedListItem(items[1], 100);
checkRemoteSystemDefinedListItem(items[2], 1000);
});
await saveDbConfig(dbConfig);
it("should render remote repository list nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-2",
repositories: ["owner1/repo1", "owner2/repo1", "owner2/repo2"],
},
],
});
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
expect(dbTreeItems!.length).toBe(5);
await saveDbConfig(dbConfig);
const repoItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteRepo,
);
expect(repoItems.length).toBe(2);
checkRemoteRepoItem(repoItems[0], "owner1/repo1");
checkRemoteRepoItem(repoItems[1], "owner1/repo2");
});
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
it.skip("should render local list nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
localLists: [
const systemDefinedListItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteSystemDefinedList,
);
expect(systemDefinedListItems.length).toBe(3);
const userDefinedListItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteUserDefinedList,
);
expect(userDefinedListItems.length).toBe(2);
checkUserDefinedListItem(userDefinedListItems[0], "my-list-1", [
"owner1/repo1",
"owner1/repo2",
]);
checkUserDefinedListItem(userDefinedListItems[1], "my-list-2", [
"owner1/repo1",
"owner2/repo1",
"owner2/repo2",
]);
});
it("should render owner list nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteOwners: ["owner1", "owner2"],
});
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
expect(dbTreeItems?.length).toBe(5);
const ownerListItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteOwner,
);
expect(ownerListItems.length).toBe(2);
checkOwnerItem(ownerListItems[0], "owner1");
checkOwnerItem(ownerListItems[1], "owner2");
});
it("should render repository nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteRepos: ["owner1/repo1", "owner1/repo2"],
});
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
expect(dbTreeItems!.length).toBe(5);
const repoItems = dbTreeItems!.filter(
(item) => item.dbItem?.kind === DbItemKind.RemoteRepo,
);
expect(repoItems.length).toBe(2);
checkRemoteRepoItem(repoItems[0], "owner1/repo1");
checkRemoteRepoItem(repoItems[1], "owner1/repo2");
});
it.skip("should render local list nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
localLists: [
{
name: "my-list-1",
databases: [
{
name: "db1",
dateAdded: 1668428293677,
language: "cpp",
storagePath: "/path/to/db1/",
},
{
name: "db2",
dateAdded: 1668428472731,
language: "cpp",
storagePath: "/path/to/db2/",
},
],
},
{
name: "my-list-2",
databases: [
{
name: "db3",
dateAdded: 1668428472731,
language: "ruby",
storagePath: "/path/to/db3/",
},
],
},
],
});
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
const localRootNode = dbTreeItems?.find(
(i) => i.dbItem?.kind === DbItemKind.RootLocal,
);
expect(localRootNode).toBeTruthy();
expect(localRootNode!.dbItem).toBeTruthy();
expect(localRootNode!.collapsibleState).toBe(
TreeItemCollapsibleState.Collapsed,
);
expect(localRootNode!.children).toBeTruthy();
expect(localRootNode!.children.length).toBe(2);
const localListItems = localRootNode!.children.filter(
(item) => item.dbItem?.kind === DbItemKind.LocalList,
);
expect(localListItems.length).toBe(2);
checkLocalListItem(localListItems[0], "my-list-1", [
{
name: "my-list-1",
databases: [
{
name: "db1",
dateAdded: 1668428293677,
language: "cpp",
storagePath: "/path/to/db1/",
},
{
name: "db2",
dateAdded: 1668428472731,
language: "cpp",
storagePath: "/path/to/db2/",
},
],
kind: DbItemKind.LocalDatabase,
databaseName: "db1",
dateAdded: 1668428293677,
language: "cpp",
storagePath: "/path/to/db1/",
selected: false,
},
{
name: "my-list-2",
databases: [
{
name: "db3",
dateAdded: 1668428472731,
language: "ruby",
storagePath: "/path/to/db3/",
},
],
kind: DbItemKind.LocalDatabase,
databaseName: "db2",
dateAdded: 1668428472731,
language: "cpp",
storagePath: "/path/to/db2/",
selected: false,
},
],
]);
checkLocalListItem(localListItems[1], "my-list-2", [
{
kind: DbItemKind.LocalDatabase,
databaseName: "db3",
dateAdded: 1668428472731,
language: "ruby",
storagePath: "/path/to/db3/",
selected: false,
},
]);
});
await saveDbConfig(dbConfig);
it.skip("should render local database nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
localDbs: [
{
name: "db1",
dateAdded: 1668428293677,
language: "csharp",
storagePath: "/path/to/db1/",
},
{
name: "db2",
dateAdded: 1668428472731,
language: "go",
storagePath: "/path/to/db2/",
},
],
});
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
await saveDbConfig(dbConfig);
const localRootNode = dbTreeItems?.find(
(i) => i.dbItem?.kind === DbItemKind.RootLocal,
);
expect(localRootNode).toBeTruthy();
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(localRootNode!.dbItem).toBeTruthy();
expect(localRootNode!.collapsibleState).toBe(
TreeItemCollapsibleState.Collapsed,
);
expect(localRootNode!.children).toBeTruthy();
expect(localRootNode!.children.length).toBe(2);
expect(dbTreeItems).toBeTruthy();
const localRootNode = dbTreeItems?.find(
(i) => i.dbItem?.kind === DbItemKind.RootLocal,
);
expect(localRootNode).toBeTruthy();
const localListItems = localRootNode!.children.filter(
(item) => item.dbItem?.kind === DbItemKind.LocalList,
);
expect(localListItems.length).toBe(2);
checkLocalListItem(localListItems[0], "my-list-1", [
{
expect(localRootNode!.dbItem).toBeTruthy();
expect(localRootNode!.collapsibleState).toBe(
TreeItemCollapsibleState.Collapsed,
);
expect(localRootNode!.children).toBeTruthy();
expect(localRootNode!.children.length).toBe(2);
const localDatabaseItems = localRootNode!.children.filter(
(item) => item.dbItem?.kind === DbItemKind.LocalDatabase,
);
expect(localDatabaseItems.length).toBe(2);
checkLocalDatabaseItem(localDatabaseItems[0], {
kind: DbItemKind.LocalDatabase,
databaseName: "db1",
dateAdded: 1668428293677,
language: "cpp",
language: "csharp",
storagePath: "/path/to/db1/",
selected: false,
},
{
});
checkLocalDatabaseItem(localDatabaseItems[1], {
kind: DbItemKind.LocalDatabase,
databaseName: "db2",
dateAdded: 1668428472731,
language: "cpp",
language: "go",
storagePath: "/path/to/db2/",
selected: false,
},
]);
checkLocalListItem(localListItems[1], "my-list-2", [
{
kind: DbItemKind.LocalDatabase,
databaseName: "db3",
dateAdded: 1668428472731,
language: "ruby",
storagePath: "/path/to/db3/",
selected: false,
},
]);
});
it.skip("should render local database nodes", async () => {
const dbConfig: DbConfig = createDbConfig({
localDbs: [
{
name: "db1",
dateAdded: 1668428293677,
language: "csharp",
storagePath: "/path/to/db1/",
},
{
name: "db2",
dateAdded: 1668428472731,
language: "go",
storagePath: "/path/to/db2/",
},
],
});
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
const localRootNode = dbTreeItems?.find(
(i) => i.dbItem?.kind === DbItemKind.RootLocal,
);
expect(localRootNode).toBeTruthy();
expect(localRootNode!.dbItem).toBeTruthy();
expect(localRootNode!.collapsibleState).toBe(
TreeItemCollapsibleState.Collapsed,
);
expect(localRootNode!.children).toBeTruthy();
expect(localRootNode!.children.length).toBe(2);
const localDatabaseItems = localRootNode!.children.filter(
(item) => item.dbItem?.kind === DbItemKind.LocalDatabase,
);
expect(localDatabaseItems.length).toBe(2);
checkLocalDatabaseItem(localDatabaseItems[0], {
kind: DbItemKind.LocalDatabase,
databaseName: "db1",
dateAdded: 1668428293677,
language: "csharp",
storagePath: "/path/to/db1/",
selected: false,
});
checkLocalDatabaseItem(localDatabaseItems[1], {
kind: DbItemKind.LocalDatabase,
databaseName: "db2",
dateAdded: 1668428472731,
language: "go",
storagePath: "/path/to/db2/",
selected: false,
});
});
});

View File

@@ -9,6 +9,7 @@ import { DbTreeViewItem } from "../../../../src/databases/ui/db-tree-view-item";
import { ExtensionApp } from "../../../../src/common/vscode/vscode-app";
import { createMockExtensionContext } from "../../../factories/extension-context";
import { createDbConfig } from "../../../factories/db-config-factories";
import { mockConfiguration } from "../../utils/configuration-helpers";
describe("db panel", () => {
const workspaceStoragePath = join(__dirname, "test-workspace-storage");
@@ -38,6 +39,14 @@ describe("db panel", () => {
beforeEach(async () => {
await ensureDir(workspaceStoragePath);
mockConfiguration({
values: {
"codeQL.variantAnalysis": {
controllerRepo: "github/codeql",
},
},
});
});
afterEach(async () => {

View File

@@ -15,6 +15,7 @@ import {
import { ExtensionApp } from "../../../../src/common/vscode/vscode-app";
import { createMockExtensionContext } from "../../../factories/extension-context";
import { createDbConfig } from "../../../factories/db-config-factories";
import { mockConfiguration } from "../../utils/configuration-helpers";
describe("db panel selection", () => {
const workspaceStoragePath = join(__dirname, "test-workspace-storage");
@@ -44,6 +45,14 @@ describe("db panel selection", () => {
beforeEach(async () => {
await ensureDir(workspaceStoragePath);
mockConfiguration({
values: {
"codeQL.variantAnalysis": {
controllerRepo: "github/codeql",
},
},
});
});
afterEach(async () => {

View File

@@ -1,9 +1,7 @@
import { readdirSync, mkdirSync, writeFileSync } from "fs-extra";
import { join } from "path";
import * as vscode from "vscode";
import { extLogger } from "../../../../src/common";
import { registerQueryHistoryScrubber } from "../../../../src/query-history/query-history-scrubber";
import { QueryHistoryManager } from "../../../../src/query-history/query-history-manager";
import {
QueryHistoryConfig,
@@ -11,13 +9,6 @@ import {
} from "../../../../src/config";
import { LocalQueryInfo } from "../../../../src/query-results";
import { DatabaseManager } from "../../../../src/databases";
import { dirSync } from "tmp-promise";
import {
ONE_DAY_IN_MS,
ONE_HOUR_IN_MS,
THREE_HOURS_IN_MS,
TWO_HOURS_IN_MS,
} from "../../../../src/pure/time";
import { tmpDir } from "../../../../src/helpers";
import { HistoryItemLabelProvider } from "../../../../src/query-history/history-item-label-provider";
import { RemoteQueriesManager } from "../../../../src/remote-queries/remote-queries-manager";
@@ -1468,189 +1459,6 @@ describe("QueryHistoryManager", () => {
});
});
describe("query history scrubber", () => {
const now = Date.now();
let deregister: vscode.Disposable | undefined;
let mockCtx: vscode.ExtensionContext;
let runCount = 0;
// We don't want our times to align exactly with the hour,
// so we can better mimic real life
const LESS_THAN_ONE_DAY = ONE_DAY_IN_MS - 1000;
const tmpDir = dirSync({
unsafeCleanup: true,
});
beforeEach(() => {
jest.useFakeTimers({
doNotFake: ["setTimeout"],
now,
});
mockCtx = {
globalState: {
lastScrubTime: now,
get(key: string) {
if (key !== "lastScrubTime") {
throw new Error(`Unexpected key: ${key}`);
}
return this.lastScrubTime;
},
async update(key: string, value: any) {
if (key !== "lastScrubTime") {
throw new Error(`Unexpected key: ${key}`);
}
this.lastScrubTime = value;
},
},
} as any as vscode.ExtensionContext;
});
afterEach(() => {
if (deregister) {
deregister.dispose();
deregister = undefined;
}
});
it("should not throw an error when the query directory does not exist", async () => {
registerScrubber("idontexist");
jest.advanceTimersByTime(ONE_HOUR_IN_MS);
await wait();
// "Should not have called the scrubber"
expect(runCount).toBe(0);
jest.advanceTimersByTime(ONE_HOUR_IN_MS - 1);
await wait();
// "Should not have called the scrubber"
expect(runCount).toBe(0);
jest.advanceTimersByTime(1);
await wait();
// "Should have called the scrubber once"
expect(runCount).toBe(1);
jest.advanceTimersByTime(TWO_HOURS_IN_MS);
await wait();
// "Should have called the scrubber a second time"
expect(runCount).toBe(2);
expect((mockCtx.globalState as any).lastScrubTime).toBe(
now + TWO_HOURS_IN_MS * 2,
);
});
it("should scrub directories", async () => {
// create two query directories that are right around the cut off time
const queryDir = createMockQueryDir(
ONE_HOUR_IN_MS,
TWO_HOURS_IN_MS,
THREE_HOURS_IN_MS,
);
registerScrubber(queryDir);
jest.advanceTimersByTime(TWO_HOURS_IN_MS);
await wait();
// should have deleted only the invalid locations
expectDirectories(
queryDir,
toQueryDirName(ONE_HOUR_IN_MS),
toQueryDirName(TWO_HOURS_IN_MS),
toQueryDirName(THREE_HOURS_IN_MS),
);
jest.advanceTimersByTime(LESS_THAN_ONE_DAY);
await wait();
// nothing should have happened...yet
expectDirectories(
queryDir,
toQueryDirName(ONE_HOUR_IN_MS),
toQueryDirName(TWO_HOURS_IN_MS),
toQueryDirName(THREE_HOURS_IN_MS),
);
jest.advanceTimersByTime(1000);
await wait();
// should have deleted the two older directories
// even though they have different time stamps,
// they both expire during the same scrubbing period
expectDirectories(queryDir, toQueryDirName(THREE_HOURS_IN_MS));
// Wait until the next scrub time and the final directory is deleted
jest.advanceTimersByTime(TWO_HOURS_IN_MS);
await wait();
// should have deleted everything
expectDirectories(queryDir);
});
function expectDirectories(queryDir: string, ...dirNames: string[]) {
const files = readdirSync(queryDir);
expect(files.sort()).toEqual(dirNames.sort());
}
function createMockQueryDir(...timestamps: number[]) {
const dir = tmpDir.name;
const queryDir = join(dir, "query");
// create qyuery directory and fill it with some query directories
mkdirSync(queryDir);
// create an invalid file
const invalidFile = join(queryDir, "invalid.txt");
writeFileSync(invalidFile, "invalid");
// create a directory without a timestamp file
const noTimestampDir = join(queryDir, "noTimestampDir");
mkdirSync(noTimestampDir);
writeFileSync(join(noTimestampDir, "invalid.txt"), "invalid");
// create a directory with a timestamp file, but is invalid
const invalidTimestampDir = join(queryDir, "invalidTimestampDir");
mkdirSync(invalidTimestampDir);
writeFileSync(join(invalidTimestampDir, "timestamp"), "invalid");
// create a directories with a valid timestamp files from the args
timestamps.forEach((timestamp) => {
const dir = join(queryDir, toQueryDirName(timestamp));
mkdirSync(dir);
writeFileSync(join(dir, "timestamp"), `${now + timestamp}`);
});
return queryDir;
}
function toQueryDirName(timestamp: number) {
return `query-${timestamp}`;
}
function registerScrubber(dir: string) {
deregister = registerQueryHistoryScrubber(
ONE_HOUR_IN_MS,
TWO_HOURS_IN_MS,
LESS_THAN_ONE_DAY,
dir,
{
removeDeletedQueries: () => {
return Promise.resolve();
},
} as QueryHistoryManager,
mockCtx,
{
increment: () => runCount++,
},
);
}
async function wait(ms = 500) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
});
async function createMockQueryHistory(
allHistory: QueryHistoryInfo[],
credentials?: Credentials,

View File

@@ -0,0 +1,199 @@
import { readdirSync, mkdirSync, writeFileSync } from "fs-extra";
import { join } from "path";
import * as vscode from "vscode";
import { extLogger } from "../../../../src/common";
import { registerQueryHistoryScrubber } from "../../../../src/query-history/query-history-scrubber";
import { QueryHistoryManager } from "../../../../src/query-history/query-history-manager";
import { dirSync } from "tmp-promise";
import {
ONE_DAY_IN_MS,
ONE_HOUR_IN_MS,
THREE_HOURS_IN_MS,
TWO_HOURS_IN_MS,
} from "../../../../src/pure/time";
describe("query history scrubber", () => {
const now = Date.now();
let deregister: vscode.Disposable | undefined;
let mockCtx: vscode.ExtensionContext;
let runCount = 0;
// We don't want our times to align exactly with the hour,
// so we can better mimic real life
const LESS_THAN_ONE_DAY = ONE_DAY_IN_MS - 1000;
const tmpDir = dirSync({
unsafeCleanup: true,
});
beforeEach(() => {
jest.spyOn(extLogger, "log").mockResolvedValue(undefined);
jest.useFakeTimers({
doNotFake: ["setTimeout"],
now,
});
mockCtx = {
globalState: {
lastScrubTime: now,
get(key: string) {
if (key !== "lastScrubTime") {
throw new Error(`Unexpected key: ${key}`);
}
return this.lastScrubTime;
},
async update(key: string, value: any) {
if (key !== "lastScrubTime") {
throw new Error(`Unexpected key: ${key}`);
}
this.lastScrubTime = value;
},
},
} as any as vscode.ExtensionContext;
});
afterEach(() => {
if (deregister) {
deregister.dispose();
deregister = undefined;
}
});
it("should not throw an error when the query directory does not exist", async () => {
registerScrubber("idontexist");
jest.advanceTimersByTime(ONE_HOUR_IN_MS);
await wait();
// "Should not have called the scrubber"
expect(runCount).toBe(0);
jest.advanceTimersByTime(ONE_HOUR_IN_MS - 1);
await wait();
// "Should not have called the scrubber"
expect(runCount).toBe(0);
jest.advanceTimersByTime(1);
await wait();
// "Should have called the scrubber once"
expect(runCount).toBe(1);
jest.advanceTimersByTime(TWO_HOURS_IN_MS);
await wait();
// "Should have called the scrubber a second time"
expect(runCount).toBe(2);
expect((mockCtx.globalState as any).lastScrubTime).toBe(
now + TWO_HOURS_IN_MS * 2,
);
});
it("should scrub directories", async () => {
// create two query directories that are right around the cut off time
const queryDir = createMockQueryDir(
ONE_HOUR_IN_MS,
TWO_HOURS_IN_MS,
THREE_HOURS_IN_MS,
);
registerScrubber(queryDir);
jest.advanceTimersByTime(TWO_HOURS_IN_MS);
await wait();
// should have deleted only the invalid locations
expectDirectories(
queryDir,
toQueryDirName(ONE_HOUR_IN_MS),
toQueryDirName(TWO_HOURS_IN_MS),
toQueryDirName(THREE_HOURS_IN_MS),
);
jest.advanceTimersByTime(LESS_THAN_ONE_DAY);
await wait();
// nothing should have happened...yet
expectDirectories(
queryDir,
toQueryDirName(ONE_HOUR_IN_MS),
toQueryDirName(TWO_HOURS_IN_MS),
toQueryDirName(THREE_HOURS_IN_MS),
);
jest.advanceTimersByTime(1000);
await wait();
// should have deleted the two older directories
// even though they have different time stamps,
// they both expire during the same scrubbing period
expectDirectories(queryDir, toQueryDirName(THREE_HOURS_IN_MS));
// Wait until the next scrub time and the final directory is deleted
jest.advanceTimersByTime(TWO_HOURS_IN_MS);
await wait();
// should have deleted everything
expectDirectories(queryDir);
});
function expectDirectories(queryDir: string, ...dirNames: string[]) {
const files = readdirSync(queryDir);
expect(files.sort()).toEqual(dirNames.sort());
}
function createMockQueryDir(...timestamps: number[]) {
const dir = tmpDir.name;
const queryDir = join(dir, "query");
// create qyuery directory and fill it with some query directories
mkdirSync(queryDir);
// create an invalid file
const invalidFile = join(queryDir, "invalid.txt");
writeFileSync(invalidFile, "invalid");
// create a directory without a timestamp file
const noTimestampDir = join(queryDir, "noTimestampDir");
mkdirSync(noTimestampDir);
writeFileSync(join(noTimestampDir, "invalid.txt"), "invalid");
// create a directory with a timestamp file, but is invalid
const invalidTimestampDir = join(queryDir, "invalidTimestampDir");
mkdirSync(invalidTimestampDir);
writeFileSync(join(invalidTimestampDir, "timestamp"), "invalid");
// create a directories with a valid timestamp files from the args
timestamps.forEach((timestamp) => {
const dir = join(queryDir, toQueryDirName(timestamp));
mkdirSync(dir);
writeFileSync(join(dir, "timestamp"), `${now + timestamp}`);
});
return queryDir;
}
function toQueryDirName(timestamp: number) {
return `query-${timestamp}`;
}
function registerScrubber(dir: string) {
deregister = registerQueryHistoryScrubber(
ONE_HOUR_IN_MS,
TWO_HOURS_IN_MS,
LESS_THAN_ONE_DAY,
dir,
{
removeDeletedQueries: () => {
return Promise.resolve();
},
} as QueryHistoryManager,
mockCtx,
{
increment: () => runCount++,
},
);
}
async function wait(ms = 500) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
});

View File

@@ -11,6 +11,7 @@ import {
} from "../../../src/telemetry";
import { UserCancellationException } from "../../../src/commandRunner";
import { ENABLE_TELEMETRY } from "../../../src/config";
import * as Config from "../../../src/config";
import { createMockExtensionContext } from "./index";
// setting preferences can trigger lots of background activity
@@ -388,6 +389,37 @@ describe("telemetry reporting", () => {
expect(showInformationMessageSpy).toBeCalledTimes(1);
});
describe("when new telementry is not enabled", () => {
it("should not send a telementry event", async () => {
await telemetryListener.initialize();
telemetryListener.sendUIInteraction("test");
expect(sendTelemetryEventSpy).not.toBeCalled();
});
});
describe("when new telementry is enabled", () => {
beforeEach(async () => {
jest.spyOn(Config, "newTelemetryEnabled").mockReturnValue(true);
});
it("should not send a telementry event", async () => {
await telemetryListener.initialize();
telemetryListener.sendUIInteraction("test");
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
"ui-interaction",
{
name: "test",
isCanary,
},
{},
);
});
});
async function enableTelemetry(section: string, value: boolean | undefined) {
await workspace
.getConfiguration(section)

View File

@@ -0,0 +1,53 @@
import { workspace } from "vscode";
type MockConfigurationConfig = {
values: {
[section: string]: {
[scope: string]: any | (() => any);
};
};
};
export function mockConfiguration(config: MockConfigurationConfig) {
const originalGetConfiguration = workspace.getConfiguration;
jest
.spyOn(workspace, "getConfiguration")
.mockImplementation((section, scope) => {
const configuration = originalGetConfiguration(section, scope);
return {
get(key: string, defaultValue?: unknown) {
if (
section &&
config.values[section] &&
config.values[section][key]
) {
const value = config.values[section][key];
return typeof value === "function" ? value() : value;
}
return configuration.get(key, defaultValue);
},
has(key: string) {
return configuration.has(key);
},
inspect(key: string) {
return configuration.inspect(key);
},
update(
key: string,
value: unknown,
configurationTarget?: boolean,
overrideInLanguage?: boolean,
) {
return configuration.update(
key,
value,
configurationTarget,
overrideInLanguage,
);
},
};
});
}

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"include": ["src/**/*.ts", "**/.eslintrc.js", "jest.config.js"]
}