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.
This commit is contained in:
Elena Tanasoiu
2022-09-30 15:16:11 +01:00
parent deac8c8c02
commit 765c956481
3 changed files with 233 additions and 1 deletions

View File

@@ -1,8 +1,18 @@
import { CancellationToken, commands, ExtensionContext } from 'vscode';
import * as ghApiClient from './gh-api/gh-api-client';
import * as path from 'path';
import * as fs from 'fs-extra';
import { CancellationToken, ExtensionContext } from 'vscode';
import { DisposableObject } from '../pure/disposable-object';
import { Logger } from '../logging';
import { Credentials } from '../authentication';
import { VariantAnalysisMonitor } from './variant-analysis-monitor';
import {
VariantAnalysis as VariantAnalysisApiResponse,
VariantAnalysisRepoTask,
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository
} from './gh-api/variant-analysis';
import { VariantAnalysis } from './shared/variant-analysis';
import { getErrorMessage } from '../pure/helpers-pure';
export class VariantAnalysisManager extends DisposableObject {
private readonly variantAnalysisMonitor: VariantAnalysisMonitor;
@@ -21,4 +31,51 @@ export class VariantAnalysisManager extends DisposableObject {
): Promise<void> {
await this.variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
}
public async autoDownloadVariantAnalysisResult(
scannedRepo: ApiVariantAnalysisScannedRepository,
variantAnalysisSummary: VariantAnalysisApiResponse,
cancellationToken: CancellationToken
): Promise<void> {
const credentials = await Credentials.initialize(this.ctx);
if (!credentials) { throw Error('Error authenticating with GitHub'); }
if (cancellationToken && cancellationToken.isCancellationRequested) {
return;
}
let repoTask: VariantAnalysisRepoTask;
try {
repoTask = await ghApiClient.getVariantAnalysisRepo(
credentials,
variantAnalysisSummary.controller_repo.id,
variantAnalysisSummary.id,
scannedRepo.repository.id
);
}
catch (e) { throw new Error(`Could not download the results for variant analysis with id: ${variantAnalysisSummary.id}. Error: ${getErrorMessage(e)}`); }
if (repoTask.artifact_url) {
const resultDirectory = path.join(
this.ctx.globalStorageUri.fsPath,
'variant-analyses',
`${variantAnalysisSummary.id}`,
scannedRepo.repository.full_name
);
const storagePath = path.join(
resultDirectory,
scannedRepo.repository.full_name
);
const result = await ghApiClient.getVariantAnalysisRepoResult(
credentials,
repoTask.artifact_url
);
fs.mkdirSync(resultDirectory, { recursive: true });
await fs.writeFile(storagePath, JSON.stringify(result, null, 2), 'utf8');
}
}
}

View File

@@ -0,0 +1,156 @@
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 { Credentials } from '../../../authentication';
import * as fs from 'fs-extra';
import { VariantAnalysisManager } from '../../../remote-queries/variant-analysis-manager';
import {
VariantAnalysis as VariantAnalysisApiResponse,
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository
} from '../../../remote-queries/gh-api/variant-analysis';
import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
import { createMockScannedRepos } from '../../factories/remote-queries/gh-api/scanned-repositories';
import { createMockVariantAnalysisRepoTask } from '../../factories/remote-queries/gh-api/variant-analysis-repo-task';
describe('Variant Analysis Manager', async function() {
let sandbox: sinon.SinonSandbox;
let cancellationToken: CancellationToken;
let variantAnalysisManager: VariantAnalysisManager;
let variantAnalysis: VariantAnalysisApiResponse;
let scannedRepos: ApiVariantAnalysisScannedRepository[];
let getVariantAnalysisRepoStub: sinon.SinonStub;
let getVariantAnalysisRepoResultStub: sinon.SinonStub;
beforeEach(async () => {
sandbox = sinon.createSandbox();
sandbox.stub(logger, 'log');
sandbox.stub(config, 'isVariantAnalysisLiveResultsEnabled').returns(false);
sandbox.stub(fs, 'mkdirSync');
sandbox.stub(fs, 'writeFile');
cancellationToken = {
isCancellationRequested: false
} as unknown as CancellationToken;
scannedRepos = createMockScannedRepos();
variantAnalysis = createMockApiResponse('in_progress', scannedRepos);
try {
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
variantAnalysisManager = new VariantAnalysisManager(extension.ctx, logger);
} catch (e) {
fail(e as Error);
}
});
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 variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationToken
);
} catch (error: any) {
expect(error.message).to.equal('Error authenticating with GitHub');
}
});
});
describe('when credentials are valid', async () => {
let getOctokitStub: sinon.SinonStub;
beforeEach(async () => {
const mockCredentials = {
getOctokit: () => Promise.resolve({
request: getOctokitStub
})
} as unknown as Credentials;
sandbox.stub(Credentials, 'initialize').resolves(mockCredentials);
});
describe('when the artifact_url is missing', async () => {
beforeEach(async () => {
const dummyRepoTask = createMockVariantAnalysisRepoTask();
delete dummyRepoTask.artifact_url;
getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask);
const dummyResult = 'this-is-a-repo-result';
getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(dummyResult);
});
it('should not try to download the result', async () => {
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationToken
);
expect(getVariantAnalysisRepoResultStub.notCalled).to.be.true;
});
});
describe('when the artifact_url is present', async () => {
beforeEach(async () => {
const dummyRepoTask = createMockVariantAnalysisRepoTask();
getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask);
const dummyResult = 'this-is-a-repo-result';
getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(dummyResult);
});
it('should return early if variant analysis is cancelled', async () => {
cancellationToken.isCancellationRequested = true;
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationToken
);
expect(getVariantAnalysisRepoStub.notCalled).to.be.true;
});
it('should fetch a repo task', async () => {
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationToken
);
expect(getVariantAnalysisRepoStub.calledOnce).to.be.true;
});
it('should fetch a repo result', async () => {
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationToken
);
expect(getVariantAnalysisRepoResultStub.calledOnce).to.be.true;
});
it('should save the result to disk', async () => {
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationToken
);
expect(getVariantAnalysisRepoResultStub.calledOnce).to.be.true;
});
});
});
});

View File

@@ -0,0 +1,19 @@
import { faker } from '@faker-js/faker';
import { VariantAnalysisRepoTask } from '../../../../remote-queries/gh-api/variant-analysis';
import { VariantAnalysisRepoStatus } from '../../../../remote-queries/shared/variant-analysis';
export function createMockVariantAnalysisRepoTask(): VariantAnalysisRepoTask {
return {
repository: {
id: faker.datatype.number(),
name: faker.random.word(),
full_name: 'github/' + faker.random.word(),
private: false,
},
analysis_status: VariantAnalysisRepoStatus.Succeeded,
result_count: faker.datatype.number(),
artifact_size_in_bytes: faker.datatype.number(),
artifact_url: 'https://www.pickles.com'
};
}