Debugger tests

This commit is contained in:
Dave Bartolomeo
2023-04-10 18:18:07 -04:00
parent 0a0a9c6428
commit 7dfa52bbab
14 changed files with 948 additions and 266 deletions

View File

@@ -7,12 +7,12 @@ import {
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers";
import { LocalQueries } from "../local-queries";
import { getQuickEvalContext, validateQueryPath } from "../run-queries-shared";
import * as CodeQLDebugProtocol from "./debug-protocol";
import * as CodeQLProtocol from "./debug-protocol";
/**
* The CodeQL launch arguments, as specified in "launch.json".
*/
interface QLDebugArgs {
export interface QLDebugArgs {
query?: string;
database?: string;
additionalPacks?: string[] | string;
@@ -26,14 +26,14 @@ interface QLDebugArgs {
*
* This just combines `QLDebugArgs` with the standard debug configuration properties.
*/
type QLDebugConfiguration = DebugConfiguration & QLDebugArgs;
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 &
CodeQLDebugProtocol.LaunchConfig;
CodeQLProtocol.LaunchConfig;
/**
* Implementation of `DebugConfigurationProvider` for CodeQL.
@@ -114,7 +114,7 @@ export class QLDebugConfigurationProvider
database: qlConfiguration.database,
additionalPacks,
extensionPacks,
quickEvalPosition: quickEvalContext?.quickEvalPosition,
quickEvalContext,
noDebug: qlConfiguration.noDebug ?? false,
};

View File

@@ -1,5 +1,8 @@
import { DebugProtocol } from "@vscode/debugprotocol";
import { QueryResultType } from "../pure/new-messages";
import { QuickEvalContext } from "../run-queries-shared";
// Events
export type Event = { type: "event" };
@@ -9,86 +12,51 @@ export type StoppedEvent = DebugProtocol.StoppedEvent &
export type InitializedEvent = DebugProtocol.InitializedEvent &
Event & { event: "initialized" };
export type ExitedEvent = DebugProtocol.ExitedEvent &
Event & { event: "exited" };
export type OutputEvent = DebugProtocol.OutputEvent &
Event & { event: "output" };
export interface EvaluationStartedEventBody {
id: string;
outputDir: string;
quickEvalPosition: Position | undefined;
}
/**
* Custom event to provide additional information about a running evaluation.
*/
export interface EvaluationStartedEvent extends DebugProtocol.Event {
export interface EvaluationStartedEvent extends Event {
event: "codeql-evaluation-started";
body: EvaluationStartedEventBody;
}
export interface EvaluationCompletedEventBody {
resultType: QueryResultType;
message: string | undefined;
evaluationTime: number;
body: {
id: string;
outputDir: string;
quickEvalContext: QuickEvalContext | undefined;
};
}
/**
* Custom event to provide additional information about a completed evaluation.
*/
export interface EvaluationCompletedEvent extends DebugProtocol.Event {
export interface EvaluationCompletedEvent extends Event {
event: "codeql-evaluation-completed";
body: EvaluationCompletedEventBody;
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 interface QuickEvalRequest extends Request {
command: "codeql-quickeval";
arguments: {
quickEvalPosition: Position;
};
}
export interface DebugResultRequest extends Request {
command: "codeql-debug-result";
arguments: undefined;
}
export type InitializeRequest = DebugProtocol.InitializeRequest &
Request & { command: "initialize" };
export type AnyRequest =
| InitializeRequest
| DebugResultRequest
| QuickEvalRequest;
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;
export type AnyProtocolMessage = AnyEvent | AnyRequest | AnyResponse;
export interface Position {
fileName: string;
line: number;
column: number;
endLine: number;
endColumn: number;
}
export interface LaunchConfig {
/** Full path to query (.ql) file. */
query: string;
@@ -98,11 +66,37 @@ export interface LaunchConfig {
additionalPacks: string[];
/** Pack names of extension packs. */
extensionPacks: string[];
/** Optional quick evaluation position. */
quickEvalPosition: Position | undefined;
/** Optional quick evaluation context. */
quickEvalContext: QuickEvalContext | undefined;
/** Run the query without debugging it. */
noDebug: boolean;
}
export type LaunchRequestArguments = DebugProtocol.LaunchRequestArguments &
LaunchConfig;
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;

View File

@@ -9,7 +9,7 @@ import {
StoppedEvent,
TerminatedEvent,
} from "@vscode/debugadapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import { DebugProtocol as Protocol } from "@vscode/debugprotocol";
import { Disposable } from "vscode";
import { CancellationTokenSource } from "vscode-jsonrpc";
import { BaseLogger, LogOptions, queryServerLogger } from "../common";
@@ -20,15 +20,13 @@ import {
CoreQueryRun,
QueryRunner,
} from "../queryRunner";
import * as CodeQLDebugProtocol from "./debug-protocol";
import * as CodeQLProtocol from "./debug-protocol";
import { QuickEvalContext } from "../run-queries-shared";
// 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 DebugProtocol.ProgressStartEvent
{
class ProgressStartEvent extends Event implements Protocol.ProgressStartEvent {
public readonly event = "progressStart";
public readonly body: {
progressId: string;
@@ -57,7 +55,7 @@ class ProgressStartEvent
class ProgressUpdateEvent
extends Event
implements DebugProtocol.ProgressUpdateEvent
implements Protocol.ProgressUpdateEvent
{
public readonly event = "progressUpdate";
public readonly body: {
@@ -78,31 +76,33 @@ class ProgressUpdateEvent
class EvaluationStartedEvent
extends Event
implements CodeQLDebugProtocol.EvaluationStartedEvent
implements CodeQLProtocol.EvaluationStartedEvent
{
public readonly type = "event";
public readonly event = "codeql-evaluation-started";
public readonly body: CodeQLDebugProtocol.EvaluationStartedEventBody;
public readonly body: CodeQLProtocol.EvaluationStartedEvent["body"];
constructor(
id: string,
outputDir: string,
quickEvalPosition: CodeQLDebugProtocol.Position | undefined,
quickEvalContext: QuickEvalContext | undefined,
) {
super("codeql-evaluation-started");
this.body = {
id,
outputDir,
quickEvalPosition,
quickEvalContext,
};
}
}
class EvaluationCompletedEvent
extends Event
implements CodeQLDebugProtocol.EvaluationCompletedEvent
implements CodeQLProtocol.EvaluationCompletedEvent
{
public readonly type = "event";
public readonly event = "codeql-evaluation-completed";
public readonly body: CodeQLDebugProtocol.EvaluationCompletedEventBody;
public readonly body: CodeQLProtocol.EvaluationCompletedEvent["body"];
constructor(results: CoreQueryResults) {
super("codeql-evaluation-completed");
@@ -139,12 +139,12 @@ const QUERY_THREAD_NAME = "Evaluation thread";
export class QLDebugSession extends LoggingDebugSession implements Disposable {
private state: State = "uninitialized";
private terminateOnComplete = false;
private args: CodeQLDebugProtocol.LaunchRequestArguments | undefined =
private args: CodeQLProtocol.LaunchRequest["arguments"] | undefined =
undefined;
private tokenSource: CancellationTokenSource | undefined = undefined;
private queryRun: CoreQueryRun | undefined = undefined;
private lastResult:
| CodeQLDebugProtocol.EvaluationCompletedEventBody
| CodeQLProtocol.EvaluationCompletedEvent["body"]
| undefined = undefined;
constructor(
@@ -158,14 +158,14 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
this.cancelEvaluation();
}
protected dispatchRequest(request: DebugProtocol.Request): void {
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: DebugProtocol.Response): void {
private unexpectedState(response: Protocol.Response): void {
this.sendErrorResponse(
response,
ERROR_UNEXPECTED_STATE,
@@ -178,8 +178,8 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
}
protected initializeRequest(
response: DebugProtocol.InitializeResponse,
_args: DebugProtocol.InitializeRequestArguments,
response: Protocol.InitializeResponse,
_args: Protocol.InitializeRequestArguments,
): void {
switch (this.state) {
case "uninitialized":
@@ -206,30 +206,30 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
}
protected configurationDoneRequest(
response: DebugProtocol.ConfigurationDoneResponse,
args: DebugProtocol.ConfigurationDoneArguments,
request?: DebugProtocol.Request,
response: Protocol.ConfigurationDoneResponse,
args: Protocol.ConfigurationDoneArguments,
request?: Protocol.Request,
): void {
super.configurationDoneRequest(response, args, request);
}
protected disconnectRequest(
response: DebugProtocol.DisconnectResponse,
_args: DebugProtocol.DisconnectArguments,
_request?: DebugProtocol.Request,
response: Protocol.DisconnectResponse,
_args: Protocol.DisconnectArguments,
_request?: Protocol.Request,
): void {
this.terminateOrDisconnect(response);
}
protected terminateRequest(
response: DebugProtocol.TerminateResponse,
_args: DebugProtocol.TerminateArguments,
_request?: DebugProtocol.Request,
response: Protocol.TerminateResponse,
_args: Protocol.TerminateArguments,
_request?: Protocol.Request,
): void {
this.terminateOrDisconnect(response);
}
private terminateOrDisconnect(response: DebugProtocol.Response): void {
private terminateOrDisconnect(response: Protocol.Response): void {
switch (this.state) {
case "running":
this.terminateOnComplete = true;
@@ -249,9 +249,9 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
}
protected launchRequest(
response: DebugProtocol.LaunchResponse,
args: CodeQLDebugProtocol.LaunchRequestArguments,
_request?: DebugProtocol.Request,
response: Protocol.LaunchResponse,
args: CodeQLProtocol.LaunchRequest["arguments"],
_request?: Protocol.Request,
): void {
switch (this.state) {
case "initialized":
@@ -265,7 +265,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
// Send the response immediately. We'll send a "stopped" message when the evaluation is complete.
this.sendResponse(response);
void this.evaluate(this.args.quickEvalPosition);
void this.evaluate(this.args.quickEvalContext);
break;
default:
@@ -275,38 +275,38 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
}
protected nextRequest(
response: DebugProtocol.NextResponse,
_args: DebugProtocol.NextArguments,
_request?: DebugProtocol.Request,
response: Protocol.NextResponse,
_args: Protocol.NextArguments,
_request?: Protocol.Request,
): void {
this.stepRequest(response);
}
protected stepInRequest(
response: DebugProtocol.StepInResponse,
_args: DebugProtocol.StepInArguments,
_request?: DebugProtocol.Request,
response: Protocol.StepInResponse,
_args: Protocol.StepInArguments,
_request?: Protocol.Request,
): void {
this.stepRequest(response);
}
protected stepOutRequest(
response: DebugProtocol.Response,
_args: DebugProtocol.StepOutArguments,
_request?: DebugProtocol.Request,
response: Protocol.Response,
_args: Protocol.StepOutArguments,
_request?: Protocol.Request,
): void {
this.stepRequest(response);
}
protected stepBackRequest(
response: DebugProtocol.StepBackResponse,
_args: DebugProtocol.StepBackArguments,
_request?: DebugProtocol.Request,
response: Protocol.StepBackResponse,
_args: Protocol.StepBackArguments,
_request?: Protocol.Request,
): void {
this.stepRequest(response);
}
private stepRequest(response: DebugProtocol.Response): void {
private stepRequest(response: Protocol.Response): void {
switch (this.state) {
case "stopped":
this.sendResponse(response);
@@ -323,9 +323,9 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
}
protected continueRequest(
response: DebugProtocol.ContinueResponse,
_args: DebugProtocol.ContinueArguments,
_request?: DebugProtocol.Request,
response: Protocol.ContinueResponse,
_args: Protocol.ContinueArguments,
_request?: Protocol.Request,
): void {
switch (this.state) {
case "stopped":
@@ -345,9 +345,9 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
}
protected cancelRequest(
response: DebugProtocol.CancelResponse,
args: DebugProtocol.CancelArguments,
_request?: DebugProtocol.Request,
response: Protocol.CancelResponse,
args: Protocol.CancelArguments,
_request?: Protocol.Request,
): void {
switch (this.state) {
case "running":
@@ -367,8 +367,8 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
}
protected threadsRequest(
response: DebugProtocol.ThreadsResponse,
_request?: DebugProtocol.Request,
response: Protocol.ThreadsResponse,
_request?: Protocol.Request,
): void {
response.body = response.body ?? {};
response.body.threads = [
@@ -382,9 +382,9 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
}
protected stackTraceRequest(
response: DebugProtocol.StackTraceResponse,
_args: DebugProtocol.StackTraceArguments,
_request?: DebugProtocol.Request,
response: Protocol.StackTraceResponse,
_args: Protocol.StackTraceArguments,
_request?: Protocol.Request,
): void {
response.body = response.body ?? {};
response.body.stackFrames = []; // No frames for now.
@@ -394,15 +394,15 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
protected customRequest(
command: string,
response: CodeQLDebugProtocol.Response,
response: CodeQLProtocol.Response,
args: any,
request?: DebugProtocol.Request,
request?: Protocol.Request,
): void {
switch (command) {
case "codeql-quickeval": {
this.quickEvalRequest(
response,
<CodeQLDebugProtocol.QuickEvalRequest["arguments"]>args,
<CodeQLProtocol.QuickEvalRequest["arguments"]>args,
);
break;
}
@@ -414,8 +414,8 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
}
protected quickEvalRequest(
response: CodeQLDebugProtocol.QuickEvalResponse,
args: CodeQLDebugProtocol.QuickEvalRequest["arguments"],
response: CodeQLProtocol.QuickEvalResponse,
args: CodeQLProtocol.QuickEvalRequest["arguments"],
): void {
switch (this.state) {
case "stopped":
@@ -427,7 +427,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
// 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.quickEvalPosition);
void this.evaluate(args.quickEvalContext);
break;
default:
@@ -452,7 +452,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
* result.
*/
private async evaluate(
quickEvalPosition: CodeQLDebugProtocol.Position | undefined,
quickEvalContext: QuickEvalContext | undefined,
): Promise<void> {
const args = this.args!;
@@ -464,7 +464,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
args.database,
{
queryPath: args.query,
quickEvalPosition,
quickEvalPosition: quickEvalContext?.quickEvalPosition,
},
true,
args.additionalPacks,
@@ -482,7 +482,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
new EvaluationStartedEvent(
this.queryRun.id,
this.queryRun.outputDir.querySaveDir,
quickEvalPosition,
quickEvalContext,
),
);
@@ -532,7 +532,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
* Mark the evaluation as completed, and notify the client of the result.
*/
private completeEvaluation(
result: CodeQLDebugProtocol.EvaluationCompletedEventBody,
result: CodeQLProtocol.EvaluationCompletedEvent["body"],
): void {
this.lastResult = result;

View File

@@ -4,7 +4,6 @@ import {
DebugAdapterTrackerFactory,
DebugSession,
debug,
// window,
Uri,
CancellationTokenSource,
commands,
@@ -24,7 +23,7 @@ import {
validateQueryUri,
} from "../run-queries-shared";
import { QLResolvedDebugConfiguration } from "./debug-configuration";
import * as CodeQLDebugProtocol from "./debug-protocol";
import * as CodeQLProtocol from "./debug-protocol";
/**
* Listens to messages passing between VS Code and the debug adapter, so that we can supplement the
@@ -50,9 +49,7 @@ class QLDebugAdapterTracker
this.configuration = <QLResolvedDebugConfiguration>session.configuration;
}
public onDidSendMessage(
message: CodeQLDebugProtocol.AnyProtocolMessage,
): void {
public onDidSendMessage(message: CodeQLProtocol.AnyProtocolMessage): void {
if (message.type === "event") {
switch (message.event) {
case "codeql-evaluation-started":
@@ -80,9 +77,8 @@ class QLDebugAdapterTracker
}
public async quickEval(): Promise<void> {
const args: CodeQLDebugProtocol.QuickEvalRequest["arguments"] = {
quickEvalPosition: (await getQuickEvalContext(undefined))
.quickEvalPosition,
const args: CodeQLProtocol.QuickEvalRequest["arguments"] = {
quickEvalContext: await getQuickEvalContext(undefined),
};
await this.session.customRequest("codeql-quickeval", args);
}
@@ -106,7 +102,7 @@ class QLDebugAdapterTracker
/** Updates the UI to track the currently executing query. */
private async onEvaluationStarted(
body: CodeQLDebugProtocol.EvaluationStartedEventBody,
body: CodeQLProtocol.EvaluationStartedEvent["body"],
): Promise<void> {
const dbUri = Uri.file(this.configuration.database);
const dbItem = await this.dbm.createOrOpenDatabaseItem(dbUri);
@@ -117,17 +113,10 @@ class QLDebugAdapterTracker
debug.stopDebugging(this.session),
);
const quickEval =
body.quickEvalPosition !== undefined
? {
quickEvalPosition: body.quickEvalPosition,
quickEvalText: "<Quick Evaluation>", // TODO: Have the debug adapter return the range, and extract the text from the editor.
}
: undefined;
this.localQueryRun = await this.localQueries.createLocalQueryRun(
{
queryPath: this.configuration.query,
quickEval,
quickEval: body.quickEvalContext,
},
dbItem,
new QueryOutputDir(body.outputDir),
@@ -137,7 +126,7 @@ class QLDebugAdapterTracker
/** Update the UI after a query has finished evaluating. */
private async onEvaluationCompleted(
body: CodeQLDebugProtocol.EvaluationCompletedEventBody,
body: CodeQLProtocol.EvaluationCompletedEvent["body"],
): Promise<void> {
if (this.localQueryRun !== undefined) {
const results: CoreQueryResults = body;

View File

@@ -259,7 +259,13 @@ export class LocalQueries extends DisposableObject {
"codeQL.quickEvalContextEditor": this.quickEval.bind(this),
"codeQL.codeLensQuickEval": this.codeLensQuickEval.bind(this),
"codeQL.quickQuery": this.quickQuery.bind(this),
"codeQL.getCurrentQuery": this.getCurrentQuery.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);
},
};
}
@@ -404,7 +410,7 @@ export class LocalQueries extends DisposableObject {
* 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.
*/
private async getCurrentQuery(): Promise<string> {
private async getCurrentQuery(allowLibraryFiles: boolean): Promise<string> {
const editor = window.activeTextEditor;
if (editor === undefined) {
throw new Error(
@@ -412,7 +418,7 @@ export class LocalQueries extends DisposableObject {
);
}
return validateQueryUri(editor.document.uri, false);
return validateQueryUri(editor.document.uri, allowLibraryFiles);
}
/**
@@ -492,7 +498,7 @@ export class LocalQueries extends DisposableObject {
queryPath = validateQueryUri(queryUri, quickEval);
} else {
// Use the currently selected query.
queryPath = await this.getCurrentQuery();
queryPath = await this.getCurrentQuery(quickEval);
}
const selectedQuery: SelectedQuery = {

View File

@@ -1 +1,2 @@
.vscode
.vscode/**
!.vscode/launch.json

View 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"
}
]
}

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -0,0 +1,438 @@
import {
DebugAdapterTracker,
DebugAdapterTrackerFactory,
DebugSession,
ProviderResult,
Uri,
commands,
debug,
workspace,
} from "vscode";
import * as CodeQLProtocol from "../../../src/debugger/debug-protocol";
import { DebuggerCommands } from "../../../src/common/commands";
import { CommandManager } from "../../../src/packages/commands";
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";
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 debuggerCommands: CommandManager<DebuggerCommands>,
) {
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(
workspace.workspaceFolders![0].uri.fsPath,
".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.debuggerCommands.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 commands.executeCommand("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.debuggerCommands.execute(
"codeQL.continueDebuggingSelection",
);
}
public async stepInto(): Promise<void> {
return await commands.executeCommand("workbench.action.debug.stepInto");
}
public async stepOver(): Promise<void> {
return await commands.executeCommand("workbench.action.debug.stepOver");
}
public async stepOut(): Promise<void> {
return await commands.executeCommand("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>(
debuggerCommands: CommandManager<DebuggerCommands>,
op: (controller: DebugController) => Promise<T>,
): Promise<T> {
await workspace.getConfiguration().update("codeQL.canary", true);
try {
const controller = new DebugController(debuggerCommands);
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);
}
}

View File

@@ -0,0 +1,143 @@
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 { createVSCodeCommandManager } from "../../../src/common/vscode/commands";
import { DebuggerCommands } from "../../../src/common/commands";
import { withDebugController } from "./debug-controller";
import { CodeQLCliServer } from "../../../src/cli";
import { QueryOutputDir } from "../../../src/run-queries-shared";
jest.setTimeout(2000_000);
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 debuggerCommands = createVSCodeCommandManager<DebuggerCommands>();
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(debuggerCommands, 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(debuggerCommands, 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(debuggerCommands, 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(debuggerCommands, 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(debuggerCommands, 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();
});
});
});

View File

@@ -5,18 +5,11 @@ import * as messages from "../../../src/pure/new-messages";
import * as qsClient from "../../../src/query-server/queryserver-client";
import * as cli from "../../../src/cli";
import { CellValue } from "../../../src/pure/bqrs-cli-types";
import { Uri } from "vscode";
import { describeWithCodeQL } from "../cli";
import { QueryServerClient } from "../../../src/query-server/queryserver-client";
import { extLogger, ProgressReporter } from "../../../src/common";
import { QueryResultType } from "../../../src/pure/new-messages";
import {
cleanDatabases,
dbLoc,
getActivatedExtension,
storagePath,
} from "../global.helper";
import { importArchiveDatabase } from "../../../src/databaseFetcher";
import { ensureTestDatabase, getActivatedExtension } from "../global.helper";
import { createMockApp } from "../../__mocks__/appMock";
const baseDir = join(__dirname, "../../../test/data");
@@ -144,24 +137,11 @@ describeWithCodeQL()("using the new query server", () => {
await qs.startQueryServer();
// 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
await cleanDatabases(extension.databaseManager);
const uri = Uri.file(dbLoc);
const maybeDbItem = await importArchiveDatabase(
app.commands,
uri.toString(true),
const dbItem = await ensureTestDatabase(
extension.databaseManager,
storagePath,
() => {
/**ignore progress */
},
token,
undefined,
);
if (!maybeDbItem) {
throw new Error("Could not import database");
}
db = maybeDbItem.databaseUri.fsPath;
db = dbItem.databaseUri.fsPath;
});
for (const queryTestCase of queryTestCases) {

View File

@@ -1,4 +1,4 @@
import { CancellationToken, ExtensionContext, Uri } from "vscode";
import { debug, CancellationToken, ExtensionContext, Range, Uri } from "vscode";
import { join, dirname } from "path";
import {
pathExistsSync,
@@ -12,22 +12,68 @@ import { load, dump } from "js-yaml";
import { DatabaseItem, DatabaseManager } from "../../../src/local-databases";
import {
cleanDatabases,
dbLoc,
ensureTestDatabase,
getActivatedExtension,
storagePath,
} from "../global.helper";
import { importArchiveDatabase } from "../../../src/databaseFetcher";
import { CliVersionConstraint, CodeQLCliServer } from "../../../src/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 { createMockCommandManager } from "../../__mocks__/commandsMock";
import { LocalQueries } from "../../../src/local-queries";
import { QueryResultType } from "../../../src/pure/new-messages";
import { createVSCodeCommandManager } from "../../../src/common/vscode/commands";
import { AllCommands, QueryServerCommands } from "../../../src/common/commands";
import {
AllCommands,
DebuggerCommands,
QueryServerCommands,
} from "../../../src/common/commands";
import { ProgressCallback } from "../../../src/progress";
import { CommandManager } from "../../../src/packages/commands";
import { withDebugController } from "./debug-controller";
jest.setTimeout(20_000);
type DebugMode = "localQueries" | "launch";
async function compileAndRunQuery(
mode: DebugMode,
localQueries: LocalQueries,
debuggerCommands: CommandManager<DebuggerCommands>,
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 "launch":
return await withDebugController(debuggerCommands, async (controller) => {
await controller.debugQuery(queryUri);
await controller.expectLaunched();
const succeeded = await controller.expectSucceeded();
await controller.expectStopped();
expect(debug.activeDebugSession?.name).not.toBeUndefined();
await debug.stopDebugging();
await controller.expectTerminated();
await controller.expectSessionClosed();
return succeeded.results;
});
}
}
jest.setTimeout(2000_000);
const MODES: DebugMode[] = ["localQueries", "launch"];
/**
* Integration tests for queries
@@ -44,6 +90,7 @@ describeWithCodeQL()("Queries", () => {
const appCommandManager = createVSCodeCommandManager<AllCommands>();
const queryServerCommandManager =
createVSCodeCommandManager<QueryServerCommands>();
const debuggerCommands = createVSCodeCommandManager<DebuggerCommands>();
let qlpackFile: string;
let qlpackLockFile: string;
@@ -73,23 +120,7 @@ describeWithCodeQL()("Queries", () => {
},
} as CancellationToken;
// 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,
progress,
token,
cli,
);
if (!maybeDbItem) {
throw new Error("Could not import database");
}
dbItem = maybeDbItem;
dbItem = await ensureTestDatabase(databaseManager, cli);
});
afterEach(async () => {
@@ -98,7 +129,7 @@ describeWithCodeQL()("Queries", () => {
await cleanDatabases(databaseManager);
});
describe("extension packs", () => {
describe.each(MODES)("extension packs (%s)", (mode) => {
const queryUsingExtensionPath = join(
__dirname,
"../..",
@@ -141,7 +172,10 @@ describeWithCodeQL()("Queries", () => {
}
async function runQueryWithExtensions() {
const result = await localQueries.compileAndRunQueryInternal(
const result = await compileAndRunQuery(
mode,
localQueries,
debuggerCommands,
false,
Uri.file(queryUsingExtensionPath),
progress,
@@ -169,75 +203,85 @@ describeWithCodeQL()("Queries", () => {
}
});
it("should run a query", async () => {
const queryPath = join(__dirname, "data", "simple-query.ql");
const result = await localQueries.compileAndRunQueryInternal(
false,
Uri.file(queryPath),
progress,
token,
dbItem,
undefined,
);
describe.each(MODES)("running queries (%s)", (mode) => {
it("should run a query", async () => {
const queryPath = join(__dirname, "data", "simple-query.ql");
const result = await compileAndRunQuery(
mode,
localQueries,
debuggerCommands,
false,
Uri.file(queryPath),
progress,
token,
dbItem,
undefined,
);
// just check that the query was successful
expect(result.resultType).toBe(QueryResultType.SUCCESS);
// just check that the query was successful
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,
localQueries,
debuggerCommands,
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
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 localQueries.compileAndRunQueryInternal(
false,
Uri.file(queryPath),
progress,
token,
dbItem,
undefined,
);
describe("quick query", () => {
it("should create a quick query", async () => {
await queryServerCommandManager.execute("codeQL.quickQuery");
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 () => {
await queryServerCommandManager.execute("codeQL.quickQuery");
const qlpackContents: any = await load(readFileSync(qlpackFile, "utf8"));
// Should have chosen the js libraries
expect(qlpackContents.dependencies["codeql/javascript-all"]).toBe("*");
// should have created the quick query file and query pack file
expect(pathExistsSync(qlFile)).toBe(true);
expect(pathExistsSync(qlpackFile)).toBe(true);
// Should also have a codeql-pack.lock.yml file
const packFileToUse = pathExistsSync(qlpackLockFile)
? 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"));
// Should have chosen the js libraries
expect(qlpackContents.dependencies["codeql/javascript-all"]).toBe("*");
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 also have a codeql-pack.lock.yml file
const packFileToUse = pathExistsSync(qlpackLockFile)
? 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");
// should not have created the quick query file because database schema hasn't changed
expect(readFileSync(qlFile, "utf8")).toBe("xxx");
});
});
function safeDel(file: string) {

View File

@@ -1,12 +1,19 @@
import { join } from "path";
import { load, dump } from "js-yaml";
import { realpathSync, readFileSync, writeFileSync } from "fs-extra";
import { CancellationToken, extensions } from "vscode";
import { DatabaseManager } from "../../src/local-databases";
import {
CancellationToken,
CancellationTokenSource,
Uri,
extensions,
} from "vscode";
import { DatabaseItem, DatabaseManager } from "../../src/local-databases";
import { CodeQLCliServer } from "../../src/cli";
import { removeWorkspaceRefs } from "../../src/variant-analysis/run-remote-query";
import { CodeQLExtensionInterface } from "../../src/extension";
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.
@@ -21,6 +28,35 @@ export const dbLoc = join(
);
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) {
storagePath = path;
}