Merge remote-tracking branch 'origin/main' into koesie10/show-extension-pack-name
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## [UNRELEASED]
|
## [UNRELEASED]
|
||||||
|
|
||||||
|
- Add new configuration option to allow downloading databases from http, non-secure servers. [#2332](https://github.com/github/vscode-codeql/pull/2332)
|
||||||
|
|
||||||
## 1.8.2 - 12 April 2023
|
## 1.8.2 - 12 April 2023
|
||||||
|
|
||||||
- Fix bug where users could end up with the managed CodeQL CLI getting uninstalled during upgrades and not reinstalled. [#2294](https://github.com/github/vscode-codeql/pull/2294)
|
- Fix bug where users could end up with the managed CodeQL CLI getting uninstalled during upgrades and not reinstalled. [#2294](https://github.com/github/vscode-codeql/pull/2294)
|
||||||
|
|||||||
4070
extensions/ql-vscode/package-lock.json
generated
4070
extensions/ql-vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -76,6 +76,48 @@
|
|||||||
"editor.wordBasedSuggestions": false
|
"editor.wordBasedSuggestions": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"debuggers": [
|
||||||
|
{
|
||||||
|
"type": "codeql",
|
||||||
|
"label": "CodeQL Debugger",
|
||||||
|
"languages": [
|
||||||
|
"ql"
|
||||||
|
],
|
||||||
|
"configurationAttributes": {
|
||||||
|
"launch": {
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to query file (.ql)",
|
||||||
|
"default": "${file}"
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to the target database"
|
||||||
|
},
|
||||||
|
"additionalPacks": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
|
"description": "Additional folders to search for library packs. Defaults to searching all workspace folders."
|
||||||
|
},
|
||||||
|
"extensionPacks": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
|
"description": "Names of extension packs to include in the evaluation. These are resolved from the locations specified in `additionalPacks`."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"variables": {
|
||||||
|
"currentDatabase": "codeQL.getCurrentDatabase",
|
||||||
|
"currentQuery": "codeQL.getCurrentQuery"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"jsonValidation": [
|
"jsonValidation": [
|
||||||
{
|
{
|
||||||
"fileMatch": "GitHub.vscode-codeql/databases.json",
|
"fileMatch": "GitHub.vscode-codeql/databases.json",
|
||||||
@@ -293,6 +335,11 @@
|
|||||||
"scope": "window",
|
"scope": "window",
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"description": "Report a warning for any join order whose metric exceeds this value."
|
"description": "Report a warning for any join order whose metric exceeds this value."
|
||||||
|
},
|
||||||
|
"codeQL.databaseDownload.allowHttp": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Allow database to be downloaded via HTTP. Warning: enabling this option will allow downloading from insecure servers."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -309,6 +356,30 @@
|
|||||||
"command": "codeQL.runQueryContextEditor",
|
"command": "codeQL.runQueryContextEditor",
|
||||||
"title": "CodeQL: Run Query on Selected Database"
|
"title": "CodeQL: Run Query on Selected Database"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.debugQuery",
|
||||||
|
"title": "CodeQL: Debug Query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.debugQueryContextEditor",
|
||||||
|
"title": "CodeQL: Debug Query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.startDebuggingSelection",
|
||||||
|
"title": "CodeQL: Debug Selection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.startDebuggingSelectionContextEditor",
|
||||||
|
"title": "CodeQL: Debug Selection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.continueDebuggingSelection",
|
||||||
|
"title": "CodeQL: Debug Selection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.continueDebuggingSelectionContextEditor",
|
||||||
|
"title": "CodeQL: Debug Selection"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "codeQL.runQueryOnMultipleDatabases",
|
"command": "codeQL.runQueryOnMultipleDatabases",
|
||||||
"title": "CodeQL: Run Query on Multiple Databases"
|
"title": "CodeQL: Run Query on Multiple Databases"
|
||||||
@@ -448,6 +519,14 @@
|
|||||||
"command": "codeQL.setCurrentDatabase",
|
"command": "codeQL.setCurrentDatabase",
|
||||||
"title": "CodeQL: Set Current Database"
|
"title": "CodeQL: Set Current Database"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.getCurrentDatabase",
|
||||||
|
"title": "CodeQL: Get Current Database"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.getCurrentQuery",
|
||||||
|
"title": "CodeQL: Get Current Query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "codeQL.viewAst",
|
"command": "codeQL.viewAst",
|
||||||
"title": "CodeQL: View AST"
|
"title": "CodeQL: View AST"
|
||||||
@@ -682,6 +761,10 @@
|
|||||||
"command": "codeQLTests.acceptOutput",
|
"command": "codeQLTests.acceptOutput",
|
||||||
"title": "Accept Test Output"
|
"title": "Accept Test Output"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQLTests.acceptOutputContextTestItem",
|
||||||
|
"title": "Accept Test Output"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "codeQLAstViewer.gotoCode",
|
"command": "codeQLAstViewer.gotoCode",
|
||||||
"title": "Go To Code"
|
"title": "Go To Code"
|
||||||
@@ -977,6 +1060,13 @@
|
|||||||
"when": "viewItem == testWithSource"
|
"when": "viewItem == testWithSource"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"testing/item/context": [
|
||||||
|
{
|
||||||
|
"command": "codeQLTests.acceptOutputContextTestItem",
|
||||||
|
"group": "qltest@1",
|
||||||
|
"when": "controllerId == codeql && testId =~ /^test /"
|
||||||
|
}
|
||||||
|
],
|
||||||
"explorer/context": [
|
"explorer/context": [
|
||||||
{
|
{
|
||||||
"command": "codeQL.setCurrentDatabase",
|
"command": "codeQL.setCurrentDatabase",
|
||||||
@@ -1022,6 +1112,30 @@
|
|||||||
"command": "codeQL.runQueryContextEditor",
|
"command": "codeQL.runQueryContextEditor",
|
||||||
"when": "false"
|
"when": "false"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.debugQuery",
|
||||||
|
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql && !inDebugMode"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.debugQueryContextEditor",
|
||||||
|
"when": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.startDebuggingSelection",
|
||||||
|
"when": "config.codeQL.canary && editorLangId == ql && debugState == inactive && debugConfigurationType == codeql"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.startDebuggingSelectionContextEditor",
|
||||||
|
"when": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.continueDebuggingSelection",
|
||||||
|
"when": "config.codeQL.canary && editorLangId == ql && debugState == stopped && debugType == codeql"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.continueDebuggingSelectionContextEditor",
|
||||||
|
"when": "false"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "codeQL.runQueryOnMultipleDatabases",
|
"command": "codeQL.runQueryOnMultipleDatabases",
|
||||||
"when": "resourceLangId == ql && resourceExtname == .ql"
|
"when": "resourceLangId == ql && resourceExtname == .ql"
|
||||||
@@ -1070,6 +1184,14 @@
|
|||||||
"command": "codeQL.setCurrentDatabase",
|
"command": "codeQL.setCurrentDatabase",
|
||||||
"when": "false"
|
"when": "false"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.getCurrentDatabase",
|
||||||
|
"when": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.getCurrentQuery",
|
||||||
|
"when": "false"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "codeQL.viewAst",
|
"command": "codeQL.viewAst",
|
||||||
"when": "resourceScheme == codeql-zip-archive"
|
"when": "resourceScheme == codeql-zip-archive"
|
||||||
@@ -1325,12 +1447,16 @@
|
|||||||
{
|
{
|
||||||
"command": "codeQL.createQuery",
|
"command": "codeQL.createQuery",
|
||||||
"when": "config.codeQL.canary"
|
"when": "config.codeQL.canary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQLTests.acceptOutputContextTestItem",
|
||||||
|
"when": "false"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"editor/context": [
|
"editor/context": [
|
||||||
{
|
{
|
||||||
"command": "codeQL.runQueryContextEditor",
|
"command": "codeQL.runQueryContextEditor",
|
||||||
"when": "editorLangId == ql && resourceExtname == .ql"
|
"when": "editorLangId == ql && resourceExtname == .ql && !inDebugMode"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "codeQL.runQueryOnMultipleDatabasesContextEditor",
|
"command": "codeQL.runQueryOnMultipleDatabasesContextEditor",
|
||||||
@@ -1350,7 +1476,19 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "codeQL.quickEvalContextEditor",
|
"command": "codeQL.quickEvalContextEditor",
|
||||||
"when": "editorLangId == ql"
|
"when": "editorLangId == ql && debugState == inactive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.debugQueryContextEditor",
|
||||||
|
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql && !inDebugMode"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.startDebuggingSelectionContextEditor",
|
||||||
|
"when": "config.codeQL.canary && editorLangId == ql && debugState == inactive && debugConfigurationType == codeql"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "codeQL.continueDebuggingSelectionContextEditor",
|
||||||
|
"when": "config.codeQL.canary && editorLangId == ql && debugState == stopped && debugType == codeql"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "codeQL.openReferencedFileContextEditor",
|
"command": "codeQL.openReferencedFileContextEditor",
|
||||||
@@ -1451,6 +1589,8 @@
|
|||||||
"@octokit/plugin-retry": "^3.0.9",
|
"@octokit/plugin-retry": "^3.0.9",
|
||||||
"@octokit/rest": "^19.0.4",
|
"@octokit/rest": "^19.0.4",
|
||||||
"@vscode/codicons": "^0.0.31",
|
"@vscode/codicons": "^0.0.31",
|
||||||
|
"@vscode/debugadapter": "^1.59.0",
|
||||||
|
"@vscode/debugprotocol": "^1.59.0",
|
||||||
"@vscode/webview-ui-toolkit": "^1.0.1",
|
"@vscode/webview-ui-toolkit": "^1.0.1",
|
||||||
"ajv": "^8.11.0",
|
"ajv": "^8.11.0",
|
||||||
"child-process-promise": "^2.2.1",
|
"child-process-promise": "^2.2.1",
|
||||||
@@ -1498,7 +1638,7 @@
|
|||||||
"@storybook/addon-essentials": "^6.5.17-alpha.0",
|
"@storybook/addon-essentials": "^6.5.17-alpha.0",
|
||||||
"@storybook/addon-interactions": "^6.5.17-alpha.0",
|
"@storybook/addon-interactions": "^6.5.17-alpha.0",
|
||||||
"@storybook/addon-links": "^6.5.17-alpha.0",
|
"@storybook/addon-links": "^6.5.17-alpha.0",
|
||||||
"@storybook/builder-webpack5": "^7.0.4",
|
"@storybook/builder-webpack5": "^6.5.17-alpha.0",
|
||||||
"@storybook/manager-webpack5": "^6.5.17-alpha.0",
|
"@storybook/manager-webpack5": "^6.5.17-alpha.0",
|
||||||
"@storybook/react": "^6.5.17-alpha.0",
|
"@storybook/react": "^6.5.17-alpha.0",
|
||||||
"@storybook/testing-library": "^0.0.13",
|
"@storybook/testing-library": "^0.0.13",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
getErrorStack,
|
getErrorStack,
|
||||||
} from "./pure/helpers-pure";
|
} from "./pure/helpers-pure";
|
||||||
import { QueryMetadata, SortDirection } from "./pure/interface-types";
|
import { QueryMetadata, SortDirection } from "./pure/interface-types";
|
||||||
import { Logger, ProgressReporter } from "./common";
|
import { BaseLogger, Logger, ProgressReporter } from "./common";
|
||||||
import { CompilationMessage } from "./pure/legacy-messages";
|
import { CompilationMessage } from "./pure/legacy-messages";
|
||||||
import { sarifParser } from "./sarif-parser";
|
import { sarifParser } from "./sarif-parser";
|
||||||
import { walkDirectory } from "./helpers";
|
import { walkDirectory } from "./helpers";
|
||||||
@@ -149,6 +149,7 @@ export interface TestCompleted {
|
|||||||
compilationMs: number;
|
compilationMs: number;
|
||||||
evaluationMs: number;
|
evaluationMs: number;
|
||||||
expected: string;
|
expected: string;
|
||||||
|
actual?: string;
|
||||||
diff: string[] | undefined;
|
diff: string[] | undefined;
|
||||||
failureDescription?: string;
|
failureDescription?: string;
|
||||||
failureStage?: string;
|
failureStage?: string;
|
||||||
@@ -439,7 +440,7 @@ export class CodeQLCliServer implements Disposable {
|
|||||||
command: string[],
|
command: string[],
|
||||||
commandArgs: string[],
|
commandArgs: string[],
|
||||||
cancellationToken?: CancellationToken,
|
cancellationToken?: CancellationToken,
|
||||||
logger?: Logger,
|
logger?: BaseLogger,
|
||||||
): AsyncGenerator<string, void, unknown> {
|
): AsyncGenerator<string, void, unknown> {
|
||||||
// Add format argument first, in case commandArgs contains positional parameters.
|
// Add format argument first, in case commandArgs contains positional parameters.
|
||||||
const args = [...command, "--format", "jsonz", ...commandArgs];
|
const args = [...command, "--format", "jsonz", ...commandArgs];
|
||||||
@@ -447,6 +448,11 @@ export class CodeQLCliServer implements Disposable {
|
|||||||
// Spawn the CodeQL process
|
// Spawn the CodeQL process
|
||||||
const codeqlPath = await this.getCodeQlPath();
|
const codeqlPath = await this.getCodeQlPath();
|
||||||
const childPromise = spawn(codeqlPath, args);
|
const childPromise = spawn(codeqlPath, args);
|
||||||
|
// Avoid a runtime message about unhandled rejection.
|
||||||
|
childPromise.catch(() => {
|
||||||
|
/**/
|
||||||
|
});
|
||||||
|
|
||||||
const child = childPromise.childProcess;
|
const child = childPromise.childProcess;
|
||||||
|
|
||||||
let cancellationRegistration: Disposable | undefined = undefined;
|
let cancellationRegistration: Disposable | undefined = undefined;
|
||||||
@@ -497,7 +503,7 @@ export class CodeQLCliServer implements Disposable {
|
|||||||
logger,
|
logger,
|
||||||
}: {
|
}: {
|
||||||
cancellationToken?: CancellationToken;
|
cancellationToken?: CancellationToken;
|
||||||
logger?: Logger;
|
logger?: BaseLogger;
|
||||||
} = {},
|
} = {},
|
||||||
): AsyncGenerator<EventType, void, unknown> {
|
): AsyncGenerator<EventType, void, unknown> {
|
||||||
for await (const event of this.runAsyncCodeQlCliCommandInternal(
|
for await (const event of this.runAsyncCodeQlCliCommandInternal(
|
||||||
@@ -776,7 +782,7 @@ export class CodeQLCliServer implements Disposable {
|
|||||||
logger,
|
logger,
|
||||||
}: {
|
}: {
|
||||||
cancellationToken?: CancellationToken;
|
cancellationToken?: CancellationToken;
|
||||||
logger?: Logger;
|
logger?: BaseLogger;
|
||||||
},
|
},
|
||||||
): AsyncGenerator<TestCompleted, void, unknown> {
|
): AsyncGenerator<TestCompleted, void, unknown> {
|
||||||
const subcommandArgs = this.cliConfig.additionalTestArguments.concat([
|
const subcommandArgs = this.cliConfig.additionalTestArguments.concat([
|
||||||
@@ -1661,7 +1667,7 @@ const lineEndings = ["\r\n", "\r", "\n"];
|
|||||||
* @param stream The stream to log.
|
* @param stream The stream to log.
|
||||||
* @param logger The logger that will consume the stream output.
|
* @param logger The logger that will consume the stream output.
|
||||||
*/
|
*/
|
||||||
async function logStream(stream: Readable, logger: Logger): Promise<void> {
|
async function logStream(stream: Readable, logger: BaseLogger): Promise<void> {
|
||||||
for await (const line of splitStreamAtSeparators(stream, lineEndings)) {
|
for await (const line of splitStreamAtSeparators(stream, lineEndings)) {
|
||||||
// Await the result of log here in order to ensure the logs are written in the correct order.
|
// Await the result of log here in order to ensure the logs are written in the correct order.
|
||||||
await logger.log(line);
|
await logger.log(line);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
VariantAnalysisScannedRepository,
|
VariantAnalysisScannedRepository,
|
||||||
VariantAnalysisScannedRepositoryResult,
|
VariantAnalysisScannedRepositoryResult,
|
||||||
} from "../variant-analysis/shared/variant-analysis";
|
} from "../variant-analysis/shared/variant-analysis";
|
||||||
|
import type { QLDebugConfiguration } from "../debugger/debug-configuration";
|
||||||
|
|
||||||
// A command function matching the signature that VS Code calls when
|
// A command function matching the signature that VS Code calls when
|
||||||
// a command is invoked from the title bar of a TreeView with
|
// a command is invoked from the title bar of a TreeView with
|
||||||
@@ -88,6 +89,15 @@ export type BuiltInVsCodeCommands = {
|
|||||||
"vscode.open": (uri: Uri) => Promise<void>;
|
"vscode.open": (uri: Uri) => Promise<void>;
|
||||||
"vscode.openFolder": (uri: Uri) => Promise<void>;
|
"vscode.openFolder": (uri: Uri) => Promise<void>;
|
||||||
revealInExplorer: (uri: Uri) => Promise<void>;
|
revealInExplorer: (uri: Uri) => Promise<void>;
|
||||||
|
// We type the `config` property specifically as a CodeQL debug configuration, since that's the
|
||||||
|
// only kinds we specify anyway.
|
||||||
|
"workbench.action.debug.start": (options?: {
|
||||||
|
config?: Partial<QLDebugConfiguration>;
|
||||||
|
noDebug?: boolean;
|
||||||
|
}) => Promise<void>;
|
||||||
|
"workbench.action.debug.stepInto": () => Promise<void>;
|
||||||
|
"workbench.action.debug.stepOver": () => Promise<void>;
|
||||||
|
"workbench.action.debug.stepOut": () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Commands that are available before the extension is fully activated.
|
// Commands that are available before the extension is fully activated.
|
||||||
@@ -135,9 +145,20 @@ export type LocalQueryCommands = {
|
|||||||
"codeQL.quickEvalContextEditor": (uri: Uri) => Promise<void>;
|
"codeQL.quickEvalContextEditor": (uri: Uri) => Promise<void>;
|
||||||
"codeQL.codeLensQuickEval": (uri: Uri, range: Range) => Promise<void>;
|
"codeQL.codeLensQuickEval": (uri: Uri, range: Range) => Promise<void>;
|
||||||
"codeQL.quickQuery": () => Promise<void>;
|
"codeQL.quickQuery": () => Promise<void>;
|
||||||
|
"codeQL.getCurrentQuery": () => Promise<string>;
|
||||||
"codeQL.createQuery": () => Promise<void>;
|
"codeQL.createQuery": () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Debugger commands
|
||||||
|
export type DebuggerCommands = {
|
||||||
|
"codeQL.debugQuery": (uri: Uri | undefined) => Promise<void>;
|
||||||
|
"codeQL.debugQueryContextEditor": (uri: Uri) => Promise<void>;
|
||||||
|
"codeQL.startDebuggingSelection": () => Promise<void>;
|
||||||
|
"codeQL.startDebuggingSelectionContextEditor": () => Promise<void>;
|
||||||
|
"codeQL.continueDebuggingSelection": () => Promise<void>;
|
||||||
|
"codeQL.continueDebuggingSelectionContextEditor": () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
export type ResultsViewCommands = {
|
export type ResultsViewCommands = {
|
||||||
"codeQLQueryResults.up": () => Promise<void>;
|
"codeQLQueryResults.up": () => Promise<void>;
|
||||||
"codeQLQueryResults.down": () => Promise<void>;
|
"codeQLQueryResults.down": () => Promise<void>;
|
||||||
@@ -220,6 +241,7 @@ export type LocalDatabasesCommands = {
|
|||||||
|
|
||||||
// Internal commands
|
// Internal commands
|
||||||
"codeQLDatabases.removeOrphanedDatabases": () => Promise<void>;
|
"codeQLDatabases.removeOrphanedDatabases": () => Promise<void>;
|
||||||
|
"codeQL.getCurrentDatabase": () => Promise<string | undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Commands tied to variant analysis
|
// Commands tied to variant analysis
|
||||||
@@ -299,6 +321,9 @@ export type SummaryLanguageSupportCommands = {
|
|||||||
export type TestUICommands = {
|
export type TestUICommands = {
|
||||||
"codeQLTests.showOutputDifferences": (node: TestTreeNode) => Promise<void>;
|
"codeQLTests.showOutputDifferences": (node: TestTreeNode) => Promise<void>;
|
||||||
"codeQLTests.acceptOutput": (node: TestTreeNode) => Promise<void>;
|
"codeQLTests.acceptOutput": (node: TestTreeNode) => Promise<void>;
|
||||||
|
"codeQLTests.acceptOutputContextTestItem": (
|
||||||
|
node: TestTreeNode,
|
||||||
|
) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MockGitHubApiServerCommands = {
|
export type MockGitHubApiServerCommands = {
|
||||||
@@ -315,6 +340,7 @@ export type AllExtensionCommands = BaseCommands &
|
|||||||
ResultsViewCommands &
|
ResultsViewCommands &
|
||||||
QueryHistoryCommands &
|
QueryHistoryCommands &
|
||||||
LocalDatabasesCommands &
|
LocalDatabasesCommands &
|
||||||
|
DebuggerCommands &
|
||||||
VariantAnalysisCommands &
|
VariantAnalysisCommands &
|
||||||
DatabasePanelCommands &
|
DatabasePanelCommands &
|
||||||
AstCfgCommands &
|
AstCfgCommands &
|
||||||
|
|||||||
@@ -608,3 +608,14 @@ export const CODESPACES_TEMPLATE = new Setting(
|
|||||||
export function isCodespacesTemplate() {
|
export function isCodespacesTemplate() {
|
||||||
return !!CODESPACES_TEMPLATE.getValue<boolean>();
|
return !!CODESPACES_TEMPLATE.getValue<boolean>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DATABASE_DOWNLOAD_SETTING = new Setting("databaseDownload", ROOT_SETTING);
|
||||||
|
|
||||||
|
export const ALLOW_HTTP_SETTING = new Setting(
|
||||||
|
"allowHttp",
|
||||||
|
DATABASE_DOWNLOAD_SETTING,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function allowHttp(): boolean {
|
||||||
|
return ALLOW_HTTP_SETTING.getValue<boolean>() || false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
showAndLogExceptionWithTelemetry,
|
showAndLogExceptionWithTelemetry,
|
||||||
} from "../helpers";
|
} from "../helpers";
|
||||||
import { extLogger } from "../common";
|
import { extLogger } from "../common";
|
||||||
import { outputFile, readFile } from "fs-extra";
|
import { outputFile, pathExists, readFile } from "fs-extra";
|
||||||
import { load as loadYaml } from "js-yaml";
|
import { load as loadYaml } from "js-yaml";
|
||||||
import { DatabaseItem, DatabaseManager } from "../local-databases";
|
import { DatabaseItem, DatabaseManager } from "../local-databases";
|
||||||
import { CodeQLCliServer } from "../cli";
|
import { CodeQLCliServer } from "../cli";
|
||||||
@@ -183,6 +183,10 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
|||||||
|
|
||||||
protected async loadExistingModeledMethods(): Promise<void> {
|
protected async loadExistingModeledMethods(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
if (!(await pathExists(this.modelFile.filename))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const yaml = await readFile(this.modelFile.filename, "utf8");
|
const yaml = await readFile(this.modelFile.filename, "utf8");
|
||||||
|
|
||||||
const data = loadYaml(yaml, {
|
const data = loadYaml(yaml, {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
} from "./common/github-url-identifier-helper";
|
} from "./common/github-url-identifier-helper";
|
||||||
import { Credentials } from "./common/authentication";
|
import { Credentials } from "./common/authentication";
|
||||||
import { AppCommandManager } from "./common/commands";
|
import { AppCommandManager } from "./common/commands";
|
||||||
|
import { ALLOW_HTTP_SETTING } from "./config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
|
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
|
||||||
@@ -49,7 +50,7 @@ export async function promptImportInternetDatabase(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
validateHttpsUrl(databaseUrl);
|
validateUrl(databaseUrl);
|
||||||
|
|
||||||
const item = await databaseArchiveFetcher(
|
const item = await databaseArchiveFetcher(
|
||||||
databaseUrl,
|
databaseUrl,
|
||||||
@@ -356,7 +357,7 @@ async function getStorageFolder(storagePath: string, urlStr: string) {
|
|||||||
return folderName;
|
return folderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateHttpsUrl(databaseUrl: string) {
|
function validateUrl(databaseUrl: string) {
|
||||||
let uri;
|
let uri;
|
||||||
try {
|
try {
|
||||||
uri = Uri.parse(databaseUrl, true);
|
uri = Uri.parse(databaseUrl, true);
|
||||||
@@ -364,7 +365,7 @@ function validateHttpsUrl(databaseUrl: string) {
|
|||||||
throw new Error(`Invalid url: ${databaseUrl}`);
|
throw new Error(`Invalid url: ${databaseUrl}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uri.scheme !== "https") {
|
if (!ALLOW_HTTP_SETTING.getValue() && uri.scheme !== "https") {
|
||||||
throw new Error("Must use https for downloading a database.");
|
throw new Error("Must use https for downloading a database.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
132
extensions/ql-vscode/src/debugger/debug-configuration.ts
Normal file
132
extensions/ql-vscode/src/debugger/debug-configuration.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import {
|
||||||
|
CancellationToken,
|
||||||
|
DebugConfiguration,
|
||||||
|
DebugConfigurationProvider,
|
||||||
|
WorkspaceFolder,
|
||||||
|
} from "vscode";
|
||||||
|
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers";
|
||||||
|
import { LocalQueries } from "../local-queries";
|
||||||
|
import { getQuickEvalContext, validateQueryPath } from "../run-queries-shared";
|
||||||
|
import * as CodeQLProtocol from "./debug-protocol";
|
||||||
|
import { getErrorMessage } from "../pure/helpers-pure";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CodeQL launch arguments, as specified in "launch.json".
|
||||||
|
*/
|
||||||
|
export interface QLDebugArgs {
|
||||||
|
query?: string;
|
||||||
|
database?: string;
|
||||||
|
additionalPacks?: string[] | string;
|
||||||
|
extensionPacks?: string[] | string;
|
||||||
|
quickEval?: boolean;
|
||||||
|
noDebug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The debug configuration for a CodeQL configuration.
|
||||||
|
*
|
||||||
|
* This just combines `QLDebugArgs` with the standard debug configuration properties.
|
||||||
|
*/
|
||||||
|
export type QLDebugConfiguration = DebugConfiguration & QLDebugArgs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A CodeQL debug configuration after all variables and defaults have been resolved. This is what
|
||||||
|
* is passed to the debug adapter via the `launch` request.
|
||||||
|
*/
|
||||||
|
export type QLResolvedDebugConfiguration = DebugConfiguration &
|
||||||
|
CodeQLProtocol.LaunchConfig;
|
||||||
|
|
||||||
|
/** If the specified value is a single element, then turn it into an array containing that element. */
|
||||||
|
function makeArray<T extends Exclude<any, any[]>>(value: T | T[]): T[] {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return [value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of `DebugConfigurationProvider` for CodeQL.
|
||||||
|
*/
|
||||||
|
export class QLDebugConfigurationProvider
|
||||||
|
implements DebugConfigurationProvider
|
||||||
|
{
|
||||||
|
public constructor(private readonly localQueries: LocalQueries) {}
|
||||||
|
|
||||||
|
public resolveDebugConfiguration(
|
||||||
|
_folder: WorkspaceFolder | undefined,
|
||||||
|
debugConfiguration: DebugConfiguration,
|
||||||
|
_token?: CancellationToken,
|
||||||
|
): DebugConfiguration {
|
||||||
|
const qlConfiguration = <QLDebugConfiguration>debugConfiguration;
|
||||||
|
|
||||||
|
// Fill in defaults for properties whose default value is a command invocation. VS Code will
|
||||||
|
// invoke any commands to fill in actual values, then call
|
||||||
|
// `resolveDebugConfigurationWithSubstitutedVariables()`with the result.
|
||||||
|
const resultConfiguration: QLDebugConfiguration = {
|
||||||
|
...qlConfiguration,
|
||||||
|
query: qlConfiguration.query ?? "${command:currentQuery}",
|
||||||
|
database: qlConfiguration.database ?? "${command:currentDatabase}",
|
||||||
|
};
|
||||||
|
|
||||||
|
return resultConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async resolveDebugConfigurationWithSubstitutedVariables(
|
||||||
|
_folder: WorkspaceFolder | undefined,
|
||||||
|
debugConfiguration: DebugConfiguration,
|
||||||
|
_token?: CancellationToken,
|
||||||
|
): Promise<DebugConfiguration | null> {
|
||||||
|
try {
|
||||||
|
const qlConfiguration = debugConfiguration as QLDebugConfiguration;
|
||||||
|
if (qlConfiguration.query === undefined) {
|
||||||
|
throw new Error("No query was specified in the debug configuration.");
|
||||||
|
}
|
||||||
|
if (qlConfiguration.database === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
"No database was specified in the debug configuration.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in defaults here, instead of in `resolveDebugConfiguration`, to avoid the highly
|
||||||
|
// unusual case where one of the computed default values looks like a variable substitution.
|
||||||
|
const additionalPacks = makeArray(
|
||||||
|
qlConfiguration.additionalPacks ?? getOnDiskWorkspaceFolders(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Default to computing the extension packs based on the extension configuration and the search
|
||||||
|
// path.
|
||||||
|
const extensionPacks = makeArray(
|
||||||
|
qlConfiguration.extensionPacks ??
|
||||||
|
(await this.localQueries.getDefaultExtensionPacks(additionalPacks)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const quickEval = qlConfiguration.quickEval ?? false;
|
||||||
|
validateQueryPath(qlConfiguration.query, quickEval);
|
||||||
|
|
||||||
|
const quickEvalContext = quickEval
|
||||||
|
? await getQuickEvalContext(undefined)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const resultConfiguration: QLResolvedDebugConfiguration = {
|
||||||
|
name: qlConfiguration.name,
|
||||||
|
request: qlConfiguration.request,
|
||||||
|
type: qlConfiguration.type,
|
||||||
|
query: qlConfiguration.query,
|
||||||
|
database: qlConfiguration.database,
|
||||||
|
additionalPacks,
|
||||||
|
extensionPacks,
|
||||||
|
quickEvalContext,
|
||||||
|
noDebug: qlConfiguration.noDebug ?? false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return resultConfiguration;
|
||||||
|
} catch (e) {
|
||||||
|
// Any unhandled exception will result in an OS-native error message box, which seems ugly.
|
||||||
|
// We'll just show a real VS Code error message, then return null to prevent the debug session
|
||||||
|
// from starting.
|
||||||
|
void showAndLogErrorMessage(getErrorMessage(e));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
102
extensions/ql-vscode/src/debugger/debug-protocol.ts
Normal file
102
extensions/ql-vscode/src/debugger/debug-protocol.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { DebugProtocol } from "@vscode/debugprotocol";
|
||||||
|
import { QueryResultType } from "../pure/new-messages";
|
||||||
|
import { QuickEvalContext } from "../run-queries-shared";
|
||||||
|
|
||||||
|
// Events
|
||||||
|
|
||||||
|
export type Event = { type: "event" };
|
||||||
|
|
||||||
|
export type StoppedEvent = DebugProtocol.StoppedEvent &
|
||||||
|
Event & { event: "stopped" };
|
||||||
|
|
||||||
|
export type InitializedEvent = DebugProtocol.InitializedEvent &
|
||||||
|
Event & { event: "initialized" };
|
||||||
|
|
||||||
|
export type ExitedEvent = DebugProtocol.ExitedEvent &
|
||||||
|
Event & { event: "exited" };
|
||||||
|
|
||||||
|
export type OutputEvent = DebugProtocol.OutputEvent &
|
||||||
|
Event & { event: "output" };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom event to provide additional information about a running evaluation.
|
||||||
|
*/
|
||||||
|
export interface EvaluationStartedEvent extends Event {
|
||||||
|
event: "codeql-evaluation-started";
|
||||||
|
body: {
|
||||||
|
id: string;
|
||||||
|
outputDir: string;
|
||||||
|
quickEvalContext: QuickEvalContext | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom event to provide additional information about a completed evaluation.
|
||||||
|
*/
|
||||||
|
export interface EvaluationCompletedEvent extends Event {
|
||||||
|
event: "codeql-evaluation-completed";
|
||||||
|
body: {
|
||||||
|
resultType: QueryResultType;
|
||||||
|
message: string | undefined;
|
||||||
|
evaluationTime: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnyEvent =
|
||||||
|
| StoppedEvent
|
||||||
|
| ExitedEvent
|
||||||
|
| InitializedEvent
|
||||||
|
| OutputEvent
|
||||||
|
| EvaluationStartedEvent
|
||||||
|
| EvaluationCompletedEvent;
|
||||||
|
|
||||||
|
// Requests
|
||||||
|
|
||||||
|
export type Request = DebugProtocol.Request & { type: "request" };
|
||||||
|
|
||||||
|
export type InitializeRequest = DebugProtocol.InitializeRequest &
|
||||||
|
Request & { command: "initialize" };
|
||||||
|
|
||||||
|
export interface LaunchConfig {
|
||||||
|
/** Full path to query (.ql) file. */
|
||||||
|
query: string;
|
||||||
|
/** Full path to the database directory. */
|
||||||
|
database: string;
|
||||||
|
/** Full paths to `--additional-packs` directories. */
|
||||||
|
additionalPacks: string[];
|
||||||
|
/** Pack names of extension packs. */
|
||||||
|
extensionPacks: string[];
|
||||||
|
/** Optional quick evaluation context. */
|
||||||
|
quickEvalContext: QuickEvalContext | undefined;
|
||||||
|
/** Run the query without debugging it. */
|
||||||
|
noDebug: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LaunchRequest extends Request, DebugProtocol.LaunchRequest {
|
||||||
|
type: "request";
|
||||||
|
command: "launch";
|
||||||
|
arguments: DebugProtocol.LaunchRequestArguments & LaunchConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuickEvalRequest extends Request {
|
||||||
|
command: "codeql-quickeval";
|
||||||
|
arguments: {
|
||||||
|
quickEvalContext: QuickEvalContext;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnyRequest = InitializeRequest | LaunchRequest | QuickEvalRequest;
|
||||||
|
|
||||||
|
// Responses
|
||||||
|
|
||||||
|
export type Response = DebugProtocol.Response & { type: "response" };
|
||||||
|
|
||||||
|
export type InitializeResponse = DebugProtocol.InitializeResponse &
|
||||||
|
Response & { command: "initialize" };
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
export interface QuickEvalResponse extends Response {}
|
||||||
|
|
||||||
|
export type AnyResponse = InitializeResponse | QuickEvalResponse;
|
||||||
|
|
||||||
|
export type AnyProtocolMessage = AnyEvent | AnyRequest | AnyResponse;
|
||||||
617
extensions/ql-vscode/src/debugger/debug-session.ts
Normal file
617
extensions/ql-vscode/src/debugger/debug-session.ts
Normal file
@@ -0,0 +1,617 @@
|
|||||||
|
import {
|
||||||
|
ContinuedEvent,
|
||||||
|
Event,
|
||||||
|
ExitedEvent,
|
||||||
|
InitializedEvent,
|
||||||
|
LoggingDebugSession,
|
||||||
|
OutputEvent,
|
||||||
|
ProgressEndEvent,
|
||||||
|
StoppedEvent,
|
||||||
|
TerminatedEvent,
|
||||||
|
} from "@vscode/debugadapter";
|
||||||
|
import { DebugProtocol as Protocol } from "@vscode/debugprotocol";
|
||||||
|
import { Disposable } from "vscode";
|
||||||
|
import { CancellationTokenSource } from "vscode-jsonrpc";
|
||||||
|
import { BaseLogger, LogOptions, queryServerLogger } from "../common";
|
||||||
|
import { QueryResultType } from "../pure/new-messages";
|
||||||
|
import { CoreQueryResults, CoreQueryRun, QueryRunner } from "../queryRunner";
|
||||||
|
import * as CodeQLProtocol from "./debug-protocol";
|
||||||
|
import { QuickEvalContext } from "../run-queries-shared";
|
||||||
|
import { getErrorMessage } from "../pure/helpers-pure";
|
||||||
|
import { DisposableObject } from "../pure/disposable-object";
|
||||||
|
|
||||||
|
// More complete implementations of `Event` for certain events, because the classes from
|
||||||
|
// `@vscode/debugadapter` make it more difficult to provide some of the message values.
|
||||||
|
|
||||||
|
class ProgressStartEvent extends Event implements Protocol.ProgressStartEvent {
|
||||||
|
public readonly event = "progressStart";
|
||||||
|
public readonly body: {
|
||||||
|
progressId: string;
|
||||||
|
title: string;
|
||||||
|
requestId?: number;
|
||||||
|
cancellable?: boolean;
|
||||||
|
message?: string;
|
||||||
|
percentage?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
progressId: string,
|
||||||
|
title: string,
|
||||||
|
message?: string,
|
||||||
|
percentage?: number,
|
||||||
|
) {
|
||||||
|
super("progressStart");
|
||||||
|
this.body = {
|
||||||
|
progressId,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
percentage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProgressUpdateEvent
|
||||||
|
extends Event
|
||||||
|
implements Protocol.ProgressUpdateEvent
|
||||||
|
{
|
||||||
|
public readonly event = "progressUpdate";
|
||||||
|
public readonly body: {
|
||||||
|
progressId: string;
|
||||||
|
message?: string;
|
||||||
|
percentage?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(progressId: string, message?: string, percentage?: number) {
|
||||||
|
super("progressUpdate");
|
||||||
|
this.body = {
|
||||||
|
progressId,
|
||||||
|
message,
|
||||||
|
percentage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EvaluationStartedEvent
|
||||||
|
extends Event
|
||||||
|
implements CodeQLProtocol.EvaluationStartedEvent
|
||||||
|
{
|
||||||
|
public readonly type = "event";
|
||||||
|
public readonly event = "codeql-evaluation-started";
|
||||||
|
public readonly body: CodeQLProtocol.EvaluationStartedEvent["body"];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
id: string,
|
||||||
|
outputDir: string,
|
||||||
|
quickEvalContext: QuickEvalContext | undefined,
|
||||||
|
) {
|
||||||
|
super("codeql-evaluation-started");
|
||||||
|
this.body = {
|
||||||
|
id,
|
||||||
|
outputDir,
|
||||||
|
quickEvalContext,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EvaluationCompletedEvent
|
||||||
|
extends Event
|
||||||
|
implements CodeQLProtocol.EvaluationCompletedEvent
|
||||||
|
{
|
||||||
|
public readonly type = "event";
|
||||||
|
public readonly event = "codeql-evaluation-completed";
|
||||||
|
public readonly body: CodeQLProtocol.EvaluationCompletedEvent["body"];
|
||||||
|
|
||||||
|
constructor(results: CoreQueryResults) {
|
||||||
|
super("codeql-evaluation-completed");
|
||||||
|
this.body = results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible states of the debug session. Used primarily to guard against unexpected requests.
|
||||||
|
*/
|
||||||
|
type State =
|
||||||
|
| "uninitialized"
|
||||||
|
| "initialized"
|
||||||
|
| "running"
|
||||||
|
| "stopped"
|
||||||
|
| "terminated";
|
||||||
|
|
||||||
|
// IDs for error messages generated by the debug adapter itself.
|
||||||
|
|
||||||
|
/** Received a DAP message while in an unexpected state. */
|
||||||
|
const ERROR_UNEXPECTED_STATE = 1;
|
||||||
|
|
||||||
|
/** ID of the "thread" that represents the query evaluation. */
|
||||||
|
const QUERY_THREAD_ID = 1;
|
||||||
|
|
||||||
|
/** The user-visible name of the query evaluation thread. */
|
||||||
|
const QUERY_THREAD_NAME = "Evaluation thread";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An active query evaluation within a debug session.
|
||||||
|
*
|
||||||
|
* This class encapsulates the state and resources associated with the running query, to avoid
|
||||||
|
* having multiple properties within `QLDebugSession` that are only defined during query evaluation.
|
||||||
|
*/
|
||||||
|
class RunningQuery extends DisposableObject {
|
||||||
|
private readonly tokenSource = this.push(new CancellationTokenSource());
|
||||||
|
public readonly queryRun: CoreQueryRun;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
queryRunner: QueryRunner,
|
||||||
|
config: CodeQLProtocol.LaunchConfig,
|
||||||
|
private readonly quickEvalContext: QuickEvalContext | undefined,
|
||||||
|
queryStorageDir: string,
|
||||||
|
private readonly logger: BaseLogger,
|
||||||
|
private readonly sendEvent: (event: Event) => void,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Create the query run, which will give us some information about the query even before the
|
||||||
|
// evaluation has completed.
|
||||||
|
this.queryRun = queryRunner.createQueryRun(
|
||||||
|
config.database,
|
||||||
|
{
|
||||||
|
queryPath: config.query,
|
||||||
|
quickEvalPosition: quickEvalContext?.quickEvalPosition,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
config.additionalPacks,
|
||||||
|
config.extensionPacks,
|
||||||
|
queryStorageDir,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get id(): string {
|
||||||
|
return this.queryRun.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the query, firing progress events along the way. The evaluation can be cancelled by
|
||||||
|
* calling `cancel()`.
|
||||||
|
*
|
||||||
|
* This function does not throw exceptions to report query evaluation failure. It just returns an
|
||||||
|
* evaluation result with a failure message instead.
|
||||||
|
*/
|
||||||
|
public async evaluate(): Promise<
|
||||||
|
CodeQLProtocol.EvaluationCompletedEvent["body"]
|
||||||
|
> {
|
||||||
|
// Send the `EvaluationStarted` event first, to let the client known where the outputs are
|
||||||
|
// going to show up.
|
||||||
|
this.sendEvent(
|
||||||
|
new EvaluationStartedEvent(
|
||||||
|
this.queryRun.id,
|
||||||
|
this.queryRun.outputDir.querySaveDir,
|
||||||
|
this.quickEvalContext,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Report progress via the debugger protocol.
|
||||||
|
const progressStart = new ProgressStartEvent(
|
||||||
|
this.queryRun.id,
|
||||||
|
"Running query",
|
||||||
|
undefined,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
progressStart.body.cancellable = true;
|
||||||
|
this.sendEvent(progressStart);
|
||||||
|
try {
|
||||||
|
return await this.queryRun.evaluate(
|
||||||
|
(p) => {
|
||||||
|
const progressUpdate = new ProgressUpdateEvent(
|
||||||
|
this.queryRun.id,
|
||||||
|
p.message,
|
||||||
|
(p.step * 100) / p.maxStep,
|
||||||
|
);
|
||||||
|
this.sendEvent(progressUpdate);
|
||||||
|
},
|
||||||
|
this.tokenSource.token,
|
||||||
|
this.logger,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.sendEvent(new ProgressEndEvent(this.queryRun.id));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const message = getErrorMessage(e);
|
||||||
|
return {
|
||||||
|
resultType: QueryResultType.OTHER_ERROR,
|
||||||
|
message,
|
||||||
|
evaluationTime: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to cancel the running evaluation.
|
||||||
|
*/
|
||||||
|
public cancel(): void {
|
||||||
|
this.tokenSource.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An in-process implementation of the debug adapter for CodeQL queries.
|
||||||
|
*
|
||||||
|
* For now, this is pretty much just a wrapper around the query server.
|
||||||
|
*/
|
||||||
|
export class QLDebugSession extends LoggingDebugSession implements Disposable {
|
||||||
|
/** A `BaseLogger` that sends output to the debug console. */
|
||||||
|
private readonly logger: BaseLogger = {
|
||||||
|
log: async (message: string, _options: LogOptions): Promise<void> => {
|
||||||
|
// Only send the output event if we're still connected to the query evaluation.
|
||||||
|
if (this.runningQuery !== undefined) {
|
||||||
|
this.sendEvent(new OutputEvent(message, "console"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
private state: State = "uninitialized";
|
||||||
|
private terminateOnComplete = false;
|
||||||
|
private args: CodeQLProtocol.LaunchRequest["arguments"] | undefined =
|
||||||
|
undefined;
|
||||||
|
private runningQuery: RunningQuery | undefined = undefined;
|
||||||
|
private lastResultType: QueryResultType = QueryResultType.CANCELLATION;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly queryStorageDir: string,
|
||||||
|
private readonly queryRunner: QueryRunner,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
if (this.runningQuery !== undefined) {
|
||||||
|
this.runningQuery.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected dispatchRequest(request: Protocol.Request): void {
|
||||||
|
// We just defer to the base class implementation, but having this override makes it easy to set
|
||||||
|
// a breakpoint that will be hit for any message received by the debug adapter.
|
||||||
|
void queryServerLogger.log(`DAP request: ${request.command}`);
|
||||||
|
super.dispatchRequest(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unexpectedState(response: Protocol.Response): void {
|
||||||
|
this.sendErrorResponse(
|
||||||
|
response,
|
||||||
|
ERROR_UNEXPECTED_STATE,
|
||||||
|
"CodeQL debug adapter received request '{_request}' while in unexpected state '{_actualState}'.",
|
||||||
|
{
|
||||||
|
_request: response.command,
|
||||||
|
_actualState: this.state,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initializeRequest(
|
||||||
|
response: Protocol.InitializeResponse,
|
||||||
|
_args: Protocol.InitializeRequestArguments,
|
||||||
|
): void {
|
||||||
|
switch (this.state) {
|
||||||
|
case "uninitialized":
|
||||||
|
response.body = response.body ?? {};
|
||||||
|
response.body.supportsStepBack = false;
|
||||||
|
response.body.supportsStepInTargetsRequest = false;
|
||||||
|
response.body.supportsRestartFrame = false;
|
||||||
|
response.body.supportsGotoTargetsRequest = false;
|
||||||
|
response.body.supportsCancelRequest = true;
|
||||||
|
response.body.supportsTerminateRequest = true;
|
||||||
|
response.body.supportsModulesRequest = false;
|
||||||
|
response.body.supportsConfigurationDoneRequest = true;
|
||||||
|
response.body.supportsRestartRequest = false;
|
||||||
|
this.state = "initialized";
|
||||||
|
this.sendResponse(response);
|
||||||
|
|
||||||
|
this.sendEvent(new InitializedEvent());
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.unexpectedState(response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected disconnectRequest(
|
||||||
|
response: Protocol.DisconnectResponse,
|
||||||
|
_args: Protocol.DisconnectArguments,
|
||||||
|
_request?: Protocol.Request,
|
||||||
|
): void {
|
||||||
|
// The client is forcing a disconnect. We'll signal cancellation, but since this request means
|
||||||
|
// that the debug session itself is about to go away, we'll stop processing events from the
|
||||||
|
// evaluation to avoid sending them to the client that is no longer interested in them.
|
||||||
|
this.terminateOrDisconnect(response, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected terminateRequest(
|
||||||
|
response: Protocol.TerminateResponse,
|
||||||
|
_args: Protocol.TerminateArguments,
|
||||||
|
_request?: Protocol.Request,
|
||||||
|
): void {
|
||||||
|
// The client is requesting a graceful termination. This will signal the cancellation token of
|
||||||
|
// any in-progress evaluation, but that evaluation will continue to report events (like
|
||||||
|
// progress) until the cancellation takes effect.
|
||||||
|
this.terminateOrDisconnect(response, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private terminateOrDisconnect(
|
||||||
|
response: Protocol.Response,
|
||||||
|
force: boolean,
|
||||||
|
): void {
|
||||||
|
const runningQuery = this.runningQuery;
|
||||||
|
if (force) {
|
||||||
|
// Disconnect from the running query so that we stop processing its progress events.
|
||||||
|
this.runningQuery = undefined;
|
||||||
|
}
|
||||||
|
if (runningQuery !== undefined) {
|
||||||
|
this.terminateOnComplete = true;
|
||||||
|
runningQuery.cancel();
|
||||||
|
} else if (this.state === "stopped") {
|
||||||
|
this.terminateAndExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected launchRequest(
|
||||||
|
response: Protocol.LaunchResponse,
|
||||||
|
args: CodeQLProtocol.LaunchRequest["arguments"],
|
||||||
|
_request?: Protocol.Request,
|
||||||
|
): void {
|
||||||
|
switch (this.state) {
|
||||||
|
case "initialized":
|
||||||
|
this.args = args;
|
||||||
|
|
||||||
|
// If `noDebug` is set, then terminate after evaluation instead of stopping.
|
||||||
|
this.terminateOnComplete = this.args.noDebug === true;
|
||||||
|
|
||||||
|
response.body = response.body ?? {};
|
||||||
|
|
||||||
|
// Send the response immediately. We'll send a "stopped" message when the evaluation is complete.
|
||||||
|
this.sendResponse(response);
|
||||||
|
|
||||||
|
void this.evaluate(this.args.quickEvalContext);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.unexpectedState(response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected nextRequest(
|
||||||
|
response: Protocol.NextResponse,
|
||||||
|
_args: Protocol.NextArguments,
|
||||||
|
_request?: Protocol.Request,
|
||||||
|
): void {
|
||||||
|
this.stepRequest(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected stepInRequest(
|
||||||
|
response: Protocol.StepInResponse,
|
||||||
|
_args: Protocol.StepInArguments,
|
||||||
|
_request?: Protocol.Request,
|
||||||
|
): void {
|
||||||
|
this.stepRequest(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected stepOutRequest(
|
||||||
|
response: Protocol.Response,
|
||||||
|
_args: Protocol.StepOutArguments,
|
||||||
|
_request?: Protocol.Request,
|
||||||
|
): void {
|
||||||
|
this.stepRequest(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected stepBackRequest(
|
||||||
|
response: Protocol.StepBackResponse,
|
||||||
|
_args: Protocol.StepBackArguments,
|
||||||
|
_request?: Protocol.Request,
|
||||||
|
): void {
|
||||||
|
this.stepRequest(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stepRequest(response: Protocol.Response): void {
|
||||||
|
switch (this.state) {
|
||||||
|
case "stopped":
|
||||||
|
this.sendResponse(response);
|
||||||
|
// We don't do anything with stepping yet, so just announce that we've stopped without
|
||||||
|
// actually doing anything.
|
||||||
|
// We don't even send the `EvaluationCompletedEvent`.
|
||||||
|
this.reportStopped();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.unexpectedState(response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected continueRequest(
|
||||||
|
response: Protocol.ContinueResponse,
|
||||||
|
_args: Protocol.ContinueArguments,
|
||||||
|
_request?: Protocol.Request,
|
||||||
|
): void {
|
||||||
|
switch (this.state) {
|
||||||
|
case "stopped":
|
||||||
|
response.body = response.body ?? {};
|
||||||
|
response.body.allThreadsContinued = true;
|
||||||
|
|
||||||
|
// Send the response immediately. We'll send a "stopped" message when the evaluation is complete.
|
||||||
|
this.sendResponse(response);
|
||||||
|
|
||||||
|
void this.evaluate(undefined);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.unexpectedState(response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected cancelRequest(
|
||||||
|
response: Protocol.CancelResponse,
|
||||||
|
args: Protocol.CancelArguments,
|
||||||
|
_request?: Protocol.Request,
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
args.progressId !== undefined &&
|
||||||
|
this.runningQuery?.id === args.progressId
|
||||||
|
) {
|
||||||
|
this.runningQuery.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected threadsRequest(
|
||||||
|
response: Protocol.ThreadsResponse,
|
||||||
|
_request?: Protocol.Request,
|
||||||
|
): void {
|
||||||
|
response.body = response.body ?? {};
|
||||||
|
response.body.threads = [
|
||||||
|
{
|
||||||
|
id: QUERY_THREAD_ID,
|
||||||
|
name: QUERY_THREAD_NAME,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
this.sendResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected stackTraceRequest(
|
||||||
|
response: Protocol.StackTraceResponse,
|
||||||
|
_args: Protocol.StackTraceArguments,
|
||||||
|
_request?: Protocol.Request,
|
||||||
|
): void {
|
||||||
|
response.body = response.body ?? {};
|
||||||
|
response.body.stackFrames = []; // No frames for now.
|
||||||
|
|
||||||
|
super.stackTraceRequest(response, _args, _request);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected customRequest(
|
||||||
|
command: string,
|
||||||
|
response: CodeQLProtocol.Response,
|
||||||
|
args: any,
|
||||||
|
request?: Protocol.Request,
|
||||||
|
): void {
|
||||||
|
switch (command) {
|
||||||
|
case "codeql-quickeval": {
|
||||||
|
this.quickEvalRequest(
|
||||||
|
response,
|
||||||
|
args as CodeQLProtocol.QuickEvalRequest["arguments"],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
super.customRequest(command, response, args, request);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected quickEvalRequest(
|
||||||
|
response: CodeQLProtocol.QuickEvalResponse,
|
||||||
|
args: CodeQLProtocol.QuickEvalRequest["arguments"],
|
||||||
|
): void {
|
||||||
|
switch (this.state) {
|
||||||
|
case "stopped":
|
||||||
|
// Send the response immediately. We'll send a "stopped" message when the evaluation is complete.
|
||||||
|
this.sendResponse(response);
|
||||||
|
|
||||||
|
// For built-in requests that are expected to cause execution (`launch`, `continue`, `step`, etc.),
|
||||||
|
// the adapter does not send a `continued` event because the client already knows that's what
|
||||||
|
// is supposed to happen. For a custom request, though, we have to notify the client.
|
||||||
|
this.sendEvent(new ContinuedEvent(QUERY_THREAD_ID, true));
|
||||||
|
|
||||||
|
void this.evaluate(args.quickEvalContext);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.unexpectedState(response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the query or quickeval, and notifies the debugger client when the evaluation completes.
|
||||||
|
*
|
||||||
|
* This function is invoked from the `launch` and `continue` handlers, without awaiting its
|
||||||
|
* result.
|
||||||
|
*/
|
||||||
|
private async evaluate(
|
||||||
|
quickEvalContext: QuickEvalContext | undefined,
|
||||||
|
): Promise<void> {
|
||||||
|
const args = this.args!;
|
||||||
|
|
||||||
|
const runningQuery = new RunningQuery(
|
||||||
|
this.queryRunner,
|
||||||
|
args,
|
||||||
|
quickEvalContext,
|
||||||
|
this.queryStorageDir,
|
||||||
|
this.logger,
|
||||||
|
(event) => {
|
||||||
|
// If `this.runningQuery` is undefined, it means that we've already disconnected from this
|
||||||
|
// evaluation, and do not want any further events.
|
||||||
|
if (this.runningQuery !== undefined) {
|
||||||
|
this.sendEvent(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.runningQuery = runningQuery;
|
||||||
|
this.state = "running";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await runningQuery.evaluate();
|
||||||
|
this.completeEvaluation(result);
|
||||||
|
} finally {
|
||||||
|
this.runningQuery = undefined;
|
||||||
|
runningQuery.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the evaluation as completed, and notify the client of the result.
|
||||||
|
*/
|
||||||
|
private completeEvaluation(
|
||||||
|
result: CodeQLProtocol.EvaluationCompletedEvent["body"],
|
||||||
|
): void {
|
||||||
|
this.lastResultType = result.resultType;
|
||||||
|
|
||||||
|
// Report the evaluation result
|
||||||
|
this.sendEvent(new EvaluationCompletedEvent(result));
|
||||||
|
if (result.resultType !== QueryResultType.SUCCESS) {
|
||||||
|
// Report the result message as "important" output
|
||||||
|
const message = result.message ?? "Unknown error";
|
||||||
|
const outputEvent = new OutputEvent(message, "console");
|
||||||
|
this.sendEvent(outputEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reportStopped();
|
||||||
|
}
|
||||||
|
|
||||||
|
private reportStopped(): void {
|
||||||
|
if (this.terminateOnComplete) {
|
||||||
|
this.terminateAndExit();
|
||||||
|
} else {
|
||||||
|
// Report the session as "stopped", but keep the session open.
|
||||||
|
this.sendEvent(new StoppedEvent("entry", QUERY_THREAD_ID));
|
||||||
|
|
||||||
|
this.state = "stopped";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private terminateAndExit(): void {
|
||||||
|
// Report the debugging session as terminated.
|
||||||
|
this.sendEvent(new TerminatedEvent());
|
||||||
|
|
||||||
|
// Report the debuggee as exited.
|
||||||
|
this.sendEvent(new ExitedEvent(this.lastResultType));
|
||||||
|
|
||||||
|
this.state = "terminated";
|
||||||
|
}
|
||||||
|
}
|
||||||
50
extensions/ql-vscode/src/debugger/debugger-factory.ts
Normal file
50
extensions/ql-vscode/src/debugger/debugger-factory.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
debug,
|
||||||
|
DebugAdapterDescriptor,
|
||||||
|
DebugAdapterDescriptorFactory,
|
||||||
|
DebugAdapterExecutable,
|
||||||
|
DebugAdapterInlineImplementation,
|
||||||
|
DebugConfigurationProviderTriggerKind,
|
||||||
|
DebugSession,
|
||||||
|
ProviderResult,
|
||||||
|
} from "vscode";
|
||||||
|
import { isCanary } from "../config";
|
||||||
|
import { LocalQueries } from "../local-queries";
|
||||||
|
import { DisposableObject } from "../pure/disposable-object";
|
||||||
|
import { QueryRunner } from "../queryRunner";
|
||||||
|
import { QLDebugConfigurationProvider } from "./debug-configuration";
|
||||||
|
import { QLDebugSession } from "./debug-session";
|
||||||
|
|
||||||
|
export class QLDebugAdapterDescriptorFactory
|
||||||
|
extends DisposableObject
|
||||||
|
implements DebugAdapterDescriptorFactory
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly queryStorageDir: string,
|
||||||
|
private readonly queryRunner: QueryRunner,
|
||||||
|
localQueries: LocalQueries,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.push(debug.registerDebugAdapterDescriptorFactory("codeql", this));
|
||||||
|
this.push(
|
||||||
|
debug.registerDebugConfigurationProvider(
|
||||||
|
"codeql",
|
||||||
|
new QLDebugConfigurationProvider(localQueries),
|
||||||
|
DebugConfigurationProviderTriggerKind.Dynamic,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public createDebugAdapterDescriptor(
|
||||||
|
_session: DebugSession,
|
||||||
|
_executable: DebugAdapterExecutable | undefined,
|
||||||
|
): ProviderResult<DebugAdapterDescriptor> {
|
||||||
|
if (!isCanary()) {
|
||||||
|
throw new Error("The CodeQL debugger feature is not available yet.");
|
||||||
|
}
|
||||||
|
return new DebugAdapterInlineImplementation(
|
||||||
|
new QLDebugSession(this.queryStorageDir, this.queryRunner),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
235
extensions/ql-vscode/src/debugger/debugger-ui.ts
Normal file
235
extensions/ql-vscode/src/debugger/debugger-ui.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { basename } from "path";
|
||||||
|
import {
|
||||||
|
DebugAdapterTracker,
|
||||||
|
DebugAdapterTrackerFactory,
|
||||||
|
DebugSession,
|
||||||
|
debug,
|
||||||
|
Uri,
|
||||||
|
CancellationTokenSource,
|
||||||
|
} from "vscode";
|
||||||
|
import { DebuggerCommands } from "../common/commands";
|
||||||
|
import { DatabaseManager } from "../local-databases";
|
||||||
|
import { LocalQueries, LocalQueryRun } from "../local-queries";
|
||||||
|
import { DisposableObject } from "../pure/disposable-object";
|
||||||
|
import { CoreQueryResults } from "../queryRunner";
|
||||||
|
import {
|
||||||
|
getQuickEvalContext,
|
||||||
|
QueryOutputDir,
|
||||||
|
validateQueryUri,
|
||||||
|
} from "../run-queries-shared";
|
||||||
|
import { QLResolvedDebugConfiguration } from "./debug-configuration";
|
||||||
|
import * as CodeQLProtocol from "./debug-protocol";
|
||||||
|
import { App } from "../common/app";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens to messages passing between VS Code and the debug adapter, so that we can supplement the
|
||||||
|
* UI.
|
||||||
|
*/
|
||||||
|
class QLDebugAdapterTracker
|
||||||
|
extends DisposableObject
|
||||||
|
implements DebugAdapterTracker
|
||||||
|
{
|
||||||
|
private readonly configuration: QLResolvedDebugConfiguration;
|
||||||
|
/** The `LocalQueryRun` of the current evaluation, if one is running. */
|
||||||
|
private localQueryRun: LocalQueryRun | undefined;
|
||||||
|
/** The promise of the most recently queued deferred message handler. */
|
||||||
|
private lastDeferredMessageHandler: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly session: DebugSession,
|
||||||
|
private readonly ui: DebuggerUI,
|
||||||
|
private readonly localQueries: LocalQueries,
|
||||||
|
private readonly dbm: DatabaseManager,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.configuration = <QLResolvedDebugConfiguration>session.configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDidSendMessage(message: CodeQLProtocol.AnyProtocolMessage): void {
|
||||||
|
if (message.type === "event") {
|
||||||
|
switch (message.event) {
|
||||||
|
case "codeql-evaluation-started":
|
||||||
|
this.queueMessageHandler(() =>
|
||||||
|
this.onEvaluationStarted(message.body),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "codeql-evaluation-completed":
|
||||||
|
this.queueMessageHandler(() =>
|
||||||
|
this.onEvaluationCompleted(message.body),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "output":
|
||||||
|
if (message.body.category === "console") {
|
||||||
|
void this.localQueryRun?.logger.log(message.body.output);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onWillStopSession(): void {
|
||||||
|
this.ui.onSessionClosed(this.session);
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async quickEval(): Promise<void> {
|
||||||
|
const args: CodeQLProtocol.QuickEvalRequest["arguments"] = {
|
||||||
|
quickEvalContext: await getQuickEvalContext(undefined),
|
||||||
|
};
|
||||||
|
await this.session.customRequest("codeql-quickeval", args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queues a message handler to be executed once all other pending message handlers have completed.
|
||||||
|
*
|
||||||
|
* The `onDidSendMessage()` function is synchronous, so it needs to return before any async
|
||||||
|
* handling of the msssage is completed. We can't just launch the message handler directly from
|
||||||
|
* `onDidSendMessage()`, though, because if the message handler's implementation blocks awaiting
|
||||||
|
* a promise, then another event might be received by `onDidSendMessage()` while the first message
|
||||||
|
* handler is still incomplete.
|
||||||
|
*
|
||||||
|
* To enforce sequential execution of event handlers, we queue each new handler as a `finally()`
|
||||||
|
* handler for the most recently queued message.
|
||||||
|
*/
|
||||||
|
private queueMessageHandler(handler: () => Promise<void>): void {
|
||||||
|
this.lastDeferredMessageHandler =
|
||||||
|
this.lastDeferredMessageHandler.finally(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the UI to track the currently executing query. */
|
||||||
|
private async onEvaluationStarted(
|
||||||
|
body: CodeQLProtocol.EvaluationStartedEvent["body"],
|
||||||
|
): Promise<void> {
|
||||||
|
const dbUri = Uri.file(this.configuration.database);
|
||||||
|
const dbItem = await this.dbm.createOrOpenDatabaseItem(dbUri);
|
||||||
|
|
||||||
|
// When cancellation is requested from the query history view, we just stop the debug session.
|
||||||
|
const tokenSource = new CancellationTokenSource();
|
||||||
|
tokenSource.token.onCancellationRequested(() =>
|
||||||
|
debug.stopDebugging(this.session),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.localQueryRun = await this.localQueries.createLocalQueryRun(
|
||||||
|
{
|
||||||
|
queryPath: this.configuration.query,
|
||||||
|
quickEval: body.quickEvalContext,
|
||||||
|
},
|
||||||
|
dbItem,
|
||||||
|
new QueryOutputDir(body.outputDir),
|
||||||
|
tokenSource,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update the UI after a query has finished evaluating. */
|
||||||
|
private async onEvaluationCompleted(
|
||||||
|
body: CodeQLProtocol.EvaluationCompletedEvent["body"],
|
||||||
|
): Promise<void> {
|
||||||
|
if (this.localQueryRun !== undefined) {
|
||||||
|
const results: CoreQueryResults = body;
|
||||||
|
await this.localQueryRun.complete(results);
|
||||||
|
this.localQueryRun = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Service handling the UI for CodeQL debugging. */
|
||||||
|
export class DebuggerUI
|
||||||
|
extends DisposableObject
|
||||||
|
implements DebugAdapterTrackerFactory
|
||||||
|
{
|
||||||
|
private readonly sessions = new Map<string, QLDebugAdapterTracker>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly app: App,
|
||||||
|
private readonly localQueries: LocalQueries,
|
||||||
|
private readonly dbm: DatabaseManager,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.push(debug.registerDebugAdapterTrackerFactory("codeql", this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCommands(): DebuggerCommands {
|
||||||
|
return {
|
||||||
|
"codeQL.debugQuery": this.debugQuery.bind(this),
|
||||||
|
"codeQL.debugQueryContextEditor": this.debugQuery.bind(this),
|
||||||
|
"codeQL.startDebuggingSelectionContextEditor":
|
||||||
|
this.startDebuggingSelection.bind(this),
|
||||||
|
"codeQL.startDebuggingSelection": this.startDebuggingSelection.bind(this),
|
||||||
|
"codeQL.continueDebuggingSelection":
|
||||||
|
this.continueDebuggingSelection.bind(this),
|
||||||
|
"codeQL.continueDebuggingSelectionContextEditor":
|
||||||
|
this.continueDebuggingSelection.bind(this),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public createDebugAdapterTracker(
|
||||||
|
session: DebugSession,
|
||||||
|
): DebugAdapterTracker | undefined {
|
||||||
|
if (session.type === "codeql") {
|
||||||
|
// The tracker will be disposed in its own `onWillStopSession` handler.
|
||||||
|
const tracker = new QLDebugAdapterTracker(
|
||||||
|
session,
|
||||||
|
this,
|
||||||
|
this.localQueries,
|
||||||
|
this.dbm,
|
||||||
|
);
|
||||||
|
this.sessions.set(session.id, tracker);
|
||||||
|
return tracker;
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSessionClosed(session: DebugSession): void {
|
||||||
|
this.sessions.delete(session.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async debugQuery(uri: Uri | undefined): Promise<void> {
|
||||||
|
const queryPath =
|
||||||
|
uri !== undefined
|
||||||
|
? validateQueryUri(uri, false)
|
||||||
|
: await this.localQueries.getCurrentQuery(false);
|
||||||
|
|
||||||
|
// Start debugging with a default configuration that just specifies the query path.
|
||||||
|
await debug.startDebugging(undefined, {
|
||||||
|
name: basename(queryPath),
|
||||||
|
type: "codeql",
|
||||||
|
request: "launch",
|
||||||
|
query: queryPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startDebuggingSelection(): Promise<void> {
|
||||||
|
// Launch the currently selected debug configuration, but specifying QuickEval mode.
|
||||||
|
await this.app.commands.execute("workbench.action.debug.start", {
|
||||||
|
config: {
|
||||||
|
quickEval: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async continueDebuggingSelection(): Promise<void> {
|
||||||
|
const activeTracker = this.activeTracker;
|
||||||
|
if (activeTracker === undefined) {
|
||||||
|
throw new Error("No CodeQL debug session is active.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await activeTracker.quickEval();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTrackerForSession(
|
||||||
|
session: DebugSession,
|
||||||
|
): QLDebugAdapterTracker | undefined {
|
||||||
|
return this.sessions.get(session.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get activeTracker(): QLDebugAdapterTracker | undefined {
|
||||||
|
const session = debug.activeDebugSession;
|
||||||
|
if (session === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getTrackerForSession(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ import { CodeQLCliServer } from "./cli";
|
|||||||
import {
|
import {
|
||||||
CliConfigListener,
|
CliConfigListener,
|
||||||
DistributionConfigListener,
|
DistributionConfigListener,
|
||||||
|
isCanary,
|
||||||
joinOrderWarningThreshold,
|
joinOrderWarningThreshold,
|
||||||
QueryHistoryConfigListener,
|
QueryHistoryConfigListener,
|
||||||
QueryServerConfigListener,
|
QueryServerConfigListener,
|
||||||
@@ -108,20 +109,24 @@ import { VariantAnalysisResultsManager } from "./variant-analysis/variant-analys
|
|||||||
import { ExtensionApp } from "./common/vscode/vscode-app";
|
import { ExtensionApp } from "./common/vscode/vscode-app";
|
||||||
import { DbModule } from "./databases/db-module";
|
import { DbModule } from "./databases/db-module";
|
||||||
import { redactableError } from "./pure/errors";
|
import { redactableError } from "./pure/errors";
|
||||||
|
import { QLDebugAdapterDescriptorFactory } from "./debugger/debugger-factory";
|
||||||
import { QueryHistoryDirs } from "./query-history/query-history-dirs";
|
import { QueryHistoryDirs } from "./query-history/query-history-dirs";
|
||||||
import {
|
import {
|
||||||
AllExtensionCommands,
|
AllExtensionCommands,
|
||||||
BaseCommands,
|
BaseCommands,
|
||||||
PreActivationCommands,
|
PreActivationCommands,
|
||||||
QueryServerCommands,
|
QueryServerCommands,
|
||||||
TestUICommands,
|
|
||||||
} from "./common/commands";
|
} from "./common/commands";
|
||||||
import { LocalQueries } from "./local-queries";
|
import { LocalQueries } from "./local-queries";
|
||||||
import { getAstCfgCommands } from "./ast-cfg-commands";
|
import { getAstCfgCommands } from "./ast-cfg-commands";
|
||||||
import { getQueryEditorCommands } from "./query-editor";
|
import { getQueryEditorCommands } from "./query-editor";
|
||||||
import { App } from "./common/app";
|
import { App } from "./common/app";
|
||||||
import { registerCommandWithErrorHandling } from "./common/vscode/commands";
|
import { registerCommandWithErrorHandling } from "./common/vscode/commands";
|
||||||
|
import { DebuggerUI } from "./debugger/debugger-ui";
|
||||||
import { DataExtensionsEditorModule } from "./data-extensions-editor/data-extensions-editor-module";
|
import { DataExtensionsEditorModule } from "./data-extensions-editor/data-extensions-editor-module";
|
||||||
|
import { TestManager } from "./test-manager";
|
||||||
|
import { TestRunner } from "./test-runner";
|
||||||
|
import { TestManagerBase } from "./test-manager-base";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* extension.ts
|
* extension.ts
|
||||||
@@ -177,7 +182,13 @@ function getCommands(
|
|||||||
cliServer.restartCliServer();
|
cliServer.restartCliServer();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryRunner.restartQueryServer(progress, token),
|
queryRunner.restartQueryServer(progress, token),
|
||||||
ideServer.restart(),
|
async () => {
|
||||||
|
if (ideServer.isRunning()) {
|
||||||
|
await ideServer.restart();
|
||||||
|
} else {
|
||||||
|
await ideServer.start();
|
||||||
|
}
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
void showAndLogInformationMessage("CodeQL Query Server restarted.", {
|
void showAndLogInformationMessage("CodeQL Query Server restarted.", {
|
||||||
outputLogger: queryServerLogger,
|
outputLogger: queryServerLogger,
|
||||||
@@ -868,6 +879,15 @@ async function activateWithInstalledDistribution(
|
|||||||
);
|
);
|
||||||
ctx.subscriptions.push(localQueries);
|
ctx.subscriptions.push(localQueries);
|
||||||
|
|
||||||
|
void extLogger.log("Initializing debugger factory.");
|
||||||
|
ctx.subscriptions.push(
|
||||||
|
new QLDebugAdapterDescriptorFactory(queryStorageDir, qs, localQueries),
|
||||||
|
);
|
||||||
|
|
||||||
|
void extLogger.log("Initializing debugger UI.");
|
||||||
|
const debuggerUI = new DebuggerUI(app, localQueries, dbm);
|
||||||
|
ctx.subscriptions.push(debuggerUI);
|
||||||
|
|
||||||
const dataExtensionsEditorModule =
|
const dataExtensionsEditorModule =
|
||||||
await DataExtensionsEditorModule.initialize(
|
await DataExtensionsEditorModule.initialize(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -879,25 +899,34 @@ async function activateWithInstalledDistribution(
|
|||||||
);
|
);
|
||||||
|
|
||||||
void extLogger.log("Initializing QLTest interface.");
|
void extLogger.log("Initializing QLTest interface.");
|
||||||
const testExplorerExtension = extensions.getExtension<TestHub>(
|
|
||||||
testExplorerExtensionId,
|
const testRunner = new TestRunner(dbm, cliServer);
|
||||||
);
|
ctx.subscriptions.push(testRunner);
|
||||||
let testUiCommands: Partial<TestUICommands> = {};
|
|
||||||
if (testExplorerExtension) {
|
let testManager: TestManagerBase | undefined = undefined;
|
||||||
const testHub = testExplorerExtension.exports;
|
if (isCanary()) {
|
||||||
const testAdapterFactory = new QLTestAdapterFactory(
|
testManager = new TestManager(app, testRunner, cliServer);
|
||||||
testHub,
|
ctx.subscriptions.push(testManager);
|
||||||
cliServer,
|
} else {
|
||||||
dbm,
|
const testExplorerExtension = extensions.getExtension<TestHub>(
|
||||||
|
testExplorerExtensionId,
|
||||||
);
|
);
|
||||||
ctx.subscriptions.push(testAdapterFactory);
|
if (testExplorerExtension) {
|
||||||
|
const testHub = testExplorerExtension.exports;
|
||||||
|
const testAdapterFactory = new QLTestAdapterFactory(
|
||||||
|
testHub,
|
||||||
|
testRunner,
|
||||||
|
cliServer,
|
||||||
|
);
|
||||||
|
ctx.subscriptions.push(testAdapterFactory);
|
||||||
|
|
||||||
const testUIService = new TestUIService(app, testHub);
|
testManager = new TestUIService(app, testHub);
|
||||||
ctx.subscriptions.push(testUIService);
|
ctx.subscriptions.push(testManager);
|
||||||
|
}
|
||||||
testUiCommands = testUIService.getCommands();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const testUiCommands = testManager?.getCommands() ?? {};
|
||||||
|
|
||||||
const astViewer = new AstViewer();
|
const astViewer = new AstViewer();
|
||||||
const astTemplateProvider = new TemplatePrintAstProvider(
|
const astTemplateProvider = new TemplatePrintAstProvider(
|
||||||
cliServer,
|
cliServer,
|
||||||
@@ -945,6 +974,7 @@ async function activateWithInstalledDistribution(
|
|||||||
...summaryLanguageSupport.getCommands(),
|
...summaryLanguageSupport.getCommands(),
|
||||||
...testUiCommands,
|
...testUiCommands,
|
||||||
...mockServer.getCommands(),
|
...mockServer.getCommands(),
|
||||||
|
...debuggerUI.getCommands(),
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [commandName, command] of Object.entries(allCommands)) {
|
for (const [commandName, command] of Object.entries(allCommands)) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export class ServerProcess implements Disposable {
|
|||||||
dispose(): void {
|
dispose(): void {
|
||||||
void this.logger.log(`Stopping ${this.name}...`);
|
void this.logger.log(`Stopping ${this.name}...`);
|
||||||
this.connection.dispose();
|
this.connection.dispose();
|
||||||
|
this.connection.end();
|
||||||
this.child.stdin!.end();
|
this.child.stdin!.end();
|
||||||
this.child.stderr!.destroy();
|
this.child.stderr!.destroy();
|
||||||
// TODO kill the process if it doesn't terminate after a certain time limit.
|
// TODO kill the process if it doesn't terminate after a certain time limit.
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ export async function compileAndRunQueryAgainstDatabaseCore(
|
|||||||
logger: Logger,
|
logger: Logger,
|
||||||
): Promise<CoreQueryResults> {
|
): Promise<CoreQueryResults> {
|
||||||
if (extensionPacks !== undefined && extensionPacks.length > 0) {
|
if (extensionPacks !== undefined && extensionPacks.length > 0) {
|
||||||
await showAndLogWarningMessage(
|
void showAndLogWarningMessage(
|
||||||
"Legacy query server does not support extension packs.",
|
"Legacy query server does not support extension packs.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
ThemeColor,
|
ThemeColor,
|
||||||
workspace,
|
workspace,
|
||||||
|
ProgressLocation,
|
||||||
} from "vscode";
|
} from "vscode";
|
||||||
import { pathExists, stat, readdir, remove } from "fs-extra";
|
import { pathExists, stat, readdir, remove } from "fs-extra";
|
||||||
|
|
||||||
@@ -21,7 +22,12 @@ import {
|
|||||||
DatabaseItem,
|
DatabaseItem,
|
||||||
DatabaseManager,
|
DatabaseManager,
|
||||||
} from "./local-databases";
|
} from "./local-databases";
|
||||||
import { ProgressCallback, withProgress } from "./progress";
|
import {
|
||||||
|
ProgressCallback,
|
||||||
|
ProgressContext,
|
||||||
|
withInheritedProgress,
|
||||||
|
withProgress,
|
||||||
|
} from "./progress";
|
||||||
import {
|
import {
|
||||||
isLikelyDatabaseRoot,
|
isLikelyDatabaseRoot,
|
||||||
isLikelyDbLanguageFolder,
|
isLikelyDbLanguageFolder,
|
||||||
@@ -208,6 +214,7 @@ export class DatabaseUI extends DisposableObject {
|
|||||||
|
|
||||||
public getCommands(): LocalDatabasesCommands {
|
public getCommands(): LocalDatabasesCommands {
|
||||||
return {
|
return {
|
||||||
|
"codeQL.getCurrentDatabase": this.handleGetCurrentDatabase.bind(this),
|
||||||
"codeQL.chooseDatabaseFolder":
|
"codeQL.chooseDatabaseFolder":
|
||||||
this.handleChooseDatabaseFolderFromPalette.bind(this),
|
this.handleChooseDatabaseFolderFromPalette.bind(this),
|
||||||
"codeQL.chooseDatabaseArchive":
|
"codeQL.chooseDatabaseArchive":
|
||||||
@@ -254,7 +261,7 @@ export class DatabaseUI extends DisposableObject {
|
|||||||
token: CancellationToken,
|
token: CancellationToken,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.chooseAndSetDatabase(true, progress, token);
|
await this.chooseAndSetDatabase(true, { progress, token });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
void showAndLogExceptionWithTelemetry(
|
void showAndLogExceptionWithTelemetry(
|
||||||
redactableError(
|
redactableError(
|
||||||
@@ -415,7 +422,7 @@ export class DatabaseUI extends DisposableObject {
|
|||||||
token: CancellationToken,
|
token: CancellationToken,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.chooseAndSetDatabase(false, progress, token);
|
await this.chooseAndSetDatabase(false, { progress, token });
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
void showAndLogExceptionWithTelemetry(
|
void showAndLogExceptionWithTelemetry(
|
||||||
redactableError(
|
redactableError(
|
||||||
@@ -602,6 +609,11 @@ export class DatabaseUI extends DisposableObject {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleGetCurrentDatabase(): Promise<string | undefined> {
|
||||||
|
const dbItem = await this.getDatabaseItemInternal(undefined);
|
||||||
|
return dbItem?.databaseUri.fsPath;
|
||||||
|
}
|
||||||
|
|
||||||
private async handleSetCurrentDatabase(uri: Uri): Promise<void> {
|
private async handleSetCurrentDatabase(uri: Uri): Promise<void> {
|
||||||
return withProgress(
|
return withProgress(
|
||||||
async (progress, token) => {
|
async (progress, token) => {
|
||||||
@@ -717,9 +729,24 @@ export class DatabaseUI extends DisposableObject {
|
|||||||
public async getDatabaseItem(
|
public async getDatabaseItem(
|
||||||
progress: ProgressCallback,
|
progress: ProgressCallback,
|
||||||
token: CancellationToken,
|
token: CancellationToken,
|
||||||
|
): Promise<DatabaseItem | undefined> {
|
||||||
|
return await this.getDatabaseItemInternal({ progress, token });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current database directory. If we don't already have a
|
||||||
|
* current database, ask the user for one, and return that, or
|
||||||
|
* undefined if they cancel.
|
||||||
|
*
|
||||||
|
* Unlike `getDatabaseItem()`, this function does not require the caller to pass in a progress
|
||||||
|
* context. If `progress` is `undefined`, then this command will create a new progress
|
||||||
|
* notification if it tries to perform any long-running operations.
|
||||||
|
*/
|
||||||
|
private async getDatabaseItemInternal(
|
||||||
|
progress: ProgressContext | undefined,
|
||||||
): Promise<DatabaseItem | undefined> {
|
): Promise<DatabaseItem | undefined> {
|
||||||
if (this.databaseManager.currentDatabaseItem === undefined) {
|
if (this.databaseManager.currentDatabaseItem === undefined) {
|
||||||
await this.chooseAndSetDatabase(false, progress, token);
|
await this.chooseAndSetDatabase(false, progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.databaseManager.currentDatabaseItem;
|
return this.databaseManager.currentDatabaseItem;
|
||||||
@@ -749,31 +776,40 @@ export class DatabaseUI extends DisposableObject {
|
|||||||
*/
|
*/
|
||||||
private async chooseAndSetDatabase(
|
private async chooseAndSetDatabase(
|
||||||
byFolder: boolean,
|
byFolder: boolean,
|
||||||
progress: ProgressCallback,
|
progress: ProgressContext | undefined,
|
||||||
token: CancellationToken,
|
|
||||||
): Promise<DatabaseItem | undefined> {
|
): Promise<DatabaseItem | undefined> {
|
||||||
const uri = await chooseDatabaseDir(byFolder);
|
const uri = await chooseDatabaseDir(byFolder);
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (byFolder) {
|
return await withInheritedProgress(
|
||||||
const fixedUri = await this.fixDbUri(uri);
|
progress,
|
||||||
// we are selecting a database folder
|
async (progress, token) => {
|
||||||
return await this.setCurrentDatabase(progress, token, fixedUri);
|
if (byFolder) {
|
||||||
} else {
|
const fixedUri = await this.fixDbUri(uri);
|
||||||
// we are selecting a database archive. Must unzip into a workspace-controlled area
|
// we are selecting a database folder
|
||||||
// before importing.
|
return await this.setCurrentDatabase(progress, token, fixedUri);
|
||||||
return await importArchiveDatabase(
|
} else {
|
||||||
this.app.commands,
|
// we are selecting a database archive. Must unzip into a workspace-controlled area
|
||||||
uri.toString(true),
|
// before importing.
|
||||||
this.databaseManager,
|
return await importArchiveDatabase(
|
||||||
this.storagePath,
|
this.app.commands,
|
||||||
progress,
|
uri.toString(true),
|
||||||
token,
|
this.databaseManager,
|
||||||
this.queryServer?.cliServer,
|
this.storagePath,
|
||||||
);
|
progress,
|
||||||
}
|
token,
|
||||||
|
this.queryServer?.cliServer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
location: ProgressLocation.Notification,
|
||||||
|
cancellable: true,
|
||||||
|
title: "Opening database",
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -613,12 +613,61 @@ export class DatabaseManager extends DisposableObject {
|
|||||||
qs.onStart(this.reregisterDatabases.bind(this));
|
qs.onStart(this.reregisterDatabases.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link DatabaseItem} for the specified database, and adds it to the list of open
|
||||||
|
* databases.
|
||||||
|
*/
|
||||||
public async openDatabase(
|
public async openDatabase(
|
||||||
progress: ProgressCallback,
|
progress: ProgressCallback,
|
||||||
token: vscode.CancellationToken,
|
token: vscode.CancellationToken,
|
||||||
uri: vscode.Uri,
|
uri: vscode.Uri,
|
||||||
displayName?: string,
|
displayName?: string,
|
||||||
isTutorialDatabase?: boolean,
|
isTutorialDatabase?: boolean,
|
||||||
|
): Promise<DatabaseItem> {
|
||||||
|
const databaseItem = await this.createDatabaseItem(uri, displayName);
|
||||||
|
|
||||||
|
return await this.addExistingDatabaseItem(
|
||||||
|
databaseItem,
|
||||||
|
progress,
|
||||||
|
token,
|
||||||
|
isTutorialDatabase,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a {@link DatabaseItem} to the list of open databases, if that database is not already on
|
||||||
|
* the list.
|
||||||
|
*
|
||||||
|
* Typically, the item will have been created by {@link createOrOpenDatabaseItem} or {@link openDatabase}.
|
||||||
|
*/
|
||||||
|
public async addExistingDatabaseItem(
|
||||||
|
databaseItem: DatabaseItem,
|
||||||
|
progress: ProgressCallback,
|
||||||
|
token: vscode.CancellationToken,
|
||||||
|
isTutorialDatabase?: boolean,
|
||||||
|
): Promise<DatabaseItem> {
|
||||||
|
const existingItem = this.findDatabaseItem(databaseItem.databaseUri);
|
||||||
|
if (existingItem !== undefined) {
|
||||||
|
return existingItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.addDatabaseItem(progress, token, databaseItem);
|
||||||
|
await this.addDatabaseSourceArchiveFolder(databaseItem);
|
||||||
|
|
||||||
|
if (isCodespacesTemplate() && !isTutorialDatabase) {
|
||||||
|
await this.createSkeletonPacks(databaseItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return databaseItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link DatabaseItem} for the specified database, without adding it to the list of
|
||||||
|
* open databases.
|
||||||
|
*/
|
||||||
|
private async createDatabaseItem(
|
||||||
|
uri: vscode.Uri,
|
||||||
|
displayName: string | undefined,
|
||||||
): Promise<DatabaseItem> {
|
): Promise<DatabaseItem> {
|
||||||
const contents = await DatabaseResolver.resolveDatabaseContents(uri);
|
const contents = await DatabaseResolver.resolveDatabaseContents(uri);
|
||||||
// Ignore the source archive for QLTest databases by default.
|
// Ignore the source archive for QLTest databases by default.
|
||||||
@@ -639,14 +688,27 @@ export class DatabaseManager extends DisposableObject {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.addDatabaseItem(progress, token, databaseItem);
|
return databaseItem;
|
||||||
await this.addDatabaseSourceArchiveFolder(databaseItem);
|
}
|
||||||
|
|
||||||
if (isCodespacesTemplate() && !isTutorialDatabase) {
|
/**
|
||||||
await this.createSkeletonPacks(databaseItem);
|
* If the specified database is already on the list of open databases, returns that database's
|
||||||
|
* {@link DatabaseItem}. Otherwise, creates a new {@link DatabaseItem} without adding it to the
|
||||||
|
* list of open databases.
|
||||||
|
*
|
||||||
|
* The {@link DatabaseItem} can be added to the list of open databases later, via {@link addExistingDatabaseItem}.
|
||||||
|
*/
|
||||||
|
public async createOrOpenDatabaseItem(
|
||||||
|
uri: vscode.Uri,
|
||||||
|
): Promise<DatabaseItem> {
|
||||||
|
const existingItem = this.findDatabaseItem(uri);
|
||||||
|
if (existingItem !== undefined) {
|
||||||
|
// Use the one we already have.
|
||||||
|
return existingItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
return databaseItem;
|
// We don't add this to the list automatically, but the user can add it later.
|
||||||
|
return this.createDatabaseItem(uri, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createSkeletonPacks(databaseItem: DatabaseItem) {
|
public async createSkeletonPacks(databaseItem: DatabaseItem) {
|
||||||
@@ -1029,7 +1091,19 @@ export class DatabaseManager extends DisposableObject {
|
|||||||
token: vscode.CancellationToken,
|
token: vscode.CancellationToken,
|
||||||
dbItem: DatabaseItem,
|
dbItem: DatabaseItem,
|
||||||
) {
|
) {
|
||||||
await this.qs.deregisterDatabase(progress, token, dbItem);
|
try {
|
||||||
|
await this.qs.deregisterDatabase(progress, token, dbItem);
|
||||||
|
} catch (e) {
|
||||||
|
const message = getErrorMessage(e);
|
||||||
|
if (message === "Connection is disposed.") {
|
||||||
|
// This is expected if the query server is not running.
|
||||||
|
void extLogger.log(
|
||||||
|
`Could not de-register database '${dbItem.name}' because query server is not running.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private async registerDatabase(
|
private async registerDatabase(
|
||||||
progress: ProgressCallback,
|
progress: ProgressCallback,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Range,
|
Range,
|
||||||
Uri,
|
Uri,
|
||||||
window,
|
window,
|
||||||
|
workspace,
|
||||||
} from "vscode";
|
} from "vscode";
|
||||||
import { BaseLogger, extLogger, Logger, TeeLogger } from "./common";
|
import { BaseLogger, extLogger, Logger, TeeLogger } from "./common";
|
||||||
import { isCanary, MAX_QUERIES } from "./config";
|
import { isCanary, MAX_QUERIES } from "./config";
|
||||||
@@ -33,14 +34,16 @@ import { ResultsView } from "./interface";
|
|||||||
import { DatabaseItem, DatabaseManager } from "./local-databases";
|
import { DatabaseItem, DatabaseManager } from "./local-databases";
|
||||||
import {
|
import {
|
||||||
createInitialQueryInfo,
|
createInitialQueryInfo,
|
||||||
determineSelectedQuery,
|
|
||||||
EvaluatorLogPaths,
|
EvaluatorLogPaths,
|
||||||
generateEvalLogSummaries,
|
generateEvalLogSummaries,
|
||||||
|
getQuickEvalContext,
|
||||||
logEndSummary,
|
logEndSummary,
|
||||||
|
promptUserToSaveChanges,
|
||||||
QueryEvaluationInfo,
|
QueryEvaluationInfo,
|
||||||
QueryOutputDir,
|
QueryOutputDir,
|
||||||
QueryWithResults,
|
QueryWithResults,
|
||||||
SelectedQuery,
|
SelectedQuery,
|
||||||
|
validateQueryUri,
|
||||||
} from "./run-queries-shared";
|
} from "./run-queries-shared";
|
||||||
import { CompletedLocalQueryInfo, LocalQueryInfo } from "./query-results";
|
import { CompletedLocalQueryInfo, LocalQueryInfo } from "./query-results";
|
||||||
import { WebviewReveal } from "./interface-utils";
|
import { WebviewReveal } from "./interface-utils";
|
||||||
@@ -75,6 +78,25 @@ function formatResultMessage(result: CoreQueryResults): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If either the query file or the quickeval file is dirty, give the user the chance to save them.
|
||||||
|
*/
|
||||||
|
async function promptToSaveQueryIfNeeded(query: SelectedQuery): Promise<void> {
|
||||||
|
// There seems to be no way to ask VS Code to find an existing text document by name, without
|
||||||
|
// automatically opening the document if it is not found.
|
||||||
|
const queryUri = Uri.file(query.queryPath).toString();
|
||||||
|
const quickEvalUri =
|
||||||
|
query.quickEval !== undefined
|
||||||
|
? Uri.file(query.quickEval.quickEvalPosition.fileName).toString()
|
||||||
|
: undefined;
|
||||||
|
for (const openDocument of workspace.textDocuments) {
|
||||||
|
const documentUri = openDocument.uri.toString();
|
||||||
|
if (documentUri === queryUri || documentUri === quickEvalUri) {
|
||||||
|
await promptUserToSaveChanges(openDocument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the evaluation of a local query, including its interactions with the UI.
|
* Tracks the evaluation of a local query, including its interactions with the UI.
|
||||||
*
|
*
|
||||||
@@ -238,6 +260,13 @@ export class LocalQueries extends DisposableObject {
|
|||||||
"codeQL.quickEvalContextEditor": this.quickEval.bind(this),
|
"codeQL.quickEvalContextEditor": this.quickEval.bind(this),
|
||||||
"codeQL.codeLensQuickEval": this.codeLensQuickEval.bind(this),
|
"codeQL.codeLensQuickEval": this.codeLensQuickEval.bind(this),
|
||||||
"codeQL.quickQuery": this.quickQuery.bind(this),
|
"codeQL.quickQuery": this.quickQuery.bind(this),
|
||||||
|
"codeQL.getCurrentQuery": () => {
|
||||||
|
// When invoked as a command, such as when resolving variables in a debug configuration,
|
||||||
|
// always allow ".qll" files, because we don't know if the configuration will be for
|
||||||
|
// quickeval yet. The debug configuration code will do further validation once it knows for
|
||||||
|
// sure.
|
||||||
|
return this.getCurrentQuery(true);
|
||||||
|
},
|
||||||
"codeQL.createQuery": this.createSkeletonQuery.bind(this),
|
"codeQL.createQuery": this.createSkeletonQuery.bind(this),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -377,6 +406,23 @@ export class LocalQueries extends DisposableObject {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current active query.
|
||||||
|
*
|
||||||
|
* For now, the "active query" is just whatever query is in the active text editor. Once we have a
|
||||||
|
* propery "queries" panel, we can provide a way to select the current query there.
|
||||||
|
*/
|
||||||
|
public async getCurrentQuery(allowLibraryFiles: boolean): Promise<string> {
|
||||||
|
const editor = window.activeTextEditor;
|
||||||
|
if (editor === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
"No query was selected. Please select a query and try again.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateQueryUri(editor.document.uri, allowLibraryFiles);
|
||||||
|
}
|
||||||
|
|
||||||
private async createSkeletonQuery(): Promise<void> {
|
private async createSkeletonQuery(): Promise<void> {
|
||||||
await withProgress(
|
await withProgress(
|
||||||
async (progress: ProgressCallback, token: CancellationToken) => {
|
async (progress: ProgressCallback, token: CancellationToken) => {
|
||||||
@@ -470,29 +516,38 @@ export class LocalQueries extends DisposableObject {
|
|||||||
databaseItem: DatabaseItem | undefined,
|
databaseItem: DatabaseItem | undefined,
|
||||||
range?: Range,
|
range?: Range,
|
||||||
): Promise<CoreCompletedQuery> {
|
): Promise<CoreCompletedQuery> {
|
||||||
const selectedQuery = await determineSelectedQuery(
|
let queryPath: string;
|
||||||
queryUri,
|
if (queryUri !== undefined) {
|
||||||
quickEval,
|
// The query URI is provided by the command, most likely because the command was run from an
|
||||||
range,
|
// editor context menu. Use the provided URI, but make sure it's a valid query.
|
||||||
);
|
queryPath = validateQueryUri(queryUri, quickEval);
|
||||||
|
} else {
|
||||||
|
// Use the currently selected query.
|
||||||
|
queryPath = await this.getCurrentQuery(quickEval);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedQuery: SelectedQuery = {
|
||||||
|
queryPath,
|
||||||
|
quickEval: quickEval ? await getQuickEvalContext(range) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
// If no databaseItem is specified, use the database currently selected in the Databases UI
|
// If no databaseItem is specified, use the database currently selected in the Databases UI
|
||||||
databaseItem =
|
databaseItem =
|
||||||
databaseItem || (await this.databaseUI.getDatabaseItem(progress, token));
|
databaseItem ?? (await this.databaseUI.getDatabaseItem(progress, token));
|
||||||
if (databaseItem === undefined) {
|
if (databaseItem === undefined) {
|
||||||
throw new Error("Can't run query without a selected database");
|
throw new Error("Can't run query without a selected database");
|
||||||
}
|
}
|
||||||
|
|
||||||
const additionalPacks = getOnDiskWorkspaceFolders();
|
const additionalPacks = getOnDiskWorkspaceFolders();
|
||||||
const extensionPacks = (await this.cliServer.useExtensionPacks())
|
const extensionPacks = await this.getDefaultExtensionPacks(additionalPacks);
|
||||||
? Object.keys(await this.cliServer.resolveQlpacks(additionalPacks, true))
|
|
||||||
: undefined;
|
await promptToSaveQueryIfNeeded(selectedQuery);
|
||||||
|
|
||||||
const coreQueryRun = this.queryRunner.createQueryRun(
|
const coreQueryRun = this.queryRunner.createQueryRun(
|
||||||
databaseItem.databaseUri.fsPath,
|
databaseItem.databaseUri.fsPath,
|
||||||
{
|
{
|
||||||
queryPath: selectedQuery.queryPath,
|
queryPath: selectedQuery.queryPath,
|
||||||
quickEvalPosition: selectedQuery.quickEvalPosition,
|
quickEvalPosition: selectedQuery.quickEval?.quickEvalPosition,
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
additionalPacks,
|
additionalPacks,
|
||||||
@@ -612,4 +667,12 @@ export class LocalQueries extends DisposableObject {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.localQueryResultsView.showResults(query, forceReveal, false);
|
await this.localQueryResultsView.showResults(query, forceReveal, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDefaultExtensionPacks(
|
||||||
|
additionalPacks: string[],
|
||||||
|
): Promise<string[]> {
|
||||||
|
return (await this.cliServer.useExtensionPacks())
|
||||||
|
? Object.keys(await this.cliServer.resolveQlpacks(additionalPacks, true))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,29 @@ export function withProgress<R>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProgressContext {
|
||||||
|
progress: ProgressCallback;
|
||||||
|
token: CancellationToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `withProgress()`, except that the caller is not required to provide a progress context. If
|
||||||
|
* the caller does provide one, any long-running operations performed by `task` will use the
|
||||||
|
* supplied progress context. Otherwise, this function wraps `task` in a new progress context with
|
||||||
|
* the supplied options.
|
||||||
|
*/
|
||||||
|
export function withInheritedProgress<R>(
|
||||||
|
parent: ProgressContext | undefined,
|
||||||
|
task: ProgressTask<R>,
|
||||||
|
options: ProgressOptions,
|
||||||
|
): Thenable<R> {
|
||||||
|
if (parent !== undefined) {
|
||||||
|
return task(parent.progress, parent.token);
|
||||||
|
} else {
|
||||||
|
return withProgress(task, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a progress monitor that indicates how much progess has been made
|
* Displays a progress monitor that indicates how much progess has been made
|
||||||
* reading from a stream.
|
* reading from a stream.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ensureFile } from "fs-extra";
|
import { ensureFile } from "fs-extra";
|
||||||
|
|
||||||
import { DisposableObject } from "../pure/disposable-object";
|
import { DisposableObject, DisposeHandler } from "../pure/disposable-object";
|
||||||
import { CancellationToken } from "vscode";
|
import { CancellationToken } from "vscode";
|
||||||
import { createMessageConnection, RequestType } from "vscode-jsonrpc/node";
|
import { createMessageConnection, RequestType } from "vscode-jsonrpc/node";
|
||||||
import * as cli from "../cli";
|
import * as cli from "../cli";
|
||||||
@@ -224,4 +224,10 @@ export class QueryServerClient extends DisposableObject {
|
|||||||
delete this.progressCallbacks[id];
|
delete this.progressCallbacks[id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public dispose(disposeHandler?: DisposeHandler | undefined): void {
|
||||||
|
this.progressCallbacks = {};
|
||||||
|
this.stopQueryServer();
|
||||||
|
super.dispose(disposeHandler);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -389,55 +389,33 @@ export interface QueryWithResults {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information about which query will be to be run. `quickEvalPosition` and `quickEvalText`
|
* Validates that the specified URI represents a QL query, and returns the file system path to that
|
||||||
* is only filled in if the query is a quick query.
|
* query.
|
||||||
|
*
|
||||||
|
* If `allowLibraryFiles` is set, ".qll" files will also be allowed as query files.
|
||||||
*/
|
*/
|
||||||
export interface SelectedQuery {
|
export function validateQueryUri(
|
||||||
queryPath: string;
|
queryUri: Uri,
|
||||||
quickEvalPosition?: messages.Position;
|
allowLibraryFiles: boolean,
|
||||||
quickEvalText?: string;
|
): string {
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines which QL file to run during an invocation of `Run Query` or `Quick Evaluation`, as follows:
|
|
||||||
* - If the command was called by clicking on a file, then use that file.
|
|
||||||
* - Otherwise, use the file open in the current editor.
|
|
||||||
* - In either case, prompt the user to save the file if it is open with unsaved changes.
|
|
||||||
* - For `Quick Evaluation`, ensure the selected file is also the one open in the editor,
|
|
||||||
* and use the selected region.
|
|
||||||
* @param selectedResourceUri The selected resource when the command was run.
|
|
||||||
* @param quickEval Whether the command being run is `Quick Evaluation`.
|
|
||||||
*/
|
|
||||||
export async function determineSelectedQuery(
|
|
||||||
selectedResourceUri: Uri | undefined,
|
|
||||||
quickEval: boolean,
|
|
||||||
range?: Range,
|
|
||||||
): Promise<SelectedQuery> {
|
|
||||||
const editor = window.activeTextEditor;
|
|
||||||
|
|
||||||
// Choose which QL file to use.
|
|
||||||
let queryUri: Uri;
|
|
||||||
if (selectedResourceUri) {
|
|
||||||
// A resource was passed to the command handler, so use it.
|
|
||||||
queryUri = selectedResourceUri;
|
|
||||||
} else {
|
|
||||||
// No resource was passed to the command handler, so obtain it from the active editor.
|
|
||||||
// This usually happens when the command is called from the Command Palette.
|
|
||||||
if (editor === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
"No query was selected. Please select a query and try again.",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
queryUri = editor.document.uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queryUri.scheme !== "file") {
|
if (queryUri.scheme !== "file") {
|
||||||
throw new Error("Can only run queries that are on disk.");
|
throw new Error("Can only run queries that are on disk.");
|
||||||
}
|
}
|
||||||
const queryPath = queryUri.fsPath;
|
const queryPath = queryUri.fsPath;
|
||||||
|
validateQueryPath(queryPath, allowLibraryFiles);
|
||||||
|
return queryPath;
|
||||||
|
}
|
||||||
|
|
||||||
if (quickEval) {
|
/**
|
||||||
|
* Validates that the specified path represents a QL query
|
||||||
|
*
|
||||||
|
* If `allowLibraryFiles` is set, ".qll" files will also be allowed as query files.
|
||||||
|
*/
|
||||||
|
export function validateQueryPath(
|
||||||
|
queryPath: string,
|
||||||
|
allowLibraryFiles: boolean,
|
||||||
|
): void {
|
||||||
|
if (allowLibraryFiles) {
|
||||||
if (!(queryPath.endsWith(".ql") || queryPath.endsWith(".qll"))) {
|
if (!(queryPath.endsWith(".ql") || queryPath.endsWith(".qll"))) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'The selected resource is not a CodeQL file; It should have the extension ".ql" or ".qll".',
|
'The selected resource is not a CodeQL file; It should have the extension ".ql" or ".qll".',
|
||||||
@@ -450,40 +428,52 @@ export async function determineSelectedQuery(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Whether we chose the file from the active editor or from a context menu,
|
export interface QuickEvalContext {
|
||||||
// if the same file is open with unsaved changes in the active editor,
|
quickEvalPosition: messages.Position;
|
||||||
// then prompt the user to save it first.
|
quickEvalText: string;
|
||||||
if (editor !== undefined && editor.document.uri.fsPath === queryPath) {
|
}
|
||||||
if (await promptUserToSaveChanges(editor.document)) {
|
|
||||||
await editor.document.save();
|
/**
|
||||||
}
|
* Gets the selection to be used for quick evaluation.
|
||||||
|
*
|
||||||
|
* If `range` is specified, then that range will be used. Otherwise, the current selection will be
|
||||||
|
* used.
|
||||||
|
*/
|
||||||
|
export async function getQuickEvalContext(
|
||||||
|
range: Range | undefined,
|
||||||
|
): Promise<QuickEvalContext> {
|
||||||
|
const editor = window.activeTextEditor;
|
||||||
|
if (editor === undefined) {
|
||||||
|
throw new Error("Can't run quick evaluation without an active editor.");
|
||||||
|
}
|
||||||
|
// For Quick Evaluation, the selected position comes from the active editor, but it's possible
|
||||||
|
// that query itself was a different file. We need to validate the path of the file we're using
|
||||||
|
// for the QuickEval selection in case it was different.
|
||||||
|
validateQueryUri(editor.document.uri, true);
|
||||||
|
const quickEvalPosition = await getSelectedPosition(editor, range);
|
||||||
|
let quickEvalText: string;
|
||||||
|
if (!editor.selection?.isEmpty) {
|
||||||
|
quickEvalText = editor.document.getText(editor.selection).trim();
|
||||||
|
} else {
|
||||||
|
// capture the entire line if the user didn't select anything
|
||||||
|
const line = editor.document.lineAt(editor.selection.active.line);
|
||||||
|
quickEvalText = line.text.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
let quickEvalPosition: messages.Position | undefined = undefined;
|
return {
|
||||||
let quickEvalText: string | undefined = undefined;
|
quickEvalPosition,
|
||||||
if (quickEval) {
|
quickEvalText,
|
||||||
if (editor === undefined) {
|
};
|
||||||
throw new Error("Can't run quick evaluation without an active editor.");
|
}
|
||||||
}
|
|
||||||
if (editor.document.fileName !== queryPath) {
|
|
||||||
// For Quick Evaluation we expect these to be the same.
|
|
||||||
// Report an error if we end up in this (hopefully unlikely) situation.
|
|
||||||
throw new Error(
|
|
||||||
"The selected resource for quick evaluation should match the active editor.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
quickEvalPosition = await getSelectedPosition(editor, range);
|
|
||||||
if (!editor.selection?.isEmpty) {
|
|
||||||
quickEvalText = editor.document.getText(editor.selection);
|
|
||||||
} else {
|
|
||||||
// capture the entire line if the user didn't select anything
|
|
||||||
const line = editor.document.lineAt(editor.selection.active.line);
|
|
||||||
quickEvalText = line.text.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { queryPath, quickEvalPosition, quickEvalText };
|
/**
|
||||||
|
* Information about which query will be to be run, optionally including a QuickEval selection.
|
||||||
|
*/
|
||||||
|
export interface SelectedQuery {
|
||||||
|
queryPath: string;
|
||||||
|
quickEval?: QuickEvalContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets the selected position within the given editor. */
|
/** Gets the selected position within the given editor. */
|
||||||
@@ -512,7 +502,7 @@ async function getSelectedPosition(
|
|||||||
* @returns true if we should save changes and false if we should continue without saving changes.
|
* @returns true if we should save changes and false if we should continue without saving changes.
|
||||||
* @throws UserCancellationException if we should abort whatever operation triggered this prompt
|
* @throws UserCancellationException if we should abort whatever operation triggered this prompt
|
||||||
*/
|
*/
|
||||||
async function promptUserToSaveChanges(
|
export async function promptUserToSaveChanges(
|
||||||
document: TextDocument,
|
document: TextDocument,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (document.isDirty) {
|
if (document.isDirty) {
|
||||||
@@ -526,7 +516,9 @@ async function promptUserToSaveChanges(
|
|||||||
isCloseAffordance: false,
|
isCloseAffordance: false,
|
||||||
};
|
};
|
||||||
const cancelItem = { title: "Cancel", isCloseAffordance: true };
|
const cancelItem = { title: "Cancel", isCloseAffordance: true };
|
||||||
const message = "Query file has unsaved changes. Save now?";
|
const message = `Query file '${basename(
|
||||||
|
document.uri.fsPath,
|
||||||
|
)}' has unsaved changes. Save now?`;
|
||||||
const chosenItem = await window.showInformationMessage(
|
const chosenItem = await window.showInformationMessage(
|
||||||
message,
|
message,
|
||||||
{ modal: true },
|
{ modal: true },
|
||||||
@@ -595,7 +587,7 @@ export async function createInitialQueryInfo(
|
|||||||
selectedQuery: SelectedQuery,
|
selectedQuery: SelectedQuery,
|
||||||
databaseInfo: DatabaseInfo,
|
databaseInfo: DatabaseInfo,
|
||||||
): Promise<InitialQueryInfo> {
|
): Promise<InitialQueryInfo> {
|
||||||
const isQuickEval = selectedQuery.quickEvalPosition !== undefined;
|
const isQuickEval = selectedQuery.quickEval !== undefined;
|
||||||
return {
|
return {
|
||||||
queryPath: selectedQuery.queryPath,
|
queryPath: selectedQuery.queryPath,
|
||||||
isQuickEval,
|
isQuickEval,
|
||||||
@@ -603,10 +595,10 @@ export async function createInitialQueryInfo(
|
|||||||
databaseInfo,
|
databaseInfo,
|
||||||
id: `${basename(selectedQuery.queryPath)}-${nanoid()}`,
|
id: `${basename(selectedQuery.queryPath)}-${nanoid()}`,
|
||||||
start: new Date(),
|
start: new Date(),
|
||||||
...(isQuickEval
|
...(selectedQuery.quickEval !== undefined
|
||||||
? {
|
? {
|
||||||
queryText: selectedQuery.quickEvalText!, // if this query is quick eval, it must have quick eval text
|
queryText: selectedQuery.quickEval.quickEvalText,
|
||||||
quickEvalPosition: selectedQuery.quickEvalPosition,
|
quickEvalPosition: selectedQuery.quickEval.quickEvalPosition,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
queryText: await readFile(selectedQuery.queryPath, "utf8"),
|
queryText: await readFile(selectedQuery.queryPath, "utf8"),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { access } from "fs-extra";
|
|
||||||
import { dirname, extname } from "path";
|
import { dirname, extname } from "path";
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import {
|
import {
|
||||||
@@ -20,23 +19,11 @@ import {
|
|||||||
QLTestDirectory,
|
QLTestDirectory,
|
||||||
QLTestDiscovery,
|
QLTestDiscovery,
|
||||||
} from "./qltest-discovery";
|
} from "./qltest-discovery";
|
||||||
import {
|
import { Event, EventEmitter, CancellationTokenSource } from "vscode";
|
||||||
Event,
|
|
||||||
EventEmitter,
|
|
||||||
CancellationTokenSource,
|
|
||||||
CancellationToken,
|
|
||||||
} from "vscode";
|
|
||||||
import { DisposableObject } from "./pure/disposable-object";
|
import { DisposableObject } from "./pure/disposable-object";
|
||||||
import { CodeQLCliServer } from "./cli";
|
import { CodeQLCliServer, TestCompleted } from "./cli";
|
||||||
import {
|
|
||||||
getOnDiskWorkspaceFolders,
|
|
||||||
showAndLogExceptionWithTelemetry,
|
|
||||||
showAndLogWarningMessage,
|
|
||||||
} from "./helpers";
|
|
||||||
import { testLogger } from "./common";
|
import { testLogger } from "./common";
|
||||||
import { DatabaseItem, DatabaseManager } from "./local-databases";
|
import { TestRunner } from "./test-runner";
|
||||||
import { asError, getErrorMessage } from "./pure/helpers-pure";
|
|
||||||
import { redactableError } from "./pure/errors";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the full path of the `.expected` file for the specified QL test.
|
* Get the full path of the `.expected` file for the specified QL test.
|
||||||
@@ -77,8 +64,8 @@ function getTestOutputFile(testPath: string, extension: string): string {
|
|||||||
export class QLTestAdapterFactory extends DisposableObject {
|
export class QLTestAdapterFactory extends DisposableObject {
|
||||||
constructor(
|
constructor(
|
||||||
testHub: TestHub,
|
testHub: TestHub,
|
||||||
|
testRunner: TestRunner,
|
||||||
cliServer: CodeQLCliServer,
|
cliServer: CodeQLCliServer,
|
||||||
databaseManager: DatabaseManager,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -87,7 +74,7 @@ export class QLTestAdapterFactory extends DisposableObject {
|
|||||||
new TestAdapterRegistrar(
|
new TestAdapterRegistrar(
|
||||||
testHub,
|
testHub,
|
||||||
(workspaceFolder) =>
|
(workspaceFolder) =>
|
||||||
new QLTestAdapter(workspaceFolder, cliServer, databaseManager),
|
new QLTestAdapter(workspaceFolder, testRunner, cliServer),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -120,8 +107,8 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly workspaceFolder: vscode.WorkspaceFolder,
|
public readonly workspaceFolder: vscode.WorkspaceFolder,
|
||||||
private readonly cliServer: CodeQLCliServer,
|
private readonly testRunner: TestRunner,
|
||||||
private readonly databaseManager: DatabaseManager,
|
cliServer: CodeQLCliServer,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -232,110 +219,14 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
|||||||
tests,
|
tests,
|
||||||
} as TestRunStartedEvent);
|
} as TestRunStartedEvent);
|
||||||
|
|
||||||
const currentDatabaseUri =
|
await this.testRunner.run(tests, testLogger, token, (event) =>
|
||||||
this.databaseManager.currentDatabaseItem?.databaseUri;
|
this.processTestEvent(event),
|
||||||
const databasesUnderTest: DatabaseItem[] = [];
|
|
||||||
for (const database of this.databaseManager.databaseItems) {
|
|
||||||
for (const test of tests) {
|
|
||||||
if (await database.isAffectedByTest(test)) {
|
|
||||||
databasesUnderTest.push(database);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.removeDatabasesBeforeTests(databasesUnderTest, token);
|
|
||||||
try {
|
|
||||||
await this.runTests(tests, token);
|
|
||||||
} catch (e) {
|
|
||||||
// CodeQL testing can throw exception even in normal scenarios. For example, if the test run
|
|
||||||
// produces no output (which is normal), the testing command would throw an exception on
|
|
||||||
// unexpected EOF during json parsing. So nothing needs to be done here - all the relevant
|
|
||||||
// error information (if any) should have already been written to the test logger.
|
|
||||||
}
|
|
||||||
await this.reopenDatabasesAfterTests(
|
|
||||||
databasesUnderTest,
|
|
||||||
currentDatabaseUri,
|
|
||||||
token,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this._testStates.fire({ type: "finished" } as TestRunFinishedEvent);
|
this._testStates.fire({ type: "finished" } as TestRunFinishedEvent);
|
||||||
this.clearTask();
|
this.clearTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async removeDatabasesBeforeTests(
|
|
||||||
databasesUnderTest: DatabaseItem[],
|
|
||||||
token: vscode.CancellationToken,
|
|
||||||
): Promise<void> {
|
|
||||||
for (const database of databasesUnderTest) {
|
|
||||||
try {
|
|
||||||
await this.databaseManager.removeDatabaseItem(
|
|
||||||
(_) => {
|
|
||||||
/* no progress reporting */
|
|
||||||
},
|
|
||||||
token,
|
|
||||||
database,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// This method is invoked from Test Explorer UI, and testing indicates that Test
|
|
||||||
// Explorer UI swallows any thrown exception without reporting it to the user.
|
|
||||||
// So we need to display the error message ourselves and then rethrow.
|
|
||||||
void showAndLogExceptionWithTelemetry(
|
|
||||||
redactableError(asError(e))`Cannot remove database ${
|
|
||||||
database.name
|
|
||||||
}: ${getErrorMessage(e)}`,
|
|
||||||
);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async reopenDatabasesAfterTests(
|
|
||||||
databasesUnderTest: DatabaseItem[],
|
|
||||||
currentDatabaseUri: vscode.Uri | undefined,
|
|
||||||
token: vscode.CancellationToken,
|
|
||||||
): Promise<void> {
|
|
||||||
for (const closedDatabase of databasesUnderTest) {
|
|
||||||
const uri = closedDatabase.databaseUri;
|
|
||||||
if (await this.isFileAccessible(uri)) {
|
|
||||||
try {
|
|
||||||
const reopenedDatabase = await this.databaseManager.openDatabase(
|
|
||||||
(_) => {
|
|
||||||
/* no progress reporting */
|
|
||||||
},
|
|
||||||
token,
|
|
||||||
uri,
|
|
||||||
);
|
|
||||||
await this.databaseManager.renameDatabaseItem(
|
|
||||||
reopenedDatabase,
|
|
||||||
closedDatabase.name,
|
|
||||||
);
|
|
||||||
if (currentDatabaseUri?.toString() === uri.toString()) {
|
|
||||||
await this.databaseManager.setCurrentDatabaseItem(
|
|
||||||
reopenedDatabase,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// This method is invoked from Test Explorer UI, and testing indicates that Test
|
|
||||||
// Explorer UI swallows any thrown exception without reporting it to the user.
|
|
||||||
// So we need to display the error message ourselves and then rethrow.
|
|
||||||
void showAndLogWarningMessage(`Cannot reopen database ${uri}: ${e}`);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async isFileAccessible(uri: vscode.Uri): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await access(uri.fsPath);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearTask(): void {
|
private clearTask(): void {
|
||||||
if (this.runningTask !== undefined) {
|
if (this.runningTask !== undefined) {
|
||||||
const runningTask = this.runningTask;
|
const runningTask = this.runningTask;
|
||||||
@@ -352,49 +243,42 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runTests(
|
private async processTestEvent(event: TestCompleted): Promise<void> {
|
||||||
tests: string[],
|
const state = event.pass
|
||||||
cancellationToken: CancellationToken,
|
? "passed"
|
||||||
): Promise<void> {
|
: event.messages?.length
|
||||||
const workspacePaths = getOnDiskWorkspaceFolders();
|
? "errored"
|
||||||
for await (const event of this.cliServer.runTests(tests, workspacePaths, {
|
: "failed";
|
||||||
cancellationToken,
|
let message: string | undefined;
|
||||||
logger: testLogger,
|
if (event.failureDescription || event.diff?.length) {
|
||||||
})) {
|
message =
|
||||||
const state = event.pass
|
event.failureStage === "RESULT"
|
||||||
? "passed"
|
? [
|
||||||
: event.messages?.length
|
"",
|
||||||
? "errored"
|
`${state}: ${event.test}`,
|
||||||
: "failed";
|
event.failureDescription || event.diff?.join("\n"),
|
||||||
let message: string | undefined;
|
"",
|
||||||
if (event.failureDescription || event.diff?.length) {
|
].join("\n")
|
||||||
message =
|
: [
|
||||||
event.failureStage === "RESULT"
|
"",
|
||||||
? [
|
`${event.failureStage?.toLowerCase() ?? "unknown stage"} error: ${
|
||||||
"",
|
event.test
|
||||||
`${state}: ${event.test}`,
|
}`,
|
||||||
event.failureDescription || event.diff?.join("\n"),
|
event.failureDescription ||
|
||||||
"",
|
`${event.messages[0].severity}: ${event.messages[0].message}`,
|
||||||
].join("\n")
|
"",
|
||||||
: [
|
].join("\n");
|
||||||
"",
|
void testLogger.log(message);
|
||||||
`${event.failureStage?.toLowerCase()} error: ${event.test}`,
|
|
||||||
event.failureDescription ||
|
|
||||||
`${event.messages[0].severity}: ${event.messages[0].message}`,
|
|
||||||
"",
|
|
||||||
].join("\n");
|
|
||||||
void testLogger.log(message);
|
|
||||||
}
|
|
||||||
this._testStates.fire({
|
|
||||||
type: "test",
|
|
||||||
state,
|
|
||||||
test: event.test,
|
|
||||||
message,
|
|
||||||
decorations: event.messages?.map((msg) => ({
|
|
||||||
line: msg.position.line,
|
|
||||||
message: msg.message,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
this._testStates.fire({
|
||||||
|
type: "test",
|
||||||
|
state,
|
||||||
|
test: event.test,
|
||||||
|
message,
|
||||||
|
decorations: event.messages?.map((msg) => ({
|
||||||
|
line: msg.position.line,
|
||||||
|
message: msg.message,
|
||||||
|
})),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
74
extensions/ql-vscode/src/test-manager-base.ts
Normal file
74
extensions/ql-vscode/src/test-manager-base.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { copy, createFile, lstat, pathExists } from "fs-extra";
|
||||||
|
import { TestUICommands } from "./common/commands";
|
||||||
|
import { DisposableObject } from "./pure/disposable-object";
|
||||||
|
import { getActualFile, getExpectedFile } from "./test-adapter";
|
||||||
|
import { TestItem, TextDocumentShowOptions, Uri, window } from "vscode";
|
||||||
|
import { basename } from "path";
|
||||||
|
import { App } from "./common/app";
|
||||||
|
import { TestTreeNode } from "./test-tree-node";
|
||||||
|
|
||||||
|
export type TestNode = TestTreeNode | TestItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for both the legacy and new test services. Implements commands that are common to
|
||||||
|
* both.
|
||||||
|
*/
|
||||||
|
export abstract class TestManagerBase extends DisposableObject {
|
||||||
|
protected constructor(private readonly app: App) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCommands(): TestUICommands {
|
||||||
|
return {
|
||||||
|
"codeQLTests.showOutputDifferences":
|
||||||
|
this.showOutputDifferences.bind(this),
|
||||||
|
"codeQLTests.acceptOutput": this.acceptOutput.bind(this),
|
||||||
|
"codeQLTests.acceptOutputContextTestItem": this.acceptOutput.bind(this),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Override to compute the path of the test file from the selected node. */
|
||||||
|
protected abstract getTestPath(node: TestNode): string;
|
||||||
|
|
||||||
|
private async acceptOutput(node: TestNode): Promise<void> {
|
||||||
|
const testPath = this.getTestPath(node);
|
||||||
|
const stat = await lstat(testPath);
|
||||||
|
if (stat.isFile()) {
|
||||||
|
const expectedPath = getExpectedFile(testPath);
|
||||||
|
const actualPath = getActualFile(testPath);
|
||||||
|
await copy(actualPath, expectedPath, { overwrite: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showOutputDifferences(node: TestNode): Promise<void> {
|
||||||
|
const testId = this.getTestPath(node);
|
||||||
|
const stat = await lstat(testId);
|
||||||
|
if (stat.isFile()) {
|
||||||
|
const expectedPath = getExpectedFile(testId);
|
||||||
|
const expectedUri = Uri.file(expectedPath);
|
||||||
|
const actualPath = getActualFile(testId);
|
||||||
|
const options: TextDocumentShowOptions = {
|
||||||
|
preserveFocus: true,
|
||||||
|
preview: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!(await pathExists(expectedPath))) {
|
||||||
|
// Just create a new file.
|
||||||
|
await createFile(expectedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await pathExists(actualPath)) {
|
||||||
|
const actualUri = Uri.file(actualPath);
|
||||||
|
await this.app.commands.execute(
|
||||||
|
"vscode.diff",
|
||||||
|
expectedUri,
|
||||||
|
actualUri,
|
||||||
|
`Expected vs. Actual for ${basename(testId)}`,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await window.showTextDocument(expectedUri, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
373
extensions/ql-vscode/src/test-manager.ts
Normal file
373
extensions/ql-vscode/src/test-manager.ts
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
import { readFile } from "fs-extra";
|
||||||
|
import {
|
||||||
|
CancellationToken,
|
||||||
|
Location,
|
||||||
|
Range,
|
||||||
|
TestController,
|
||||||
|
TestItem,
|
||||||
|
TestMessage,
|
||||||
|
TestRun,
|
||||||
|
TestRunProfileKind,
|
||||||
|
TestRunRequest,
|
||||||
|
Uri,
|
||||||
|
WorkspaceFolder,
|
||||||
|
WorkspaceFoldersChangeEvent,
|
||||||
|
tests,
|
||||||
|
workspace,
|
||||||
|
} from "vscode";
|
||||||
|
import { DisposableObject } from "./pure/disposable-object";
|
||||||
|
import {
|
||||||
|
QLTestDirectory,
|
||||||
|
QLTestDiscovery,
|
||||||
|
QLTestFile,
|
||||||
|
QLTestNode,
|
||||||
|
} from "./qltest-discovery";
|
||||||
|
import { CodeQLCliServer } from "./cli";
|
||||||
|
import { getErrorMessage } from "./pure/helpers-pure";
|
||||||
|
import { BaseLogger, LogOptions } from "./common";
|
||||||
|
import { TestRunner } from "./test-runner";
|
||||||
|
import { TestManagerBase } from "./test-manager-base";
|
||||||
|
import { App } from "./common/app";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the complete text content of the specified file. If there is an error reading the file,
|
||||||
|
* an error message is added to `testMessages` and this function returns undefined.
|
||||||
|
*/
|
||||||
|
async function tryReadFileContents(
|
||||||
|
path: string,
|
||||||
|
testMessages: TestMessage[],
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
return await readFile(path, { encoding: "utf-8" });
|
||||||
|
} catch (e) {
|
||||||
|
testMessages.push(
|
||||||
|
new TestMessage(
|
||||||
|
`Error reading from file '${path}': ${getErrorMessage(e)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function forEachTest(testItem: TestItem, op: (test: TestItem) => void): void {
|
||||||
|
if (testItem.children.size > 0) {
|
||||||
|
// This is a directory, so recurse into the children.
|
||||||
|
for (const [, child] of testItem.children) {
|
||||||
|
forEachTest(child, op);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is a leaf node, so it's a test.
|
||||||
|
op(testItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of `BaseLogger` that logs to the output of a `TestRun`.
|
||||||
|
*/
|
||||||
|
class TestRunLogger implements BaseLogger {
|
||||||
|
public constructor(private readonly testRun: TestRun) {}
|
||||||
|
|
||||||
|
public async log(message: string, options?: LogOptions): Promise<void> {
|
||||||
|
// "\r\n" because that's what the test terminal wants.
|
||||||
|
const lineEnding = options?.trailingNewline === false ? "" : "\r\n";
|
||||||
|
this.testRun.appendOutput(message + lineEnding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles test discovery for a specific workspace folder, and reports back to `TestManager`.
|
||||||
|
*/
|
||||||
|
class WorkspaceFolderHandler extends DisposableObject {
|
||||||
|
private readonly testDiscovery: QLTestDiscovery;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly workspaceFolder: WorkspaceFolder,
|
||||||
|
private readonly testUI: TestManager,
|
||||||
|
cliServer: CodeQLCliServer,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.testDiscovery = new QLTestDiscovery(workspaceFolder, cliServer);
|
||||||
|
this.push(
|
||||||
|
this.testDiscovery.onDidChangeTests(this.handleDidChangeTests, this),
|
||||||
|
);
|
||||||
|
this.testDiscovery.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDidChangeTests(): void {
|
||||||
|
const testDirectory = this.testDiscovery.testDirectory;
|
||||||
|
|
||||||
|
this.testUI.updateTestsForWorkspaceFolder(
|
||||||
|
this.workspaceFolder,
|
||||||
|
testDirectory,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that populates the VS Code "Test Explorer" panel for CodeQL, and handles running and
|
||||||
|
* debugging of tests.
|
||||||
|
*/
|
||||||
|
export class TestManager extends TestManagerBase {
|
||||||
|
/**
|
||||||
|
* Maps from each workspace folder being tracked to the `WorkspaceFolderHandler` responsible for
|
||||||
|
* tracking it.
|
||||||
|
*/
|
||||||
|
private readonly workspaceFolderHandlers = new Map<
|
||||||
|
WorkspaceFolder,
|
||||||
|
WorkspaceFolderHandler
|
||||||
|
>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
app: App,
|
||||||
|
private readonly testRunner: TestRunner,
|
||||||
|
private readonly cliServer: CodeQLCliServer,
|
||||||
|
// Having this as a parameter with a default value makes passing in a mock easier.
|
||||||
|
private readonly testController: TestController = tests.createTestController(
|
||||||
|
"codeql",
|
||||||
|
"CodeQL Tests",
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
super(app);
|
||||||
|
|
||||||
|
this.testController.createRunProfile(
|
||||||
|
"Run",
|
||||||
|
TestRunProfileKind.Run,
|
||||||
|
this.run.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start by tracking whatever folders are currently in the workspace.
|
||||||
|
this.startTrackingWorkspaceFolders(workspace.workspaceFolders ?? []);
|
||||||
|
|
||||||
|
// Listen for changes to the set of workspace folders.
|
||||||
|
workspace.onDidChangeWorkspaceFolders(
|
||||||
|
this.handleDidChangeWorkspaceFolders,
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this.workspaceFolderHandlers.clear(); // These will be disposed in the `super.dispose()` call.
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getTestPath(node: TestItem): string {
|
||||||
|
if (node.uri === undefined || node.uri.scheme !== "file") {
|
||||||
|
throw new Error("Selected test is not a CodeQL test.");
|
||||||
|
}
|
||||||
|
return node.uri.fsPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Start tracking tests in the specified workspace folders. */
|
||||||
|
private startTrackingWorkspaceFolders(
|
||||||
|
workspaceFolders: readonly WorkspaceFolder[],
|
||||||
|
): void {
|
||||||
|
for (const workspaceFolder of workspaceFolders) {
|
||||||
|
const workspaceFolderHandler = new WorkspaceFolderHandler(
|
||||||
|
workspaceFolder,
|
||||||
|
this,
|
||||||
|
this.cliServer,
|
||||||
|
);
|
||||||
|
this.track(workspaceFolderHandler);
|
||||||
|
this.workspaceFolderHandlers.set(workspaceFolder, workspaceFolderHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stop tracking tests in the specified workspace folders. */
|
||||||
|
private stopTrackingWorkspaceFolders(
|
||||||
|
workspaceFolders: readonly WorkspaceFolder[],
|
||||||
|
): void {
|
||||||
|
for (const workspaceFolder of workspaceFolders) {
|
||||||
|
const workspaceFolderHandler =
|
||||||
|
this.workspaceFolderHandlers.get(workspaceFolder);
|
||||||
|
if (workspaceFolderHandler !== undefined) {
|
||||||
|
// Delete the root item for this workspace folder, if any.
|
||||||
|
this.testController.items.delete(workspaceFolder.uri.toString());
|
||||||
|
this.disposeAndStopTracking(workspaceFolderHandler);
|
||||||
|
this.workspaceFolderHandlers.delete(workspaceFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDidChangeWorkspaceFolders(
|
||||||
|
e: WorkspaceFoldersChangeEvent,
|
||||||
|
): void {
|
||||||
|
this.startTrackingWorkspaceFolders(e.added);
|
||||||
|
this.stopTrackingWorkspaceFolders(e.removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the test controller when we discover changes to the tests in the workspace folder.
|
||||||
|
*/
|
||||||
|
public updateTestsForWorkspaceFolder(
|
||||||
|
workspaceFolder: WorkspaceFolder,
|
||||||
|
testDirectory: QLTestDirectory | undefined,
|
||||||
|
): void {
|
||||||
|
if (testDirectory !== undefined) {
|
||||||
|
// Adding an item with the same ID as an existing item will replace it, which is exactly what
|
||||||
|
// we want.
|
||||||
|
// Test discovery returns a root `QLTestDirectory` representing the workspace folder itself,
|
||||||
|
// named after the `WorkspaceFolder` object's `name` property. We can map this directly to a
|
||||||
|
// `TestItem`.
|
||||||
|
this.testController.items.add(
|
||||||
|
this.createTestItemTree(testDirectory, true),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// No tests, so delete any existing item.
|
||||||
|
this.testController.items.delete(workspaceFolder.uri.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a tree of `TestItem`s from the root `QlTestNode` provided by test discovery.
|
||||||
|
*/
|
||||||
|
private createTestItemTree(node: QLTestNode, isRoot: boolean): TestItem {
|
||||||
|
// Prefix the ID to identify it as a directory or a test
|
||||||
|
const itemType = node instanceof QLTestDirectory ? "dir" : "test";
|
||||||
|
const testItem = this.testController.createTestItem(
|
||||||
|
// For the root of a workspace folder, use the full path as the ID. Otherwise, use the node's
|
||||||
|
// name as the ID, since it's shorter but still unique.
|
||||||
|
`${itemType} ${isRoot ? node.path : node.name}`,
|
||||||
|
node.name,
|
||||||
|
Uri.file(node.path),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const childNode of node.children) {
|
||||||
|
const childItem = this.createTestItemTree(childNode, false);
|
||||||
|
if (childNode instanceof QLTestFile) {
|
||||||
|
childItem.range = new Range(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
testItem.children.add(childItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return testItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the tests specified by the `TestRunRequest` parameter.
|
||||||
|
*
|
||||||
|
* Public because this is used in unit tests.
|
||||||
|
*/
|
||||||
|
public async run(
|
||||||
|
request: TestRunRequest,
|
||||||
|
token: CancellationToken,
|
||||||
|
): Promise<void> {
|
||||||
|
const testsToRun = this.computeTestsToRun(request);
|
||||||
|
const testRun = this.testController.createTestRun(request, undefined, true);
|
||||||
|
try {
|
||||||
|
const tests: string[] = [];
|
||||||
|
testsToRun.forEach((testItem, testPath) => {
|
||||||
|
testRun.enqueued(testItem);
|
||||||
|
tests.push(testPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
const logger = new TestRunLogger(testRun);
|
||||||
|
|
||||||
|
await this.testRunner.run(tests, logger, token, async (event) => {
|
||||||
|
// Pass the test path from the event through `Uri` and back via `fsPath` so that it matches
|
||||||
|
// the canonicalization of the URI that we used to create the `TestItem`.
|
||||||
|
const testItem = testsToRun.get(Uri.file(event.test).fsPath);
|
||||||
|
if (testItem === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected result from unknown test '${event.test}'.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = event.compilationMs + event.evaluationMs;
|
||||||
|
if (event.pass) {
|
||||||
|
testRun.passed(testItem, duration);
|
||||||
|
} else {
|
||||||
|
// Construct a list of `TestMessage`s to report for the failure.
|
||||||
|
const testMessages: TestMessage[] = [];
|
||||||
|
if (event.failureDescription !== undefined) {
|
||||||
|
testMessages.push(new TestMessage(event.failureDescription));
|
||||||
|
}
|
||||||
|
if (event.diff?.length && event.actual !== undefined) {
|
||||||
|
// Actual results differ from expected results. Read both sets of results and create a
|
||||||
|
// diff to put in the message.
|
||||||
|
const expected = await tryReadFileContents(
|
||||||
|
event.expected,
|
||||||
|
testMessages,
|
||||||
|
);
|
||||||
|
const actual = await tryReadFileContents(
|
||||||
|
event.actual,
|
||||||
|
testMessages,
|
||||||
|
);
|
||||||
|
if (expected !== undefined && actual !== undefined) {
|
||||||
|
testMessages.push(
|
||||||
|
TestMessage.diff(
|
||||||
|
"Actual output differs from expected",
|
||||||
|
expected,
|
||||||
|
actual,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.messages?.length > 0) {
|
||||||
|
// The test didn't make it far enough to produce results. Transform any error messages
|
||||||
|
// into `TestMessage`s and report the test as "errored".
|
||||||
|
const testMessages = event.messages.map((m) => {
|
||||||
|
const location = new Location(
|
||||||
|
Uri.file(m.position.fileName),
|
||||||
|
new Range(
|
||||||
|
m.position.line - 1,
|
||||||
|
m.position.column - 1,
|
||||||
|
m.position.endLine - 1,
|
||||||
|
m.position.endColumn - 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const testMessage = new TestMessage(m.message);
|
||||||
|
testMessage.location = location;
|
||||||
|
return testMessage;
|
||||||
|
});
|
||||||
|
testRun.errored(testItem, testMessages, duration);
|
||||||
|
} else {
|
||||||
|
// Results didn't match expectations. Report the test as "failed".
|
||||||
|
if (testMessages.length === 0) {
|
||||||
|
// If we managed to get here without creating any `TestMessage`s, create a default one
|
||||||
|
// here. Any failed test needs at least one message.
|
||||||
|
testMessages.push(new TestMessage("Test failed"));
|
||||||
|
}
|
||||||
|
testRun.failed(testItem, testMessages, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
testRun.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the set of tests to run as specified in the `TestRunRequest` object.
|
||||||
|
*/
|
||||||
|
private computeTestsToRun(request: TestRunRequest): Map<string, TestItem> {
|
||||||
|
const testsToRun = new Map<string, TestItem>();
|
||||||
|
if (request.include !== undefined) {
|
||||||
|
// Include these tests, recursively expanding test directories into their list of contained
|
||||||
|
// tests.
|
||||||
|
for (const includedTestItem of request.include) {
|
||||||
|
forEachTest(includedTestItem, (testItem) =>
|
||||||
|
testsToRun.set(testItem.uri!.fsPath, testItem),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Include all of the tests.
|
||||||
|
for (const [, includedTestItem] of this.testController.items) {
|
||||||
|
forEachTest(includedTestItem, (testItem) =>
|
||||||
|
testsToRun.set(testItem.uri!.fsPath, testItem),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (request.exclude !== undefined) {
|
||||||
|
// Exclude the specified tests from the set we've computed so far, again recursively expanding
|
||||||
|
// test directories into their list of contained tests.
|
||||||
|
for (const excludedTestItem of request.exclude) {
|
||||||
|
forEachTest(excludedTestItem, (testItem) =>
|
||||||
|
testsToRun.delete(testItem.uri!.fsPath),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return testsToRun;
|
||||||
|
}
|
||||||
|
}
|
||||||
136
extensions/ql-vscode/src/test-runner.ts
Normal file
136
extensions/ql-vscode/src/test-runner.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { CancellationToken, Uri } from "vscode";
|
||||||
|
import { CodeQLCliServer, TestCompleted } from "./cli";
|
||||||
|
import { DatabaseItem, DatabaseManager } from "./local-databases";
|
||||||
|
import {
|
||||||
|
getOnDiskWorkspaceFolders,
|
||||||
|
showAndLogExceptionWithTelemetry,
|
||||||
|
showAndLogWarningMessage,
|
||||||
|
} from "./helpers";
|
||||||
|
import { asError, getErrorMessage } from "./pure/helpers-pure";
|
||||||
|
import { redactableError } from "./pure/errors";
|
||||||
|
import { access } from "fs-extra";
|
||||||
|
import { BaseLogger } from "./common";
|
||||||
|
import { DisposableObject } from "./pure/disposable-object";
|
||||||
|
|
||||||
|
async function isFileAccessible(uri: Uri): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await access(uri.fsPath);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestRunner extends DisposableObject {
|
||||||
|
public constructor(
|
||||||
|
private readonly databaseManager: DatabaseManager,
|
||||||
|
private readonly cliServer: CodeQLCliServer,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async run(
|
||||||
|
tests: string[],
|
||||||
|
logger: BaseLogger,
|
||||||
|
token: CancellationToken,
|
||||||
|
eventHandler: (event: TestCompleted) => Promise<void>,
|
||||||
|
): Promise<void> {
|
||||||
|
const currentDatabaseUri =
|
||||||
|
this.databaseManager.currentDatabaseItem?.databaseUri;
|
||||||
|
const databasesUnderTest: DatabaseItem[] = [];
|
||||||
|
for (const database of this.databaseManager.databaseItems) {
|
||||||
|
for (const test of tests) {
|
||||||
|
if (await database.isAffectedByTest(test)) {
|
||||||
|
databasesUnderTest.push(database);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.removeDatabasesBeforeTests(databasesUnderTest, token);
|
||||||
|
try {
|
||||||
|
const workspacePaths = getOnDiskWorkspaceFolders();
|
||||||
|
for await (const event of this.cliServer.runTests(tests, workspacePaths, {
|
||||||
|
cancellationToken: token,
|
||||||
|
logger,
|
||||||
|
})) {
|
||||||
|
await eventHandler(event);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// CodeQL testing can throw exception even in normal scenarios. For example, if the test run
|
||||||
|
// produces no output (which is normal), the testing command would throw an exception on
|
||||||
|
// unexpected EOF during json parsing. So nothing needs to be done here - all the relevant
|
||||||
|
// error information (if any) should have already been written to the test logger.
|
||||||
|
} finally {
|
||||||
|
await this.reopenDatabasesAfterTests(
|
||||||
|
databasesUnderTest,
|
||||||
|
currentDatabaseUri,
|
||||||
|
token,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeDatabasesBeforeTests(
|
||||||
|
databasesUnderTest: DatabaseItem[],
|
||||||
|
token: CancellationToken,
|
||||||
|
): Promise<void> {
|
||||||
|
for (const database of databasesUnderTest) {
|
||||||
|
try {
|
||||||
|
await this.databaseManager.removeDatabaseItem(
|
||||||
|
(_) => {
|
||||||
|
/* no progress reporting */
|
||||||
|
},
|
||||||
|
token,
|
||||||
|
database,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// This method is invoked from Test Explorer UI, and testing indicates that Test
|
||||||
|
// Explorer UI swallows any thrown exception without reporting it to the user.
|
||||||
|
// So we need to display the error message ourselves and then rethrow.
|
||||||
|
void showAndLogExceptionWithTelemetry(
|
||||||
|
redactableError(asError(e))`Cannot remove database ${
|
||||||
|
database.name
|
||||||
|
}: ${getErrorMessage(e)}`,
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async reopenDatabasesAfterTests(
|
||||||
|
databasesUnderTest: DatabaseItem[],
|
||||||
|
currentDatabaseUri: Uri | undefined,
|
||||||
|
token: CancellationToken,
|
||||||
|
): Promise<void> {
|
||||||
|
for (const closedDatabase of databasesUnderTest) {
|
||||||
|
const uri = closedDatabase.databaseUri;
|
||||||
|
if (await isFileAccessible(uri)) {
|
||||||
|
try {
|
||||||
|
const reopenedDatabase = await this.databaseManager.openDatabase(
|
||||||
|
(_) => {
|
||||||
|
/* no progress reporting */
|
||||||
|
},
|
||||||
|
token,
|
||||||
|
uri,
|
||||||
|
);
|
||||||
|
await this.databaseManager.renameDatabaseItem(
|
||||||
|
reopenedDatabase,
|
||||||
|
closedDatabase.name,
|
||||||
|
);
|
||||||
|
if (currentDatabaseUri?.toString() === uri.toString()) {
|
||||||
|
await this.databaseManager.setCurrentDatabaseItem(
|
||||||
|
reopenedDatabase,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// This method is invoked from Test Explorer UI, and testing indicates that Test
|
||||||
|
// Explorer UI swallows any thrown exception without reporting it to the user.
|
||||||
|
// So we need to display the error message ourselves and then rethrow.
|
||||||
|
void showAndLogWarningMessage(`Cannot reopen database ${uri}: ${e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
import { lstat, copy, pathExists, createFile } from "fs-extra";
|
|
||||||
import { basename } from "path";
|
|
||||||
import { Uri, TextDocumentShowOptions, window } from "vscode";
|
|
||||||
import {
|
import {
|
||||||
TestHub,
|
TestHub,
|
||||||
TestController,
|
TestController,
|
||||||
@@ -10,13 +7,11 @@ import {
|
|||||||
TestEvent,
|
TestEvent,
|
||||||
TestSuiteEvent,
|
TestSuiteEvent,
|
||||||
} from "vscode-test-adapter-api";
|
} from "vscode-test-adapter-api";
|
||||||
|
|
||||||
import { showAndLogWarningMessage } from "./helpers";
|
|
||||||
import { TestTreeNode } from "./test-tree-node";
|
import { TestTreeNode } from "./test-tree-node";
|
||||||
import { DisposableObject } from "./pure/disposable-object";
|
import { DisposableObject } from "./pure/disposable-object";
|
||||||
import { QLTestAdapter, getExpectedFile, getActualFile } from "./test-adapter";
|
import { QLTestAdapter } from "./test-adapter";
|
||||||
import { TestUICommands } from "./common/commands";
|
|
||||||
import { App } from "./common/app";
|
import { App } from "./common/app";
|
||||||
|
import { TestManagerBase } from "./test-manager-base";
|
||||||
|
|
||||||
type VSCodeTestEvent =
|
type VSCodeTestEvent =
|
||||||
| TestRunStartedEvent
|
| TestRunStartedEvent
|
||||||
@@ -42,23 +37,15 @@ class QLTestListener extends DisposableObject {
|
|||||||
/**
|
/**
|
||||||
* Service that implements all UI and commands for QL tests.
|
* Service that implements all UI and commands for QL tests.
|
||||||
*/
|
*/
|
||||||
export class TestUIService extends DisposableObject implements TestController {
|
export class TestUIService extends TestManagerBase implements TestController {
|
||||||
private readonly listeners: Map<TestAdapter, QLTestListener> = new Map();
|
private readonly listeners: Map<TestAdapter, QLTestListener> = new Map();
|
||||||
|
|
||||||
constructor(private readonly app: App, private readonly testHub: TestHub) {
|
public constructor(app: App, private readonly testHub: TestHub) {
|
||||||
super();
|
super(app);
|
||||||
|
|
||||||
testHub.registerTestController(this);
|
testHub.registerTestController(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCommands(): TestUICommands {
|
|
||||||
return {
|
|
||||||
"codeQLTests.showOutputDifferences":
|
|
||||||
this.showOutputDifferences.bind(this),
|
|
||||||
"codeQLTests.acceptOutput": this.acceptOutput.bind(this),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.testHub.unregisterTestController(this);
|
this.testHub.unregisterTestController(this);
|
||||||
|
|
||||||
@@ -75,47 +62,7 @@ export class TestUIService extends DisposableObject implements TestController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async acceptOutput(node: TestTreeNode): Promise<void> {
|
protected getTestPath(node: TestTreeNode): string {
|
||||||
const testId = node.info.id;
|
return node.info.id;
|
||||||
const stat = await lstat(testId);
|
|
||||||
if (stat.isFile()) {
|
|
||||||
const expectedPath = getExpectedFile(testId);
|
|
||||||
const actualPath = getActualFile(testId);
|
|
||||||
await copy(actualPath, expectedPath, { overwrite: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async showOutputDifferences(node: TestTreeNode): Promise<void> {
|
|
||||||
const testId = node.info.id;
|
|
||||||
const stat = await lstat(testId);
|
|
||||||
if (stat.isFile()) {
|
|
||||||
const expectedPath = getExpectedFile(testId);
|
|
||||||
const expectedUri = Uri.file(expectedPath);
|
|
||||||
const actualPath = getActualFile(testId);
|
|
||||||
const options: TextDocumentShowOptions = {
|
|
||||||
preserveFocus: true,
|
|
||||||
preview: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!(await pathExists(expectedPath))) {
|
|
||||||
void showAndLogWarningMessage(
|
|
||||||
`'${basename(expectedPath)}' does not exist. Creating an empty file.`,
|
|
||||||
);
|
|
||||||
await createFile(expectedPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await pathExists(actualPath)) {
|
|
||||||
const actualUri = Uri.file(actualPath);
|
|
||||||
await this.app.commands.execute(
|
|
||||||
"vscode.diff",
|
|
||||||
expectedUri,
|
|
||||||
actualUri,
|
|
||||||
`Expected vs. Actual for ${basename(testId)}`,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await window.showTextDocument(expectedUri, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
extensions/ql-vscode/test/data/.gitignore
vendored
3
extensions/ql-vscode/test/data/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
.vscode
|
.vscode/**
|
||||||
|
!.vscode/launch.json
|
||||||
|
|||||||
11
extensions/ql-vscode/test/data/.vscode/launch.json
vendored
Normal file
11
extensions/ql-vscode/test/data/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// A launch configuration that compiles the extension and then opens it inside a new window
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "simple-query",
|
||||||
|
"type": "codeql",
|
||||||
|
"request": "launch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
extensions/ql-vscode/test/data/textfile.txt
Normal file
0
extensions/ql-vscode/test/data/textfile.txt
Normal file
@@ -7,10 +7,15 @@ type CmdDecl = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DebuggerDecl = {
|
||||||
|
variables?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
describe("commands declared in package.json", () => {
|
describe("commands declared in package.json", () => {
|
||||||
const manifest = readJsonSync(join(__dirname, "../../package.json"));
|
const manifest = readJsonSync(join(__dirname, "../../package.json"));
|
||||||
const commands = manifest.contributes.commands;
|
const commands = manifest.contributes.commands;
|
||||||
const menus = manifest.contributes.menus;
|
const menus = manifest.contributes.menus;
|
||||||
|
const debuggers = manifest.contributes.debuggers;
|
||||||
|
|
||||||
const disabledInPalette: Set<string> = new Set<string>();
|
const disabledInPalette: Set<string> = new Set<string>();
|
||||||
|
|
||||||
@@ -60,6 +65,15 @@ describe("commands declared in package.json", () => {
|
|||||||
contribContextMenuCmds.add(command);
|
contribContextMenuCmds.add(command);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
debuggers.forEach((debuggerDecl: DebuggerDecl) => {
|
||||||
|
if (debuggerDecl.variables !== undefined) {
|
||||||
|
for (const command of Object.values(debuggerDecl.variables)) {
|
||||||
|
// Commands used as debug configuration variables need not be enabled in the command palette.
|
||||||
|
paletteCmds.delete(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
menus.commandPalette.forEach((commandDecl: CmdDecl) => {
|
menus.commandPalette.forEach((commandDecl: CmdDecl) => {
|
||||||
if (commandDecl.when === "false")
|
if (commandDecl.when === "false")
|
||||||
disabledInPalette.add(commandDecl.command);
|
disabledInPalette.add(commandDecl.command);
|
||||||
@@ -85,6 +99,9 @@ describe("commands declared in package.json", () => {
|
|||||||
it("should have the right commands accessible from the command palette", () => {
|
it("should have the right commands accessible from the command palette", () => {
|
||||||
paletteCmds.forEach((command) => {
|
paletteCmds.forEach((command) => {
|
||||||
// command ${command} should be enabled in the command palette
|
// command ${command} should be enabled in the command palette
|
||||||
|
if (disabledInPalette.has(command) !== false) {
|
||||||
|
expect(command).toBe("enabled");
|
||||||
|
}
|
||||||
expect(disabledInPalette.has(command)).toBe(false);
|
expect(disabledInPalette.has(command)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
newtype TNumber = MkNumber(int n) {
|
||||||
|
n in [0..20]
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class InterestingNumber extends TNumber
|
||||||
|
{
|
||||||
|
int value;
|
||||||
|
|
||||||
|
InterestingNumber() {
|
||||||
|
this = MkNumber(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
string toString() {
|
||||||
|
result = value.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
final int getValue() {
|
||||||
|
result = value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import QuickEvalLib
|
||||||
|
|
||||||
|
class PrimeNumber extends InterestingNumber {
|
||||||
|
PrimeNumber() {
|
||||||
|
exists(int n | this = MkNumber(n) |
|
||||||
|
n in [
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
11,
|
||||||
|
13,
|
||||||
|
17,
|
||||||
|
19
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
from InterestingNumber n
|
||||||
|
select n.toString()
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
import {
|
||||||
|
DebugAdapterTracker,
|
||||||
|
DebugAdapterTrackerFactory,
|
||||||
|
DebugSession,
|
||||||
|
ProviderResult,
|
||||||
|
Uri,
|
||||||
|
debug,
|
||||||
|
workspace,
|
||||||
|
} from "vscode";
|
||||||
|
import * as CodeQLProtocol from "../../../../src/debugger/debug-protocol";
|
||||||
|
import { DisposableObject } from "../../../../src/pure/disposable-object";
|
||||||
|
import { QueryResultType } from "../../../../src/pure/legacy-messages";
|
||||||
|
import { CoreCompletedQuery } from "../../../../src/queryRunner";
|
||||||
|
import { QueryOutputDir } from "../../../../src/run-queries-shared";
|
||||||
|
import {
|
||||||
|
QLDebugArgs,
|
||||||
|
QLDebugConfiguration,
|
||||||
|
} from "../../../../src/debugger/debug-configuration";
|
||||||
|
import { join } from "path";
|
||||||
|
import { writeFile } from "fs-extra";
|
||||||
|
import { expect } from "@jest/globals";
|
||||||
|
import { AppCommandManager } from "../../../../src/common/commands";
|
||||||
|
import { getOnDiskWorkspaceFolders } from "../../../../src/helpers";
|
||||||
|
|
||||||
|
type Resolver<T> = (value: T) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for Debug Adapter Protocol messages from a particular debug session, and reports the
|
||||||
|
* interesting events back to the `DebugController`.
|
||||||
|
*/
|
||||||
|
class Tracker implements DebugAdapterTracker {
|
||||||
|
private database: string | undefined;
|
||||||
|
private queryPath: string | undefined;
|
||||||
|
private started: CodeQLProtocol.EvaluationStartedEvent["body"] | undefined =
|
||||||
|
undefined;
|
||||||
|
private completed:
|
||||||
|
| CodeQLProtocol.EvaluationCompletedEvent["body"]
|
||||||
|
| undefined = undefined;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly session: DebugSession,
|
||||||
|
private readonly controller: DebugController,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public onWillReceiveMessage(
|
||||||
|
message: CodeQLProtocol.AnyProtocolMessage,
|
||||||
|
): void {
|
||||||
|
switch (message.type) {
|
||||||
|
case "request":
|
||||||
|
this.onWillReceiveRequest(message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDidSendMessage(message: CodeQLProtocol.AnyProtocolMessage): void {
|
||||||
|
void this.session;
|
||||||
|
switch (message.type) {
|
||||||
|
case "event":
|
||||||
|
this.onDidSendEvent(message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onWillReceiveRequest(request: CodeQLProtocol.AnyRequest): void {
|
||||||
|
switch (request.command) {
|
||||||
|
case "launch":
|
||||||
|
this.controller.handleEvent({
|
||||||
|
kind: "launched",
|
||||||
|
request,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDidSendEvent(event: CodeQLProtocol.AnyEvent): void {
|
||||||
|
switch (event.event) {
|
||||||
|
case "codeql-evaluation-started":
|
||||||
|
this.started = event.body;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "codeql-evaluation-completed":
|
||||||
|
this.completed = event.body;
|
||||||
|
this.controller.handleEvent({
|
||||||
|
kind: "evaluationCompleted",
|
||||||
|
started: this.started!,
|
||||||
|
results: {
|
||||||
|
...this.started!,
|
||||||
|
...this.completed!,
|
||||||
|
outputDir: new QueryOutputDir(this.started!.outputDir),
|
||||||
|
queryTarget: {
|
||||||
|
queryPath: this.queryPath!,
|
||||||
|
quickEvalPosition:
|
||||||
|
this.started!.quickEvalContext?.quickEvalPosition,
|
||||||
|
},
|
||||||
|
dbPath: this.database!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "exited":
|
||||||
|
this.controller.handleEvent({
|
||||||
|
kind: "exited",
|
||||||
|
body: event.body,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "stopped":
|
||||||
|
this.controller.handleEvent({
|
||||||
|
kind: "stopped",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interesting event from the debug session. These are queued by the `DebugContoller`. The test
|
||||||
|
* code consumes these events and asserts that they are in the correct order and have the correct
|
||||||
|
* data.
|
||||||
|
*/
|
||||||
|
export type DebugEventKind =
|
||||||
|
| "launched"
|
||||||
|
| "evaluationCompleted"
|
||||||
|
| "terminated"
|
||||||
|
| "stopped"
|
||||||
|
| "exited"
|
||||||
|
| "sessionClosed";
|
||||||
|
|
||||||
|
export interface DebugEvent {
|
||||||
|
kind: DebugEventKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LaunchedEvent extends DebugEvent {
|
||||||
|
kind: "launched";
|
||||||
|
request: CodeQLProtocol.LaunchRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EvaluationCompletedEvent extends DebugEvent {
|
||||||
|
kind: "evaluationCompleted";
|
||||||
|
started: CodeQLProtocol.EvaluationStartedEvent["body"];
|
||||||
|
results: CoreCompletedQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerminatedEvent extends DebugEvent {
|
||||||
|
kind: "terminated";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoppedEvent extends DebugEvent {
|
||||||
|
kind: "stopped";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExitedEvent extends DebugEvent {
|
||||||
|
kind: "exited";
|
||||||
|
body: CodeQLProtocol.ExitedEvent["body"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionClosedEvent extends DebugEvent {
|
||||||
|
kind: "sessionClosed";
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnyDebugEvent =
|
||||||
|
| LaunchedEvent
|
||||||
|
| EvaluationCompletedEvent
|
||||||
|
| StoppedEvent
|
||||||
|
| ExitedEvent
|
||||||
|
| TerminatedEvent
|
||||||
|
| SessionClosedEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes a simple facade over a debugging session. Test code invokes the various commands as
|
||||||
|
* async functions, and consumes events reported by the session to ensure the correct sequence and
|
||||||
|
* data.
|
||||||
|
*/
|
||||||
|
export class DebugController
|
||||||
|
extends DisposableObject
|
||||||
|
implements DebugAdapterTrackerFactory
|
||||||
|
{
|
||||||
|
/** Queue of events reported by the session. */
|
||||||
|
private readonly eventQueue: AnyDebugEvent[] = [];
|
||||||
|
/**
|
||||||
|
* The index of the next event to be read from the queue. This index may be equal to the length of
|
||||||
|
* the queue, in which case all events received so far have been consumed, and the next attempt to
|
||||||
|
* consume an event will block waiting for that event.
|
||||||
|
* */
|
||||||
|
private nextEventIndex = 0;
|
||||||
|
/**
|
||||||
|
* If the client is currently blocked waiting for a new event, this property holds the `resolve()`
|
||||||
|
* function that will resolve the promise on which the client is blocked.
|
||||||
|
*/
|
||||||
|
private resolver: Resolver<AnyDebugEvent> | undefined = undefined;
|
||||||
|
|
||||||
|
public constructor(private readonly appCommands: AppCommandManager) {
|
||||||
|
super();
|
||||||
|
this.push(debug.registerDebugAdapterTrackerFactory("codeql", this));
|
||||||
|
this.push(
|
||||||
|
debug.onDidTerminateDebugSession(
|
||||||
|
this.handleDidTerminateDebugSession.bind(this),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.push(
|
||||||
|
debug.onDidChangeActiveDebugSession(
|
||||||
|
this.handleDidChangeActiveDebugSession.bind(this),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public createDebugAdapterTracker(
|
||||||
|
session: DebugSession,
|
||||||
|
): ProviderResult<DebugAdapterTracker> {
|
||||||
|
return new Tracker(session, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createLaunchJson(config: QLDebugConfiguration): Promise<void> {
|
||||||
|
const launchJsonPath = join(
|
||||||
|
getOnDiskWorkspaceFolders()[0],
|
||||||
|
".vscode/launch.json",
|
||||||
|
);
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
launchJsonPath,
|
||||||
|
JSON.stringify({
|
||||||
|
version: "0.2.0",
|
||||||
|
configurations: [config],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a debug session via the "codeQL.debugQuery" copmmand.
|
||||||
|
*/
|
||||||
|
public debugQuery(uri: Uri): Promise<void> {
|
||||||
|
return this.appCommands.execute("codeQL.debugQuery", uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async startDebugging(
|
||||||
|
config: QLDebugArgs,
|
||||||
|
noDebug = false,
|
||||||
|
): Promise<void> {
|
||||||
|
const fullConfig: QLDebugConfiguration = {
|
||||||
|
...config,
|
||||||
|
name: "test",
|
||||||
|
type: "codeql",
|
||||||
|
request: "launch",
|
||||||
|
};
|
||||||
|
const options = noDebug
|
||||||
|
? {
|
||||||
|
noDebug: true,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return await this.appCommands.execute("workbench.action.debug.start", {
|
||||||
|
config: fullConfig,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async startDebuggingSelection(config: QLDebugArgs): Promise<void> {
|
||||||
|
return await this.startDebugging({
|
||||||
|
...config,
|
||||||
|
quickEval: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async continueDebuggingSelection(): Promise<void> {
|
||||||
|
return await this.appCommands.execute("codeQL.continueDebuggingSelection");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stepInto(): Promise<void> {
|
||||||
|
return await this.appCommands.execute("workbench.action.debug.stepInto");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stepOver(): Promise<void> {
|
||||||
|
return await this.appCommands.execute("workbench.action.debug.stepOver");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stepOut(): Promise<void> {
|
||||||
|
return await this.appCommands.execute("workbench.action.debug.stepOut");
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleEvent(event: AnyDebugEvent): void {
|
||||||
|
this.eventQueue.push(event);
|
||||||
|
if (this.resolver !== undefined) {
|
||||||
|
// We were waiting for this one. Resolve it.
|
||||||
|
this.nextEventIndex++;
|
||||||
|
const resolver = this.resolver;
|
||||||
|
this.resolver = undefined;
|
||||||
|
resolver(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDidTerminateDebugSession(_session: DebugSession): void {
|
||||||
|
this.handleEvent({
|
||||||
|
kind: "terminated",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDidChangeActiveDebugSession(
|
||||||
|
session: DebugSession | undefined,
|
||||||
|
): void {
|
||||||
|
if (session === undefined) {
|
||||||
|
this.handleEvent({
|
||||||
|
kind: "sessionClosed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes the next event in the queue. If all received messages have already been consumed, this
|
||||||
|
* function blocks until another event is received.
|
||||||
|
*/
|
||||||
|
private async nextEvent(): Promise<AnyDebugEvent> {
|
||||||
|
if (this.resolver !== undefined) {
|
||||||
|
const error = new Error(
|
||||||
|
"Attempt to wait for multiple debugger events at once.",
|
||||||
|
);
|
||||||
|
fail(error);
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
if (this.nextEventIndex < this.eventQueue.length) {
|
||||||
|
// No need to wait.
|
||||||
|
const event = this.eventQueue[this.nextEventIndex];
|
||||||
|
this.nextEventIndex++;
|
||||||
|
return Promise.resolve(event);
|
||||||
|
} else {
|
||||||
|
// No event available yet, so we need to wait.
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
this.resolver = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consume the next event in the queue, and assert that it is of the specified type.
|
||||||
|
*/
|
||||||
|
private async expectEvent<T extends DebugEvent>(kind: T["kind"]): Promise<T> {
|
||||||
|
const event = await this.nextEvent();
|
||||||
|
expect(event.kind).toBe(kind);
|
||||||
|
return <T>event;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async expectLaunched(): Promise<LaunchedEvent> {
|
||||||
|
return this.expectEvent<LaunchedEvent>("launched");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async expectExited(): Promise<ExitedEvent> {
|
||||||
|
return this.expectEvent<ExitedEvent>("exited");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async expectCompleted(): Promise<EvaluationCompletedEvent> {
|
||||||
|
return await this.expectEvent<EvaluationCompletedEvent>(
|
||||||
|
"evaluationCompleted",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async expectSucceeded(): Promise<EvaluationCompletedEvent> {
|
||||||
|
const event = await this.expectCompleted();
|
||||||
|
if (event.results.resultType !== QueryResultType.SUCCESS) {
|
||||||
|
expect(event.results.message).toBe("success");
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async expectFailed(): Promise<EvaluationCompletedEvent> {
|
||||||
|
const event = await this.expectCompleted();
|
||||||
|
expect(event.results.resultType).not.toEqual(QueryResultType.SUCCESS);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async expectStopped(): Promise<StoppedEvent> {
|
||||||
|
return await this.expectEvent<StoppedEvent>("stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async expectTerminated(): Promise<TerminatedEvent> {
|
||||||
|
return this.expectEvent<TerminatedEvent>("terminated");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async expectSessionClosed(): Promise<SessionClosedEvent> {
|
||||||
|
return this.expectEvent<SessionClosedEvent>("sessionClosed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait the specified number of milliseconds, and fail the test if any events are received within
|
||||||
|
* that timeframe.
|
||||||
|
*/
|
||||||
|
public async expectNoEvents(duration: number): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.nextEventIndex < this.eventQueue.length) {
|
||||||
|
const event = this.eventQueue[this.nextEventIndex];
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Did not expect to receive any events, but received '${event.kind}'.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, duration);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a function with a new instance of `DebugContoller`. Once the function completes, the
|
||||||
|
* debug controller is cleaned up.
|
||||||
|
*/
|
||||||
|
export async function withDebugController<T>(
|
||||||
|
appCommands: AppCommandManager,
|
||||||
|
op: (controller: DebugController) => Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
|
await workspace.getConfiguration().update("codeQL.canary", true);
|
||||||
|
try {
|
||||||
|
const controller = new DebugController(appCommands);
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
const result = await op(controller);
|
||||||
|
// The test should have consumed all expected events. Wait a couple seconds to make sure
|
||||||
|
// no more come in.
|
||||||
|
await controller.expectNoEvents(2000);
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
await debug.stopDebugging();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// In a separate finally block so that the controller gets disposed even if `stopDebugging()`
|
||||||
|
// fails.
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await workspace.getConfiguration().update("codeQL.canary", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { Selection, Uri, window, workspace } from "vscode";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
import { DatabaseManager } from "../../../../src/local-databases";
|
||||||
|
import {
|
||||||
|
cleanDatabases,
|
||||||
|
ensureTestDatabase,
|
||||||
|
getActivatedExtension,
|
||||||
|
} from "../../global.helper";
|
||||||
|
import { describeWithCodeQL } from "../../cli";
|
||||||
|
import { withDebugController } from "./debug-controller";
|
||||||
|
import { CodeQLCliServer } from "../../../../src/cli";
|
||||||
|
import { QueryOutputDir } from "../../../../src/run-queries-shared";
|
||||||
|
import { createVSCodeCommandManager } from "../../../../src/common/vscode/commands";
|
||||||
|
import { AllCommands } from "../../../../src/common/commands";
|
||||||
|
|
||||||
|
async function selectForQuickEval(
|
||||||
|
path: string,
|
||||||
|
line: number,
|
||||||
|
column: number,
|
||||||
|
endLine: number,
|
||||||
|
endColumn: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const document = await workspace.openTextDocument(path);
|
||||||
|
const editor = await window.showTextDocument(document);
|
||||||
|
editor.selection = new Selection(line, column, endLine, endColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getResultCount(
|
||||||
|
outputDir: QueryOutputDir,
|
||||||
|
cli: CodeQLCliServer,
|
||||||
|
): Promise<number> {
|
||||||
|
const info = await cli.bqrsInfo(outputDir.bqrsPath, 100);
|
||||||
|
const resultSet = info["result-sets"][0];
|
||||||
|
return resultSet.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for the query debugger
|
||||||
|
*/
|
||||||
|
describeWithCodeQL()("Debugger", () => {
|
||||||
|
let databaseManager: DatabaseManager;
|
||||||
|
let cli: CodeQLCliServer;
|
||||||
|
const appCommands = createVSCodeCommandManager<AllCommands>();
|
||||||
|
const simpleQueryPath = join(__dirname, "..", "data", "simple-query.ql");
|
||||||
|
const quickEvalQueryPath = join(__dirname, "..", "data", "QuickEvalQuery.ql");
|
||||||
|
const quickEvalLibPath = join(__dirname, "..", "data", "QuickEvalLib.qll");
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const extension = await getActivatedExtension();
|
||||||
|
databaseManager = extension.databaseManager;
|
||||||
|
cli = extension.cliServer;
|
||||||
|
cli.quiet = true;
|
||||||
|
|
||||||
|
await ensureTestDatabase(databaseManager, cli);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await cleanDatabases(databaseManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should debug a query and keep the session active", async () => {
|
||||||
|
await withDebugController(appCommands, async (controller) => {
|
||||||
|
await controller.debugQuery(Uri.file(simpleQueryPath));
|
||||||
|
await controller.expectLaunched();
|
||||||
|
await controller.expectSucceeded();
|
||||||
|
await controller.expectStopped();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should run a query and then stop debugging", async () => {
|
||||||
|
await withDebugController(appCommands, async (controller) => {
|
||||||
|
await controller.startDebugging(
|
||||||
|
{
|
||||||
|
query: simpleQueryPath,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await controller.expectLaunched();
|
||||||
|
await controller.expectSucceeded();
|
||||||
|
await controller.expectExited();
|
||||||
|
await controller.expectTerminated();
|
||||||
|
await controller.expectSessionClosed();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should run a quick evaluation", async () => {
|
||||||
|
await withDebugController(appCommands, async (controller) => {
|
||||||
|
await selectForQuickEval(quickEvalQueryPath, 18, 5, 18, 22);
|
||||||
|
|
||||||
|
// Don't specify a query path, so we'll default to the active document ("QuickEvalQuery.ql")
|
||||||
|
await controller.startDebuggingSelection({});
|
||||||
|
await controller.expectLaunched();
|
||||||
|
const result = await controller.expectSucceeded();
|
||||||
|
expect(result.started.quickEvalContext).toBeDefined();
|
||||||
|
expect(result.started.quickEvalContext!.quickEvalText).toBe(
|
||||||
|
"InterestingNumber",
|
||||||
|
);
|
||||||
|
expect(result.results.queryTarget.quickEvalPosition).toBeDefined();
|
||||||
|
expect(await getResultCount(result.results.outputDir, cli)).toBe(8);
|
||||||
|
await controller.expectStopped();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should run a quick evaluation on a library without any query context", async () => {
|
||||||
|
await withDebugController(appCommands, async (controller) => {
|
||||||
|
await selectForQuickEval(quickEvalLibPath, 4, 15, 4, 32);
|
||||||
|
|
||||||
|
// Don't specify a query path, so we'll default to the active document ("QuickEvalLib.qll")
|
||||||
|
await controller.startDebuggingSelection({});
|
||||||
|
await controller.expectLaunched();
|
||||||
|
const result = await controller.expectSucceeded();
|
||||||
|
expect(result.started.quickEvalContext).toBeDefined();
|
||||||
|
expect(result.started.quickEvalContext!.quickEvalText).toBe(
|
||||||
|
"InterestingNumber",
|
||||||
|
);
|
||||||
|
expect(result.results.queryTarget.quickEvalPosition).toBeDefined();
|
||||||
|
expect(await getResultCount(result.results.outputDir, cli)).toBe(0);
|
||||||
|
await controller.expectStopped();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should run a quick evaluation on a library in the context of a specific query", async () => {
|
||||||
|
await withDebugController(appCommands, async (controller) => {
|
||||||
|
await selectForQuickEval(quickEvalLibPath, 4, 15, 4, 32);
|
||||||
|
|
||||||
|
await controller.startDebuggingSelection({
|
||||||
|
query: quickEvalQueryPath, // The query context. This query extends the abstract class.
|
||||||
|
});
|
||||||
|
await controller.expectLaunched();
|
||||||
|
const result = await controller.expectSucceeded();
|
||||||
|
expect(result.started.quickEvalContext).toBeDefined();
|
||||||
|
expect(result.started.quickEvalContext!.quickEvalText).toBe(
|
||||||
|
"InterestingNumber",
|
||||||
|
);
|
||||||
|
expect(result.results.queryTarget.quickEvalPosition).toBeDefined();
|
||||||
|
expect(await getResultCount(result.results.outputDir, cli)).toBe(8);
|
||||||
|
await controller.expectStopped();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,18 +5,11 @@ import * as messages from "../../../src/pure/new-messages";
|
|||||||
import * as qsClient from "../../../src/query-server/queryserver-client";
|
import * as qsClient from "../../../src/query-server/queryserver-client";
|
||||||
import * as cli from "../../../src/cli";
|
import * as cli from "../../../src/cli";
|
||||||
import { CellValue } from "../../../src/pure/bqrs-cli-types";
|
import { CellValue } from "../../../src/pure/bqrs-cli-types";
|
||||||
import { Uri } from "vscode";
|
|
||||||
import { describeWithCodeQL } from "../cli";
|
import { describeWithCodeQL } from "../cli";
|
||||||
import { QueryServerClient } from "../../../src/query-server/queryserver-client";
|
import { QueryServerClient } from "../../../src/query-server/queryserver-client";
|
||||||
import { extLogger, ProgressReporter } from "../../../src/common";
|
import { extLogger, ProgressReporter } from "../../../src/common";
|
||||||
import { QueryResultType } from "../../../src/pure/new-messages";
|
import { QueryResultType } from "../../../src/pure/new-messages";
|
||||||
import {
|
import { ensureTestDatabase, getActivatedExtension } from "../global.helper";
|
||||||
cleanDatabases,
|
|
||||||
dbLoc,
|
|
||||||
getActivatedExtension,
|
|
||||||
storagePath,
|
|
||||||
} from "../global.helper";
|
|
||||||
import { importArchiveDatabase } from "../../../src/databaseFetcher";
|
|
||||||
import { createMockApp } from "../../__mocks__/appMock";
|
import { createMockApp } from "../../__mocks__/appMock";
|
||||||
|
|
||||||
const baseDir = join(__dirname, "../../../test/data");
|
const baseDir = join(__dirname, "../../../test/data");
|
||||||
@@ -142,24 +135,11 @@ describeWithCodeQL()("using the new query server", () => {
|
|||||||
await qs.startQueryServer();
|
await qs.startQueryServer();
|
||||||
|
|
||||||
// Unlike the old query sevre the new one wants a database and the empty direcrtory is not valid.
|
// Unlike the old query sevre the new one wants a database and the empty direcrtory is not valid.
|
||||||
// Add a database, but make sure the database manager is empty first
|
const dbItem = await ensureTestDatabase(
|
||||||
await cleanDatabases(extension.databaseManager);
|
|
||||||
const uri = Uri.file(dbLoc);
|
|
||||||
const maybeDbItem = await importArchiveDatabase(
|
|
||||||
app.commands,
|
|
||||||
uri.toString(true),
|
|
||||||
extension.databaseManager,
|
extension.databaseManager,
|
||||||
storagePath,
|
undefined,
|
||||||
() => {
|
|
||||||
/**ignore progress */
|
|
||||||
},
|
|
||||||
token,
|
|
||||||
);
|
);
|
||||||
|
db = dbItem.databaseUri.fsPath;
|
||||||
if (!maybeDbItem) {
|
|
||||||
throw new Error("Could not import database");
|
|
||||||
}
|
|
||||||
db = maybeDbItem.databaseUri.fsPath;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const queryTestCase of queryTestCases) {
|
for (const queryTestCase of queryTestCases) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CancellationToken, ExtensionContext, Uri } from "vscode";
|
import { CancellationToken, ExtensionContext, Range, Uri } from "vscode";
|
||||||
import { join, dirname } from "path";
|
import { join, dirname } from "path";
|
||||||
import {
|
import {
|
||||||
pathExistsSync,
|
pathExistsSync,
|
||||||
@@ -12,20 +12,68 @@ import { load, dump } from "js-yaml";
|
|||||||
import { DatabaseItem, DatabaseManager } from "../../../src/local-databases";
|
import { DatabaseItem, DatabaseManager } from "../../../src/local-databases";
|
||||||
import {
|
import {
|
||||||
cleanDatabases,
|
cleanDatabases,
|
||||||
dbLoc,
|
ensureTestDatabase,
|
||||||
getActivatedExtension,
|
getActivatedExtension,
|
||||||
storagePath,
|
|
||||||
} from "../global.helper";
|
} from "../global.helper";
|
||||||
import { importArchiveDatabase } from "../../../src/databaseFetcher";
|
|
||||||
import { CliVersionConstraint, CodeQLCliServer } from "../../../src/cli";
|
import { CliVersionConstraint, CodeQLCliServer } from "../../../src/cli";
|
||||||
import { describeWithCodeQL } from "../cli";
|
import { describeWithCodeQL } from "../cli";
|
||||||
import { QueryRunner } from "../../../src/queryRunner";
|
import { CoreCompletedQuery, QueryRunner } from "../../../src/queryRunner";
|
||||||
import { SELECT_QUERY_NAME } from "../../../src/contextual/locationFinder";
|
import { SELECT_QUERY_NAME } from "../../../src/contextual/locationFinder";
|
||||||
import { createMockCommandManager } from "../../__mocks__/commandsMock";
|
|
||||||
import { LocalQueries } from "../../../src/local-queries";
|
import { LocalQueries } from "../../../src/local-queries";
|
||||||
import { QueryResultType } from "../../../src/pure/new-messages";
|
import { QueryResultType } from "../../../src/pure/new-messages";
|
||||||
import { createVSCodeCommandManager } from "../../../src/common/vscode/commands";
|
import { createVSCodeCommandManager } from "../../../src/common/vscode/commands";
|
||||||
import { AllCommands, QueryServerCommands } from "../../../src/common/commands";
|
import {
|
||||||
|
AllCommands,
|
||||||
|
AppCommandManager,
|
||||||
|
QueryServerCommands,
|
||||||
|
} from "../../../src/common/commands";
|
||||||
|
import { ProgressCallback } from "../../../src/progress";
|
||||||
|
import { withDebugController } from "./debugger/debug-controller";
|
||||||
|
|
||||||
|
type DebugMode = "localQueries" | "debug";
|
||||||
|
|
||||||
|
async function compileAndRunQuery(
|
||||||
|
mode: DebugMode,
|
||||||
|
appCommands: AppCommandManager,
|
||||||
|
localQueries: LocalQueries,
|
||||||
|
quickEval: boolean,
|
||||||
|
queryUri: Uri,
|
||||||
|
progress: ProgressCallback,
|
||||||
|
token: CancellationToken,
|
||||||
|
databaseItem: DatabaseItem | undefined,
|
||||||
|
range?: Range,
|
||||||
|
): Promise<CoreCompletedQuery> {
|
||||||
|
switch (mode) {
|
||||||
|
case "localQueries":
|
||||||
|
return await localQueries.compileAndRunQueryInternal(
|
||||||
|
quickEval,
|
||||||
|
queryUri,
|
||||||
|
progress,
|
||||||
|
token,
|
||||||
|
databaseItem,
|
||||||
|
range,
|
||||||
|
);
|
||||||
|
|
||||||
|
case "debug":
|
||||||
|
return await withDebugController(appCommands, async (controller) => {
|
||||||
|
await controller.startDebugging(
|
||||||
|
{
|
||||||
|
query: queryUri.fsPath,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await controller.expectLaunched();
|
||||||
|
const succeeded = await controller.expectSucceeded();
|
||||||
|
await controller.expectExited();
|
||||||
|
await controller.expectTerminated();
|
||||||
|
await controller.expectSessionClosed();
|
||||||
|
|
||||||
|
return succeeded.results;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MODES: DebugMode[] = ["localQueries", "debug"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integration tests for queries
|
* Integration tests for queries
|
||||||
@@ -71,23 +119,7 @@ describeWithCodeQL()("Queries", () => {
|
|||||||
},
|
},
|
||||||
} as CancellationToken;
|
} as CancellationToken;
|
||||||
|
|
||||||
// Add a database, but make sure the database manager is empty first
|
dbItem = await ensureTestDatabase(databaseManager, cli);
|
||||||
await cleanDatabases(databaseManager);
|
|
||||||
const uri = Uri.file(dbLoc);
|
|
||||||
const maybeDbItem = await importArchiveDatabase(
|
|
||||||
createMockCommandManager(),
|
|
||||||
uri.toString(true),
|
|
||||||
databaseManager,
|
|
||||||
storagePath,
|
|
||||||
progress,
|
|
||||||
token,
|
|
||||||
cli,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!maybeDbItem) {
|
|
||||||
throw new Error("Could not import database");
|
|
||||||
}
|
|
||||||
dbItem = maybeDbItem;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -96,7 +128,7 @@ describeWithCodeQL()("Queries", () => {
|
|||||||
await cleanDatabases(databaseManager);
|
await cleanDatabases(databaseManager);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("extension packs", () => {
|
describe.each(MODES)("extension packs (%s)", (mode) => {
|
||||||
const queryUsingExtensionPath = join(
|
const queryUsingExtensionPath = join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"../..",
|
"../..",
|
||||||
@@ -139,7 +171,10 @@ describeWithCodeQL()("Queries", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runQueryWithExtensions() {
|
async function runQueryWithExtensions() {
|
||||||
const result = await localQueries.compileAndRunQueryInternal(
|
const result = await compileAndRunQuery(
|
||||||
|
mode,
|
||||||
|
appCommandManager,
|
||||||
|
localQueries,
|
||||||
false,
|
false,
|
||||||
Uri.file(queryUsingExtensionPath),
|
Uri.file(queryUsingExtensionPath),
|
||||||
progress,
|
progress,
|
||||||
@@ -167,75 +202,85 @@ describeWithCodeQL()("Queries", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should run a query", async () => {
|
describe.each(MODES)("running queries (%s)", (mode) => {
|
||||||
const queryPath = join(__dirname, "data", "simple-query.ql");
|
it("should run a query", async () => {
|
||||||
const result = await localQueries.compileAndRunQueryInternal(
|
const queryPath = join(__dirname, "data", "simple-query.ql");
|
||||||
false,
|
const result = await compileAndRunQuery(
|
||||||
Uri.file(queryPath),
|
mode,
|
||||||
progress,
|
appCommandManager,
|
||||||
token,
|
localQueries,
|
||||||
dbItem,
|
false,
|
||||||
undefined,
|
Uri.file(queryPath),
|
||||||
);
|
progress,
|
||||||
|
token,
|
||||||
|
dbItem,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
// just check that the query was successful
|
// just check that the query was successful
|
||||||
expect(result.resultType).toBe(QueryResultType.SUCCESS);
|
expect(result.resultType).toBe(QueryResultType.SUCCESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Asserts a fix for bug https://github.com/github/vscode-codeql/issues/733
|
||||||
|
it("should restart the database and run a query", async () => {
|
||||||
|
await appCommandManager.execute("codeQL.restartQueryServer");
|
||||||
|
const queryPath = join(__dirname, "data", "simple-query.ql");
|
||||||
|
const result = await compileAndRunQuery(
|
||||||
|
mode,
|
||||||
|
appCommandManager,
|
||||||
|
localQueries,
|
||||||
|
false,
|
||||||
|
Uri.file(queryPath),
|
||||||
|
progress,
|
||||||
|
token,
|
||||||
|
dbItem,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.resultType).toBe(QueryResultType.SUCCESS);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Asserts a fix for bug https://github.com/github/vscode-codeql/issues/733
|
describe("quick query", () => {
|
||||||
it("should restart the database and run a query", async () => {
|
it("should create a quick query", async () => {
|
||||||
await appCommandManager.execute("codeQL.restartQueryServer");
|
await queryServerCommandManager.execute("codeQL.quickQuery");
|
||||||
const queryPath = join(__dirname, "data", "simple-query.ql");
|
|
||||||
const result = await localQueries.compileAndRunQueryInternal(
|
|
||||||
false,
|
|
||||||
Uri.file(queryPath),
|
|
||||||
progress,
|
|
||||||
token,
|
|
||||||
dbItem,
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.resultType).toBe(QueryResultType.SUCCESS);
|
// should have created the quick query file and query pack file
|
||||||
});
|
expect(pathExistsSync(qlFile)).toBe(true);
|
||||||
|
expect(pathExistsSync(qlpackFile)).toBe(true);
|
||||||
|
|
||||||
it("should create a quick query", async () => {
|
const qlpackContents: any = await load(readFileSync(qlpackFile, "utf8"));
|
||||||
await queryServerCommandManager.execute("codeQL.quickQuery");
|
// Should have chosen the js libraries
|
||||||
|
expect(qlpackContents.dependencies["codeql/javascript-all"]).toBe("*");
|
||||||
|
|
||||||
// should have created the quick query file and query pack file
|
// Should also have a codeql-pack.lock.yml file
|
||||||
expect(pathExistsSync(qlFile)).toBe(true);
|
const packFileToUse = pathExistsSync(qlpackLockFile)
|
||||||
expect(pathExistsSync(qlpackFile)).toBe(true);
|
? qlpackLockFile
|
||||||
|
: oldQlpackLockFile;
|
||||||
|
const qlpackLock: any = await load(readFileSync(packFileToUse, "utf8"));
|
||||||
|
expect(!!qlpackLock.dependencies["codeql/javascript-all"].version).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const qlpackContents: any = await load(readFileSync(qlpackFile, "utf8"));
|
it("should avoid creating a quick query", async () => {
|
||||||
// Should have chosen the js libraries
|
mkdirpSync(dirname(qlpackFile));
|
||||||
expect(qlpackContents.dependencies["codeql/javascript-all"]).toBe("*");
|
writeFileSync(
|
||||||
|
qlpackFile,
|
||||||
|
dump({
|
||||||
|
name: "quick-query",
|
||||||
|
version: "1.0.0",
|
||||||
|
dependencies: {
|
||||||
|
"codeql/javascript-all": "*",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
writeFileSync(qlFile, "xxx");
|
||||||
|
await queryServerCommandManager.execute("codeQL.quickQuery");
|
||||||
|
|
||||||
// Should also have a codeql-pack.lock.yml file
|
// should not have created the quick query file because database schema hasn't changed
|
||||||
const packFileToUse = pathExistsSync(qlpackLockFile)
|
expect(readFileSync(qlFile, "utf8")).toBe("xxx");
|
||||||
? qlpackLockFile
|
});
|
||||||
: oldQlpackLockFile;
|
|
||||||
const qlpackLock: any = await load(readFileSync(packFileToUse, "utf8"));
|
|
||||||
expect(!!qlpackLock.dependencies["codeql/javascript-all"].version).toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should avoid creating a quick query", async () => {
|
|
||||||
mkdirpSync(dirname(qlpackFile));
|
|
||||||
writeFileSync(
|
|
||||||
qlpackFile,
|
|
||||||
dump({
|
|
||||||
name: "quick-query",
|
|
||||||
version: "1.0.0",
|
|
||||||
dependencies: {
|
|
||||||
"codeql/javascript-all": "*",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
writeFileSync(qlFile, "xxx");
|
|
||||||
await queryServerCommandManager.execute("codeQL.quickQuery");
|
|
||||||
|
|
||||||
// should not have created the quick query file because database schema hasn't changed
|
|
||||||
expect(readFileSync(qlFile, "utf8")).toBe("xxx");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function safeDel(file: string) {
|
function safeDel(file: string) {
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { load, dump } from "js-yaml";
|
import { load, dump } from "js-yaml";
|
||||||
import { realpathSync, readFileSync, writeFileSync } from "fs-extra";
|
import { realpathSync, readFileSync, writeFileSync } from "fs-extra";
|
||||||
import { CancellationToken, extensions } from "vscode";
|
import {
|
||||||
import { DatabaseManager } from "../../src/local-databases";
|
CancellationToken,
|
||||||
|
CancellationTokenSource,
|
||||||
|
Uri,
|
||||||
|
extensions,
|
||||||
|
} from "vscode";
|
||||||
|
import { DatabaseItem, DatabaseManager } from "../../src/local-databases";
|
||||||
import { CodeQLCliServer } from "../../src/cli";
|
import { CodeQLCliServer } from "../../src/cli";
|
||||||
import { removeWorkspaceRefs } from "../../src/variant-analysis/run-remote-query";
|
import { removeWorkspaceRefs } from "../../src/variant-analysis/run-remote-query";
|
||||||
import { CodeQLExtensionInterface } from "../../src/extension";
|
import { CodeQLExtensionInterface } from "../../src/extension";
|
||||||
import { ProgressCallback } from "../../src/progress";
|
import { ProgressCallback } from "../../src/progress";
|
||||||
|
import { importArchiveDatabase } from "../../src/databaseFetcher";
|
||||||
|
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 +28,35 @@ export const dbLoc = join(
|
|||||||
);
|
);
|
||||||
export let storagePath: string;
|
export let storagePath: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes any existing databases from the database panel, and loads the test database.
|
||||||
|
*/
|
||||||
|
export async function ensureTestDatabase(
|
||||||
|
databaseManager: DatabaseManager,
|
||||||
|
cli: CodeQLCliServer | undefined,
|
||||||
|
): Promise<DatabaseItem> {
|
||||||
|
// Add a database, but make sure the database manager is empty first
|
||||||
|
await cleanDatabases(databaseManager);
|
||||||
|
const uri = Uri.file(dbLoc);
|
||||||
|
const maybeDbItem = await importArchiveDatabase(
|
||||||
|
createMockCommandManager(),
|
||||||
|
uri.toString(true),
|
||||||
|
databaseManager,
|
||||||
|
storagePath,
|
||||||
|
(_p) => {
|
||||||
|
/**/
|
||||||
|
},
|
||||||
|
new CancellationTokenSource().token,
|
||||||
|
cli,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!maybeDbItem) {
|
||||||
|
throw new Error("Could not import database");
|
||||||
|
}
|
||||||
|
|
||||||
|
return maybeDbItem;
|
||||||
|
}
|
||||||
|
|
||||||
export function setStoragePath(path: string) {
|
export function setStoragePath(path: string) {
|
||||||
storagePath = path;
|
storagePath = path;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { resolve, join } from "path";
|
import { resolve, join } from "path";
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import { Uri } from "vscode";
|
import { Uri } from "vscode";
|
||||||
import { determineSelectedQuery } from "../../../src/run-queries-shared";
|
import {
|
||||||
|
getQuickEvalContext,
|
||||||
|
validateQueryUri,
|
||||||
|
} from "../../../src/run-queries-shared";
|
||||||
|
|
||||||
async function showQlDocument(name: string): Promise<vscode.TextDocument> {
|
async function showQlDocument(name: string): Promise<vscode.TextDocument> {
|
||||||
const folderPath = vscode.workspace.workspaceFolders![0].uri.fsPath;
|
const folderPath = vscode.workspace.workspaceFolders![0].uri.fsPath;
|
||||||
@@ -14,43 +17,47 @@ async function showQlDocument(name: string): Promise<vscode.TextDocument> {
|
|||||||
export function run() {
|
export function run() {
|
||||||
describe("Determining selected query", () => {
|
describe("Determining selected query", () => {
|
||||||
it("should allow ql files to be queried", async () => {
|
it("should allow ql files to be queried", async () => {
|
||||||
const q = await determineSelectedQuery(
|
const queryPath = validateQueryUri(
|
||||||
Uri.parse("file:///tmp/queryname.ql"),
|
Uri.parse("file:///tmp/queryname.ql"),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
expect(q.queryPath).toBe(join("/", "tmp", "queryname.ql"));
|
expect(queryPath).toBe(join("/", "tmp", "queryname.ql"));
|
||||||
expect(q.quickEvalPosition).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow ql files to be quick-evaled", async () => {
|
it("should allow ql files to be quick-evaled", async () => {
|
||||||
const doc = await showQlDocument("query.ql");
|
await showQlDocument("query.ql");
|
||||||
const q = await determineSelectedQuery(doc.uri, true);
|
const q = await getQuickEvalContext(undefined);
|
||||||
expect(
|
expect(
|
||||||
q.queryPath.endsWith(join("ql-vscode", "test", "data", "query.ql")),
|
q.quickEvalPosition.fileName.endsWith(
|
||||||
|
join("ql-vscode", "test", "data", "query.ql"),
|
||||||
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow qll files to be quick-evaled", async () => {
|
it("should allow qll files to be quick-evaled", async () => {
|
||||||
const doc = await showQlDocument("library.qll");
|
await showQlDocument("library.qll");
|
||||||
const q = await determineSelectedQuery(doc.uri, true);
|
const q = await getQuickEvalContext(undefined);
|
||||||
expect(
|
expect(
|
||||||
q.queryPath.endsWith(join("ql-vscode", "test", "data", "library.qll")),
|
q.quickEvalPosition.fileName.endsWith(
|
||||||
|
join("ql-vscode", "test", "data", "library.qll"),
|
||||||
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject non-ql files when running a query", async () => {
|
it("should reject non-ql files when running a query", async () => {
|
||||||
await expect(
|
expect(() =>
|
||||||
determineSelectedQuery(Uri.parse("file:///tmp/queryname.txt"), false),
|
validateQueryUri(Uri.parse("file:///tmp/queryname.txt"), false),
|
||||||
).rejects.toThrow("The selected resource is not a CodeQL query file");
|
).toThrow("The selected resource is not a CodeQL query file");
|
||||||
await expect(
|
expect(() =>
|
||||||
determineSelectedQuery(Uri.parse("file:///tmp/queryname.qll"), false),
|
validateQueryUri(Uri.parse("file:///tmp/queryname.qll"), false),
|
||||||
).rejects.toThrow("The selected resource is not a CodeQL query file");
|
).toThrow("The selected resource is not a CodeQL query file");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject non-ql[l] files when running a quick eval", async () => {
|
it("should reject non-ql[l] files when running a quick eval", async () => {
|
||||||
await expect(
|
await showQlDocument("textfile.txt");
|
||||||
determineSelectedQuery(Uri.parse("file:///tmp/queryname.txt"), true),
|
await expect(getQuickEvalContext(undefined)).rejects.toThrow(
|
||||||
).rejects.toThrow("The selected resource is not a CodeQL file");
|
"The selected resource is not a CodeQL file",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,205 +1,175 @@
|
|||||||
import { Uri, WorkspaceFolder } from "vscode";
|
import {
|
||||||
|
CancellationTokenSource,
|
||||||
|
Range,
|
||||||
|
TestItem,
|
||||||
|
TestItemCollection,
|
||||||
|
TestRun,
|
||||||
|
TestRunRequest,
|
||||||
|
Uri,
|
||||||
|
WorkspaceFolder,
|
||||||
|
tests,
|
||||||
|
} from "vscode";
|
||||||
|
|
||||||
import { QLTestAdapter } from "../../../src/test-adapter";
|
import { QLTestAdapter } from "../../../src/test-adapter";
|
||||||
import { CodeQLCliServer } from "../../../src/cli";
|
import { CodeQLCliServer } from "../../../src/cli";
|
||||||
import {
|
import { DatabaseManager } from "../../../src/local-databases";
|
||||||
DatabaseItem,
|
|
||||||
DatabaseItemImpl,
|
|
||||||
DatabaseManager,
|
|
||||||
FullDatabaseOptions,
|
|
||||||
} from "../../../src/local-databases";
|
|
||||||
import { mockedObject } from "../utils/mocking.helpers";
|
import { mockedObject } from "../utils/mocking.helpers";
|
||||||
|
import { TestRunner } from "../../../src/test-runner";
|
||||||
|
import {
|
||||||
|
createMockCliServerForTestRun,
|
||||||
|
mockEmptyDatabaseManager,
|
||||||
|
mockTestsInfo,
|
||||||
|
} from "./test-runner-helpers";
|
||||||
|
import { TestManager } from "../../../src/test-manager";
|
||||||
|
import { createMockApp } from "../../__mocks__/appMock";
|
||||||
|
|
||||||
jest.mock("fs-extra", () => {
|
type IdTestItemPair = [id: string, testItem: TestItem];
|
||||||
const original = jest.requireActual("fs-extra");
|
|
||||||
return {
|
|
||||||
...original,
|
|
||||||
access: jest.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("test-adapter", () => {
|
describe("test-adapter", () => {
|
||||||
let adapter: QLTestAdapter;
|
let testRunner: TestRunner;
|
||||||
let fakeDatabaseManager: DatabaseManager;
|
let fakeDatabaseManager: DatabaseManager;
|
||||||
let currentDatabaseItem: DatabaseItem | undefined;
|
let fakeCliServer: CodeQLCliServer;
|
||||||
let databaseItems: DatabaseItem[] = [];
|
|
||||||
const openDatabaseSpy = jest.fn();
|
|
||||||
const removeDatabaseItemSpy = jest.fn();
|
|
||||||
const renameDatabaseItemSpy = jest.fn();
|
|
||||||
const setCurrentDatabaseItemSpy = jest.fn();
|
|
||||||
const runTestsSpy = jest.fn();
|
|
||||||
const resolveTestsSpy = jest.fn();
|
|
||||||
const resolveQlpacksSpy = jest.fn();
|
|
||||||
|
|
||||||
const preTestDatabaseItem = new DatabaseItemImpl(
|
|
||||||
Uri.file("/path/to/test/dir/dir.testproj"),
|
|
||||||
undefined,
|
|
||||||
mockedObject<FullDatabaseOptions>({ displayName: "custom display name" }),
|
|
||||||
(_) => {
|
|
||||||
/* no change event listener */
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const postTestDatabaseItem = new DatabaseItemImpl(
|
|
||||||
Uri.file("/path/to/test/dir/dir.testproj"),
|
|
||||||
undefined,
|
|
||||||
mockedObject<FullDatabaseOptions>({ displayName: "default name" }),
|
|
||||||
(_) => {
|
|
||||||
/* no change event listener */
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockRunTests();
|
fakeDatabaseManager = mockEmptyDatabaseManager();
|
||||||
openDatabaseSpy.mockResolvedValue(postTestDatabaseItem);
|
|
||||||
removeDatabaseItemSpy.mockResolvedValue(undefined);
|
|
||||||
renameDatabaseItemSpy.mockResolvedValue(undefined);
|
|
||||||
setCurrentDatabaseItemSpy.mockResolvedValue(undefined);
|
|
||||||
resolveQlpacksSpy.mockResolvedValue({});
|
|
||||||
resolveTestsSpy.mockResolvedValue([]);
|
|
||||||
fakeDatabaseManager = mockedObject<DatabaseManager>(
|
|
||||||
{
|
|
||||||
openDatabase: openDatabaseSpy,
|
|
||||||
removeDatabaseItem: removeDatabaseItemSpy,
|
|
||||||
renameDatabaseItem: renameDatabaseItemSpy,
|
|
||||||
setCurrentDatabaseItem: setCurrentDatabaseItemSpy,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dynamicProperties: {
|
|
||||||
currentDatabaseItem: () => currentDatabaseItem,
|
|
||||||
databaseItems: () => databaseItems,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
jest.spyOn(preTestDatabaseItem, "isAffectedByTest").mockResolvedValue(true);
|
const mockCli = createMockCliServerForTestRun();
|
||||||
|
fakeCliServer = mockCli.cliServer;
|
||||||
|
|
||||||
adapter = new QLTestAdapter(
|
testRunner = new TestRunner(fakeDatabaseManager, fakeCliServer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("legacy test adapter should run some tests", async () => {
|
||||||
|
const adapter = new QLTestAdapter(
|
||||||
mockedObject<WorkspaceFolder>({
|
mockedObject<WorkspaceFolder>({
|
||||||
name: "ABC",
|
name: "ABC",
|
||||||
uri: Uri.parse("file:/ab/c"),
|
uri: Uri.parse("file:/ab/c"),
|
||||||
}),
|
}),
|
||||||
mockedObject<CodeQLCliServer>({
|
testRunner,
|
||||||
runTests: runTestsSpy,
|
fakeCliServer,
|
||||||
resolveQlpacks: resolveQlpacksSpy,
|
|
||||||
resolveTests: resolveTestsSpy,
|
|
||||||
}),
|
|
||||||
fakeDatabaseManager,
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
it("should run some tests", async () => {
|
|
||||||
const listenerSpy = jest.fn();
|
const listenerSpy = jest.fn();
|
||||||
adapter.testStates(listenerSpy);
|
adapter.testStates(listenerSpy);
|
||||||
const testsPath = Uri.parse("file:/ab/c").fsPath;
|
await adapter.run([mockTestsInfo.testsPath]);
|
||||||
const dPath = Uri.parse("file:/ab/c/d.ql").fsPath;
|
|
||||||
const gPath = Uri.parse("file:/ab/c/e/f/g.ql").fsPath;
|
|
||||||
const hPath = Uri.parse("file:/ab/c/e/f/h.ql").fsPath;
|
|
||||||
|
|
||||||
await adapter.run([testsPath]);
|
|
||||||
|
|
||||||
expect(listenerSpy).toBeCalledTimes(5);
|
expect(listenerSpy).toBeCalledTimes(5);
|
||||||
|
|
||||||
expect(listenerSpy).toHaveBeenNthCalledWith(1, {
|
expect(listenerSpy).toHaveBeenNthCalledWith(1, {
|
||||||
type: "started",
|
type: "started",
|
||||||
tests: [testsPath],
|
tests: [mockTestsInfo.testsPath],
|
||||||
});
|
});
|
||||||
expect(listenerSpy).toHaveBeenNthCalledWith(2, {
|
expect(listenerSpy).toHaveBeenNthCalledWith(2, {
|
||||||
type: "test",
|
type: "test",
|
||||||
state: "passed",
|
state: "passed",
|
||||||
test: dPath,
|
test: mockTestsInfo.dPath,
|
||||||
message: undefined,
|
message: undefined,
|
||||||
decorations: [],
|
decorations: [],
|
||||||
});
|
});
|
||||||
expect(listenerSpy).toHaveBeenNthCalledWith(3, {
|
expect(listenerSpy).toHaveBeenNthCalledWith(3, {
|
||||||
type: "test",
|
type: "test",
|
||||||
state: "errored",
|
state: "errored",
|
||||||
test: gPath,
|
test: mockTestsInfo.gPath,
|
||||||
message: `\ncompilation error: ${gPath}\nERROR: abc\n`,
|
message: `\ncompilation error: ${mockTestsInfo.gPath}\nERROR: abc\n`,
|
||||||
decorations: [{ line: 1, message: "abc" }],
|
decorations: [{ line: 1, message: "abc" }],
|
||||||
});
|
});
|
||||||
expect(listenerSpy).toHaveBeenNthCalledWith(4, {
|
expect(listenerSpy).toHaveBeenNthCalledWith(4, {
|
||||||
type: "test",
|
type: "test",
|
||||||
state: "failed",
|
state: "failed",
|
||||||
test: hPath,
|
test: mockTestsInfo.hPath,
|
||||||
message: `\nfailed: ${hPath}\njkh\ntuv\n`,
|
message: `\nfailed: ${mockTestsInfo.hPath}\njkh\ntuv\n`,
|
||||||
decorations: [],
|
decorations: [],
|
||||||
});
|
});
|
||||||
expect(listenerSpy).toHaveBeenNthCalledWith(5, { type: "finished" });
|
expect(listenerSpy).toHaveBeenNthCalledWith(5, { type: "finished" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reregister testproj databases around test run", async () => {
|
it("native test manager should run some tests", async () => {
|
||||||
currentDatabaseItem = preTestDatabaseItem;
|
const enqueuedSpy = jest.fn();
|
||||||
databaseItems = [preTestDatabaseItem];
|
const passedSpy = jest.fn();
|
||||||
await adapter.run(["/path/to/test/dir"]);
|
const erroredSpy = jest.fn();
|
||||||
|
const failedSpy = jest.fn();
|
||||||
|
const endSpy = jest.fn();
|
||||||
|
|
||||||
expect(removeDatabaseItemSpy.mock.invocationCallOrder[0]).toBeLessThan(
|
const testController = tests.createTestController("codeql", "CodeQL Tests");
|
||||||
runTestsSpy.mock.invocationCallOrder[0],
|
testController.createTestRun = jest.fn().mockImplementation(() =>
|
||||||
|
mockedObject<TestRun>({
|
||||||
|
enqueued: enqueuedSpy,
|
||||||
|
passed: passedSpy,
|
||||||
|
errored: erroredSpy,
|
||||||
|
failed: failedSpy,
|
||||||
|
end: endSpy,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
expect(openDatabaseSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
|
const testManager = new TestManager(
|
||||||
runTestsSpy.mock.invocationCallOrder[0],
|
createMockApp({}),
|
||||||
);
|
testRunner,
|
||||||
expect(renameDatabaseItemSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
|
fakeCliServer,
|
||||||
openDatabaseSpy.mock.invocationCallOrder[0],
|
testController,
|
||||||
);
|
|
||||||
expect(
|
|
||||||
setCurrentDatabaseItemSpy.mock.invocationCallOrder[0],
|
|
||||||
).toBeGreaterThan(openDatabaseSpy.mock.invocationCallOrder[0]);
|
|
||||||
|
|
||||||
expect(removeDatabaseItemSpy).toBeCalledTimes(1);
|
|
||||||
expect(removeDatabaseItemSpy).toBeCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
preTestDatabaseItem,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(openDatabaseSpy).toBeCalledTimes(1);
|
const childItems: TestItem[] = [
|
||||||
expect(openDatabaseSpy).toBeCalledWith(
|
{
|
||||||
expect.anything(),
|
children: { size: 0 } as TestItemCollection,
|
||||||
expect.anything(),
|
id: `test ${mockTestsInfo.dPath}`,
|
||||||
preTestDatabaseItem.databaseUri,
|
uri: Uri.file(mockTestsInfo.dPath),
|
||||||
);
|
} as TestItem,
|
||||||
|
{
|
||||||
|
children: { size: 0 } as TestItemCollection,
|
||||||
|
id: `test ${mockTestsInfo.gPath}`,
|
||||||
|
uri: Uri.file(mockTestsInfo.gPath),
|
||||||
|
} as TestItem,
|
||||||
|
{
|
||||||
|
children: { size: 0 } as TestItemCollection,
|
||||||
|
id: `test ${mockTestsInfo.hPath}`,
|
||||||
|
uri: Uri.file(mockTestsInfo.hPath),
|
||||||
|
} as TestItem,
|
||||||
|
];
|
||||||
|
const childElements: IdTestItemPair[] = childItems.map((childItem) => [
|
||||||
|
childItem.id,
|
||||||
|
childItem,
|
||||||
|
]);
|
||||||
|
const childIteratorFunc: () => Iterator<IdTestItemPair> = () =>
|
||||||
|
childElements[Symbol.iterator]();
|
||||||
|
|
||||||
expect(renameDatabaseItemSpy).toBeCalledTimes(1);
|
const rootItem = {
|
||||||
expect(renameDatabaseItemSpy).toBeCalledWith(
|
id: `dir ${mockTestsInfo.testsPath}`,
|
||||||
postTestDatabaseItem,
|
uri: Uri.file(mockTestsInfo.testsPath),
|
||||||
preTestDatabaseItem.name,
|
children: {
|
||||||
);
|
size: 3,
|
||||||
|
[Symbol.iterator]: childIteratorFunc,
|
||||||
|
} as TestItemCollection,
|
||||||
|
} as TestItem;
|
||||||
|
|
||||||
expect(setCurrentDatabaseItemSpy).toBeCalledTimes(1);
|
const request = new TestRunRequest([rootItem]);
|
||||||
expect(setCurrentDatabaseItemSpy).toBeCalledWith(
|
await testManager.run(request, new CancellationTokenSource().token);
|
||||||
postTestDatabaseItem,
|
|
||||||
true,
|
expect(enqueuedSpy).toBeCalledTimes(3);
|
||||||
|
expect(passedSpy).toBeCalledTimes(1);
|
||||||
|
expect(passedSpy).toHaveBeenCalledWith(childItems[0], 3000);
|
||||||
|
expect(erroredSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(erroredSpy).toHaveBeenCalledWith(
|
||||||
|
childItems[1],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
location: {
|
||||||
|
range: new Range(0, 0, 1, 1),
|
||||||
|
uri: Uri.file(mockTestsInfo.gPath),
|
||||||
|
},
|
||||||
|
message: "abc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
4000,
|
||||||
);
|
);
|
||||||
|
expect(failedSpy).toHaveBeenCalledWith(
|
||||||
|
childItems[2],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
message: "Test failed",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
11000,
|
||||||
|
);
|
||||||
|
expect(failedSpy).toBeCalledTimes(1);
|
||||||
|
expect(endSpy).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
function mockRunTests() {
|
|
||||||
// runTests is an async generator function. This is not directly supported in sinon
|
|
||||||
// However, we can pretend the same thing by just returning an async array.
|
|
||||||
runTestsSpy.mockReturnValue(
|
|
||||||
(async function* () {
|
|
||||||
yield Promise.resolve({
|
|
||||||
test: Uri.parse("file:/ab/c/d.ql").fsPath,
|
|
||||||
pass: true,
|
|
||||||
messages: [],
|
|
||||||
});
|
|
||||||
yield Promise.resolve({
|
|
||||||
test: Uri.parse("file:/ab/c/e/f/g.ql").fsPath,
|
|
||||||
pass: false,
|
|
||||||
diff: ["pqr", "xyz"],
|
|
||||||
// a compile error
|
|
||||||
failureStage: "COMPILATION",
|
|
||||||
messages: [
|
|
||||||
{ position: { line: 1 }, message: "abc", severity: "ERROR" },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
yield Promise.resolve({
|
|
||||||
test: Uri.parse("file:/ab/c/e/f/h.ql").fsPath,
|
|
||||||
pass: false,
|
|
||||||
diff: ["jkh", "tuv"],
|
|
||||||
failureStage: "RESULT",
|
|
||||||
messages: [],
|
|
||||||
});
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { Uri } from "vscode";
|
||||||
|
import { mockedObject } from "../utils/mocking.helpers";
|
||||||
|
import { CodeQLCliServer } from "../../../src/cli";
|
||||||
|
import { DatabaseManager } from "../../../src/local-databases";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fake QL tests used by various tests.
|
||||||
|
*/
|
||||||
|
export const mockTestsInfo = {
|
||||||
|
testsPath: Uri.parse("file:/ab/c").fsPath,
|
||||||
|
dPath: Uri.parse("file:/ab/c/d.ql").fsPath,
|
||||||
|
gPath: Uri.parse("file:/ab/c/e/f/g.ql").fsPath,
|
||||||
|
hPath: Uri.parse("file:/ab/c/e/f/h.ql").fsPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock of a `DatabaseManager` with no databases loaded.
|
||||||
|
*/
|
||||||
|
export function mockEmptyDatabaseManager(): DatabaseManager {
|
||||||
|
return mockedObject<DatabaseManager>({
|
||||||
|
currentDatabaseItem: undefined,
|
||||||
|
databaseItems: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a `CodeQLCliServer` that "runs" the mock tests. Also returns the spy
|
||||||
|
* hook for the `runTests` function on the CLI server.
|
||||||
|
*/
|
||||||
|
export function createMockCliServerForTestRun() {
|
||||||
|
const resolveQlpacksSpy = jest.fn();
|
||||||
|
resolveQlpacksSpy.mockResolvedValue({});
|
||||||
|
|
||||||
|
const resolveTestsSpy = jest.fn();
|
||||||
|
resolveTestsSpy.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const runTestsSpy = mockRunTests();
|
||||||
|
return {
|
||||||
|
cliServer: mockedObject<CodeQLCliServer>({
|
||||||
|
runTests: runTestsSpy,
|
||||||
|
resolveQlpacks: resolveQlpacksSpy,
|
||||||
|
resolveTests: resolveTestsSpy,
|
||||||
|
}),
|
||||||
|
runTestsSpy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockRunTests(): jest.Mock<any, any> {
|
||||||
|
const runTestsSpy = jest.fn();
|
||||||
|
// runTests is an async generator function. This is not directly supported in sinon
|
||||||
|
// However, we can pretend the same thing by just returning an async array.
|
||||||
|
runTestsSpy.mockReturnValue(
|
||||||
|
(async function* () {
|
||||||
|
yield Promise.resolve({
|
||||||
|
test: mockTestsInfo.dPath,
|
||||||
|
pass: true,
|
||||||
|
messages: [],
|
||||||
|
compilationMs: 1000,
|
||||||
|
evaluationMs: 2000,
|
||||||
|
});
|
||||||
|
yield Promise.resolve({
|
||||||
|
test: mockTestsInfo.gPath,
|
||||||
|
pass: false,
|
||||||
|
diff: ["pqr", "xyz"],
|
||||||
|
// a compile error
|
||||||
|
failureStage: "COMPILATION",
|
||||||
|
compilationMs: 4000,
|
||||||
|
evaluationMs: 0,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
position: {
|
||||||
|
fileName: mockTestsInfo.gPath,
|
||||||
|
line: 1,
|
||||||
|
column: 1,
|
||||||
|
endLine: 2,
|
||||||
|
endColumn: 2,
|
||||||
|
},
|
||||||
|
message: "abc",
|
||||||
|
severity: "ERROR",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
yield Promise.resolve({
|
||||||
|
test: mockTestsInfo.hPath,
|
||||||
|
pass: false,
|
||||||
|
diff: ["jkh", "tuv"],
|
||||||
|
failureStage: "RESULT",
|
||||||
|
compilationMs: 5000,
|
||||||
|
evaluationMs: 6000,
|
||||||
|
messages: [],
|
||||||
|
});
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return runTestsSpy;
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import { CancellationTokenSource, Uri } from "vscode";
|
||||||
|
import { CodeQLCliServer } from "../../../src/cli";
|
||||||
|
import {
|
||||||
|
DatabaseItem,
|
||||||
|
DatabaseItemImpl,
|
||||||
|
DatabaseManager,
|
||||||
|
FullDatabaseOptions,
|
||||||
|
} from "../../../src/local-databases";
|
||||||
|
import { mockedObject } from "../utils/mocking.helpers";
|
||||||
|
import { TestRunner } from "../../../src/test-runner";
|
||||||
|
import { createMockLogger } from "../../__mocks__/loggerMock";
|
||||||
|
import {
|
||||||
|
createMockCliServerForTestRun,
|
||||||
|
mockTestsInfo,
|
||||||
|
} from "./test-runner-helpers";
|
||||||
|
|
||||||
|
jest.mock("fs-extra", () => {
|
||||||
|
const original = jest.requireActual("fs-extra");
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
access: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("test-runner", () => {
|
||||||
|
let testRunner: TestRunner;
|
||||||
|
let fakeDatabaseManager: DatabaseManager;
|
||||||
|
let fakeCliServer: CodeQLCliServer;
|
||||||
|
let currentDatabaseItem: DatabaseItem | undefined;
|
||||||
|
let databaseItems: DatabaseItem[] = [];
|
||||||
|
const openDatabaseSpy = jest.fn();
|
||||||
|
const removeDatabaseItemSpy = jest.fn();
|
||||||
|
const renameDatabaseItemSpy = jest.fn();
|
||||||
|
const setCurrentDatabaseItemSpy = jest.fn();
|
||||||
|
let runTestsSpy: jest.Mock<any, any>;
|
||||||
|
const resolveTestsSpy = jest.fn();
|
||||||
|
const resolveQlpacksSpy = jest.fn();
|
||||||
|
|
||||||
|
const preTestDatabaseItem = new DatabaseItemImpl(
|
||||||
|
Uri.file("/path/to/test/dir/dir.testproj"),
|
||||||
|
undefined,
|
||||||
|
mockedObject<FullDatabaseOptions>({ displayName: "custom display name" }),
|
||||||
|
(_) => {
|
||||||
|
/* no change event listener */
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const postTestDatabaseItem = new DatabaseItemImpl(
|
||||||
|
Uri.file("/path/to/test/dir/dir.testproj"),
|
||||||
|
undefined,
|
||||||
|
mockedObject<FullDatabaseOptions>({ displayName: "default name" }),
|
||||||
|
(_) => {
|
||||||
|
/* no change event listener */
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
openDatabaseSpy.mockResolvedValue(postTestDatabaseItem);
|
||||||
|
removeDatabaseItemSpy.mockResolvedValue(undefined);
|
||||||
|
renameDatabaseItemSpy.mockResolvedValue(undefined);
|
||||||
|
setCurrentDatabaseItemSpy.mockResolvedValue(undefined);
|
||||||
|
resolveQlpacksSpy.mockResolvedValue({});
|
||||||
|
resolveTestsSpy.mockResolvedValue([]);
|
||||||
|
fakeDatabaseManager = mockedObject<DatabaseManager>(
|
||||||
|
{
|
||||||
|
openDatabase: openDatabaseSpy,
|
||||||
|
removeDatabaseItem: removeDatabaseItemSpy,
|
||||||
|
renameDatabaseItem: renameDatabaseItemSpy,
|
||||||
|
setCurrentDatabaseItem: setCurrentDatabaseItemSpy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dynamicProperties: {
|
||||||
|
currentDatabaseItem: () => currentDatabaseItem,
|
||||||
|
databaseItems: () => databaseItems,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.spyOn(preTestDatabaseItem, "isAffectedByTest").mockResolvedValue(true);
|
||||||
|
|
||||||
|
const mockCli = createMockCliServerForTestRun();
|
||||||
|
fakeCliServer = mockCli.cliServer;
|
||||||
|
runTestsSpy = mockCli.runTestsSpy;
|
||||||
|
|
||||||
|
testRunner = new TestRunner(fakeDatabaseManager, fakeCliServer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should run some tests", async () => {
|
||||||
|
const eventHandlerSpy = jest.fn();
|
||||||
|
|
||||||
|
await testRunner.run(
|
||||||
|
[mockTestsInfo.dPath, mockTestsInfo.gPath, mockTestsInfo.hPath],
|
||||||
|
createMockLogger(),
|
||||||
|
new CancellationTokenSource().token,
|
||||||
|
eventHandlerSpy,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(eventHandlerSpy).toBeCalledTimes(3);
|
||||||
|
|
||||||
|
expect(eventHandlerSpy).toHaveBeenNthCalledWith(1, {
|
||||||
|
test: mockTestsInfo.dPath,
|
||||||
|
pass: true,
|
||||||
|
compilationMs: 1000,
|
||||||
|
evaluationMs: 2000,
|
||||||
|
messages: [],
|
||||||
|
});
|
||||||
|
expect(eventHandlerSpy).toHaveBeenNthCalledWith(2, {
|
||||||
|
test: mockTestsInfo.gPath,
|
||||||
|
pass: false,
|
||||||
|
compilationMs: 4000,
|
||||||
|
evaluationMs: 0,
|
||||||
|
diff: ["pqr", "xyz"],
|
||||||
|
failureStage: "COMPILATION",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
message: "abc",
|
||||||
|
position: {
|
||||||
|
line: 1,
|
||||||
|
column: 1,
|
||||||
|
endLine: 2,
|
||||||
|
endColumn: 2,
|
||||||
|
fileName: mockTestsInfo.gPath,
|
||||||
|
},
|
||||||
|
severity: "ERROR",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(eventHandlerSpy).toHaveBeenNthCalledWith(3, {
|
||||||
|
test: mockTestsInfo.hPath,
|
||||||
|
pass: false,
|
||||||
|
compilationMs: 5000,
|
||||||
|
evaluationMs: 6000,
|
||||||
|
diff: ["jkh", "tuv"],
|
||||||
|
failureStage: "RESULT",
|
||||||
|
messages: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reregister testproj databases around test run", async () => {
|
||||||
|
currentDatabaseItem = preTestDatabaseItem;
|
||||||
|
databaseItems = [preTestDatabaseItem];
|
||||||
|
await testRunner.run(
|
||||||
|
["/path/to/test/dir"],
|
||||||
|
createMockLogger(),
|
||||||
|
new CancellationTokenSource().token,
|
||||||
|
async () => {
|
||||||
|
/***/
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(removeDatabaseItemSpy.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
runTestsSpy.mock.invocationCallOrder[0],
|
||||||
|
);
|
||||||
|
expect(openDatabaseSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
|
||||||
|
runTestsSpy.mock.invocationCallOrder[0],
|
||||||
|
);
|
||||||
|
expect(renameDatabaseItemSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
|
||||||
|
openDatabaseSpy.mock.invocationCallOrder[0],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
setCurrentDatabaseItemSpy.mock.invocationCallOrder[0],
|
||||||
|
).toBeGreaterThan(openDatabaseSpy.mock.invocationCallOrder[0]);
|
||||||
|
|
||||||
|
expect(removeDatabaseItemSpy).toBeCalledTimes(1);
|
||||||
|
expect(removeDatabaseItemSpy).toBeCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
preTestDatabaseItem,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(openDatabaseSpy).toBeCalledTimes(1);
|
||||||
|
expect(openDatabaseSpy).toBeCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
preTestDatabaseItem.databaseUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(renameDatabaseItemSpy).toBeCalledTimes(1);
|
||||||
|
expect(renameDatabaseItemSpy).toBeCalledWith(
|
||||||
|
postTestDatabaseItem,
|
||||||
|
preTestDatabaseItem.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(setCurrentDatabaseItemSpy).toBeCalledTimes(1);
|
||||||
|
expect(setCurrentDatabaseItemSpy).toBeCalledWith(
|
||||||
|
postTestDatabaseItem,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user