Skip to content

Commit ae31a17

Browse files
Merge pull request #1672 from github/robertbrignull/always_trigger_monitoring
When rehydrating, always trigger a monitoring command unless the variant analysis is fully complete
2 parents 9359d5d + 952f033 commit ae31a17

File tree

9 files changed

+251
-43
lines changed

9 files changed

+251
-43
lines changed

extensions/ql-vscode/.mocharc.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
{
22
"exit": true,
3-
"require": [
4-
"test/mocha.setup.js"
5-
]
3+
"require": ["test/mocha.setup.js"]
64
}

extensions/ql-vscode/src/query-history.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,7 @@ export class QueryHistoryManager extends DisposableObject {
694694
await this.remoteQueriesManager.rehydrateRemoteQuery(item.queryId, item.remoteQuery, item.status);
695695
}
696696
if (item.t === 'variant-analysis') {
697-
await this.variantAnalysisManager.rehydrateVariantAnalysis(item.variantAnalysis, item.status);
697+
await this.variantAnalysisManager.rehydrateVariantAnalysis(item.variantAnalysis);
698698
}
699699
}));
700700
}

extensions/ql-vscode/src/remote-queries/shared/variant-analysis.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,26 @@ export interface VariantAnalysisSubmission {
130130
}
131131
}
132132

133+
export async function isVariantAnalysisComplete(
134+
variantAnalysis: VariantAnalysis,
135+
artifactDownloaded: (repo: VariantAnalysisScannedRepository) => Promise<boolean>
136+
): Promise<boolean> {
137+
// It's only acceptable to have no scanned repos if the variant analysis is not in a final state.
138+
// Otherwise it means the analysis hit some kind of internal error or there were no repos to scan.
139+
if (variantAnalysis.scannedRepos === undefined || variantAnalysis.scannedRepos.length === 0) {
140+
return variantAnalysis.status !== VariantAnalysisStatus.InProgress;
141+
}
142+
143+
return (await Promise.all(variantAnalysis.scannedRepos.map(repo => isVariantAnalysisRepoComplete(repo, artifactDownloaded)))).every(x => x);
144+
}
145+
146+
async function isVariantAnalysisRepoComplete(
147+
repo: VariantAnalysisScannedRepository,
148+
artifactDownloaded: (repo: VariantAnalysisScannedRepository) => Promise<boolean>
149+
): Promise<boolean> {
150+
return hasRepoScanCompleted(repo) && (!repoHasDownloadableArtifact(repo) || await artifactDownloaded(repo));
151+
}
152+
133153
/**
134154
* @param status
135155
* @returns whether the status is in a completed state, i.e. it cannot normally change state anymore
@@ -151,6 +171,14 @@ export function hasRepoScanCompleted(repo: VariantAnalysisScannedRepository): bo
151171
return isCompletedAnalysisRepoStatus(repo.analysisStatus);
152172
}
153173

174+
/**
175+
* @param repo
176+
* @returns whether the repo scan has an artifact that can be downloaded
177+
*/
178+
export function repoHasDownloadableArtifact(repo: VariantAnalysisScannedRepository): boolean {
179+
return repo.analysisStatus === VariantAnalysisRepoStatus.Succeeded;
180+
}
181+
154182
/**
155183
* @param repos
156184
* @returns the total number of results. Will be `undefined` when there are no repos with results.

extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import {
1111
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository
1212
} from './gh-api/variant-analysis';
1313
import {
14+
isVariantAnalysisComplete,
1415
VariantAnalysis, VariantAnalysisQueryLanguage,
16+
VariantAnalysisScannedRepository,
1517
VariantAnalysisScannedRepositoryDownloadStatus,
1618
VariantAnalysisScannedRepositoryResult,
1719
VariantAnalysisScannedRepositoryState
@@ -24,7 +26,6 @@ import { getControllerRepo } from './run-remote-query';
2426
import { processUpdatedVariantAnalysis } from './variant-analysis-processor';
2527
import PQueue from 'p-queue';
2628
import { createTimestampFile, showAndLogErrorMessage } from '../helpers';
27-
import { QueryStatus } from '../query-status';
2829
import * as fs from 'fs-extra';
2930

3031
export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager<VariantAnalysisView> {
@@ -55,21 +56,24 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
5556
this.variantAnalysisResultsManager.onResultLoaded(this.onRepoResultLoaded.bind(this));
5657
}
5758

58-
public async rehydrateVariantAnalysis(variantAnalysis: VariantAnalysis, status: QueryStatus) {
59+
public async rehydrateVariantAnalysis(variantAnalysis: VariantAnalysis) {
5960
if (!(await this.variantAnalysisRecordExists(variantAnalysis.id))) {
6061
// In this case, the variant analysis was deleted from disk, most likely because
6162
// it was purged by another workspace.
6263
this._onVariantAnalysisRemoved.fire(variantAnalysis);
6364
} else {
6465
await this.setVariantAnalysis(variantAnalysis);
65-
if (status === QueryStatus.InProgress) {
66-
// In this case, last time we checked, the query was still in progress.
67-
// We need to setup the monitor to check for completion.
66+
if (!await isVariantAnalysisComplete(variantAnalysis, this.makeResultDownloadChecker(variantAnalysis))) {
6867
await commands.executeCommand('codeQL.monitorVariantAnalysis', variantAnalysis);
6968
}
7069
}
7170
}
7271

72+
private makeResultDownloadChecker(variantAnalysis: VariantAnalysis): (repo: VariantAnalysisScannedRepository) => Promise<boolean> {
73+
const storageLocation = this.getVariantAnalysisStorageLocation(variantAnalysis.id);
74+
return (repo) => this.variantAnalysisResultsManager.isVariantAnalysisRepoDownloaded(storageLocation, repo.repository.fullName);
75+
}
76+
7377
public async removeVariantAnalysis(variantAnalysis: VariantAnalysis) {
7478
this.variantAnalysisResultsManager.removeAnalysisResults(variantAnalysis);
7579
await this.removeStorageDirectory(variantAnalysis.id);

extensions/ql-vscode/src/remote-queries/variant-analysis-results-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export class VariantAnalysisResultsManager extends DisposableObject {
146146
throw new Error('Missing results file');
147147
}
148148

149-
private async isVariantAnalysisRepoDownloaded(
149+
public async isVariantAnalysisRepoDownloaded(
150150
variantAnalysisStoragePath: string,
151151
repositoryFullName: string,
152152
): Promise<boolean> {

extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as sinon from 'sinon';
22
import { expect } from 'chai';
3-
import { CancellationTokenSource, extensions } from 'vscode';
3+
import { CancellationTokenSource, commands, extensions } from 'vscode';
44
import { CodeQLExtensionInterface } from '../../../extension';
55
import { logger } from '../../../logging';
66
import * as config from '../../../config';
@@ -21,15 +21,17 @@ import { createMockVariantAnalysisRepoTask } from '../../factories/remote-querie
2121
import { CodeQLCliServer } from '../../../cli';
2222
import { storagePath } from '../global.helper';
2323
import { VariantAnalysisResultsManager } from '../../../remote-queries/variant-analysis-results-manager';
24-
import { VariantAnalysis } from '../../../remote-queries/shared/variant-analysis';
2524
import { createMockVariantAnalysis } from '../../factories/remote-queries/shared/variant-analysis';
25+
import { VariantAnalysis } from '../../../remote-queries/shared/variant-analysis';
26+
import * as VariantAnalysisModule from '../../../remote-queries/shared/variant-analysis';
27+
import { createTimestampFile } from '../../../helpers';
2628

2729
describe('Variant Analysis Manager', async function() {
2830
let sandbox: sinon.SinonSandbox;
2931
let cli: CodeQLCliServer;
3032
let cancellationTokenSource: CancellationTokenSource;
3133
let variantAnalysisManager: VariantAnalysisManager;
32-
let variantAnalysis: VariantAnalysisApiResponse;
34+
let variantAnalysisApiResponse: VariantAnalysisApiResponse;
3335
let scannedRepos: ApiVariantAnalysisScannedRepository[];
3436
let getVariantAnalysisRepoStub: sinon.SinonStub;
3537
let getVariantAnalysisRepoResultStub: sinon.SinonStub;
@@ -45,7 +47,7 @@ describe('Variant Analysis Manager', async function() {
4547
cancellationTokenSource = new CancellationTokenSource();
4648

4749
scannedRepos = createMockScannedRepos();
48-
variantAnalysis = createMockApiResponse('in_progress', scannedRepos);
50+
variantAnalysisApiResponse = createMockApiResponse('in_progress', scannedRepos);
4951

5052
try {
5153
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
@@ -68,7 +70,7 @@ describe('Variant Analysis Manager', async function() {
6870
try {
6971
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
7072
scannedRepos[0],
71-
variantAnalysis,
73+
variantAnalysisApiResponse,
7274
cancellationTokenSource.token
7375
);
7476
} catch (error: any) {
@@ -105,7 +107,7 @@ describe('Variant Analysis Manager', async function() {
105107
it('should not try to download the result', async () => {
106108
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
107109
scannedRepos[0],
108-
variantAnalysis,
110+
variantAnalysisApiResponse,
109111
cancellationTokenSource.token
110112
);
111113

@@ -129,7 +131,7 @@ describe('Variant Analysis Manager', async function() {
129131

130132
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
131133
scannedRepos[0],
132-
variantAnalysis,
134+
variantAnalysisApiResponse,
133135
cancellationTokenSource.token
134136
);
135137

@@ -139,7 +141,7 @@ describe('Variant Analysis Manager', async function() {
139141
it('should fetch a repo task', async () => {
140142
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
141143
scannedRepos[0],
142-
variantAnalysis,
144+
variantAnalysisApiResponse,
143145
cancellationTokenSource.token
144146
);
145147

@@ -149,7 +151,7 @@ describe('Variant Analysis Manager', async function() {
149151
it('should fetch a repo result', async () => {
150152
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
151153
scannedRepos[0],
152-
variantAnalysis,
154+
variantAnalysisApiResponse,
153155
cancellationTokenSource.token
154156
);
155157

@@ -161,9 +163,9 @@ describe('Variant Analysis Manager', async function() {
161163
it('should pop download tasks off the queue', async () => {
162164
const getResultsSpy = sandbox.spy(variantAnalysisManager, 'autoDownloadVariantAnalysisResult');
163165

164-
await variantAnalysisManager.enqueueDownload(scannedRepos[0], variantAnalysis, cancellationTokenSource.token);
165-
await variantAnalysisManager.enqueueDownload(scannedRepos[1], variantAnalysis, cancellationTokenSource.token);
166-
await variantAnalysisManager.enqueueDownload(scannedRepos[2], variantAnalysis, cancellationTokenSource.token);
166+
await variantAnalysisManager.enqueueDownload(scannedRepos[0], variantAnalysisApiResponse, cancellationTokenSource.token);
167+
await variantAnalysisManager.enqueueDownload(scannedRepos[1], variantAnalysisApiResponse, cancellationTokenSource.token);
168+
await variantAnalysisManager.enqueueDownload(scannedRepos[2], variantAnalysisApiResponse, cancellationTokenSource.token);
167169

168170
expect(variantAnalysisManager.downloadsQueueSize()).to.equal(0);
169171
expect(getResultsSpy).to.have.been.calledThrice;
@@ -194,4 +196,77 @@ describe('Variant Analysis Manager', async function() {
194196
});
195197
});
196198
});
199+
200+
describe('when rehydrating a query', async () => {
201+
let variantAnalysis: VariantAnalysis;
202+
let variantAnalysisRemovedSpy: sinon.SinonSpy;
203+
let monitorVariantAnalysisCommandSpy: sinon.SinonSpy;
204+
205+
beforeEach(() => {
206+
variantAnalysis = createMockVariantAnalysis();
207+
208+
variantAnalysisRemovedSpy = sinon.spy();
209+
variantAnalysisManager.onVariantAnalysisRemoved(variantAnalysisRemovedSpy);
210+
211+
monitorVariantAnalysisCommandSpy = sinon.spy();
212+
sandbox.stub(commands, 'executeCommand').callsFake(monitorVariantAnalysisCommandSpy);
213+
});
214+
215+
describe('when variant analysis record doesn\'t exist', async () => {
216+
it('should remove the variant analysis', async () => {
217+
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis);
218+
sinon.assert.calledOnce(variantAnalysisRemovedSpy);
219+
});
220+
221+
it('should not trigger a monitoring command', async () => {
222+
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis);
223+
sinon.assert.notCalled(monitorVariantAnalysisCommandSpy);
224+
});
225+
});
226+
227+
describe('when variant analysis record does exist', async () => {
228+
let variantAnalysisStorageLocation: string;
229+
230+
beforeEach(async () => {
231+
variantAnalysisStorageLocation = variantAnalysisManager.getVariantAnalysisStorageLocation(variantAnalysis.id);
232+
await createTimestampFile(variantAnalysisStorageLocation);
233+
});
234+
235+
afterEach(() => {
236+
fs.rmSync(variantAnalysisStorageLocation, { recursive: true });
237+
});
238+
239+
describe('when the variant analysis is not complete', async () => {
240+
beforeEach(() => {
241+
sandbox.stub(VariantAnalysisModule, 'isVariantAnalysisComplete').resolves(false);
242+
});
243+
244+
it('should not remove the variant analysis', async () => {
245+
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis);
246+
sinon.assert.notCalled(variantAnalysisRemovedSpy);
247+
});
248+
249+
it('should trigger a monitoring command', async () => {
250+
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis);
251+
sinon.assert.calledWith(monitorVariantAnalysisCommandSpy, 'codeQL.monitorVariantAnalysis');
252+
});
253+
});
254+
255+
describe('when the variant analysis is complete', async () => {
256+
beforeEach(() => {
257+
sandbox.stub(VariantAnalysisModule, 'isVariantAnalysisComplete').resolves(true);
258+
});
259+
260+
it('should not remove the variant analysis', async () => {
261+
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis);
262+
sinon.assert.notCalled(variantAnalysisRemovedSpy);
263+
});
264+
265+
it('should not trigger a monitoring command', async () => {
266+
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis);
267+
sinon.assert.notCalled(monitorVariantAnalysisCommandSpy);
268+
});
269+
});
270+
});
271+
});
197272
});

extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-results-manager.test.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { CodeQLCliServer } from '../../../cli';
1313
import { storagePath } from '../global.helper';
1414
import { faker } from '@faker-js/faker';
1515
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
16-
import { VariantAnalysisRepoTask } from '../../../remote-queries/gh-api/variant-analysis';
1716

1817
describe(VariantAnalysisResultsManager.name, function() {
1918
this.timeout(10000);
@@ -47,15 +46,32 @@ describe(VariantAnalysisResultsManager.name, function() {
4746

4847
describe('download', () => {
4948
let getOctokitStub: sinon.SinonStub;
50-
let variantAnalysisStoragePath: string;
5149
const mockCredentials = {
5250
getOctokit: () => Promise.resolve({
5351
request: getOctokitStub
5452
})
5553
} as unknown as Credentials;
54+
let dummyRepoTask = createMockVariantAnalysisRepoTask();
55+
let variantAnalysisStoragePath: string;
56+
let repoTaskStorageDirectory: string;
5657

5758
beforeEach(async () => {
59+
dummyRepoTask = createMockVariantAnalysisRepoTask();
60+
5861
variantAnalysisStoragePath = path.join(storagePath, variantAnalysisId.toString());
62+
repoTaskStorageDirectory = variantAnalysisResultsManager.getRepoStorageDirectory(variantAnalysisStoragePath, dummyRepoTask.repository.full_name);
63+
});
64+
65+
afterEach(async () => {
66+
if (fs.existsSync(variantAnalysisStoragePath)) {
67+
fs.rmSync(variantAnalysisStoragePath, { recursive: true });
68+
}
69+
});
70+
71+
describe('isVariantAnalysisRepoDownloaded', () => {
72+
it('should return false when no results are downloaded', async () => {
73+
expect(await variantAnalysisResultsManager.isVariantAnalysisRepoDownloaded(variantAnalysisStoragePath, dummyRepoTask.repository.full_name)).to.equal(false);
74+
});
5975
});
6076

6177
describe('when the artifact_url is missing', async () => {
@@ -79,14 +95,9 @@ describe(VariantAnalysisResultsManager.name, function() {
7995
});
8096

8197
describe('when the artifact_url is present', async () => {
82-
let dummyRepoTask: VariantAnalysisRepoTask;
83-
let storageDirectory: string;
8498
let arrayBuffer: ArrayBuffer;
8599

86100
beforeEach(async () => {
87-
dummyRepoTask = createMockVariantAnalysisRepoTask();
88-
89-
storageDirectory = variantAnalysisResultsManager.getRepoStorageDirectory(variantAnalysisStoragePath, dummyRepoTask.repository.full_name);
90101
const sourceFilePath = path.join(__dirname, '../../../../src/vscode-tests/cli-integration/data/variant-analysis-results.zip');
91102
arrayBuffer = fs.readFileSync(sourceFilePath).buffer;
92103

@@ -96,11 +107,6 @@ describe(VariantAnalysisResultsManager.name, function() {
96107
.resolves(arrayBuffer);
97108
});
98109

99-
afterEach(async () => {
100-
fs.removeSync(`${storageDirectory}/results.zip`);
101-
fs.removeSync(`${storageDirectory}/results`);
102-
});
103-
104110
it('should call the API to download the results', async () => {
105111
await variantAnalysisResultsManager.download(
106112
mockCredentials,
@@ -120,7 +126,7 @@ describe(VariantAnalysisResultsManager.name, function() {
120126
variantAnalysisStoragePath
121127
);
122128

123-
expect(fs.existsSync(`${storageDirectory}/results.zip`)).to.be.true;
129+
expect(fs.existsSync(`${repoTaskStorageDirectory}/results.zip`)).to.be.true;
124130
});
125131

126132
it('should unzip the results in a `results/` folder', async () => {
@@ -131,7 +137,20 @@ describe(VariantAnalysisResultsManager.name, function() {
131137
variantAnalysisStoragePath
132138
);
133139

134-
expect(fs.existsSync(`${storageDirectory}/results/results.sarif`)).to.be.true;
140+
expect(fs.existsSync(`${repoTaskStorageDirectory}/results/results.sarif`)).to.be.true;
141+
});
142+
143+
describe('isVariantAnalysisRepoDownloaded', () => {
144+
it('should return true once results are downloaded', async () => {
145+
await variantAnalysisResultsManager.download(
146+
mockCredentials,
147+
variantAnalysisId,
148+
dummyRepoTask,
149+
variantAnalysisStoragePath
150+
);
151+
152+
expect(await variantAnalysisResultsManager.isVariantAnalysisRepoDownloaded(variantAnalysisStoragePath, dummyRepoTask.repository.full_name)).to.equal(true);
153+
});
135154
});
136155
});
137156
});

extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/variant-analysis-history.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,7 @@ describe('Variant Analyses and QueryHistoryManager', function() {
121121

122122
expect(rehydrateVariantAnalysisStub).to.have.callCount(2);
123123
expect(rehydrateVariantAnalysisStub.getCall(0).args[0]).to.deep.eq(rawQueryHistory[0].variantAnalysis);
124-
expect(rehydrateVariantAnalysisStub.getCall(0).args[1]).to.deep.eq(rawQueryHistory[0].status);
125124
expect(rehydrateVariantAnalysisStub.getCall(1).args[0]).to.deep.eq(rawQueryHistory[1].variantAnalysis);
126-
expect(rehydrateVariantAnalysisStub.getCall(1).args[1]).to.deep.eq(rawQueryHistory[1].status);
127125

128126
expect(qhm.treeDataProvider.allHistory[0]).to.deep.eq(rawQueryHistory[0]);
129127
expect(qhm.treeDataProvider.allHistory[1]).to.deep.eq(rawQueryHistory[1]);

0 commit comments

Comments
 (0)