Introduce a VariantAnalysisMonitor class
This will poll the API every 5 seconds for changes to the variant analysis. By default it will continue to run for a maximum of 2 days, or when the user closes VSCode. The monitor will receive a variantAnalysis summary from the API that will contain an up-to-date list of scanned repos. The monitor will then return a list of scanned repo ids. In a future PR we'll add the functionality to: - update the UI for in progress/completed states - raise error on timeout - download the results
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { VariantAnalysis } from './variant-analysis';
|
||||
|
||||
export type VariantAnalysisMonitorStatus =
|
||||
| 'InProgress'
|
||||
| 'CompletedSuccessfully'
|
||||
| 'CompletedUnsuccessfully'
|
||||
| 'Failed'
|
||||
| 'Cancelled'
|
||||
| 'TimedOut';
|
||||
|
||||
export interface VariantAnalysisMonitorResult {
|
||||
status: VariantAnalysisMonitorStatus;
|
||||
error?: string;
|
||||
scannedReposDownloaded?: number[],
|
||||
variantAnalysis?: VariantAnalysis
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Credentials } from '../authentication';
|
||||
import { Logger } from '../logging';
|
||||
import * as ghApiClient from './gh-api/gh-api-client';
|
||||
|
||||
import { VariantAnalysis, VariantAnalysisStatus } from './shared/variant-analysis';
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisApiResponse
|
||||
} from './gh-api/variant-analysis';
|
||||
import { VariantAnalysisMonitorResult } from './shared/variant-analysis-monitor-result';
|
||||
import { processFailureReason } from './variant-analysis-processor';
|
||||
|
||||
export class VariantAnalysisMonitor {
|
||||
// With a sleep of 5 seconds, the maximum number of attempts takes
|
||||
// us to just over 2 days worth of monitoring.
|
||||
public static maxAttemptCount = 17280;
|
||||
public static sleepTime = 5000;
|
||||
|
||||
constructor(
|
||||
private readonly extensionContext: vscode.ExtensionContext,
|
||||
private readonly logger: Logger
|
||||
) {
|
||||
}
|
||||
|
||||
public async monitorVariantAnalysis(
|
||||
variantAnalysis: VariantAnalysis,
|
||||
cancellationToken: vscode.CancellationToken
|
||||
): Promise<VariantAnalysisMonitorResult> {
|
||||
|
||||
const credentials = await Credentials.initialize(this.extensionContext);
|
||||
if (!credentials) {
|
||||
throw Error('Error authenticating with GitHub');
|
||||
}
|
||||
|
||||
let variantAnalysisSummary: VariantAnalysisApiResponse;
|
||||
let attemptCount = 0;
|
||||
const scannedReposDownloaded: number[] = [];
|
||||
|
||||
while (attemptCount <= VariantAnalysisMonitor.maxAttemptCount) {
|
||||
await this.sleep(VariantAnalysisMonitor.sleepTime);
|
||||
|
||||
if (cancellationToken && cancellationToken.isCancellationRequested) {
|
||||
return { status: 'Cancelled', error: 'Variant Analysis was canceled.' };
|
||||
}
|
||||
|
||||
variantAnalysisSummary = await ghApiClient.getVariantAnalysis(
|
||||
credentials,
|
||||
variantAnalysis.controllerRepoId,
|
||||
variantAnalysis.id
|
||||
);
|
||||
|
||||
if (variantAnalysisSummary.status == 'in_progress' && variantAnalysisSummary.failure_reason) {
|
||||
variantAnalysis.status = VariantAnalysisStatus.Failed;
|
||||
variantAnalysis.failureReason = processFailureReason(variantAnalysisSummary.failure_reason);
|
||||
return {
|
||||
status: 'Failed',
|
||||
error: `Variant Analysis has failed: ${variantAnalysisSummary.failure_reason}`,
|
||||
variantAnalysis: variantAnalysis
|
||||
};
|
||||
}
|
||||
|
||||
void this.logger.log('****** Retrieved variant analysis' + JSON.stringify(variantAnalysisSummary));
|
||||
|
||||
if (variantAnalysisSummary.scanned_repositories) {
|
||||
variantAnalysisSummary.scanned_repositories.forEach(scannedRepo => {
|
||||
if (!scannedReposDownloaded.includes(scannedRepo.repository.id) && scannedRepo.analysis_status === 'succeeded') {
|
||||
scannedReposDownloaded.push(scannedRepo.repository.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
attemptCount++;
|
||||
}
|
||||
|
||||
return { status: 'CompletedSuccessfully', scannedReposDownloaded: scannedReposDownloaded };
|
||||
}
|
||||
|
||||
private async sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import * as sinon from 'sinon';
|
||||
import { expect } from 'chai';
|
||||
import { CancellationToken, extensions } from 'vscode';
|
||||
import { CodeQLExtensionInterface } from '../../../extension';
|
||||
import { logger } from '../../../logging';
|
||||
import * as config from '../../../config';
|
||||
|
||||
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
|
||||
import { VariantAnalysisMonitor } from '../../../remote-queries/variant-analysis-monitor';
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisApiResponse,
|
||||
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository,
|
||||
VariantAnalysisFailureReason
|
||||
} from '../../../remote-queries/gh-api/variant-analysis';
|
||||
import { createFailedMockApiResponse, createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
|
||||
import { VariantAnalysisStatus } from '../../../remote-queries/shared/variant-analysis';
|
||||
import { createMockScannedRepos } from '../../factories/remote-queries/gh-api/scanned-repositories';
|
||||
import { processFailureReason } from '../../../remote-queries/variant-analysis-processor';
|
||||
import { Credentials } from '../../../authentication';
|
||||
|
||||
describe('Variant Analysis Monitor', async function() {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let mockGetVariantAnalysis: sinon.SinonStub;
|
||||
let cancellationToken: CancellationToken;
|
||||
let variantAnalysisMonitor: VariantAnalysisMonitor;
|
||||
let variantAnalysis: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox();
|
||||
sandbox.stub(logger, 'log');
|
||||
sandbox.stub(config, 'isVariantAnalysisLiveResultsEnabled').returns(false);
|
||||
|
||||
cancellationToken = {
|
||||
isCancellationRequested: false
|
||||
} as unknown as CancellationToken;
|
||||
|
||||
variantAnalysis = {
|
||||
id: 123,
|
||||
controllerRepoId: 1,
|
||||
};
|
||||
|
||||
try {
|
||||
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
|
||||
variantAnalysisMonitor = new VariantAnalysisMonitor(extension.ctx, logger);
|
||||
} catch (e) {
|
||||
fail(e as Error);
|
||||
}
|
||||
|
||||
limitNumberOfAttemptsToMonitor();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('when credentials are invalid', async () => {
|
||||
beforeEach(async () => { sandbox.stub(Credentials, 'initialize').resolves(undefined); });
|
||||
|
||||
it('should return early if credentials are wrong', async () => {
|
||||
try {
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
|
||||
} catch (error: any) {
|
||||
expect(error.message).to.equal('Error authenticating with GitHub');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('when credentials are valid', async () => {
|
||||
beforeEach(async () => {
|
||||
const mockCredentials = {
|
||||
getOctokit: () => Promise.resolve({
|
||||
request: mockGetVariantAnalysis
|
||||
})
|
||||
} as unknown as Credentials;
|
||||
sandbox.stub(Credentials, 'initialize').resolves(mockCredentials);
|
||||
});
|
||||
|
||||
it('should return early if variant analysis is cancelled', async () => {
|
||||
cancellationToken.isCancellationRequested = true;
|
||||
|
||||
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
|
||||
|
||||
expect(result).to.eql({ status: 'Cancelled', error: 'Variant Analysis was canceled.' });
|
||||
});
|
||||
|
||||
describe('when the variant analysis fails', async () => {
|
||||
let mockFailedApiResponse: VariantAnalysisApiResponse;
|
||||
|
||||
beforeEach(async function() {
|
||||
mockFailedApiResponse = createFailedMockApiResponse('in_progress');
|
||||
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockFailedApiResponse);
|
||||
});
|
||||
|
||||
it('should mark as failed locally and stop monitoring', async () => {
|
||||
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
|
||||
variantAnalysis = result.variantAnalysis;
|
||||
|
||||
expect(mockGetVariantAnalysis.calledOnce).to.be.true;
|
||||
expect(result.status).to.eql('Failed');
|
||||
expect(result.error).to.eql(`Variant Analysis has failed: ${mockFailedApiResponse.failure_reason}`);
|
||||
expect(variantAnalysis.status).to.equal(VariantAnalysisStatus.Failed);
|
||||
expect(variantAnalysis.failureReason).to.equal(processFailureReason(mockFailedApiResponse.failure_reason as VariantAnalysisFailureReason));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the variant analysis completes', async () => {
|
||||
let mockApiResponse: VariantAnalysisApiResponse;
|
||||
let scannedRepos: ApiVariantAnalysisScannedRepository[];
|
||||
|
||||
describe('when there are successfully scanned repos', async () => {
|
||||
beforeEach(async function() {
|
||||
scannedRepos = createMockScannedRepos(['pending', 'in_progress', 'succeeded']);
|
||||
mockApiResponse = createMockApiResponse('completed', scannedRepos);
|
||||
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockApiResponse);
|
||||
});
|
||||
|
||||
it('should succeed and return a list of scanned repo ids', async () => {
|
||||
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
|
||||
const scannedRepoIds = scannedRepos.filter(r => r.analysis_status == 'succeeded').map(r => r.repository.id);
|
||||
|
||||
expect(result.status).to.equal('CompletedSuccessfully');
|
||||
expect(result.scannedReposDownloaded).to.eql(scannedRepoIds);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are only in progress repos', async () => {
|
||||
let scannedRepos: ApiVariantAnalysisScannedRepository[];
|
||||
|
||||
beforeEach(async function() {
|
||||
scannedRepos = createMockScannedRepos(['pending', 'in_progress']);
|
||||
mockApiResponse = createMockApiResponse('in_progress', scannedRepos);
|
||||
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockApiResponse);
|
||||
});
|
||||
|
||||
it('should succeed and return an empty list of scanned repo ids', async () => {
|
||||
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
|
||||
|
||||
expect(result.status).to.equal('CompletedSuccessfully');
|
||||
expect(result.scannedReposDownloaded).to.eql([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no repos to scan', async () => {
|
||||
beforeEach(async function() {
|
||||
scannedRepos = [];
|
||||
mockApiResponse = createMockApiResponse('completed', scannedRepos);
|
||||
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockApiResponse);
|
||||
});
|
||||
|
||||
it('should succeed and return an empty list of scanned repo ids', async () => {
|
||||
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
|
||||
|
||||
expect(result.status).to.equal('CompletedSuccessfully');
|
||||
expect(result.scannedReposDownloaded).to.eql([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function limitNumberOfAttemptsToMonitor() {
|
||||
VariantAnalysisMonitor.maxAttemptCount = 3;
|
||||
VariantAnalysisMonitor.sleepTime = 1;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user