Skip to content

Commit 49f97e1

Browse files
committed
Add tests for repo states
1 parent ccf03cb commit 49f97e1

File tree

2 files changed

+253
-4
lines changed

2 files changed

+253
-4
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
7979
if (status === QueryStatus.InProgress) {
8080
// In this case, last time we checked, the query was still in progress.
8181
// We need to setup the monitor to check for completion.
82-
await commands.executeCommand('codeQL.monitorVariantAnalysis', variantAnalysis);
82+
void commands.executeCommand('codeQL.monitorVariantAnalysis', variantAnalysis);
8383
}
8484
}
8585
}
@@ -241,7 +241,13 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
241241
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.InProgress;
242242
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
243243

244-
await this.variantAnalysisResultsManager.download(credentials, variantAnalysisSummary.id, repoTask, this.getVariantAnalysisStorageLocation(variantAnalysisSummary.id));
244+
try {
245+
await this.variantAnalysisResultsManager.download(credentials, variantAnalysisSummary.id, repoTask, this.getVariantAnalysisStorageLocation(variantAnalysisSummary.id));
246+
} catch (e) {
247+
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.Failed;
248+
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
249+
throw new Error(`Could not download the results for variant analysis with id: ${variantAnalysisSummary.id}. Error: ${getErrorMessage(e)}`);
250+
}
245251
}
246252

247253
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.Succeeded;

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

Lines changed: 245 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,18 @@ 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';
24+
import {
25+
VariantAnalysis,
26+
VariantAnalysisScannedRepositoryDownloadStatus
27+
} from '../../../remote-queries/shared/variant-analysis';
2528
import { createMockVariantAnalysis } from '../../factories/remote-queries/shared/variant-analysis';
29+
import { QueryStatus } from '../../../query-status';
2630

27-
describe('Variant Analysis Manager', async function() {
31+
describe.only('Variant Analysis Manager', async function() {
2832
let sandbox: sinon.SinonSandbox;
33+
let pathExistsStub: sinon.SinonStub;
34+
let readJsonStub: sinon.SinonStub;
35+
let outputJsonStub: sinon.SinonStub;
2936
let cli: CodeQLCliServer;
3037
let cancellationTokenSource: CancellationTokenSource;
3138
let variantAnalysisManager: VariantAnalysisManager;
@@ -41,6 +48,9 @@ describe('Variant Analysis Manager', async function() {
4148
sandbox.stub(config, 'isVariantAnalysisLiveResultsEnabled').returns(false);
4249
sandbox.stub(fs, 'mkdirSync');
4350
sandbox.stub(fs, 'writeFile');
51+
pathExistsStub = sandbox.stub(fs, 'pathExists').callThrough();
52+
readJsonStub = sandbox.stub(fs, 'readJson').callThrough();
53+
outputJsonStub = sandbox.stub(fs, 'outputJson');
4454

4555
cancellationTokenSource = new CancellationTokenSource();
4656

@@ -61,6 +71,74 @@ describe('Variant Analysis Manager', async function() {
6171
sandbox.restore();
6272
});
6373

74+
describe('rehydrateVariantAnalysis', () => {
75+
const variantAnalysis = createMockVariantAnalysis();
76+
77+
describe('when the directory does not exist', () => {
78+
beforeEach(() => {
79+
pathExistsStub.withArgs(path.join(storagePath, variantAnalysis.id.toString())).resolves(false);
80+
});
81+
82+
it('should fire the removed event if the file does not exist', async () => {
83+
const stub = sandbox.stub();
84+
variantAnalysisManager.onVariantAnalysisRemoved(stub);
85+
86+
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis, QueryStatus.InProgress);
87+
88+
expect(stub).to.have.been.calledOnce;
89+
sinon.assert.calledWith(pathExistsStub, path.join(storagePath, variantAnalysis.id.toString()));
90+
});
91+
});
92+
93+
describe('when the directory exists', () => {
94+
beforeEach(() => {
95+
pathExistsStub.withArgs(path.join(storagePath, variantAnalysis.id.toString())).resolves(true);
96+
});
97+
98+
it('should store the variant analysis', async () => {
99+
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis, QueryStatus.InProgress);
100+
101+
expect(await variantAnalysisManager.getVariantAnalysis(variantAnalysis.id)).to.deep.equal(variantAnalysis);
102+
});
103+
104+
it('should not error if the repo states file does not exist', async () => {
105+
readJsonStub.withArgs(path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json')).rejects(new Error('File does not exist'));
106+
107+
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis, QueryStatus.InProgress);
108+
109+
sinon.assert.calledWith(readJsonStub, path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json'));
110+
expect(await variantAnalysisManager.getRepoStates(variantAnalysis.id)).to.deep.equal([]);
111+
});
112+
113+
it('should read in the repo states if it exists', async () => {
114+
readJsonStub.withArgs(path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json')).resolves({
115+
[scannedRepos[0].repository.id]: {
116+
repositoryId: scannedRepos[0].repository.id,
117+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
118+
},
119+
[scannedRepos[1].repository.id]: {
120+
repositoryId: scannedRepos[1].repository.id,
121+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
122+
},
123+
});
124+
125+
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis, QueryStatus.InProgress);
126+
127+
sinon.assert.calledWith(readJsonStub, path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json'));
128+
expect(await variantAnalysisManager.getRepoStates(variantAnalysis.id)).to.have.same.deep.members([
129+
{
130+
repositoryId: scannedRepos[0].repository.id,
131+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
132+
},
133+
{
134+
repositoryId: scannedRepos[1].repository.id,
135+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
136+
},
137+
]);
138+
});
139+
});
140+
});
141+
64142
describe('when credentials are invalid', async () => {
65143
beforeEach(async () => { sandbox.stub(Credentials, 'initialize').resolves(undefined); });
66144

@@ -155,6 +233,171 @@ describe('Variant Analysis Manager', async function() {
155233

156234
expect(getVariantAnalysisRepoResultStub.calledOnce).to.be.true;
157235
});
236+
237+
it('should skip the download if the repository has already been downloaded', async () => {
238+
// First, do a download so it is downloaded. This avoids having to mock the repo states.
239+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
240+
scannedRepos[0],
241+
variantAnalysis,
242+
cancellationTokenSource.token
243+
);
244+
245+
getVariantAnalysisRepoStub.resetHistory();
246+
247+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
248+
scannedRepos[0],
249+
variantAnalysis,
250+
cancellationTokenSource.token
251+
);
252+
253+
expect(getVariantAnalysisRepoStub.notCalled).to.be.true;
254+
});
255+
256+
it('should write the repo state when the download is successful', async () => {
257+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
258+
scannedRepos[0],
259+
variantAnalysis,
260+
cancellationTokenSource.token
261+
);
262+
263+
sinon.assert.calledWith(outputJsonStub, path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json'), {
264+
[scannedRepos[0].repository.id]: {
265+
repositoryId: scannedRepos[0].repository.id,
266+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
267+
},
268+
});
269+
});
270+
271+
it('should not write the repo state when the download fails', async () => {
272+
getVariantAnalysisRepoResultStub.rejects(new Error('Failed to download'));
273+
274+
try {
275+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
276+
scannedRepos[0],
277+
variantAnalysis,
278+
cancellationTokenSource.token
279+
);
280+
fail('Expected an error to be thrown');
281+
} catch (e: any) {
282+
// we can ignore this error, we expect this
283+
}
284+
285+
sinon.assert.notCalled(outputJsonStub);
286+
});
287+
288+
it('should have a failed repo state when the repo task API fails', async () => {
289+
getVariantAnalysisRepoStub.onFirstCall().rejects(new Error('Failed to download'));
290+
291+
try {
292+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
293+
scannedRepos[0],
294+
variantAnalysis,
295+
cancellationTokenSource.token
296+
);
297+
fail('Expected an error to be thrown');
298+
} catch (e) {
299+
// we can ignore this error, we expect this
300+
}
301+
302+
sinon.assert.notCalled(outputJsonStub);
303+
304+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
305+
scannedRepos[1],
306+
variantAnalysis,
307+
cancellationTokenSource.token
308+
);
309+
310+
sinon.assert.calledWith(outputJsonStub, path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json'), {
311+
[scannedRepos[0].repository.id]: {
312+
repositoryId: scannedRepos[0].repository.id,
313+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
314+
},
315+
[scannedRepos[1].repository.id]: {
316+
repositoryId: scannedRepos[1].repository.id,
317+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
318+
},
319+
});
320+
});
321+
322+
it('should have a failed repo state when the download fails', async () => {
323+
getVariantAnalysisRepoResultStub.onFirstCall().rejects(new Error('Failed to download'));
324+
325+
try {
326+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
327+
scannedRepos[0],
328+
variantAnalysis,
329+
cancellationTokenSource.token
330+
);
331+
fail('Expected an error to be thrown');
332+
} catch (e) {
333+
// we can ignore this error, we expect this
334+
}
335+
336+
sinon.assert.notCalled(outputJsonStub);
337+
338+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
339+
scannedRepos[1],
340+
variantAnalysis,
341+
cancellationTokenSource.token
342+
);
343+
344+
sinon.assert.calledWith(outputJsonStub, path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json'), {
345+
[scannedRepos[0].repository.id]: {
346+
repositoryId: scannedRepos[0].repository.id,
347+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
348+
},
349+
[scannedRepos[1].repository.id]: {
350+
repositoryId: scannedRepos[1].repository.id,
351+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
352+
},
353+
});
354+
});
355+
356+
it('should update the repo state correctly', async () => {
357+
// To set some initial repo states, we need to mock the correct methods so that the repo states are read in.
358+
// The actual tests for these are in rehydrateVariantAnalysis, so we can just mock them here and test that
359+
// the methods are called.
360+
361+
pathExistsStub.withArgs(path.join(storagePath, variantAnalysis.id.toString())).resolves(true);
362+
// This will read in the correct repo states
363+
readJsonStub.withArgs(path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json')).resolves({
364+
[scannedRepos[1].repository.id]: {
365+
repositoryId: scannedRepos[1].repository.id,
366+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
367+
},
368+
[scannedRepos[2].repository.id]: {
369+
repositoryId: scannedRepos[2].repository.id,
370+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
371+
},
372+
});
373+
374+
await variantAnalysisManager.rehydrateVariantAnalysis({
375+
...createMockVariantAnalysis(),
376+
id: variantAnalysis.id,
377+
}, QueryStatus.InProgress);
378+
sinon.assert.calledWith(readJsonStub, path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json'));
379+
380+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
381+
scannedRepos[0],
382+
variantAnalysis,
383+
cancellationTokenSource.token
384+
);
385+
386+
sinon.assert.calledWith(outputJsonStub, path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json'), {
387+
[scannedRepos[1].repository.id]: {
388+
repositoryId: scannedRepos[1].repository.id,
389+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
390+
},
391+
[scannedRepos[2].repository.id]: {
392+
repositoryId: scannedRepos[2].repository.id,
393+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
394+
},
395+
[scannedRepos[0].repository.id]: {
396+
repositoryId: scannedRepos[0].repository.id,
397+
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
398+
}
399+
});
400+
});
158401
});
159402

160403
describe('enqueueDownload', async () => {

0 commit comments

Comments
 (0)