Merge branch 'main' into robertbrignull/remove_withInheritedProgress

This commit is contained in:
Robert
2024-03-14 13:24:36 +00:00
28 changed files with 491 additions and 45 deletions

View File

@@ -2,6 +2,8 @@
## [UNRELEASED] ## [UNRELEASED]
- Databases created from [CodeQL test cases](https://docs.github.com/en/code-security/codeql-cli/using-the-advanced-functionality-of-the-codeql-cli/testing-custom-queries) are now copied into a shared VS Code storage location. This avoids a bug where re-running test cases would fail if the test's database is already imported into the workspace. [#3433](https://github.com/github/vscode-codeql/pull/3433)
## 1.12.3 - 29 February 2024 ## 1.12.3 - 29 February 2024
- Update variant analysis view to show when cancelation is in progress. [#3405](https://github.com/github/vscode-codeql/pull/3405) - Update variant analysis view to show when cancelation is in progress. [#3405](https://github.com/github/vscode-codeql/pull/3405)

View File

@@ -738,6 +738,10 @@
"command": "codeQL.setCurrentDatabase", "command": "codeQL.setCurrentDatabase",
"title": "CodeQL: Set Current Database" "title": "CodeQL: Set Current Database"
}, },
{
"command": "codeQL.importTestDatabase",
"title": "CodeQL: (Re-)Import Test Database"
},
{ {
"command": "codeQL.getCurrentDatabase", "command": "codeQL.getCurrentDatabase",
"title": "CodeQL: Get Current Database" "title": "CodeQL: Get Current Database"
@@ -1322,7 +1326,12 @@
{ {
"command": "codeQL.setCurrentDatabase", "command": "codeQL.setCurrentDatabase",
"group": "9_qlCommands", "group": "9_qlCommands",
"when": "resourceScheme == codeql-zip-archive || explorerResourceIsFolder || resourceExtname == .zip" "when": "resourceExtname != .testproj && (resourceScheme == codeql-zip-archive || explorerResourceIsFolder || resourceExtname == .zipz)"
},
{
"command": "codeQL.importTestDatabase",
"group": "9_qlCommands",
"when": "explorerResourceIsFolder && resourceExtname == .testproj"
}, },
{ {
"command": "codeQL.viewAstContextExplorer", "command": "codeQL.viewAstContextExplorer",
@@ -1476,6 +1485,10 @@
"command": "codeQL.setCurrentDatabase", "command": "codeQL.setCurrentDatabase",
"when": "false" "when": "false"
}, },
{
"command": "codeQL.importTestDatabase",
"when": "false"
},
{ {
"command": "codeQL.getCurrentDatabase", "command": "codeQL.getCurrentDatabase",
"when": "false" "when": "false"
@@ -2018,7 +2031,6 @@
"@types/tar-stream": "^3.1.3", "@types/tar-stream": "^3.1.3",
"@types/through2": "^2.0.36", "@types/through2": "^2.0.36",
"@types/tmp": "^0.2.6", "@types/tmp": "^0.2.6",
"@types/unzipper": "^0.10.1",
"@types/vscode": "^1.82.0", "@types/vscode": "^1.82.0",
"@types/yauzl": "^2.10.3", "@types/yauzl": "^2.10.3",
"@typescript-eslint/eslint-plugin": "^6.19.0", "@typescript-eslint/eslint-plugin": "^6.19.0",

View File

@@ -220,6 +220,7 @@ export type LocalDatabasesCommands = {
// Explorer context menu // Explorer context menu
"codeQL.setCurrentDatabase": (uri: Uri) => Promise<void>; "codeQL.setCurrentDatabase": (uri: Uri) => Promise<void>;
"codeQL.importTestDatabase": (uri: Uri) => Promise<void>;
// Database panel view title commands // Database panel view title commands
"codeQLDatabases.chooseDatabaseFolder": () => Promise<void>; "codeQLDatabases.chooseDatabaseFolder": () => Promise<void>;

View File

@@ -14,6 +14,7 @@ import type { Method } from "../model-editor/method";
import type { ModeledMethod } from "../model-editor/modeled-method"; import type { ModeledMethod } from "../model-editor/modeled-method";
import type { import type {
MethodModelingPanelViewState, MethodModelingPanelViewState,
ModelAlertsViewState,
ModelEditorViewState, ModelEditorViewState,
} from "../model-editor/shared/view-state"; } from "../model-editor/shared/view-state";
import type { Mode } from "../model-editor/shared/mode"; import type { Mode } from "../model-editor/shared/mode";
@@ -726,10 +727,11 @@ export type ToMethodModelingMessage =
| SetInProgressMessage | SetInProgressMessage
| SetProcessedByAutoModelMessage; | SetProcessedByAutoModelMessage;
interface SetModelAlertsMessage { interface SetModelAlertsViewStateMessage {
t: "setModelAlerts"; t: "setModelAlertsViewState";
viewState: ModelAlertsViewState;
} }
export type ToModelAlertsMessage = SetModelAlertsMessage; export type ToModelAlertsMessage = SetModelAlertsViewStateMessage;
export type FromModelAlertsMessage = CommonFromViewMessages; export type FromModelAlertsMessage = CommonFromViewMessages;

View File

@@ -0,0 +1,4 @@
export interface ModelPackDetails {
name: string;
path: string;
}

View File

@@ -11,6 +11,7 @@ import {
createWriteStream, createWriteStream,
remove, remove,
readdir, readdir,
copy,
} from "fs-extra"; } from "fs-extra";
import { basename, join } from "path"; import { basename, join } from "path";
import type { Octokit } from "@octokit/rest"; import type { Octokit } from "@octokit/rest";
@@ -64,7 +65,7 @@ export async function promptImportInternetDatabase(
validateUrl(databaseUrl); validateUrl(databaseUrl);
const item = await databaseArchiveFetcher( const item = await fetchDatabaseToWorkspaceStorage(
databaseUrl, databaseUrl,
{}, {},
databaseManager, databaseManager,
@@ -258,7 +259,7 @@ export async function downloadGitHubDatabaseFromUrl(
* We only need the actual token string. * We only need the actual token string.
*/ */
const octokitToken = ((await octokit.auth()) as { token: string })?.token; const octokitToken = ((await octokit.auth()) as { token: string })?.token;
return await databaseArchiveFetcher( return await fetchDatabaseToWorkspaceStorage(
databaseUrl, databaseUrl,
{ {
Accept: "application/zip", Accept: "application/zip",
@@ -282,14 +283,15 @@ export async function downloadGitHubDatabaseFromUrl(
} }
/** /**
* Imports a database from a local archive. * Imports a database from a local archive or a test database that is in a folder
* ending with `.testproj`.
* *
* @param databaseUrl the file url of the archive to import * @param databaseUrl the file url of the archive or directory to import
* @param databaseManager the DatabaseManager * @param databaseManager the DatabaseManager
* @param storagePath where to store the unzipped database. * @param storagePath where to store the unzipped database.
* @param cli the CodeQL CLI server * @param cli the CodeQL CLI server
*/ */
export async function importArchiveDatabase( export async function importLocalDatabase(
commandManager: AppCommandManager, commandManager: AppCommandManager,
databaseUrl: string, databaseUrl: string,
databaseManager: DatabaseManager, databaseManager: DatabaseManager,
@@ -298,16 +300,17 @@ export async function importArchiveDatabase(
cli: CodeQLCliServer, cli: CodeQLCliServer,
): Promise<DatabaseItem | undefined> { ): Promise<DatabaseItem | undefined> {
try { try {
const item = await databaseArchiveFetcher( const origin: DatabaseOrigin = {
type: databaseUrl.endsWith(".testproj") ? "testproj" : "archive",
path: Uri.parse(databaseUrl).fsPath,
};
const item = await fetchDatabaseToWorkspaceStorage(
databaseUrl, databaseUrl,
{}, {},
databaseManager, databaseManager,
storagePath, storagePath,
undefined, undefined,
{ origin,
type: "archive",
path: databaseUrl,
},
progress, progress,
cli, cli,
); );
@@ -315,7 +318,9 @@ export async function importArchiveDatabase(
await commandManager.execute("codeQLDatabases.focus"); await commandManager.execute("codeQLDatabases.focus");
void showAndLogInformationMessage( void showAndLogInformationMessage(
extLogger, extLogger,
"Database unzipped and imported successfully.", origin.type === "testproj"
? "Test database imported successfully."
: "Database unzipped and imported successfully.",
); );
} }
return item; return item;
@@ -332,10 +337,10 @@ export async function importArchiveDatabase(
} }
/** /**
* Fetches an archive database. The database might be on the internet * Fetches a database into workspace storage. The database might be on the internet
* or in the local filesystem. * or in the local filesystem.
* *
* @param databaseUrl URL from which to grab the database * @param databaseUrl URL from which to grab the database. This could be a local archive file, a local directory, or a remote URL.
* @param requestHeaders Headers to send with the request * @param requestHeaders Headers to send with the request
* @param databaseManager the DatabaseManager * @param databaseManager the DatabaseManager
* @param storagePath where to store the unzipped database. * @param storagePath where to store the unzipped database.
@@ -346,7 +351,7 @@ export async function importArchiveDatabase(
* @param makeSelected make the new database selected in the databases panel (default: true) * @param makeSelected make the new database selected in the databases panel (default: true)
* @param addSourceArchiveFolder whether to add a workspace folder containing the source archive to the workspace * @param addSourceArchiveFolder whether to add a workspace folder containing the source archive to the workspace
*/ */
async function databaseArchiveFetcher( async function fetchDatabaseToWorkspaceStorage(
databaseUrl: string, databaseUrl: string,
requestHeaders: { [key: string]: string }, requestHeaders: { [key: string]: string },
databaseManager: DatabaseManager, databaseManager: DatabaseManager,
@@ -374,7 +379,11 @@ async function databaseArchiveFetcher(
); );
if (isFile(databaseUrl)) { if (isFile(databaseUrl)) {
await readAndUnzip(databaseUrl, unzipPath, cli, progress); if (origin.type === "testproj") {
await copyDatabase(databaseUrl, unzipPath, progress);
} else {
await readAndUnzip(databaseUrl, unzipPath, cli, progress);
}
} else { } else {
await fetchAndUnzip(databaseUrl, requestHeaders, unzipPath, cli, progress); await fetchAndUnzip(databaseUrl, requestHeaders, unzipPath, cli, progress);
} }
@@ -438,6 +447,8 @@ async function getStorageFolder(
lastName = basename(url.path).substring(0, 250); lastName = basename(url.path).substring(0, 250);
if (lastName.endsWith(".zip")) { if (lastName.endsWith(".zip")) {
lastName = lastName.substring(0, lastName.length - 4); lastName = lastName.substring(0, lastName.length - 4);
} else if (lastName.endsWith(".testproj")) {
lastName = lastName.substring(0, lastName.length - 9);
} }
} }
@@ -484,6 +495,26 @@ function validateUrl(databaseUrl: string) {
} }
} }
/**
* Copies a database folder from the file system into the workspace storage.
* @param scrDirURL the original location of the database as a URL string
* @param destDir the location to copy the database to. This should be a folder in the workspace storage.
* @param progress callback to send progress messages to
*/
async function copyDatabase(
srcDirURL: string,
destDir: string,
progress?: ProgressCallback,
) {
progress?.({
maxStep: 10,
step: 9,
message: `Copying database ${basename(destDir)} into the workspace`,
});
await ensureDir(destDir);
await copy(Uri.parse(srcDirURL).fsPath, destDir);
}
async function readAndUnzip( async function readAndUnzip(
zipUrl: string, zipUrl: string,
unzipPath: string, unzipPath: string,

View File

@@ -38,7 +38,7 @@ import {
showAndLogErrorMessage, showAndLogErrorMessage,
} from "../common/logging"; } from "../common/logging";
import { import {
importArchiveDatabase, importLocalDatabase,
promptImportGithubDatabase, promptImportGithubDatabase,
promptImportInternetDatabase, promptImportInternetDatabase,
} from "./database-fetcher"; } from "./database-fetcher";
@@ -140,7 +140,8 @@ class DatabaseTreeDataProvider
item.iconPath = new ThemeIcon("error", new ThemeColor("errorForeground")); item.iconPath = new ThemeIcon("error", new ThemeColor("errorForeground"));
} }
item.tooltip = element.databaseUri.fsPath; item.tooltip = element.databaseUri.fsPath;
item.description = element.language; item.description =
element.language + (element.origin?.type === "testproj" ? " (test)" : "");
return item; return item;
} }
@@ -276,6 +277,7 @@ export class DatabaseUI extends DisposableObject {
this.handleChooseDatabaseInternet.bind(this), this.handleChooseDatabaseInternet.bind(this),
"codeQL.chooseDatabaseGithub": this.handleChooseDatabaseGithub.bind(this), "codeQL.chooseDatabaseGithub": this.handleChooseDatabaseGithub.bind(this),
"codeQL.setCurrentDatabase": this.handleSetCurrentDatabase.bind(this), "codeQL.setCurrentDatabase": this.handleSetCurrentDatabase.bind(this),
"codeQL.importTestDatabase": this.handleImportTestDatabase.bind(this),
"codeQL.setDefaultTourDatabase": "codeQL.setDefaultTourDatabase":
this.handleSetDefaultTourDatabase.bind(this), this.handleSetDefaultTourDatabase.bind(this),
"codeQL.upgradeCurrentDatabase": "codeQL.upgradeCurrentDatabase":
@@ -705,7 +707,7 @@ export class DatabaseUI extends DisposableObject {
try { try {
// Assume user has selected an archive if the file has a .zip extension // Assume user has selected an archive if the file has a .zip extension
if (uri.path.endsWith(".zip")) { if (uri.path.endsWith(".zip")) {
await importArchiveDatabase( await importLocalDatabase(
this.app.commands, this.app.commands,
uri.toString(true), uri.toString(true),
this.databaseManager, this.databaseManager,
@@ -733,6 +735,60 @@ export class DatabaseUI extends DisposableObject {
); );
} }
private async handleImportTestDatabase(uri: Uri): Promise<void> {
return withProgress(
async (progress) => {
try {
if (!uri.path.endsWith(".testproj")) {
throw new Error(
"Please select a valid test database to import. Test databases end with `.testproj`.",
);
}
// Check if the database is already in the workspace. If
// so, delete it first before importing the new one.
const existingItem = this.databaseManager.findTestDatabase(uri);
const baseName = basename(uri.fsPath);
if (existingItem !== undefined) {
progress({
maxStep: 9,
step: 1,
message: `Removing existing test database ${baseName}`,
});
await this.databaseManager.removeDatabaseItem(existingItem);
}
await importLocalDatabase(
this.app.commands,
uri.toString(true),
this.databaseManager,
this.storagePath,
progress,
this.queryServer.cliServer,
);
if (existingItem !== undefined) {
progress({
maxStep: 9,
step: 9,
message: `Successfully re-imported ${baseName}`,
});
}
} catch (e) {
// rethrow and let this be handled by default error handling.
throw new Error(
`Could not set database to ${basename(
uri.fsPath,
)}. Reason: ${getErrorMessage(e)}`,
);
}
},
{
title: "(Re-)importing test database from directory",
},
);
}
private async handleRemoveDatabase( private async handleRemoveDatabase(
databaseItems: DatabaseItem[], databaseItems: DatabaseItem[],
): Promise<void> { ): Promise<void> {
@@ -948,7 +1004,7 @@ export class DatabaseUI extends DisposableObject {
} else { } else {
// we are selecting a database archive. Must unzip into a workspace-controlled area // we are selecting a database archive. Must unzip into a workspace-controlled area
// before importing. // before importing.
return await importArchiveDatabase( return await importLocalDatabase(
this.app.commands, this.app.commands,
uri.toString(true), uri.toString(true),
this.databaseManager, this.databaseManager,

View File

@@ -18,7 +18,10 @@ import {
import { join } from "path"; import { join } from "path";
import type { FullDatabaseOptions } from "./database-options"; import type { FullDatabaseOptions } from "./database-options";
import { DatabaseItemImpl } from "./database-item-impl"; import { DatabaseItemImpl } from "./database-item-impl";
import { showNeverAskAgainDialog } from "../../common/vscode/dialog"; import {
showBinaryChoiceDialog,
showNeverAskAgainDialog,
} from "../../common/vscode/dialog";
import { import {
getFirstWorkspaceFolder, getFirstWorkspaceFolder,
isFolderAlreadyInWorkspace, isFolderAlreadyInWorkspace,
@@ -32,7 +35,7 @@ import { QlPackGenerator } from "../../local-queries/qlpack-generator";
import { asError, getErrorMessage } from "../../common/helpers-pure"; import { asError, getErrorMessage } from "../../common/helpers-pure";
import type { DatabaseItem, PersistedDatabaseItem } from "./database-item"; import type { DatabaseItem, PersistedDatabaseItem } from "./database-item";
import { redactableError } from "../../common/errors"; import { redactableError } from "../../common/errors";
import { remove } from "fs-extra"; import { copy, remove, stat } from "fs-extra";
import { containsPath } from "../../common/files"; import { containsPath } from "../../common/files";
import type { DatabaseChangedEvent } from "./database-events"; import type { DatabaseChangedEvent } from "./database-events";
import { DatabaseEventKind } from "./database-events"; import { DatabaseEventKind } from "./database-events";
@@ -120,6 +123,7 @@ export class DatabaseManager extends DisposableObject {
super(); super();
qs.onStart(this.reregisterDatabases.bind(this)); qs.onStart(this.reregisterDatabases.bind(this));
qs.onQueryRunStarting(this.maybeReimportTestDatabase.bind(this));
this.push( this.push(
this.languageContext.onLanguageContextChanged(async () => { this.languageContext.onLanguageContextChanged(async () => {
@@ -165,6 +169,99 @@ export class DatabaseManager extends DisposableObject {
); );
} }
/**
* Finds a test database that was originally imported from `uri`.
* A test database is creeated by the `codeql test run` command
* and ends with `.testproj`.
* @param uri The original location of the database
* @returns The first database item found that matches the uri
*/
public findTestDatabase(uri: vscode.Uri): DatabaseItem | undefined {
const originPath = uri.fsPath;
for (const item of this._databaseItems) {
if (item.origin?.type === "testproj" && item.origin.path === originPath) {
return item;
}
}
return undefined;
}
public async maybeReimportTestDatabase(
databaseUri: vscode.Uri,
forceImport = false,
): Promise<void> {
const res = await this.isTestDatabaseOutdated(databaseUri);
if (!res) {
return;
}
const doit =
forceImport ||
(await showBinaryChoiceDialog(
"This test database is outdated. Do you want to reimport it?",
));
if (doit) {
await this.reimportTestDatabase(databaseUri);
}
}
/**
* Checks if the origin of the imported database is newer.
* The imported database must be a test database.
* @param databaseUri the URI of the imported database to check
* @returns true if both databases exist and the origin database is newer.
*/
private async isTestDatabaseOutdated(
databaseUri: vscode.Uri,
): Promise<boolean> {
const dbItem = this.findDatabaseItem(databaseUri);
if (dbItem === undefined || dbItem.origin?.type !== "testproj") {
return false;
}
// Compare timestmps of the codeql-database.yml files of the original and the
// imported databases.
const originDbYml = join(dbItem.origin.path, "codeql-database.yml");
const importedDbYml = join(
dbItem.databaseUri.fsPath,
"codeql-database.yml",
);
try {
const originStat = await stat(originDbYml);
const importedStat = await stat(importedDbYml);
return originStat.mtimeMs > importedStat.mtimeMs;
} catch (e) {
// If either of the files does not exist, we assume the origin is newer.
// This shouldn't happen unless the user manually deleted one of the files.
return true;
}
}
/**
* Reimport the specified imported database from its origin.
* The imported databsae must be a testproj database.
*
* @param databaseUri the URI of the imported database to reimport
*/
private async reimportTestDatabase(databaseUri: vscode.Uri): Promise<void> {
const dbItem = this.findDatabaseItem(databaseUri);
if (dbItem === undefined || dbItem.origin?.type !== "testproj") {
throw new Error(`Database ${databaseUri} is not a testproj.`);
}
await this.removeDatabaseItem(dbItem);
await copy(dbItem.origin.path, databaseUri.fsPath);
const newDbItem = new DatabaseItemImpl(databaseUri, dbItem.contents, {
dateAdded: Date.now(),
language: dbItem.language,
origin: dbItem.origin,
extensionManagedLocation: dbItem.extensionManagedLocation,
});
await this.addDatabaseItem(newDbItem);
await this.setCurrentDatabaseItem(newDbItem);
}
/** /**
* Adds a {@link DatabaseItem} to the list of open databases, if that database is not already on * Adds a {@link DatabaseItem} to the list of open databases, if that database is not already on
* the list. * the list.

View File

@@ -24,9 +24,15 @@ interface DatabaseOriginDebugger {
type: "debugger"; type: "debugger";
} }
interface DatabaseOriginTestProj {
type: "testproj";
path: string;
}
export type DatabaseOrigin = export type DatabaseOrigin =
| DatabaseOriginFolder | DatabaseOriginFolder
| DatabaseOriginArchive | DatabaseOriginArchive
| DatabaseOriginGitHub | DatabaseOriginGitHub
| DatabaseOriginInternet | DatabaseOriginInternet
| DatabaseOriginDebugger; | DatabaseOriginDebugger
| DatabaseOriginTestProj;

View File

@@ -14,6 +14,7 @@ import { showAndLogExceptionWithTelemetry } from "../../common/logging";
import type { ModelingEvents } from "../modeling-events"; import type { ModelingEvents } from "../modeling-events";
import type { ModelingStore } from "../modeling-store"; import type { ModelingStore } from "../modeling-store";
import type { DatabaseItem } from "../../databases/local-databases"; import type { DatabaseItem } from "../../databases/local-databases";
import type { ExtensionPack } from "../shared/extension-pack";
export class ModelAlertsView extends AbstractWebview< export class ModelAlertsView extends AbstractWebview<
ToModelAlertsMessage, ToModelAlertsMessage,
@@ -26,6 +27,7 @@ export class ModelAlertsView extends AbstractWebview<
private readonly modelingEvents: ModelingEvents, private readonly modelingEvents: ModelingEvents,
private readonly modelingStore: ModelingStore, private readonly modelingStore: ModelingStore,
private readonly dbItem: DatabaseItem, private readonly dbItem: DatabaseItem,
private readonly extensionPack: ExtensionPack,
) { ) {
super(app); super(app);
@@ -37,6 +39,7 @@ export class ModelAlertsView extends AbstractWebview<
panel.reveal(undefined, true); panel.reveal(undefined, true);
await this.waitForPanelLoaded(); await this.waitForPanelLoaded();
await this.setViewState();
} }
protected async getPanelConfig(): Promise<WebviewPanelConfig> { protected async getPanelConfig(): Promise<WebviewPanelConfig> {
@@ -75,6 +78,15 @@ export class ModelAlertsView extends AbstractWebview<
} }
} }
private async setViewState(): Promise<void> {
await this.postMessage({
t: "setModelAlertsViewState",
viewState: {
title: this.extensionPack.name,
},
});
}
public async focusView(): Promise<void> { public async focusView(): Promise<void> {
this.panel?.reveal(); this.panel?.reveal();
} }
@@ -87,5 +99,13 @@ export class ModelAlertsView extends AbstractWebview<
} }
}), }),
); );
this.push(
this.modelingEvents.onDbClosed(async (event) => {
if (event === this.dbItem.databaseUri.toString()) {
this.dispose();
}
}),
);
} }
} }

View File

@@ -132,6 +132,7 @@ export class ModelEditorView extends AbstractWebview<
this.variantAnalysisManager, this.variantAnalysisManager,
databaseItem, databaseItem,
language, language,
this.extensionPack,
this.updateModelEvaluationRun.bind(this), this.updateModelEvaluationRun.bind(this),
); );
this.push(this.modelEvaluator); this.push(this.modelEvaluator);

View File

@@ -19,6 +19,7 @@ import { CancellationTokenSource } from "vscode";
import type { QlPackDetails } from "../variant-analysis/ql-pack-details"; import type { QlPackDetails } from "../variant-analysis/ql-pack-details";
import type { App } from "../common/app"; import type { App } from "../common/app";
import { ModelAlertsView } from "./model-alerts/model-alerts-view"; import { ModelAlertsView } from "./model-alerts/model-alerts-view";
import type { ExtensionPack } from "./shared/extension-pack";
export class ModelEvaluator extends DisposableObject { export class ModelEvaluator extends DisposableObject {
// Cancellation token source to allow cancelling of the current run // Cancellation token source to allow cancelling of the current run
@@ -34,6 +35,7 @@ export class ModelEvaluator extends DisposableObject {
private readonly variantAnalysisManager: VariantAnalysisManager, private readonly variantAnalysisManager: VariantAnalysisManager,
private readonly dbItem: DatabaseItem, private readonly dbItem: DatabaseItem,
private readonly language: QueryLanguage, private readonly language: QueryLanguage,
private readonly extensionPack: ExtensionPack,
private readonly updateView: ( private readonly updateView: (
run: ModelEvaluationRunState, run: ModelEvaluationRunState,
) => Promise<void>, ) => Promise<void>,
@@ -120,6 +122,7 @@ export class ModelEvaluator extends DisposableObject {
this.modelingEvents, this.modelingEvents,
this.modelingStore, this.modelingStore,
this.dbItem, this.dbItem,
this.extensionPack,
); );
await view.showView(); await view.showView();
} }

View File

@@ -19,3 +19,7 @@ export interface MethodModelingPanelViewState {
language: QueryLanguage | undefined; language: QueryLanguage | undefined;
modelConfig: ModelConfig; modelConfig: ModelConfig;
} }
export interface ModelAlertsViewState {
title: string;
}

View File

@@ -1,4 +1,4 @@
import { window } from "vscode"; import { window, Uri } from "vscode";
import type { CancellationToken, MessageItem } from "vscode"; import type { CancellationToken, MessageItem } from "vscode";
import type { CodeQLCliServer } from "../codeql-cli/cli"; import type { CodeQLCliServer } from "../codeql-cli/cli";
import type { ProgressCallback } from "../common/vscode/progress"; import type { ProgressCallback } from "../common/vscode/progress";
@@ -63,9 +63,22 @@ export interface CoreQueryRun {
export type CoreCompletedQuery = CoreQueryResults & export type CoreCompletedQuery = CoreQueryResults &
Omit<CoreQueryRun, "evaluate">; Omit<CoreQueryRun, "evaluate">;
type OnQueryRunStartingListener = (dbPath: Uri) => Promise<void>;
export class QueryRunner { export class QueryRunner {
constructor(public readonly qs: QueryServerClient) {} constructor(public readonly qs: QueryServerClient) {}
// Event handlers that get notified whenever a query is about to start running.
// Can't use vscode EventEmitters since they are not asynchronous.
private readonly onQueryRunStartingListeners: OnQueryRunStartingListener[] =
[];
public onQueryRunStarting(listener: OnQueryRunStartingListener) {
this.onQueryRunStartingListeners.push(listener);
}
private async fireQueryRunStarting(dbPath: Uri) {
await Promise.all(this.onQueryRunStartingListeners.map((l) => l(dbPath)));
}
get cliServer(): CodeQLCliServer { get cliServer(): CodeQLCliServer {
return this.qs.cliServer; return this.qs.cliServer;
} }
@@ -138,6 +151,8 @@ export class QueryRunner {
templates: Record<string, string> | undefined, templates: Record<string, string> | undefined,
logger: BaseLogger, logger: BaseLogger,
): Promise<CoreQueryResults> { ): Promise<CoreQueryResults> {
await this.fireQueryRunStarting(Uri.file(dbPath));
return await compileAndRunQueryAgainstDatabaseCore( return await compileAndRunQueryAgainstDatabaseCore(
this.qs, this.qs,
dbPath, dbPath,

View File

@@ -0,0 +1,17 @@
import type { Meta, StoryFn } from "@storybook/react";
import { ModelAlerts as ModelAlertsComponent } from "../../view/model-alerts/ModelAlerts";
export default {
title: "CodeQL Model Alerts/CodeQL Model Alerts",
component: ModelAlertsComponent,
} as Meta<typeof ModelAlertsComponent>;
const Template: StoryFn<typeof ModelAlertsComponent> = (args) => (
<ModelAlertsComponent {...args} />
);
export const ModelAlerts = Template.bind({});
ModelAlerts.args = {
initialViewState: { title: "codeql/sql2o-models" },
};

View File

@@ -0,0 +1,34 @@
import type { Meta, StoryFn } from "@storybook/react";
import { ModelPacks as ModelPacksComponent } from "../../view/model-alerts/ModelPacks";
export default {
title: "Model Alerts/Model Packs",
component: ModelPacksComponent,
argTypes: {
openModelPackClick: {
action: "open-model-pack-clicked",
table: {
disable: true,
},
},
},
} as Meta<typeof ModelPacksComponent>;
const Template: StoryFn<typeof ModelPacksComponent> = (args) => (
<ModelPacksComponent {...args} />
);
export const ModelPacks = Template.bind({});
ModelPacks.args = {
modelPacks: [
{
name: "Model pack 1",
path: "/path/to/model-pack-1",
},
{
name: "Model pack 2",
path: "/path/to/model-pack-2",
},
],
};

View File

@@ -1,10 +1,27 @@
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { ModelAlertsHeader } from "./ModelAlertsHeader";
import type { ModelAlertsViewState } from "../../model-editor/shared/view-state";
import type { ToModelAlertsMessage } from "../../common/interface-types";
type Props = {
initialViewState?: ModelAlertsViewState;
};
export function ModelAlerts({ initialViewState }: Props): React.JSX.Element {
const [viewState, setViewState] = useState<ModelAlertsViewState | undefined>(
initialViewState,
);
export function ModelAlerts(): React.JSX.Element {
useEffect(() => { useEffect(() => {
const listener = (evt: MessageEvent) => { const listener = (evt: MessageEvent) => {
if (evt.origin === window.origin) { if (evt.origin === window.origin) {
// TODO: handle messages const msg: ToModelAlertsMessage = evt.data;
switch (msg.t) {
case "setModelAlertsViewState": {
setViewState(msg.viewState);
break;
}
}
} else { } else {
// sanitize origin // sanitize origin
const origin = evt.origin.replace(/\n|\r/g, ""); const origin = evt.origin.replace(/\n|\r/g, "");
@@ -18,5 +35,9 @@ export function ModelAlerts(): React.JSX.Element {
}; };
}, []); }, []);
return <>hello world</>; if (viewState === undefined) {
return <></>;
}
return <ModelAlertsHeader viewState={viewState}></ModelAlertsHeader>;
} }

View File

@@ -0,0 +1,8 @@
import type { ModelAlertsViewState } from "../../model-editor/shared/view-state";
import { ViewTitle } from "../common";
type Props = { viewState: ModelAlertsViewState };
export const ModelAlertsHeader = ({ viewState }: Props) => {
return <ViewTitle>Model evaluation results for {viewState.title}</ViewTitle>;
};

View File

@@ -0,0 +1,43 @@
import { styled } from "styled-components";
import { LinkIconButton } from "../common/LinkIconButton";
import type { ModelPackDetails } from "../../common/model-pack-details";
const Title = styled.h3`
font-size: medium;
font-weight: 500;
margin: 0;
`;
const List = styled.ul`
list-style: none;
padding: 0;
margin-top: 5px;
`;
export const ModelPacks = ({
modelPacks,
openModelPackClick,
}: {
modelPacks: ModelPackDetails[];
openModelPackClick: (path: string) => void;
}): React.JSX.Element => {
if (modelPacks.length <= 0) {
return <></>;
}
return (
<>
<Title>Model packs</Title>
<List>
{modelPacks.map((modelPack) => (
<li key={modelPack.path}>
<LinkIconButton onClick={() => openModelPackClick(modelPack.path)}>
<span slot="start" className="codicon codicon-file-code"></span>
{modelPack.name}
</LinkIconButton>
</li>
))}
</List>
</>
);
};

View File

@@ -11,7 +11,7 @@ import type { ModeledMethod } from "../../model-editor/modeled-method";
import { assertNever } from "../../common/helpers-pure"; import { assertNever } from "../../common/helpers-pure";
import { vscode } from "../vscode-api"; import { vscode } from "../vscode-api";
import { calculateModeledPercentage } from "../../model-editor/shared/modeled-percentage"; import { calculateModeledPercentage } from "../../model-editor/shared/modeled-percentage";
import { LinkIconButton } from "../variant-analysis/LinkIconButton"; import { LinkIconButton } from "../common/LinkIconButton";
import type { ModelEditorViewState } from "../../model-editor/shared/view-state"; import type { ModelEditorViewState } from "../../model-editor/shared/view-state";
import { ModeledMethodsList } from "./ModeledMethodsList"; import { ModeledMethodsList } from "./ModeledMethodsList";
import { percentFormatter } from "./formatters"; import { percentFormatter } from "./formatters";

View File

@@ -4,7 +4,7 @@ import type { ModelEditorViewState } from "../../model-editor/shared/view-state"
import type { ModelEvaluationRunState } from "../../model-editor/shared/model-evaluation-run-state"; import type { ModelEvaluationRunState } from "../../model-editor/shared/model-evaluation-run-state";
import { modelEvaluationRunIsRunning } from "../../model-editor/shared/model-evaluation-run-state"; import { modelEvaluationRunIsRunning } from "../../model-editor/shared/model-evaluation-run-state";
import { ModelEditorProgressRing } from "./ModelEditorProgressRing"; import { ModelEditorProgressRing } from "./ModelEditorProgressRing";
import { LinkIconButton } from "../variant-analysis/LinkIconButton"; import { LinkIconButton } from "../common/LinkIconButton";
export type Props = { export type Props = {
viewState: ModelEditorViewState; viewState: ModelEditorViewState;

View File

@@ -1,6 +1,6 @@
import { styled } from "styled-components"; import { styled } from "styled-components";
import { ViewTitle } from "../common"; import { ViewTitle } from "../common";
import { LinkIconButton } from "./LinkIconButton"; import { LinkIconButton } from "../common/LinkIconButton";
export type QueryDetailsProps = { export type QueryDetailsProps = {
queryName: string; queryName: string;

View File

@@ -7,6 +7,7 @@ import type { ModelEvaluationRun } from "../../../../src/model-editor/model-eval
import { ModelEvaluator } from "../../../../src/model-editor/model-evaluator"; import { ModelEvaluator } from "../../../../src/model-editor/model-evaluator";
import type { ModelingEvents } from "../../../../src/model-editor/modeling-events"; import type { ModelingEvents } from "../../../../src/model-editor/modeling-events";
import type { ModelingStore } from "../../../../src/model-editor/modeling-store"; import type { ModelingStore } from "../../../../src/model-editor/modeling-store";
import type { ExtensionPack } from "../../../../src/model-editor/shared/extension-pack";
import type { VariantAnalysisManager } from "../../../../src/variant-analysis/variant-analysis-manager"; import type { VariantAnalysisManager } from "../../../../src/variant-analysis/variant-analysis-manager";
import { createMockLogger } from "../../../__mocks__/loggerMock"; import { createMockLogger } from "../../../__mocks__/loggerMock";
import { createMockModelingEvents } from "../../../__mocks__/model-editor/modelingEventsMock"; import { createMockModelingEvents } from "../../../__mocks__/model-editor/modelingEventsMock";
@@ -23,6 +24,7 @@ describe("Model Evaluator", () => {
let variantAnalysisManager: VariantAnalysisManager; let variantAnalysisManager: VariantAnalysisManager;
let dbItem: DatabaseItem; let dbItem: DatabaseItem;
let language: QueryLanguage; let language: QueryLanguage;
let extensionPack: ExtensionPack;
let updateView: jest.Mock; let updateView: jest.Mock;
let getModelEvaluationRunMock = jest.fn(); let getModelEvaluationRunMock = jest.fn();
@@ -40,6 +42,7 @@ describe("Model Evaluator", () => {
}); });
dbItem = mockedObject<DatabaseItem>({}); dbItem = mockedObject<DatabaseItem>({});
language = QueryLanguage.Java; language = QueryLanguage.Java;
extensionPack = mockedObject<ExtensionPack>({});
updateView = jest.fn(); updateView = jest.fn();
modelEvaluator = new ModelEvaluator( modelEvaluator = new ModelEvaluator(
@@ -50,6 +53,7 @@ describe("Model Evaluator", () => {
variantAnalysisManager, variantAnalysisManager,
dbItem, dbItem,
language, language,
extensionPack,
updateView, updateView,
); );
}); });

View File

@@ -4,7 +4,7 @@ import { Uri, window } from "vscode";
import type { CodeQLCliServer } from "../../../../src/codeql-cli/cli"; import type { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
import type { DatabaseManager } from "../../../../src/databases/local-databases"; import type { DatabaseManager } from "../../../../src/databases/local-databases";
import { import {
importArchiveDatabase, importLocalDatabase,
promptImportInternetDatabase, promptImportInternetDatabase,
} from "../../../../src/databases/database-fetcher"; } from "../../../../src/databases/database-fetcher";
import { import {
@@ -13,9 +13,11 @@ import {
DB_URL, DB_URL,
getActivatedExtension, getActivatedExtension,
storagePath, storagePath,
testprojLoc,
} from "../../global.helper"; } from "../../global.helper";
import { createMockCommandManager } from "../../../__mocks__/commandsMock"; import { createMockCommandManager } from "../../../__mocks__/commandsMock";
import { remove } from "fs-extra"; import { utimesSync } from "fs";
import { remove, existsSync } from "fs-extra";
/** /**
* Run various integration tests for databases * Run various integration tests for databases
@@ -46,10 +48,10 @@ describe("database-fetcher", () => {
await remove(storagePath); await remove(storagePath);
}); });
describe("importArchiveDatabase", () => { describe("importLocalDatabase", () => {
it("should add a database from a folder", async () => { it("should add a database from an archive", async () => {
const uri = Uri.file(dbLoc); const uri = Uri.file(dbLoc);
let dbItem = await importArchiveDatabase( let dbItem = await importLocalDatabase(
createMockCommandManager(), createMockCommandManager(),
uri.toString(true), uri.toString(true),
databaseManager, databaseManager,
@@ -64,6 +66,42 @@ describe("database-fetcher", () => {
expect(dbItem.name).toBe("db"); expect(dbItem.name).toBe("db");
expect(dbItem.databaseUri.fsPath).toBe(join(storagePath, "db", "db")); expect(dbItem.databaseUri.fsPath).toBe(join(storagePath, "db", "db"));
}); });
it("should import a testproj database", async () => {
let dbItem = await importLocalDatabase(
createMockCommandManager(),
Uri.file(testprojLoc).toString(true),
databaseManager,
storagePath,
progressCallback,
cli,
);
expect(dbItem).toBe(databaseManager.currentDatabaseItem);
expect(dbItem).toBe(databaseManager.databaseItems[0]);
expect(dbItem).toBeDefined();
dbItem = dbItem!;
expect(dbItem.name).toBe("db");
expect(dbItem.databaseUri.fsPath).toBe(join(storagePath, "db"));
// Now that we have fetched it. Check for re-importing
// Delete a file in the imported database and we can check if the file is recreated
const srczip = join(dbItem.databaseUri.fsPath, "src.zip");
await remove(srczip);
// Attempt 1: re-import database should be a no-op since timestamp of imported database is newer
await databaseManager.maybeReimportTestDatabase(dbItem.databaseUri);
expect(existsSync(srczip)).toBeFalsy();
// Attempt 3: re-import database should re-import the database after updating modified time
utimesSync(
join(testprojLoc, "codeql-database.yml"),
new Date(),
new Date(),
);
await databaseManager.maybeReimportTestDatabase(dbItem.databaseUri, true);
expect(existsSync(srczip)).toBeTruthy();
});
}); });
describe("promptImportInternetDatabase", () => { describe("promptImportInternetDatabase", () => {

View File

@@ -6,13 +6,18 @@ import {
beforeEachAction, beforeEachAction,
} from "../jest.activated-extension.setup"; } from "../jest.activated-extension.setup";
import { createWriteStream, existsSync, mkdirpSync } from "fs-extra"; import { createWriteStream, existsSync, mkdirpSync } from "fs-extra";
import { dirname } from "path"; import { dirname, join } from "path";
import { DB_URL, dbLoc } from "../global.helper"; import { DB_URL, dbLoc, testprojLoc } from "../global.helper";
import fetch from "node-fetch"; import fetch from "node-fetch";
import { renameSync } from "fs";
import { unzipToDirectoryConcurrently } from "../../../src/common/unzip-concurrently";
import { platform } from "os";
import { sleep } from "../../../src/common/time";
beforeAll(async () => { beforeAll(async () => {
// ensure the test database is downloaded // ensure the test database is downloaded
mkdirpSync(dirname(dbLoc)); const dbParentDir = dirname(dbLoc);
mkdirpSync(dbParentDir);
if (!existsSync(dbLoc)) { if (!existsSync(dbLoc)) {
console.log(`Downloading test database to ${dbLoc}`); console.log(`Downloading test database to ${dbLoc}`);
@@ -30,6 +35,19 @@ beforeAll(async () => {
}); });
} }
// unzip the database from dbLoc to testprojLoc
if (!existsSync(testprojLoc)) {
console.log(`Unzipping test database to ${testprojLoc}`);
await unzipToDirectoryConcurrently(dbLoc, dbParentDir);
// On Windows, wait a few seconds to make sure all background processes
// release their lock on the files before renaming the directory.
if (platform() === "win32") {
await sleep(3000);
}
renameSync(join(dbParentDir, "db"), testprojLoc);
console.log("Unzip completed.");
}
await beforeAllAction(); await beforeAllAction();
}); });

View File

@@ -7,7 +7,7 @@ import type {
} from "../../src/databases/local-databases"; } from "../../src/databases/local-databases";
import type { CodeQLCliServer } from "../../src/codeql-cli/cli"; import type { CodeQLCliServer } from "../../src/codeql-cli/cli";
import type { CodeQLExtensionInterface } from "../../src/extension"; import type { CodeQLExtensionInterface } from "../../src/extension";
import { importArchiveDatabase } from "../../src/databases/database-fetcher"; import { importLocalDatabase } from "../../src/databases/database-fetcher";
import { createMockCommandManager } from "../__mocks__/commandsMock"; import { createMockCommandManager } from "../__mocks__/commandsMock";
// This file contains helpers shared between tests that work with an activated extension. // This file contains helpers shared between tests that work with an activated extension.
@@ -21,6 +21,12 @@ export const dbLoc = join(
realpathSync(join(__dirname, "../../../")), realpathSync(join(__dirname, "../../../")),
"build/tests/db.zip", "build/tests/db.zip",
); );
export const testprojLoc = join(
realpathSync(join(__dirname, "../../../")),
"build/tests/db.testproj",
);
// eslint-disable-next-line import/no-mutable-exports // eslint-disable-next-line import/no-mutable-exports
export let storagePath: string; export let storagePath: string;
@@ -34,7 +40,7 @@ export async function ensureTestDatabase(
// Add a database, but make sure the database manager is empty first // Add a database, but make sure the database manager is empty first
await cleanDatabases(databaseManager); await cleanDatabases(databaseManager);
const uri = Uri.file(dbLoc); const uri = Uri.file(dbLoc);
const maybeDbItem = await importArchiveDatabase( const maybeDbItem = await importLocalDatabase(
createMockCommandManager(), createMockCommandManager(),
uri.toString(true), uri.toString(true),
databaseManager, databaseManager,

View File

@@ -101,6 +101,9 @@ describe("local databases", () => {
onStart: () => { onStart: () => {
/**/ /**/
}, },
onQueryRunStarting: () => {
/**/
},
}), }),
mockedObject<CodeQLCliServer>({ mockedObject<CodeQLCliServer>({
resolveDatabase: resolveDatabaseSpy, resolveDatabase: resolveDatabaseSpy,