Skip to content

Commit 765c956

Browse files
committed
Introduce download method on VariantAnalysisManager
This method will be called from the VariantAnalysisMonitor once a new repo has been scanned. It will then perform an API request to get the repo task for it, which will contain an `artifact_url`. Finally it will use the API method we introduced in the previous commit to download the result for the repo and then save it on disk.
1 parent deac8c8 commit 765c956

3 files changed

Lines changed: 233 additions & 1 deletion

File tree

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

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1-
import { CancellationToken, commands, ExtensionContext } from 'vscode';
1+
import * as ghApiClient from './gh-api/gh-api-client';
2+
import * as path from 'path';
3+
import * as fs from 'fs-extra';
4+
import { CancellationToken, ExtensionContext } from 'vscode';
25
import { DisposableObject } from '../pure/disposable-object';
36
import { Logger } from '../logging';
7+
import { Credentials } from '../authentication';
48
import { VariantAnalysisMonitor } from './variant-analysis-monitor';
9+
import {
10+
VariantAnalysis as VariantAnalysisApiResponse,
11+
VariantAnalysisRepoTask,
12+
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository
13+
} from './gh-api/variant-analysis';
514
import { VariantAnalysis } from './shared/variant-analysis';
15+
import { getErrorMessage } from '../pure/helpers-pure';
616

717
export class VariantAnalysisManager extends DisposableObject {
818
private readonly variantAnalysisMonitor: VariantAnalysisMonitor;
@@ -21,4 +31,51 @@ export class VariantAnalysisManager extends DisposableObject {
2131
): Promise<void> {
2232
await this.variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
2333
}
34+
35+
public async autoDownloadVariantAnalysisResult(
36+
scannedRepo: ApiVariantAnalysisScannedRepository,
37+
variantAnalysisSummary: VariantAnalysisApiResponse,
38+
cancellationToken: CancellationToken
39+
): Promise<void> {
40+
41+
const credentials = await Credentials.initialize(this.ctx);
42+
if (!credentials) { throw Error('Error authenticating with GitHub'); }
43+
44+
if (cancellationToken && cancellationToken.isCancellationRequested) {
45+
return;
46+
}
47+
48+
let repoTask: VariantAnalysisRepoTask;
49+
try {
50+
repoTask = await ghApiClient.getVariantAnalysisRepo(
51+
credentials,
52+
variantAnalysisSummary.controller_repo.id,
53+
variantAnalysisSummary.id,
54+
scannedRepo.repository.id
55+
);
56+
}
57+
catch (e) { throw new Error(`Could not download the results for variant analysis with id: ${variantAnalysisSummary.id}. Error: ${getErrorMessage(e)}`); }
58+
59+
if (repoTask.artifact_url) {
60+
const resultDirectory = path.join(
61+
this.ctx.globalStorageUri.fsPath,
62+
'variant-analyses',
63+
`${variantAnalysisSummary.id}`,
64+
scannedRepo.repository.full_name
65+
);
66+
67+
const storagePath = path.join(
68+
resultDirectory,
69+
scannedRepo.repository.full_name
70+
);
71+
72+
const result = await ghApiClient.getVariantAnalysisRepoResult(
73+
credentials,
74+
repoTask.artifact_url
75+
);
76+
77+
fs.mkdirSync(resultDirectory, { recursive: true });
78+
await fs.writeFile(storagePath, JSON.stringify(result, null, 2), 'utf8');
79+
}
80+
}
2481
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
8+
import { Credentials } from '../../../authentication';
9+
import * as fs from 'fs-extra';
10+
11+
import { VariantAnalysisManager } from '../../../remote-queries/variant-analysis-manager';
12+
import {
13+
VariantAnalysis as VariantAnalysisApiResponse,
14+
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository
15+
} from '../../../remote-queries/gh-api/variant-analysis';
16+
import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
17+
import { createMockScannedRepos } from '../../factories/remote-queries/gh-api/scanned-repositories';
18+
import { createMockVariantAnalysisRepoTask } from '../../factories/remote-queries/gh-api/variant-analysis-repo-task';
19+
20+
describe('Variant Analysis Manager', async function() {
21+
let sandbox: sinon.SinonSandbox;
22+
let cancellationToken: CancellationToken;
23+
let variantAnalysisManager: VariantAnalysisManager;
24+
let variantAnalysis: VariantAnalysisApiResponse;
25+
let scannedRepos: ApiVariantAnalysisScannedRepository[];
26+
let getVariantAnalysisRepoStub: sinon.SinonStub;
27+
let getVariantAnalysisRepoResultStub: sinon.SinonStub;
28+
29+
beforeEach(async () => {
30+
sandbox = sinon.createSandbox();
31+
sandbox.stub(logger, 'log');
32+
sandbox.stub(config, 'isVariantAnalysisLiveResultsEnabled').returns(false);
33+
sandbox.stub(fs, 'mkdirSync');
34+
sandbox.stub(fs, 'writeFile');
35+
36+
cancellationToken = {
37+
isCancellationRequested: false
38+
} as unknown as CancellationToken;
39+
40+
scannedRepos = createMockScannedRepos();
41+
variantAnalysis = createMockApiResponse('in_progress', scannedRepos);
42+
43+
try {
44+
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
45+
variantAnalysisManager = new VariantAnalysisManager(extension.ctx, logger);
46+
} catch (e) {
47+
fail(e as Error);
48+
}
49+
});
50+
51+
afterEach(async () => {
52+
sandbox.restore();
53+
});
54+
55+
describe('when credentials are invalid', async () => {
56+
beforeEach(async () => { sandbox.stub(Credentials, 'initialize').resolves(undefined); });
57+
58+
it('should return early if credentials are wrong', async () => {
59+
try {
60+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
61+
scannedRepos[0],
62+
variantAnalysis,
63+
cancellationToken
64+
);
65+
} catch (error: any) {
66+
expect(error.message).to.equal('Error authenticating with GitHub');
67+
}
68+
});
69+
});
70+
71+
describe('when credentials are valid', async () => {
72+
let getOctokitStub: sinon.SinonStub;
73+
74+
beforeEach(async () => {
75+
const mockCredentials = {
76+
getOctokit: () => Promise.resolve({
77+
request: getOctokitStub
78+
})
79+
} as unknown as Credentials;
80+
sandbox.stub(Credentials, 'initialize').resolves(mockCredentials);
81+
});
82+
83+
describe('when the artifact_url is missing', async () => {
84+
beforeEach(async () => {
85+
const dummyRepoTask = createMockVariantAnalysisRepoTask();
86+
delete dummyRepoTask.artifact_url;
87+
getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask);
88+
89+
const dummyResult = 'this-is-a-repo-result';
90+
getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(dummyResult);
91+
});
92+
93+
it('should not try to download the result', async () => {
94+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
95+
scannedRepos[0],
96+
variantAnalysis,
97+
cancellationToken
98+
);
99+
100+
expect(getVariantAnalysisRepoResultStub.notCalled).to.be.true;
101+
});
102+
});
103+
104+
describe('when the artifact_url is present', async () => {
105+
beforeEach(async () => {
106+
const dummyRepoTask = createMockVariantAnalysisRepoTask();
107+
getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask);
108+
109+
const dummyResult = 'this-is-a-repo-result';
110+
getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(dummyResult);
111+
});
112+
113+
it('should return early if variant analysis is cancelled', async () => {
114+
cancellationToken.isCancellationRequested = true;
115+
116+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
117+
scannedRepos[0],
118+
variantAnalysis,
119+
cancellationToken
120+
);
121+
122+
expect(getVariantAnalysisRepoStub.notCalled).to.be.true;
123+
});
124+
125+
it('should fetch a repo task', async () => {
126+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
127+
scannedRepos[0],
128+
variantAnalysis,
129+
cancellationToken
130+
);
131+
132+
expect(getVariantAnalysisRepoStub.calledOnce).to.be.true;
133+
});
134+
135+
it('should fetch a repo result', async () => {
136+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
137+
scannedRepos[0],
138+
variantAnalysis,
139+
cancellationToken
140+
);
141+
142+
expect(getVariantAnalysisRepoResultStub.calledOnce).to.be.true;
143+
});
144+
145+
it('should save the result to disk', async () => {
146+
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
147+
scannedRepos[0],
148+
variantAnalysis,
149+
cancellationToken
150+
);
151+
152+
expect(getVariantAnalysisRepoResultStub.calledOnce).to.be.true;
153+
});
154+
});
155+
});
156+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { faker } from '@faker-js/faker';
2+
import { VariantAnalysisRepoTask } from '../../../../remote-queries/gh-api/variant-analysis';
3+
import { VariantAnalysisRepoStatus } from '../../../../remote-queries/shared/variant-analysis';
4+
5+
export function createMockVariantAnalysisRepoTask(): VariantAnalysisRepoTask {
6+
return {
7+
repository: {
8+
id: faker.datatype.number(),
9+
name: faker.random.word(),
10+
full_name: 'github/' + faker.random.word(),
11+
private: false,
12+
},
13+
analysis_status: VariantAnalysisRepoStatus.Succeeded,
14+
result_count: faker.datatype.number(),
15+
artifact_size_in_bytes: faker.datatype.number(),
16+
artifact_url: 'https://www.pickles.com'
17+
};
18+
}
19+

0 commit comments

Comments
 (0)