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 @@ -121,6 +125,7 @@ export class VariantAnalysisMonitor extends DisposableObject {

variantAnalysis = mapUpdatedVariantAnalysis(
variantAnalysis,
this.getVariantAnalysisStatus(variantAnalysis.id),
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.

Could you explain why we call getVariantAnalysisStatus(variantAnalysis.id) to fetch the status from the variant analysis manager instead of using the variantAnalysis.status value we already have?

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.

Because the variantAnalysis.status we already have is one we got from the API so it won't necessarily have the "true" status (which is "canceling"). I should probably at least add a code comment to explain this.

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.

Hmm, I agree this is necessary for now, but I think it's not ideal. I'd like to look at changing it in a followup tech-debt issue.

In the code here, variantAnalysisSummary comes from the API, but the variantAnalysis variable we have here is of the internal type and has gone through mapping / processing. The problem is that it's a variable we keep within this _monitorVariantAnalysis function so it doesn't see any changes that come from outside the monitor. Previously this was fine because the monitor was the only thing that changed the variant analysis state and then it just fed data back to the manager. But now changes can come from other places and the monitor doesn't see these changes and tries to overwrite them.

Adding in getVariantAnalysisStatus works right now, but only because the status field is the only bit that's changing. It could cause problems if any other fields change in other places.

We could make it more general and fetch the whole variant analysis from the manager. We could also rewrite the monitor so it can track any changes by listening to the onVariantAnalysisStatusUpdated event from the manager.

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 agree it's an area that could do with improvement. IMO it goes hand in hand with #3405 (comment).

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.

I've added a comment. I would like to make some incremental improvements around this and the mapping functions in separate PRs, if that's okay.

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