Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consume new rich test events in new Test Explorer UI #2394

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
111 changes: 96 additions & 15 deletions extensions/ql-vscode/src/codeql-cli/cli.ts
Expand Up @@ -139,10 +139,35 @@ export interface SourceInfo {
*/
export type ResolvedTests = string[];

export type TestEventType =
| "testStarted"
| "testCompleted"
| "extractionStarted"
| "extractionSucceeded"
| "extractionFailed";

export interface TestEvent<T extends TestEventType> {
type: T;
}

/**
* Fired when a test starts executing.
*/
export interface TestStartedEvent extends TestEvent<"testStarted"> {
test: string;
}

/**
* Event fired by `codeql test run`.
*/
export interface TestCompleted {

/**
* Event fired by `codeql test run` for CLI versions before 2.13.1.
*
* We translate these into `TestCompletedEvent`s so that the rest of the extension can pretend that
* it's using the new event scheme.
*/
interface TestResult {
test: string;
pass: boolean;
messages: CompilationMessage[];
Expand All @@ -155,6 +180,36 @@ export interface TestCompleted {
failureStage?: string;
}

/**
* Fired when a test completes, whether successful or not.
*/
export interface TestCompletedEvent
extends TestEvent<"testCompleted">,
TestResult {}

/** Fired when database extraction for a directory has started. */
export interface ExtractionStartedEvent extends TestEvent<"extractionStarted"> {
testDirectory: string;
}

/** Fired when database extraction for a directory has succeeded. */
export interface ExtractionSucceededEvent
extends TestEvent<"extractionSucceeded"> {
testDirectory: string;
}

/** Fired when database extraction for a directory has failed. */
export interface ExtractionFailedEvent extends TestEvent<"extractionFailed"> {
testDirectory: string;
}

export type AnyTestEvent =
| TestStartedEvent
| TestCompletedEvent
| ExtractionStartedEvent
| ExtractionSucceededEvent
| ExtractionFailedEvent;

/**
* Optional arguments for the `bqrsDecode` function
*/
Expand Down Expand Up @@ -436,18 +491,20 @@ export class CodeQLCliServer implements Disposable {
*
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
* @param commandArgs The arguments to pass to the `codeql` command.
* @param format The event format option to use.
* @param cancellationToken CancellationToken to terminate the test process.
* @param logger Logger to write text output from the command.
* @returns The sequence of async events produced by the command.
*/
private async *runAsyncCodeQlCliCommandInternal(
command: string[],
commandArgs: string[],
cancellationToken?: CancellationToken,
logger?: BaseLogger,
format: "jsonz" | "betterjsonz",
cancellationToken: CancellationToken | undefined,
logger: BaseLogger | undefined,
): AsyncGenerator<string, void, unknown> {
// Add format argument first, in case commandArgs contains positional parameters.
const args = [...command, "--format", "jsonz", ...commandArgs];
const args = [...command, "--format", format, ...commandArgs];

// Spawn the CodeQL process
const codeqlPath = await this.getCodeQlPath();
Expand Down Expand Up @@ -501,6 +558,7 @@ export class CodeQLCliServer implements Disposable {
public async *runAsyncCodeQlCliCommand<EventType>(
command: string[],
commandArgs: string[],
format: "jsonz" | "betterjsonz",
description: string,
{
cancellationToken,
Expand All @@ -513,6 +571,7 @@ export class CodeQLCliServer implements Disposable {
for await (const event of this.runAsyncCodeQlCliCommandInternal(
command,
commandArgs,
format,
cancellationToken,
logger,
)) {
Expand Down Expand Up @@ -788,24 +847,35 @@ export class CodeQLCliServer implements Disposable {
cancellationToken?: CancellationToken;
logger?: BaseLogger;
},
): AsyncGenerator<TestCompleted, void, unknown> {
): AsyncGenerator<AnyTestEvent, void, unknown> {
const subcommandArgs = this.cliConfig.additionalTestArguments.concat([
...this.getAdditionalPacksArg(workspaces),
"--threads",
this.cliConfig.numberTestThreads.toString(),
...testPaths,
]);

for await (const event of this.runAsyncCodeQlCliCommand<TestCompleted>(
["test", "run"],
subcommandArgs,
"Run CodeQL Tests",
{
cancellationToken,
logger,
},
)) {
yield event;
const format = (await this.cliConstraints.supportsRichTestEvents())
? "betterjsonz"
: "jsonz";
for await (const event of this.runAsyncCodeQlCliCommand<
AnyTestEvent | TestResult
>(["test", "run"], subcommandArgs, format, "Run CodeQL Tests", {
cancellationToken,
logger,
})) {
if (format === "jsonz") {
// The original event format only support one event kind. Translate it into a
// `TestCompletedEvent`.
const testResult = event as TestResult;
const translatedEvent: TestCompletedEvent = {
type: "testCompleted",
...testResult,
};
yield translatedEvent;
} else {
yield event as AnyTestEvent;
}
}
}

Expand Down Expand Up @@ -1782,6 +1852,11 @@ export class CliVersionConstraint {
"2.12.4",
);

/**
* CLI version that supports the `--format betterjsonz` option for the `codeql test run` command.
*/
public static CLI_VERSION_WITH_RICH_TEST_EVENTS = new SemVer("2.13.1");

constructor(private readonly cli: CodeQLCliServer) {
/**/
}
Expand Down Expand Up @@ -1851,4 +1926,10 @@ export class CliVersionConstraint {
CliVersionConstraint.CLI_VERSION_WITH_ADDITIONAL_PACKS_INSTALL,
);
}

async supportsRichTestEvents() {
return this.isVersionAtLeast(
CliVersionConstraint.CLI_VERSION_WITH_RICH_TEST_EVENTS,
);
}
}
76 changes: 39 additions & 37 deletions extensions/ql-vscode/src/query-testing/test-adapter.ts
Expand Up @@ -21,7 +21,7 @@ import {
} from "./qltest-discovery";
import { Event, EventEmitter, CancellationTokenSource } from "vscode";
import { DisposableObject } from "../pure/disposable-object";
import { CodeQLCliServer, TestCompleted } from "../codeql-cli/cli";
import { CodeQLCliServer, AnyTestEvent } from "../codeql-cli/cli";
import { testLogger } from "../common";
import { TestRunner } from "./test-runner";

Expand Down Expand Up @@ -243,42 +243,44 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
}
}

private async processTestEvent(event: TestCompleted): Promise<void> {
const state = event.pass
? "passed"
: event.messages?.length
? "errored"
: "failed";
let message: string | undefined;
if (event.failureDescription || event.diff?.length) {
message =
event.failureStage === "RESULT"
? [
"",
`${state}: ${event.test}`,
event.failureDescription || event.diff?.join("\n"),
"",
].join("\n")
: [
"",
`${event.failureStage?.toLowerCase() ?? "unknown stage"} error: ${
event.test
}`,
event.failureDescription ||
`${event.messages[0].severity}: ${event.messages[0].message}`,
"",
].join("\n");
void testLogger.log(message);
private async processTestEvent(event: AnyTestEvent): Promise<void> {
if (event.type === "testCompleted") {
const state = event.pass
? "passed"
: event.messages?.length
? "errored"
: "failed";
let message: string | undefined;
if (event.failureDescription || event.diff?.length) {
message =
event.failureStage === "RESULT"
? [
"",
`${state}: ${event.test}`,
event.failureDescription || event.diff?.join("\n"),
"",
].join("\n")
: [
"",
`${
event.failureStage?.toLowerCase() ?? "unknown stage"
} error: ${event.test}`,
event.failureDescription ||
`${event.messages[0].severity}: ${event.messages[0].message}`,
"",
].join("\n");
void testLogger.log(message);
}
this._testStates.fire({
type: "test",
state,
test: event.test,
message,
decorations: event.messages?.map((msg) => ({
line: msg.position.line,
message: msg.message,
})),
});
}
this._testStates.fire({
type: "test",
state,
test: event.test,
message,
decorations: event.messages?.map((msg) => ({
line: msg.position.line,
message: msg.message,
})),
});
}
}