diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 68dbd17e0..b2ce7a2ad 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -953,6 +953,14 @@ async function activateWithInstalledDistribution( }) ); + ctx.subscriptions.push( + commandRunner('codeQL.cancelVariantAnalysis', async ( + variantAnalysisId: number, + ) => { + await variantAnalysisManager.cancelVariantAnalysis(variantAnalysisId); + }) + ); + ctx.subscriptions.push( commandRunner('codeQL.openVariantAnalysis', async () => { await variantAnalysisManager.promptOpenVariantAnalysis(); diff --git a/extensions/ql-vscode/src/pure/interface-types.ts b/extensions/ql-vscode/src/pure/interface-types.ts index 2f03997b5..3e0b36223 100644 --- a/extensions/ql-vscode/src/pure/interface-types.ts +++ b/extensions/ql-vscode/src/pure/interface-types.ts @@ -445,11 +445,6 @@ export interface SetVariantAnalysisMessage { variantAnalysis: VariantAnalysis; } -export type StopVariantAnalysisMessage = { - t: 'stopVariantAnalysis'; - variantAnalysisId: number; -} - export type VariantAnalysisState = { variantAnalysisId: number; } @@ -481,6 +476,10 @@ export interface OpenLogsMessage { t: 'openLogs'; } +export interface CancelVariantAnalysisMessage { + t: 'cancelVariantAnalysis'; +} + export type ToVariantAnalysisMessage = | SetVariantAnalysisMessage | SetRepoResultsMessage @@ -488,8 +487,8 @@ export type ToVariantAnalysisMessage = export type FromVariantAnalysisMessage = | ViewLoadedMsg - | StopVariantAnalysisMessage | RequestRepositoryResultsMessage | OpenQueryFileMessage | OpenQueryTextMessage - | OpenLogsMessage; + | OpenLogsMessage + | CancelVariantAnalysisMessage; diff --git a/extensions/ql-vscode/src/query-history.ts b/extensions/ql-vscode/src/query-history.ts index b21bfa383..09dc82861 100644 --- a/extensions/ql-vscode/src/query-history.ts +++ b/extensions/ql-vscode/src/query-history.ts @@ -40,7 +40,7 @@ import * as fs from 'fs-extra'; import { CliVersionConstraint } from './cli'; import { HistoryItemLabelProvider } from './history-item-label-provider'; import { Credentials } from './authentication'; -import { cancelRemoteQuery, cancelVariantAnalysis } from './remote-queries/gh-api/gh-actions-api-client'; +import { cancelRemoteQuery } from './remote-queries/gh-api/gh-actions-api-client'; import { RemoteQueriesManager } from './remote-queries/remote-queries-manager'; import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item'; import { ResultsView } from './interface'; @@ -1119,9 +1119,7 @@ export class QueryHistoryManager extends DisposableObject { const credentials = await this.getCredentials(); await cancelRemoteQuery(credentials, item.remoteQuery); } else if (item.t === 'variant-analysis') { - void showAndLogInformationMessage('Cancelling variant analysis. This may take a while.'); - const credentials = await this.getCredentials(); - await cancelVariantAnalysis(credentials, item.variantAnalysis); + await commands.executeCommand('codeQL.cancelVariantAnalysis', item.variantAnalysis.id); } } }); diff --git a/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts b/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts index 38113ddc9..d8c952c3b 100644 --- a/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts +++ b/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts @@ -22,8 +22,9 @@ import { VariantAnalysisResultsManager } from './variant-analysis-results-manage import { getControllerRepo } from './run-remote-query'; import { processUpdatedVariantAnalysis, processVariantAnalysisRepositoryTask } from './variant-analysis-processor'; import PQueue from 'p-queue'; -import { createTimestampFile, showAndLogErrorMessage } from '../helpers'; +import { createTimestampFile, showAndLogErrorMessage, showAndLogInformationMessage } from '../helpers'; import * as fs from 'fs-extra'; +import { cancelVariantAnalysis } from './gh-api/gh-actions-api-client'; export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager { private static readonly REPO_STATES_FILENAME = 'repo_states.json'; @@ -281,6 +282,25 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA ); } + public async cancelVariantAnalysis(variantAnalysisId: number) { + const variantAnalysis = this.variantAnalyses.get(variantAnalysisId); + if (!variantAnalysis) { + throw new Error(`No variant analysis with id: ${variantAnalysisId}`); + } + + if (!variantAnalysis.actionsWorkflowRunId) { + throw new Error(`No workflow run id for variant analysis ${variantAnalysis.query.name}`); + } + + const credentials = await Credentials.initialize(this.ctx); + if (!credentials) { + throw Error('Error authenticating with GitHub'); + } + + void showAndLogInformationMessage('Cancelling variant analysis. This may take a while.'); + await cancelVariantAnalysis(credentials, variantAnalysis); + } + private getRepoStatesStoragePath(variantAnalysisId: number): string { return path.join( this.getVariantAnalysisStorageLocation(variantAnalysisId), diff --git a/extensions/ql-vscode/src/remote-queries/variant-analysis-view.ts b/extensions/ql-vscode/src/remote-queries/variant-analysis-view.ts index b18e3c31a..9a0e63364 100644 --- a/extensions/ql-vscode/src/remote-queries/variant-analysis-view.ts +++ b/extensions/ql-vscode/src/remote-queries/variant-analysis-view.ts @@ -91,8 +91,8 @@ export class VariantAnalysisView extends AbstractWebview { }); }; +const stopQuery = () => { + vscode.postMessage({ + t: 'cancelVariantAnalysis', + }); +}; + const openLogs = () => { vscode.postMessage({ t: 'openLogs', @@ -88,7 +94,7 @@ export function VariantAnalysis({ variantAnalysis={variantAnalysis} onOpenQueryFileClick={openQueryFile} onViewQueryTextClick={openQueryText} - onStopQueryClick={() => console.log('Stop query')} + onStopQueryClick={stopQuery} onCopyRepositoryListClick={() => console.log('Copy repository list')} onExportResultsClick={() => console.log('Export results')} onViewLogsClick={openLogs} diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisActions.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisActions.tsx index f9bf04a78..54896ddc8 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisActions.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisActions.tsx @@ -7,6 +7,7 @@ type Props = { variantAnalysisStatus: VariantAnalysisStatus; onStopQueryClick: () => void; + stopQueryDisabled?: boolean; onCopyRepositoryListClick: () => void; onExportResultsClick: () => void; @@ -26,12 +27,13 @@ export const VariantAnalysisActions = ({ variantAnalysisStatus, onStopQueryClick, onCopyRepositoryListClick, - onExportResultsClick + onExportResultsClick, + stopQueryDisabled, }: Props) => { return ( {variantAnalysisStatus === VariantAnalysisStatus.InProgress && ( - )} diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisHeader.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisHeader.tsx index 62ad6ac4a..836a7fb2a 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisHeader.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisHeader.tsx @@ -73,6 +73,7 @@ export const VariantAnalysisHeader = ({ onStopQueryClick={onStopQueryClick} onCopyRepositoryListClick={onCopyRepositoryListClick} onExportResultsClick={onExportResultsClick} + stopQueryDisabled={!variantAnalysis.actionsWorkflowRunId} /> { + let variantAnalysis: VariantAnalysis; + let mockCancelVariantAnalysis: sinon.SinonStub; + let getOctokitStub: sinon.SinonStub; + + let variantAnalysisStorageLocation: string; + + beforeEach(async () => { + variantAnalysis = createMockVariantAnalysis({}); + + mockCancelVariantAnalysis = sandbox.stub(ghActionsApiClient, 'cancelVariantAnalysis'); + + variantAnalysisStorageLocation = variantAnalysisManager.getVariantAnalysisStorageLocation(variantAnalysis.id); + await createTimestampFile(variantAnalysisStorageLocation); + await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis); + }); + + afterEach(() => { + fs.rmSync(variantAnalysisStorageLocation, { recursive: true }); + }); + + describe('when the credentials are invalid', () => { + beforeEach(async () => { + sandbox.stub(Credentials, 'initialize').resolves(undefined); + }); + + it('should return early', async () => { + try { + await variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id); + } catch (error: any) { + expect(error.message).to.equal('Error authenticating with GitHub'); + } + }); + }); + + describe('when the credentials are valid', () => { + let mockCredentials: Credentials; + + beforeEach(async () => { + mockCredentials = { + getOctokit: () => Promise.resolve({ + request: getOctokitStub + }) + } as unknown as Credentials; + sandbox.stub(Credentials, 'initialize').resolves(mockCredentials); + }); + + it('should return early if the variant analysis is not found', async () => { + try { + await variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id + 100); + } catch (error: any) { + expect(error.message).to.equal('No variant analysis with id: ' + (variantAnalysis.id + 100)); + } + }); + + it('should return early if the variant analysis does not have an actions workflow run id', async () => { + await variantAnalysisManager.onVariantAnalysisUpdated({ + ...variantAnalysis, + actionsWorkflowRunId: undefined, + }); + + try { + await variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id); + } catch (error: any) { + expect(error.message).to.equal('No workflow run id for variant analysis a-query-name'); + } + }); + + it('should return cancel if valid', async () => { + await variantAnalysisManager.cancelVariantAnalysis(variantAnalysis.id); + + expect(mockCancelVariantAnalysis).to.have.been.calledWith(mockCredentials, variantAnalysis); + }); + }); + }); }); diff --git a/extensions/ql-vscode/src/vscode-tests/no-workspace/query-history.test.ts b/extensions/ql-vscode/src/vscode-tests/no-workspace/query-history.test.ts index f8b37d16a..b43ca943d 100644 --- a/extensions/ql-vscode/src/vscode-tests/no-workspace/query-history.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/no-workspace/query-history.test.ts @@ -338,7 +338,6 @@ describe('query-history', () => { describe('handleCancel', () => { let mockCredentials: Credentials; let mockCancelRemoteQuery: sinon.SinonStub; - let mockCancelVariantAnalysis: sinon.SinonStub; let getOctokitStub: sinon.SinonStub; beforeEach(async () => { @@ -349,7 +348,6 @@ describe('query-history', () => { } as unknown as Credentials; sandbox.stub(Credentials, 'initialize').resolves(mockCredentials); mockCancelRemoteQuery = sandbox.stub(ghActionsApiClient, 'cancelRemoteQuery'); - mockCancelVariantAnalysis = sandbox.stub(ghActionsApiClient, 'cancelVariantAnalysis'); }); describe('if the item is in progress', async () => { @@ -408,7 +406,7 @@ describe('query-history', () => { const inProgress1 = variantAnalysisHistory[1]; await queryHistoryManager.handleCancel(inProgress1, [inProgress1]); - expect(mockCancelVariantAnalysis).to.have.been.calledWith(mockCredentials, inProgress1.variantAnalysis); + expect(executeCommandSpy).to.have.been.calledWith('codeQL.cancelVariantAnalysis', inProgress1.variantAnalysis.id); }); it('should cancel multiple variant analyses', async () => { @@ -419,8 +417,8 @@ describe('query-history', () => { const inProgress2 = variantAnalysisHistory[3]; await queryHistoryManager.handleCancel(inProgress1, [inProgress1, inProgress2]); - expect(mockCancelVariantAnalysis).to.have.been.calledWith(mockCredentials, inProgress1.variantAnalysis); - expect(mockCancelVariantAnalysis).to.have.been.calledWith(mockCredentials, inProgress2.variantAnalysis); + expect(executeCommandSpy).to.have.been.calledWith('codeQL.cancelVariantAnalysis', inProgress1.variantAnalysis.id); + expect(executeCommandSpy).to.have.been.calledWith('codeQL.cancelVariantAnalysis', inProgress2.variantAnalysis.id); }); }); @@ -480,7 +478,7 @@ describe('query-history', () => { const completedVariantAnalysis = variantAnalysisHistory[0]; await queryHistoryManager.handleCancel(completedVariantAnalysis, [completedVariantAnalysis]); - expect(mockCancelVariantAnalysis).to.not.have.been.calledWith(mockCredentials, completedVariantAnalysis.variantAnalysis); + expect(executeCommandSpy).to.not.have.been.calledWith('codeQL.cancelVariantAnalysis', completedVariantAnalysis.variantAnalysis); }); it('should not cancel multiple variant analyses', async () => { @@ -491,8 +489,8 @@ describe('query-history', () => { const failedVariantAnalysis = variantAnalysisHistory[2]; await queryHistoryManager.handleCancel(completedVariantAnalysis, [completedVariantAnalysis, failedVariantAnalysis]); - expect(mockCancelVariantAnalysis).to.not.have.been.calledWith(mockCredentials, completedVariantAnalysis.variantAnalysis); - expect(mockCancelVariantAnalysis).to.not.have.been.calledWith(mockCredentials, failedVariantAnalysis.variantAnalysis); + expect(executeCommandSpy).to.not.have.been.calledWith('codeQL.cancelVariantAnalysis', completedVariantAnalysis.variantAnalysis.id); + expect(executeCommandSpy).to.not.have.been.calledWith('codeQL.cancelVariantAnalysis', failedVariantAnalysis.variantAnalysis.id); }); }); });