Skip to content
1 change: 1 addition & 0 deletions extensions/ql-vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [UNRELEASED]

- Update variant analysis view to show when cancelation is in progress. [#3405](https://github.com/github/vscode-codeql/pull/3405)
- Remove support for CodeQL CLI versions older than 2.13.5. [#3371](https://github.com/github/vscode-codeql/pull/3371)
- Add a timeout to downloading databases and the CodeQL CLI. These can be changed using the `codeQL.addingDatabases.downloadTimeout` and `codeQL.cli.downloadTimeout` settings respectively. [#3373](https://github.com/github/vscode-codeql/pull/3373)

Expand Down
2 changes: 2 additions & 0 deletions extensions/ql-vscode/src/query-history/query-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export function variantAnalysisStatusToQueryStatus(
return QueryStatus.Failed;
case VariantAnalysisStatus.InProgress:
return QueryStatus.InProgress;
case VariantAnalysisStatus.Canceling:
return QueryStatus.InProgress;
case VariantAnalysisStatus.Canceled:
return QueryStatus.Completed;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ function mapVariantAnalysisStatusToDto(
return VariantAnalysisStatusDto.Succeeded;
case VariantAnalysisStatus.Failed:
return VariantAnalysisStatusDto.Failed;
case VariantAnalysisStatus.Canceling:
// The canceling state shouldn't be persisted. We can just
// assume that the analysis is still in progress, since the
// canceling state is very short-lived.
return VariantAnalysisStatusDto.InProgress;
case VariantAnalysisStatus.Canceled:
return VariantAnalysisStatusDto.Canceled;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,9 @@ Failed.args = {
...InProgress.args,
variantAnalysisStatus: VariantAnalysisStatus.Failed,
};

export const Canceling = Template.bind({});
Canceling.args = {
...InProgress.args,
variantAnalysisStatus: VariantAnalysisStatus.Canceling,
};
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export enum VariantAnalysisStatus {
InProgress = "inProgress",
Succeeded = "succeeded",
Failed = "failed",
Canceling = "canceling",
Canceled = "canceled",
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
isVariantAnalysisComplete,
parseVariantAnalysisQueryLanguage,
VariantAnalysisScannedRepositoryDownloadStatus,
VariantAnalysisStatus,
} from "./shared/variant-analysis";
import { getErrorMessage } from "../common/helpers-pure";
import { VariantAnalysisView } from "./variant-analysis-view";
Expand Down Expand Up @@ -145,6 +146,7 @@ export class VariantAnalysisManager
new VariantAnalysisMonitor(
app,
this.shouldCancelMonitorVariantAnalysis.bind(this),
this.getVariantAnalysisStatus.bind(this),
),
);
this.variantAnalysisMonitor.onVariantAnalysisChange(
Expand Down Expand Up @@ -604,14 +606,31 @@ export class VariantAnalysisManager
return !this.variantAnalyses.has(variantAnalysisId);
}

private getVariantAnalysisStatus(
variantAnalysisId: number,
): VariantAnalysisStatus {
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
if (!variantAnalysis) {
throw new Error(
`No variant analysis found with id: ${variantAnalysisId}.`,
);
}

return variantAnalysis.status;
}

public async onVariantAnalysisUpdated(
variantAnalysis: VariantAnalysis | undefined,
): Promise<void> {
if (!variantAnalysis) {
return;
}

if (!this.variantAnalyses.has(variantAnalysis.id)) {
const originalVariantAnalysis = this.variantAnalyses.get(
variantAnalysis.id,
);

if (!originalVariantAnalysis) {
return;
}

Expand Down Expand Up @@ -844,11 +863,24 @@ export class VariantAnalysisManager
);
}

await this.onVariantAnalysisUpdated({
...variantAnalysis,
status: VariantAnalysisStatus.Canceling,
});

void showAndLogInformationMessage(
this.app.logger,
"Cancelling variant analysis. This may take a while.",
);
await cancelVariantAnalysis(this.app.credentials, variantAnalysis);
try {
await cancelVariantAnalysis(this.app.credentials, variantAnalysis);
} catch (e) {
await this.onVariantAnalysisUpdated({
...variantAnalysis,
status: VariantAnalysisStatus.InProgress,
});
throw e;
}
}

public async openVariantAnalysisLogs(variantAnalysisId: number) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export function mapVariantAnalysis(
databases: submission.databases,
executionStartTime: submission.startTime,
},
undefined,
response,
);
}
Expand All @@ -49,6 +50,7 @@ export function mapUpdatedVariantAnalysis(
VariantAnalysis,
"language" | "query" | "queries" | "databases" | "executionStartTime"
>,
currentStatus: VariantAnalysisStatus | undefined,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I had to pass this as a separate arg instead of adding more items to Pick because we want to allow for it to be undefined for the case where we map a variant analysis submission to a variant analysis entity (L30).

I don't love how this function is shaping up in terms of args (specially with the Pick<>) but we could tidy that up a bit separately.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should rewrite the exposed interface of this file. Right now we have

mapVariantAnalysis(VariantAnalysisSubmission, ApiVariantAnalysis): VariantAnalysis
mapUpdatedVariantAnalysis(Pick<...>, ApiVariantAnalysis): VariantAnalysis

where mapVariantAnalysis depends on mapUpdatedVariantAnalysis. We could rewrite mapUpdatedVariantAnalysis to be

mapUpdatedVariantAnalysis(VariantAnalysis, ApiVariantAnalysis): VariantAnalysis

and then have a third un-exported method that can have Pick or other ugly things for its args, but it'll be an internal function and not exposed outside the file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's the sort of thing I had in mind (although I'm still not into using Pick and would rather avoid it). Let's play around that in a separate PR.

response: ApiVariantAnalysis,
): VariantAnalysis {
let scannedRepos: VariantAnalysisScannedRepository[] = [];
Expand All @@ -66,6 +68,13 @@ export function mapUpdatedVariantAnalysis(
);
}

// Maintain the canceling status if we are still canceling.
const status =
currentStatus === VariantAnalysisStatus.Canceling &&
response.status === "in_progress"
? VariantAnalysisStatus.Canceling
: mapApiStatus(response.status);

const variantAnalysis: VariantAnalysis = {
id: response.id,
controllerRepo: {
Expand All @@ -80,7 +89,7 @@ export function mapUpdatedVariantAnalysis(
executionStartTime: previousVariantAnalysis.executionStartTime,
createdAt: response.created_at,
updatedAt: response.updated_at,
status: mapApiStatus(response.status),
status,
completedAt: response.completed_at,
actionsWorkflowRunId: response.actions_workflow_run_id,
scannedRepos,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RequestError } from "@octokit/request-error";
import type {
VariantAnalysis,
VariantAnalysisScannedRepository,
VariantAnalysisStatus,
} from "./shared/variant-analysis";
import {
isFinalVariantAnalysisStatus,
Expand Down Expand Up @@ -36,6 +37,9 @@ export class VariantAnalysisMonitor extends DisposableObject {
private readonly shouldCancelMonitor: (
variantAnalysisId: number,
) => Promise<boolean>,
private readonly getVariantAnalysisStatus: (
variantAnalysisId: number,
) => VariantAnalysisStatus,
) {
super();
}
Expand Down Expand Up @@ -119,8 +123,13 @@ export class VariantAnalysisMonitor extends DisposableObject {
continue;
}

// Get the current status of the variant analysis as known by the rest
// of the app, because it may have changed by the user, but the API
Comment thread
charisk marked this conversation as resolved.
Outdated
// may not be aware of it yet.
const currentStatus = this.getVariantAnalysisStatus(variantAnalysis.id);
variantAnalysis = mapUpdatedVariantAnalysis(
variantAnalysis,
currentStatus,
variantAnalysisSummary,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ export const VariantAnalysisActions = ({
Stop query
</Button>
)}
{variantAnalysisStatus === VariantAnalysisStatus.Canceling && (
<Button appearance="secondary" disabled={true}>
Stopping query
</Button>
)}
</Container>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export const VariantAnalysisStats = ({
return "Failed";
}

if (variantAnalysisStatus === VariantAnalysisStatus.Canceling) {
return "Canceling";
}

if (variantAnalysisStatus === VariantAnalysisStatus.Canceled) {
return "Stopped";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export const VariantAnalysisStatusStats = ({
}: VariantAnalysisStatusStatsProps) => {
return (
<Container>
{variantAnalysisStatus === VariantAnalysisStatus.InProgress ? (
{variantAnalysisStatus === VariantAnalysisStatus.InProgress ||
variantAnalysisStatus === VariantAnalysisStatus.Canceling ? (
<div>
<Icon className="codicon codicon-loading codicon-modifier-spin" />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,24 @@ describe(VariantAnalysisActions.name, () => {
expect(onStopQueryClick).toHaveBeenCalledTimes(1);
});

it("renders the stopping query disabled button when canceling", async () => {
Comment thread
robertbrignull marked this conversation as resolved.
render({
variantAnalysisStatus: VariantAnalysisStatus.Canceling,
});

const button = screen.getByText("Stopping query");
expect(button).toBeInTheDocument();
expect(button.getElementsByTagName("input")[0]).toBeDisabled();
});

it("does not render a stop query button when canceling", async () => {
render({
variantAnalysisStatus: VariantAnalysisStatus.Canceling,
});

expect(screen.queryByText("Stop query")).not.toBeInTheDocument();
});

it("renders 3 buttons when in progress with results", async () => {
const { container } = render({
variantAnalysisStatus: VariantAnalysisStatus.InProgress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ describe(VariantAnalysisStats.name, () => {
expect(screen.getByText("Failed")).toBeInTheDocument();
});

it("renders 'Stopping' text when the variant analysis status is canceling", () => {
render({ variantAnalysisStatus: VariantAnalysisStatus.Canceling });

expect(screen.getByText("Canceling")).toBeInTheDocument();
});

it("renders 'Stopped' text when the variant analysis status is canceled", () => {
render({ variantAnalysisStatus: VariantAnalysisStatus.Canceled });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -607,14 +607,38 @@ describe("Variant Analysis Manager", () => {
}
});

it("should return cancel if valid", async () => {
it("should make API request to cancel if valid", async () => {
await variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id);

expect(mockCancelVariantAnalysis).toHaveBeenCalledWith(
app.credentials,
variantAnalysis,
);
});

it("should set the status to canceling", async () => {
await variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id);

const updatedAnalysis = await variantAnalysisManager.getVariantAnalysis(
variantAnalysis.id,
);
expect(updatedAnalysis?.status).toBe(VariantAnalysisStatus.Canceling);
});

it("should set the status back to in progress if canceling fails", async () => {
mockCancelVariantAnalysis.mockRejectedValueOnce(
new Error("Error when cancelling"),
);

await expect(
variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id),
).rejects.toThrow("Error when cancelling");

const updatedAnalysis = await variantAnalysisManager.getVariantAnalysis(
variantAnalysis.id,
);
expect(updatedAnalysis?.status).toBe(VariantAnalysisStatus.InProgress);
});
});

describe("copyRepoListToClipboard", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe("Variant Analysis Monitor", () => {
>;
let variantAnalysisMonitor: VariantAnalysisMonitor;
let shouldCancelMonitor: jest.Mock<Promise<boolean>, [number]>;
let mockGetVariantAnalysisStatus: jest.Mock<VariantAnalysisStatus, [number]>;
let variantAnalysis: VariantAnalysis;

const onVariantAnalysisChangeSpy = jest.fn();
Expand All @@ -43,6 +44,7 @@ describe("Variant Analysis Monitor", () => {
variantAnalysis = createMockVariantAnalysis({});

shouldCancelMonitor = jest.fn();
mockGetVariantAnalysisStatus = jest.fn();

logger = createMockLogger();

Expand All @@ -54,6 +56,7 @@ describe("Variant Analysis Monitor", () => {
logger,
}),
shouldCancelMonitor,
mockGetVariantAnalysisStatus,
);
variantAnalysisMonitor.onVariantAnalysisChange(onVariantAnalysisChangeSpy);

Expand Down Expand Up @@ -128,7 +131,11 @@ describe("Variant Analysis Monitor", () => {
index + 1,
"codeQL.autoDownloadVariantAnalysisResult",
mapScannedRepository(succeededRepo),
mapUpdatedVariantAnalysis(variantAnalysis, mockApiResponse),
mapUpdatedVariantAnalysis(
variantAnalysis,
variantAnalysis.status,
mockApiResponse,
),
);
});
});
Expand Down Expand Up @@ -336,6 +343,24 @@ describe("Variant Analysis Monitor", () => {
);
});
});

describe("cancelation", () => {
it("should maintain canceling status", async () => {
mockGetVariantAnalysisStatus.mockReturnValueOnce(
VariantAnalysisStatus.Canceling,
);
mockApiResponse = createMockApiResponse("in_progress");
mockGetVariantAnalysis.mockResolvedValue(mockApiResponse);

await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis);

expect(onVariantAnalysisChangeSpy).toHaveBeenCalledWith(
expect.objectContaining({
status: VariantAnalysisStatus.Canceling,
}),
);
});
});
});

function limitNumberOfAttemptsToMonitor() {
Expand Down