Debugger tests
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
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,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,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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user