Skip to content

Commit 487cc7b

Browse files
committed
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
1 parent d9e9c1b commit 487cc7b

3 files changed

Lines changed: 262 additions & 0 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { VariantAnalysis } from './variant-analysis';
2+
3+
export type VariantAnalysisMonitorStatus =
4+
| 'InProgress'
5+
| 'CompletedSuccessfully'
6+
| 'CompletedUnsuccessfully'
7+
| 'Failed'
8+
| 'Cancelled'
9+
| 'TimedOut';
10+
11+
export interface VariantAnalysisMonitorResult {
12+
status: VariantAnalysisMonitorStatus;
13+
error?: string;
14+
scannedReposDownloaded?: number[],
15+
variantAnalysis?: VariantAnalysis
16+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import * as vscode from 'vscode';
2+
import { Credentials } from '../authentication';
3+
import { Logger } from '../logging';
4+
import * as ghApiClient from './gh-api/gh-api-client';
5+
6+
import { VariantAnalysis, VariantAnalysisStatus } from './shared/variant-analysis';
7+
import {
8+
VariantAnalysis as VariantAnalysisApiResponse
9+
} from './gh-api/variant-analysis';
10+
import { VariantAnalysisMonitorResult } from './shared/variant-analysis-monitor-result';
11+
import { processFailureReason } from './variant-analysis-processor';
12+
13+
export class VariantAnalysisMonitor {
14+
// With a sleep of 5 seconds, the maximum number of attempts takes
15+
// us to just over 2 days worth of monitoring.
16+
public static maxAttemptCount = 17280;
17+
public static sleepTime = 5000;
18+
19+
constructor(
20+
private readonly extensionContext: vscode.ExtensionContext,
21+
private readonly logger: Logger
22+
) {
23+
}
24+
25+
public async monitorVariantAnalysis(
26+
variantAnalysis: VariantAnalysis,
27+
cancellationToken: vscode.CancellationToken
28+
): Promise<VariantAnalysisMonitorResult> {
29+
30+
const credentials = await Credentials.initialize(this.extensionContext);
31+
if (!credentials) {
32+
throw Error('Error authenticating with GitHub');
33+
}
34+
35+
let variantAnalysisSummary: VariantAnalysisApiResponse;
36+
let attemptCount = 0;
37+
const scannedReposDownloaded: number[] = [];
38+
39+
while (attemptCount <= VariantAnalysisMonitor.maxAttemptCount) {
40+
await this.sleep(VariantAnalysisMonitor.sleepTime);
41+
42+
if (cancellationToken && cancellationToken.isCancellationRequested) {
43+
return { status: 'Cancelled', error: 'Variant Analysis was canceled.' };
44+
}
45+
46+
variantAnalysisSummary = await ghApiClient.getVariantAnalysis(
47+
credentials,
48+
variantAnalysis.controllerRepoId,
49+
variantAnalysis.id
50+
);
51+
52+
if (variantAnalysisSummary.status == 'in_progress' && variantAnalysisSummary.failure_reason) {
53+
variantAnalysis.status = VariantAnalysisStatus.Failed;
54+
variantAnalysis.failureReason = processFailureReason(variantAnalysisSummary.failure_reason);
55+
return {
56+
status: 'Failed',
57+
error: `Variant Analysis has failed: ${variantAnalysisSummary.failure_reason}`,
58+
variantAnalysis: variantAnalysis
59+
};
60+
}
61+
62+
void this.logger.log('****** Retrieved variant analysis' + JSON.stringify(variantAnalysisSummary));
63+
64+
if (variantAnalysisSummary.scanned_repositories) {
65+
variantAnalysisSummary.scanned_repositories.forEach(scannedRepo => {
66+
if (!scannedReposDownloaded.includes(scannedRepo.repository.id) && scannedRepo.analysis_status === 'succeeded') {
67+
scannedReposDownloaded.push(scannedRepo.repository.id);
68+
}
69+
});
70+
}
71+
72+
attemptCount++;
73+
}
74+
75+
return { status: 'CompletedSuccessfully', scannedReposDownloaded: scannedReposDownloaded };
76+
}
77+
78+
private async sleep(ms: number) {
79+
return new Promise(resolve => setTimeout(resolve, ms));
80+
}
81+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import * as sinon from 'sinon';
2+
import { expect } from 'chai';
3+
import { CancellationToken, extensions } from 'vscode';
4+
import { CodeQLExtensionInterface } from '../../../extension';
5+
import { logger } from '../../../logging';
6+
import * as config from '../../../config';
7+
8+
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
9+
import { VariantAnalysisMonitor } from '../../../remote-queries/variant-analysis-monitor';
10+
import {
11+
VariantAnalysis as VariantAnalysisApiResponse,
12+
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository,
13+
VariantAnalysisFailureReason
14+
} from '../../../remote-queries/gh-api/variant-analysis';
15+
import { createFailedMockApiResponse, createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
16+
import { VariantAnalysisStatus } from '../../../remote-queries/shared/variant-analysis';
17+
import { createMockScannedRepos } from '../../factories/remote-queries/gh-api/scanned-repositories';
18+
import { processFailureReason } from '../../../remote-queries/variant-analysis-processor';
19+
import { Credentials } from '../../../authentication';
20+
21+
describe('Variant Analysis Monitor', async function() {
22+
let sandbox: sinon.SinonSandbox;
23+
let mockGetVariantAnalysis: sinon.SinonStub;
24+
let cancellationToken: CancellationToken;
25+
let variantAnalysisMonitor: VariantAnalysisMonitor;
26+
let variantAnalysis: any;
27+
28+
beforeEach(async () => {
29+
sandbox = sinon.createSandbox();
30+
sandbox.stub(logger, 'log');
31+
sandbox.stub(config, 'isVariantAnalysisLiveResultsEnabled').returns(false);
32+
33+
cancellationToken = {
34+
isCancellationRequested: false
35+
} as unknown as CancellationToken;
36+
37+
variantAnalysis = {
38+
id: 123,
39+
controllerRepoId: 1,
40+
};
41+
42+
try {
43+
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
44+
variantAnalysisMonitor = new VariantAnalysisMonitor(extension.ctx, logger);
45+
} catch (e) {
46+
fail(e as Error);
47+
}
48+
49+
limitNumberOfAttemptsToMonitor();
50+
});
51+
52+
afterEach(async () => {
53+
sandbox.restore();
54+
});
55+
56+
describe('when credentials are invalid', async () => {
57+
beforeEach(async () => { sandbox.stub(Credentials, 'initialize').resolves(undefined); });
58+
59+
it('should return early if credentials are wrong', async () => {
60+
try {
61+
await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
62+
} catch (error: any) {
63+
expect(error.message).to.equal('Error authenticating with GitHub');
64+
}
65+
});
66+
});
67+
68+
describe('when credentials are valid', async () => {
69+
beforeEach(async () => {
70+
const mockCredentials = {
71+
getOctokit: () => Promise.resolve({
72+
request: mockGetVariantAnalysis
73+
})
74+
} as unknown as Credentials;
75+
sandbox.stub(Credentials, 'initialize').resolves(mockCredentials);
76+
});
77+
78+
it('should return early if variant analysis is cancelled', async () => {
79+
cancellationToken.isCancellationRequested = true;
80+
81+
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
82+
83+
expect(result).to.eql({ status: 'Cancelled', error: 'Variant Analysis was canceled.' });
84+
});
85+
86+
describe('when the variant analysis fails', async () => {
87+
let mockFailedApiResponse: VariantAnalysisApiResponse;
88+
89+
beforeEach(async function() {
90+
mockFailedApiResponse = createFailedMockApiResponse('in_progress');
91+
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockFailedApiResponse);
92+
});
93+
94+
it('should mark as failed locally and stop monitoring', async () => {
95+
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
96+
variantAnalysis = result.variantAnalysis;
97+
98+
expect(mockGetVariantAnalysis.calledOnce).to.be.true;
99+
expect(result.status).to.eql('Failed');
100+
expect(result.error).to.eql(`Variant Analysis has failed: ${mockFailedApiResponse.failure_reason}`);
101+
expect(variantAnalysis.status).to.equal(VariantAnalysisStatus.Failed);
102+
expect(variantAnalysis.failureReason).to.equal(processFailureReason(mockFailedApiResponse.failure_reason as VariantAnalysisFailureReason));
103+
});
104+
});
105+
106+
describe('when the variant analysis completes', async () => {
107+
let mockApiResponse: VariantAnalysisApiResponse;
108+
let scannedRepos: ApiVariantAnalysisScannedRepository[];
109+
110+
describe('when there are successfully scanned repos', async () => {
111+
beforeEach(async function() {
112+
scannedRepos = createMockScannedRepos(['pending', 'in_progress', 'succeeded']);
113+
mockApiResponse = createMockApiResponse('completed', scannedRepos);
114+
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockApiResponse);
115+
});
116+
117+
it('should succeed and return a list of scanned repo ids', async () => {
118+
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
119+
const scannedRepoIds = scannedRepos.filter(r => r.analysis_status == 'succeeded').map(r => r.repository.id);
120+
121+
expect(result.status).to.equal('CompletedSuccessfully');
122+
expect(result.scannedReposDownloaded).to.eql(scannedRepoIds);
123+
});
124+
});
125+
126+
describe('when there are only in progress repos', async () => {
127+
let scannedRepos: ApiVariantAnalysisScannedRepository[];
128+
129+
beforeEach(async function() {
130+
scannedRepos = createMockScannedRepos(['pending', 'in_progress']);
131+
mockApiResponse = createMockApiResponse('in_progress', scannedRepos);
132+
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockApiResponse);
133+
});
134+
135+
it('should succeed and return an empty list of scanned repo ids', async () => {
136+
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
137+
138+
expect(result.status).to.equal('CompletedSuccessfully');
139+
expect(result.scannedReposDownloaded).to.eql([]);
140+
});
141+
});
142+
143+
describe('when there are no repos to scan', async () => {
144+
beforeEach(async function() {
145+
scannedRepos = [];
146+
mockApiResponse = createMockApiResponse('completed', scannedRepos);
147+
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockApiResponse);
148+
});
149+
150+
it('should succeed and return an empty list of scanned repo ids', async () => {
151+
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
152+
153+
expect(result.status).to.equal('CompletedSuccessfully');
154+
expect(result.scannedReposDownloaded).to.eql([]);
155+
});
156+
});
157+
});
158+
});
159+
160+
function limitNumberOfAttemptsToMonitor() {
161+
VariantAnalysisMonitor.maxAttemptCount = 3;
162+
VariantAnalysisMonitor.sleepTime = 1;
163+
}
164+
});
165+

0 commit comments

Comments
 (0)