Compare commits

...

22 Commits

Author SHA1 Message Date
Henry Mercer
b43045adbf Merge pull request #207 from alexet/add-v1.0.3-date-to-changelog
Some checks failed
Build Extension / Build (ubuntu-latest) (push) Has been cancelled
Build Extension / Build (windows-latest) (push) Has been cancelled
Build Extension / Test (ubuntu-latest) (push) Has been cancelled
Build Extension / Test (windows-latest) (push) Has been cancelled
Release / Release (push) Has been cancelled
Add v1.0.3 date to changelog.
2020-01-13 12:53:54 +00:00
alexet
ecac23a3e1 v1.0.3: Add date to changelog. 2020-01-13 12:38:49 +00:00
shati-patel
2c9c21038a Merge pull request #206 from henrymercer/update-changelog
Update changelog with recent changes
2020-01-03 16:46:18 +00:00
Henry Mercer
5a94f6f0c5 Update changelog 2020-01-03 16:36:11 +00:00
Alexander Eyers-Taylor
b7401a6c58 Merge pull request #205 from henrymercer/increase-query-unit-tests-timeout
Increase timeout for query unit tests
2020-01-03 16:10:37 +00:00
Henry Mercer
2d19498f1f Increase timeout for query unit tests
This gives the query server sufficient time to startup.
2020-01-03 15:32:03 +00:00
Alexander Eyers-Taylor
a2cffea5b0 Merge pull request #202 from henrymercer/reduce-update-check-frequency
Reduce update check frequency
2020-01-03 14:59:52 +00:00
Alexander Eyers-Taylor
e966c339d3 Merge pull request #203 from henrymercer/run-codeql-tests-on-actions
Add support for running unit tests requiring CodeQL on Actions
2020-01-03 14:38:06 +00:00
Alexander Eyers-Taylor
3fb0624ac6 Merge pull request #204 from henrymercer/webview-file-uri-encoding
Fix double-encoding of "#" in webview URI conversion
2020-01-03 14:31:05 +00:00
Henry Mercer
3811b2e9fe Fix double-encoding of "#" in webview URI conversion
This fixes sorting for result sets with a "#" in their name.
2020-01-02 15:06:59 +00:00
Henry Mercer
1ad2ed8958 Install CodeQL on Actions
This allows us to run tests requiring CodeQL on Actions.
2019-12-20 17:16:03 +00:00
Henry Mercer
5fef262d6e Add additional checkpoints to query server tests
Some of the query server tests are async, so multiple tests can be in
progress at once.
2019-12-20 16:40:25 +00:00
Henry Mercer
93ed820333 Rate limit CLI update checks
This helps to avoid hitting the GitHub API limits of 60 requests per
hour for unauthenticated IPs.
2019-12-20 11:26:22 +00:00
Henry Mercer
4df7ef425a Implement rate limiter for function invocations 2019-12-20 10:57:45 +00:00
Henry Mercer
443eafe8e1 Remove dashes from JSDoc 2019-12-19 17:20:11 +00:00
Henry Mercer
737fa11c4c Merge pull request #201 from github/jcreedcmu-patch-1
Link build status badge to build history
2019-12-18 15:15:25 +00:00
Jason Reed
5e41432c3d Link CI badge to master specifically 2019-12-18 10:07:57 -05:00
jcreedcmu
3349836397 Link build status badge to build history 2019-12-17 11:09:29 -08:00
jcreedcmu
8a8d3c5a92 Merge pull request #200 from henrymercer/manual-version-bump
Bump extension version to 1.0.3
2019-12-17 09:44:49 -08:00
jcreedcmu
d4f3c91e00 Merge pull request #199 from github/version-bump-fix
Base the version bump PR on master
2019-12-13 09:46:08 -08:00
Henry Mercer
9a6790f1d4 Bump extension version to 1.0.3 2019-12-13 17:09:54 +00:00
Henry Mercer
fa99f13846 Base the version bump PR on master 2019-12-13 16:39:53 +00:00
12 changed files with 341 additions and 77 deletions

View File

@@ -54,9 +54,25 @@ jobs:
npm run build-ci
shell: bash
- name: Run unit tests
- name: Install CodeQL
run: |
mkdir codeql-home
curl -L --silent https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip -o codeql-home/codeql.zip
unzip -q -o codeql-home/codeql.zip -d codeql-home
rm codeql-home/codeql.zip
shell: bash
- name: Run unit tests (Linux)
if: matrix.os == 'ubuntu-latest'
run: |
cd extensions/ql-vscode
CODEQL_PATH=$GITHUB_WORKSPACE/codeql-home/codeql/codeql npm run test
- name: Run unit tests (Windows)
if: matrix.os == 'windows-latest'
run: |
cd extensions/ql-vscode
$env:CODEQL_PATH=$(Join-Path $env:GITHUB_WORKSPACE -ChildPath 'codeql-home/codeql/codeql.cmd')
npm run test
- name: Run integration tests (Linux)

View File

@@ -108,4 +108,5 @@ jobs:
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) }}
branch-suffix: none
branch-suffix: none
base: master

View File

@@ -6,7 +6,7 @@ The extension is released. You can download it from the [Visual Studio Marketpla
To see what has changed in the last few versions of the extension, see the [Changelog](https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/CHANGELOG.md).
![CI status badge](https://github.com/github/vscode-codeql/workflows/Build%20Extension/badge.svg)
[![CI status badge](https://github.com/github/vscode-codeql/workflows/Build%20Extension/badge.svg)](https://github.com/github/vscode-codeql/actions?query=workflow%3A%22Build+Extension%22+branch%3Amaster)
## Features

View File

@@ -1,5 +1,11 @@
# CodeQL for Visual Studio Code: Changelog
## 1.0.3 - 13 January 2020
- Reduce the frequency of CodeQL CLI update checks to help avoid hitting GitHub API limits of 60 requests per
hour for unauthenticated IPs.
- Fix sorting of result sets with names containing special characters.
## 1.0.2 - 13 December 2019
- Fix rendering of negative numbers in results.

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.0.2",
"version": "1.0.3",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -95,14 +95,20 @@
"description": "Number of threads for running queries."
},
"codeQL.runningQueries.timeout": {
"type": ["integer", "null"],
"type": [
"integer",
"null"
],
"default": null,
"minimum": 0,
"maximum": 2147483647,
"description": "Timeout (in seconds) for running queries. Leave blank or set to zero for no timeout."
},
"codeQL.runningQueries.memory": {
"type": ["integer", "null"],
"type": [
"integer",
"null"
],
"default": null,
"minimum": 1024,
"description": "Memory (in MB) to use for running queries. Leave blank for CodeQL to choose a suitable value based on your system's available memory."

View File

@@ -6,7 +6,7 @@ import * as unzipper from "unzipper";
import * as url from "url";
import { ExtensionContext, Event } from "vscode";
import { DistributionConfig } from "./config";
import { ProgressUpdate, showAndLogErrorMessage } from "./helpers";
import { InvocationRateLimiter, InvocationRateLimiterResultKind, ProgressUpdate, showAndLogErrorMessage } from "./helpers";
import { logger } from "./logging";
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
@@ -55,6 +55,11 @@ export class DistributionManager implements DistributionProvider {
this._config = config;
this._extensionSpecificDistributionManager = new ExtensionSpecificDistributionManager(extensionContext, config, versionConstraint);
this._onDidChangeDistribution = config.onDidChangeDistributionConfiguration;
this._updateCheckRateLimiter = new InvocationRateLimiter(
extensionContext,
"extensionSpecificDistributionUpdateCheck",
() => this._extensionSpecificDistributionManager.checkForUpdatesToDistribution()
);
this._versionConstraint = versionConstraint;
}
@@ -128,14 +133,21 @@ export class DistributionManager implements DistributionProvider {
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async checkForUpdatesToExtensionManagedDistribution(): Promise<DistributionUpdateCheckResult> {
public async checkForUpdatesToExtensionManagedDistribution(
minSecondsSinceLastUpdateCheck: number): Promise<DistributionUpdateCheckResult> {
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
const extensionManagedCodeQlPath = await this._extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
if (codeQlPath !== undefined && codeQlPath !== extensionManagedCodeQlPath) {
// A distribution is present but it isn't managed by the extension.
return createInvalidDistributionLocationResult();
return createInvalidLocationResult();
}
const updateCheckResult = await this._updateCheckRateLimiter.invokeFunctionIfIntervalElapsed(minSecondsSinceLastUpdateCheck);
switch (updateCheckResult.kind) {
case InvocationRateLimiterResultKind.Invoked:
return updateCheckResult.result;
case InvocationRateLimiterResultKind.RateLimited:
return createAlreadyCheckedRecentlyResult();
}
return this._extensionSpecificDistributionManager.checkForUpdatesToDistribution();
}
/**
@@ -154,6 +166,7 @@ export class DistributionManager implements DistributionProvider {
private readonly _config: DistributionConfig;
private readonly _extensionSpecificDistributionManager: ExtensionSpecificDistributionManager;
private readonly _updateCheckRateLimiter: InvocationRateLimiter<DistributionUpdateCheckResult>;
private readonly _onDidChangeDistribution: Event<void> | undefined;
private readonly _versionConstraint: VersionConstraint;
}
@@ -196,7 +209,7 @@ class ExtensionSpecificDistributionManager {
const latestRelease = await this.getLatestRelease();
if (extensionSpecificRelease !== undefined && codeQlPath !== undefined && latestRelease.id === extensionSpecificRelease.id) {
return createDistributionAlreadyUpToDateResult();
return createAlreadyUpToDateResult();
}
return createUpdateAvailableResult(latestRelease);
}
@@ -234,7 +247,7 @@ class ExtensionSpecificDistributionManager {
if (progressCallback && contentLength !== null) {
const totalNumBytes = parseInt(contentLength, 10);
const bytesToDisplayMB = (numBytes: number) => `${(numBytes/(1024*1024)).toFixed(1)} MB`;
const bytesToDisplayMB = (numBytes: number) => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
const updateProgress = () => {
progressCallback({
step: numBytesDownloaded,
@@ -258,7 +271,7 @@ class ExtensionSpecificDistributionManager {
.on("error", reject)
);
this.bumpDistributionFolderIndex();
await this.bumpDistributionFolderIndex();
logger.log(`Extracting CodeQL CLI to ${this.getDistributionStoragePath()}`);
await extractZipArchive(archivePath, this.getDistributionStoragePath());
@@ -293,10 +306,10 @@ class ExtensionSpecificDistributionManager {
return new ReleasesApiConsumer(ownerName, repositoryName, this._config.personalAccessToken);
}
private bumpDistributionFolderIndex(): void {
private async bumpDistributionFolderIndex(): Promise<void> {
const index = this._extensionContext.globalState.get(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0);
this._extensionContext.globalState.update(
await this._extensionContext.globalState.update(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, index + 1);
}
@@ -317,8 +330,8 @@ class ExtensionSpecificDistributionManager {
return this._extensionContext.globalState.get(ExtensionSpecificDistributionManager._installedReleaseStateKey);
}
private storeInstalledRelease(release: Release | undefined): void {
this._extensionContext.globalState.update(ExtensionSpecificDistributionManager._installedReleaseStateKey, release);
private async storeInstalledRelease(release: Release | undefined): Promise<void> {
await this._extensionContext.globalState.update(ExtensionSpecificDistributionManager._installedReleaseStateKey, release);
}
private readonly _config: DistributionConfig;
@@ -532,23 +545,28 @@ interface NoDistributionResult {
}
export enum DistributionUpdateCheckResultKind {
AlreadyCheckedRecentlyResult,
AlreadyUpToDate,
InvalidDistributionLocation,
InvalidLocation,
UpdateAvailable
}
type DistributionUpdateCheckResult = DistributionAlreadyUpToDateResult | InvalidDistributionLocationResult |
type DistributionUpdateCheckResult = AlreadyCheckedRecentlyResult | AlreadyUpToDateResult | InvalidLocationResult |
UpdateAvailableResult;
export interface DistributionAlreadyUpToDateResult {
export interface AlreadyCheckedRecentlyResult {
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
}
export interface AlreadyUpToDateResult {
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate;
}
/**
* The distribution could not be installed or updated because it is not managed by the extension.
*/
export interface InvalidDistributionLocationResult {
kind: DistributionUpdateCheckResultKind.InvalidDistributionLocation;
export interface InvalidLocationResult {
kind: DistributionUpdateCheckResultKind.InvalidLocation;
}
export interface UpdateAvailableResult {
@@ -556,15 +574,21 @@ export interface UpdateAvailableResult {
updatedRelease: Release;
}
function createDistributionAlreadyUpToDateResult(): DistributionAlreadyUpToDateResult {
function createAlreadyCheckedRecentlyResult(): AlreadyCheckedRecentlyResult {
return {
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
};
}
function createAlreadyUpToDateResult(): AlreadyUpToDateResult {
return {
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate
};
}
function createInvalidDistributionLocationResult(): InvalidDistributionLocationResult {
function createInvalidLocationResult(): InvalidLocationResult {
return {
kind: DistributionUpdateCheckResultKind.InvalidDistributionLocation
kind: DistributionUpdateCheckResultKind.InvalidLocation
};
}

View File

@@ -4,8 +4,10 @@ import * as archiveFilesystemProvider from './archive-filesystem-provider';
import { DistributionConfigListener, QueryServerConfigListener, QueryHistoryConfigListener } from './config';
import { DatabaseManager } from './databases';
import { DatabaseUI } from './databases-ui';
import { DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError,
DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, GithubRateLimitedError } from './distribution';
import {
DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError,
DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, GithubRateLimitedError
} from './distribution';
import * as helpers from './helpers';
import { spawnIdeServer } from './ide-server';
import { InterfaceManager, WebviewReveal } from './interface';
@@ -83,29 +85,32 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
helpers.showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
});
interface ReportingConfig {
interface DistributionUpdateConfig {
isUserInitiated: boolean;
shouldDisplayMessageWhenNoUpdates: boolean;
shouldErrorIfUpdateFails: boolean;
}
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, reportingConfig: ReportingConfig): Promise<void> {
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution();
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, config: DistributionUpdateConfig): Promise<void> {
const minSecondsSinceLastUpdateCheck = config.isUserInitiated ? 0 : 86400;
const noUpdatesLoggingFunc = config.shouldDisplayMessageWhenNoUpdates ?
helpers.showAndLogInformationMessage : async (message: string) => logger.log(message);
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution(minSecondsSinceLastUpdateCheck);
switch (result.kind) {
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
if (reportingConfig.shouldDisplayMessageWhenNoUpdates) {
helpers.showAndLogInformationMessage("CodeQL CLI already up to date.");
}
case DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult:
logger.log("Didn't perform CodeQL CLI update check since a check was already performed within the previous " +
`${minSecondsSinceLastUpdateCheck} seconds.`);
break;
case DistributionUpdateCheckResultKind.InvalidDistributionLocation:
if (reportingConfig.shouldDisplayMessageWhenNoUpdates) {
helpers.showAndLogErrorMessage("CodeQL CLI is installed externally so could not be updated.");
}
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
await noUpdatesLoggingFunc("CodeQL CLI already up to date.");
break;
case DistributionUpdateCheckResultKind.InvalidLocation:
await noUpdatesLoggingFunc("CodeQL CLI is installed externally so could not be updated.");
break;
case DistributionUpdateCheckResultKind.UpdateAvailable:
if (beganMainExtensionActivation) {
const updateAvailableMessage = `Version "${result.updatedRelease.name}" of the CodeQL CLI is now available. ` +
"The update will be installed after Visual Studio Code restarts. Restart now to upgrade?";
ctx.globalState.update(shouldUpdateOnNextActivationKey, true);
await ctx.globalState.update(shouldUpdateOnNextActivationKey, true);
if (await helpers.showInformationMessageWithAction(updateAvailableMessage, "Restart and Upgrade")) {
await commands.executeCommand("workbench.action.reloadWindow");
}
@@ -118,7 +123,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
await helpers.withProgress(progressOptions, progress =>
distributionManager.installExtensionManagedDistributionRelease(result.updatedRelease, progress));
ctx.globalState.update(shouldUpdateOnNextActivationKey, false);
await ctx.globalState.update(shouldUpdateOnNextActivationKey, false);
helpers.showAndLogInformationMessage(`CodeQL CLI updated to version "${result.updatedRelease.name}".`);
}
break;
@@ -127,7 +132,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
}
}
async function installOrUpdateDistribution(reportingConfig: ReportingConfig): Promise<void> {
async function installOrUpdateDistribution(config: DistributionUpdateConfig): Promise<void> {
if (isInstallingOrUpdatingDistribution) {
throw new Error("Already installing or updating CodeQL CLI");
}
@@ -137,11 +142,11 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
const messageText = willUpdateCodeQl ? "Updating CodeQL CLI" :
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
try {
await installOrUpdateDistributionWithProgressTitle(messageText, reportingConfig);
await installOrUpdateDistributionWithProgressTitle(messageText, config);
} catch (e) {
// Don't rethrow the exception, because if the config is changed, we want to be able to retry installing
// or updating the distribution.
const alertFunction = (codeQlInstalled && !reportingConfig.shouldErrorIfUpdateFails) ?
const alertFunction = (codeQlInstalled && !config.isUserInitiated) ?
helpers.showAndLogWarningMessage : helpers.showAndLogErrorMessage;
const taskDescription = (willUpdateCodeQl ? "update" :
codeQlInstalled ? "check for updates to" : "install") + " CodeQL CLI";
@@ -180,8 +185,8 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
return result;
}
async function installOrUpdateThenTryActivate(reportingConfig: ReportingConfig): Promise<void> {
await installOrUpdateDistribution(reportingConfig);
async function installOrUpdateThenTryActivate(config: DistributionUpdateConfig): Promise<void> {
await installOrUpdateDistribution(config);
// Display the warnings even if the extension has already activated.
const distributionResult = await getDistributionDisplayingDistributionWarnings();
@@ -194,8 +199,8 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
const chosenAction = await helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
if (chosenAction === installActionName) {
installOrUpdateThenTryActivate({
shouldDisplayMessageWhenNoUpdates: false,
shouldErrorIfUpdateFails: true
isUserInitiated: true,
shouldDisplayMessageWhenNoUpdates: false
});
}
});
@@ -203,17 +208,17 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
}
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate({
shouldDisplayMessageWhenNoUpdates: false,
shouldErrorIfUpdateFails: true
isUserInitiated: true,
shouldDisplayMessageWhenNoUpdates: false
})));
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate({
shouldDisplayMessageWhenNoUpdates: true,
shouldErrorIfUpdateFails: true
isUserInitiated: true,
shouldDisplayMessageWhenNoUpdates: true
})));
await installOrUpdateThenTryActivate({
shouldDisplayMessageWhenNoUpdates: false,
shouldErrorIfUpdateFails: !!ctx.globalState.get(shouldUpdateOnNextActivationKey)
isUserInitiated: !!ctx.globalState.get(shouldUpdateOnNextActivationKey),
shouldDisplayMessageWhenNoUpdates: false
});
}

View File

@@ -1,5 +1,5 @@
import * as path from 'path';
import { CancellationToken, ProgressOptions, window as Window, workspace } from 'vscode';
import { CancellationToken, ExtensionContext, ProgressOptions, window as Window, workspace } from 'vscode';
import { logger } from './logging';
import { EvaluationInfo } from './queries';
@@ -46,10 +46,10 @@ export function withProgress<R>(
/**
* Show an error message and log it to the console
*
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
*
* @return A thenable that resolves to the selected item or undefined when being dismissed.
* @return A thenable that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
@@ -58,10 +58,10 @@ export function showAndLogErrorMessage(message: string, ...items: string[]): The
/**
* Show a warning message and log it to the console
*
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
*
* @return A thenable that resolves to the selected item or undefined when being dismissed.
* @return A thenable that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogWarningMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
@@ -70,10 +70,10 @@ export function showAndLogWarningMessage(message: string, ...items: string[]): T
/**
* Show an information message and log it to the console
*
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
*
* @return A thenable that resolves to the selected item or undefined when being dismissed.
* @return A thenable that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogInformationMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
@@ -82,9 +82,9 @@ export function showAndLogInformationMessage(message: string, ...items: string[]
/**
* Opens a modal dialog for the user to make a yes/no choice.
* @param message The message to show.
* @param message The message to show.
*
* @return `true` if the user clicks 'Yes', `false` if the user clicks 'No' or cancels the dialog.
* @return `true` if the user clicks 'Yes', `false` if the user clicks 'No' or cancels the dialog.
*/
export async function showBinaryChoiceDialog(message: string): Promise<boolean> {
const yesItem = { title: 'Yes', isCloseAffordance: false };
@@ -95,10 +95,10 @@ export async function showBinaryChoiceDialog(message: string): Promise<boolean>
/**
* Show an information message with a customisable action.
* @param message The message to show.
* @param actionMessage - The call to action message.
* @param message The message to show.
* @param actionMessage The call to action message.
*
* @return `true` if the user clicks the action, `false` if the user cancels the dialog.
* @return `true` if the user clicks the action, `false` if the user cancels the dialog.
*/
export async function showInformationMessageWithAction(message: string, actionMessage: string): Promise<boolean> {
const actionItem = { title: actionMessage, isCloseAffordance: false };
@@ -134,3 +134,81 @@ export function getQueryName(info: EvaluationInfo) {
return path.basename(info.query.program.queryPath);
}
}
/**
* Provides a utility method to invoke a function only if a minimum time interval has elapsed since
* the last invocation of that function.
*/
export class InvocationRateLimiter<T> {
constructor(extensionContext: ExtensionContext, funcIdentifier: string, func: () => Promise<T>) {
this._extensionContext = extensionContext;
this._func = func;
this._funcIdentifier = funcIdentifier;
}
/**
* Invoke the function if `minSecondsSinceLastInvocation` seconds have elapsed since the last invocation.
*/
public async invokeFunctionIfIntervalElapsed(minSecondsSinceLastInvocation: number): Promise<InvocationRateLimiterResult<T>> {
const updateCheckStartDate = new Date();
const lastInvocationDate = this.getLastInvocationDate();
if (minSecondsSinceLastInvocation && lastInvocationDate && lastInvocationDate <= updateCheckStartDate &&
lastInvocationDate.getTime() + minSecondsSinceLastInvocation * 1000 > updateCheckStartDate.getTime()) {
return createRateLimitedResult();
}
const result = await this._func();
await this.setLastInvocationDate(updateCheckStartDate);
return createInvokedResult(result);
}
private getLastInvocationDate(): Date | undefined {
const maybeDate: Date | undefined =
this._extensionContext.globalState.get(InvocationRateLimiter._invocationRateLimiterPrefix + this._funcIdentifier);
return maybeDate ? new Date(maybeDate) : undefined;
}
private async setLastInvocationDate(date: Date): Promise<void> {
return await this._extensionContext.globalState.update(InvocationRateLimiter._invocationRateLimiterPrefix + this._funcIdentifier, date);
}
private readonly _extensionContext: ExtensionContext;
private readonly _func: () => Promise<T>;
private readonly _funcIdentifier: string;
private static readonly _invocationRateLimiterPrefix = "invocationRateLimiter_lastInvocationDate_";
}
export enum InvocationRateLimiterResultKind {
Invoked,
RateLimited
}
/**
* The function was invoked and returned the value `result`.
*/
interface InvokedResult<T> {
kind: InvocationRateLimiterResultKind.Invoked,
result: T
}
/**
* The function was not invoked as the minimum interval since the last invocation had not elapsed.
*/
interface RateLimitedResult {
kind: InvocationRateLimiterResultKind.RateLimited
}
type InvocationRateLimiterResult<T> = InvokedResult<T> | RateLimitedResult;
function createInvokedResult<T>(result: T): InvokedResult<T> {
return {
kind: InvocationRateLimiterResultKind.Invoked,
result
};
}
function createRateLimitedResult(): RateLimitedResult {
return {
kind: InvocationRateLimiterResultKind.RateLimited
};
}

View File

@@ -74,7 +74,7 @@ function getHtmlForWebview(webview: vscode.Webview, scriptUriOnDisk: vscode.Uri,
/** Converts a filesystem URI into a webview URI string that the given panel can use to read the file. */
export function fileUriToWebviewUri(panel: vscode.WebviewPanel, fileUriOnDisk: Uri): string {
return encodeURI(panel.webview.asWebviewUri(fileUriOnDisk).toString(true));
return panel.webview.asWebviewUri(fileUriOnDisk).toString();
}
/** Converts a URI string received from a webview into a local filesystem URI for the same resource. */

View File

@@ -0,0 +1,104 @@
import { expect } from "chai";
import "mocha";
import { ExtensionContext, Memento } from "vscode";
import { InvocationRateLimiter } from "../../helpers";
describe("Invocation rate limiter", () => {
function createInvocationRateLimiter<T>(funcIdentifier: string, func: () => Promise<T>): InvocationRateLimiter<T> {
return new InvocationRateLimiter(new MockExtensionContext(), funcIdentifier, func);
}
it("initially invokes function", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
numTimesFuncCalled++;
});
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
expect(numTimesFuncCalled).to.equal(1);
});
it("doesn't invoke function within time period", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
numTimesFuncCalled++;
});
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
expect(numTimesFuncCalled).to.equal(1);
});
it("invoke function again after 0s time period has elapsed", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
numTimesFuncCalled++;
});
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(0);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(0);
expect(numTimesFuncCalled).to.equal(2);
});
it("invoke function again after 1s time period has elapsed", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
numTimesFuncCalled++;
});
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
await new Promise((resolve, _reject) => setTimeout(() => resolve(), 1000));
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
expect(numTimesFuncCalled).to.equal(2);
});
it("invokes functions with different rate limiters", async () => {
let numTimesFuncACalled = 0;
const invocationRateLimiterA = createInvocationRateLimiter("funcid", async () => {
numTimesFuncACalled++;
});
let numTimesFuncBCalled = 0;
const invocationRateLimiterB = createInvocationRateLimiter("funcid", async () => {
numTimesFuncBCalled++;
});
await invocationRateLimiterA.invokeFunctionIfIntervalElapsed(100);
await invocationRateLimiterB.invokeFunctionIfIntervalElapsed(100);
expect(numTimesFuncACalled).to.equal(1);
expect(numTimesFuncBCalled).to.equal(1);
});
});
class MockExtensionContext implements ExtensionContext {
subscriptions: { dispose(): unknown; }[] = [];
workspaceState: Memento = new MockMemento();
globalState: Memento = new MockMemento();
extensionPath: string = "";
asAbsolutePath(_relativePath: string): string {
throw new Error("Method not implemented.");
}
storagePath: string = "";
globalStoragePath: string = "";
logPath: string = "";
}
class MockMemento implements Memento {
map = new Map<any, any>();
/**
* Return a value.
*
* @param key A string.
* @param defaultValue A value that should be returned when there is no
* value (`undefined`) with the given key.
* @return The stored value or the defaultValue.
*/
get<T>(key: string, defaultValue?: T): T {
return this.map.has(key) ? this.map.get(key) : defaultValue;
}
/**
* Store a value. The value must be JSON-stringifyable.
*
* @param key A string.
* @param value A value. MUST not contain cyclic references.
*/
async update(key: string, value: any): Promise<void> {
this.map.set(key, value);
}
}

View File

@@ -1,11 +1,14 @@
import { expect } from "chai";
import * as path from "path";
import * as tmp from "tmp";
import { window, ViewColumn, Uri } from "vscode";
import { fileUriToWebviewUri, webviewUriToFileUri } from '../../interface';
describe('webview uri conversion', function () {
it('should correctly round trip from filesystem to webview and back', function () {
const tmpFile = tmp.fileSync({ prefix: 'uri_test_', postfix: '.bqrs', keep: false });
const fileSuffix = '.bqrs';
function setupWebview(filePrefix: string) {
const tmpFile = tmp.fileSync({ prefix: `uri_test_${filePrefix}_`, postfix: fileSuffix, keep: false });
const fileUriOnDisk = Uri.file(tmpFile.name);
const panel = window.createWebviewPanel(
'test panel',
@@ -26,9 +29,23 @@ describe('webview uri conversion', function () {
// CSP allowing nothing, to prevent warnings.
const html = `<html><head><meta http-equiv="Content-Security-Policy" content="default-src 'none';"></head></html>`;
panel.webview.html = html;
return {
fileUriOnDisk,
panel
}
}
it('should correctly round trip from filesystem to webview and back', function () {
const { fileUriOnDisk, panel } = setupWebview('');
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
const reconstructedFileUri = webviewUriToFileUri(webviewUri);
expect(reconstructedFileUri.toString(true)).to.equal(fileUriOnDisk.toString(true));
});
it("does not double-encode # in URIs", function () {
const { fileUriOnDisk, panel } = setupWebview('#');
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
const parsedUri = Uri.parse(webviewUri);
expect(path.basename(parsedUri.path, fileSuffix)).to.equal(path.basename(fileUriOnDisk.path, fileSuffix));
});
});

View File

@@ -83,9 +83,14 @@ describe('using the query server', function () {
}
});
// Note this does not work with arrow functions as the test case bodies:
// ensure they are all written with standard anonymous functions.
this.timeout(10000);
const codeQlPath = process.env["CODEQL_PATH"]!;
let qs: qsClient.QueryServerClient;
let cliServer: cli.CodeQLCliServer;
const queryServerStarted = new Checkpoint<void>();
after(() => {
if (qs) {
qs.dispose();
@@ -94,6 +99,7 @@ describe('using the query server', function () {
cliServer.dispose();
}
});
it('should be able to start the query server', async function () {
const consoleProgressReporter: ProgressReporter = {
report: (v: {message: string}) => console.log(`progress reporter says ${v.message}`)
@@ -122,18 +128,17 @@ describe('using the query server', function () {
task => task(consoleProgressReporter, token)
);
await qs.startQueryServer();
queryServerStarted.resolve();
});
// Note this does not work with arrow functions as the test case bodies:
// ensure they are all written with standard anonymous functions.
this.timeout(5000);
for (const queryTestCase of queryTestCases) {
const queryName = path.basename(queryTestCase.queryPath);
const compilationSucceeded = new Checkpoint<void>();
const evaluationSucceeded = new Checkpoint<void>();
const parsedResults = new Checkpoint<void>();
it(`should be able to compile query ${queryName}`, async function () {
await queryServerStarted.done();
expect(fs.existsSync(queryTestCase.queryPath)).to.be.true;
try {
const qlProgram: messages.QlProgram = {
@@ -209,6 +214,7 @@ describe('using the query server', function () {
}
actualResultSets[reader.schema.name] = actualRows;
}
parsedResults.resolve();
} finally {
if (fileReader) {
fileReader.dispose();
@@ -217,6 +223,7 @@ describe('using the query server', function () {
});
it(`should have correct results for query ${queryName}`, async function () {
await parsedResults.done();
expect(actualResultSets!).not.to.be.empty;
expect(Object.keys(actualResultSets!).sort()).to.eql(Object.keys(queryTestCase.expectedResultSets).sort());
for (const name in queryTestCase.expectedResultSets) {