Merge pull request #1545 from github/elenatanasoiu/monitor-variant-analysis

Implement monitoring for variant analysis live results
This commit is contained in:
Elena Tanasoiu
2022-09-30 10:00:03 +01:00
committed by GitHub
15 changed files with 726 additions and 50 deletions

View File

@@ -50,6 +50,7 @@
"devDependencies": {
"@babel/core": "^7.18.13",
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
"@faker-js/faker": "^7.5.0",
"@storybook/addon-actions": "^6.5.10",
"@storybook/addon-essentials": "^6.5.10",
"@storybook/addon-interactions": "^6.5.10",
@@ -2526,6 +2527,16 @@
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
},
"node_modules/@faker-js/faker": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.5.0.tgz",
"integrity": "sha512-8wNUCCUHvfvI0gQpDUho/3gPzABffnCn5um65F8dzQ86zz6dlt4+nmAA7PQUc8L+eH+9RgR/qzy5N/8kN0Ozdw==",
"dev": true,
"engines": {
"node": ">=14.0.0",
"npm": ">=6.0.0"
}
},
"node_modules/@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -41838,6 +41849,12 @@
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
},
"@faker-js/faker": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.5.0.tgz",
"integrity": "sha512-8wNUCCUHvfvI0gQpDUho/3gPzABffnCn5um65F8dzQ86zz6dlt4+nmAA7PQUc8L+eH+9RgR/qzy5N/8kN0Ozdw==",
"dev": true
},
"@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",

View File

@@ -1246,6 +1246,7 @@
"devDependencies": {
"@babel/core": "^7.18.13",
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
"@faker-js/faker": "^7.5.0",
"@storybook/addon-actions": "^6.5.10",
"@storybook/addon-essentials": "^6.5.10",
"@storybook/addon-interactions": "^6.5.10",

View File

@@ -105,6 +105,8 @@ import { createInitialQueryInfo } from './run-queries-shared';
import { LegacyQueryRunner } from './legacy-query-server/legacyRunner';
import { QueryRunner } from './queryRunner';
import { VariantAnalysisView } from './remote-queries/variant-analysis-view';
import { VariantAnalysisMonitor } from './remote-queries/variant-analysis-monitor';
import { VariantAnalysis } from './remote-queries/shared/variant-analysis';
/**
* extension.ts
@@ -894,6 +896,16 @@ async function activateWithInstalledDistribution(
})
);
const variantAnalysisMonitor = new VariantAnalysisMonitor(ctx, logger);
ctx.subscriptions.push(
commandRunner('codeQL.monitorVariantAnalysis', async (
variantAnalysis: VariantAnalysis,
token: CancellationToken
) => {
await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, token);
})
);
ctx.subscriptions.push(
commandRunner('codeQL.autoDownloadRemoteQueryResults', async (
queryResult: RemoteQueryResult,

View File

@@ -62,7 +62,7 @@ export interface VariantAnalysisSkippedRepositoryGroup {
export interface VariantAnalysisNotFoundRepositoryGroup {
repository_count: number,
repository_nwos: string[]
repository_full_names: string[]
}
export interface VariantAnalysisRepoTask {
repository: Repository,

View File

@@ -1,4 +1,4 @@
import { CancellationToken, Uri, window } from 'vscode';
import { CancellationToken, commands, Uri, window } from 'vscode';
import * as path from 'path';
import * as yaml from 'js-yaml';
import * as fs from 'fs-extra';
@@ -26,8 +26,9 @@ import { QueryMetadata } from '../pure/interface-types';
import { getErrorMessage, REPO_REGEX } from '../pure/helpers-pure';
import * as ghApiClient from './gh-api/gh-api-client';
import { getRepositorySelection, isValidSelection, RepositorySelection } from './repository-selection';
import { parseVariantAnalysisQueryLanguage, VariantAnalysis, VariantAnalysisStatus, VariantAnalysisSubmission } from './shared/variant-analysis';
import { parseVariantAnalysisQueryLanguage, VariantAnalysisSubmission } from './shared/variant-analysis';
import { Repository } from './shared/repository';
import { processVariantAnalysis } from './variant-analysis-processor';
export interface QlPack {
name: string;
@@ -270,28 +271,15 @@ export async function runRemoteQuery(
variantAnalysisSubmission
);
const variantAnalysis: VariantAnalysis = {
id: variantAnalysisResponse.id,
controllerRepoId: variantAnalysisResponse.controller_repo.id,
query: {
name: variantAnalysisSubmission.query.name,
filePath: variantAnalysisSubmission.query.filePath,
language: variantAnalysisSubmission.query.language,
},
databases: {
repositories: variantAnalysisSubmission.databases.repositories,
repositoryLists: variantAnalysisSubmission.databases.repositoryLists,
repositoryOwners: variantAnalysisSubmission.databases.repositoryOwners,
},
status: VariantAnalysisStatus.InProgress,
};
const processedVariantAnalysis = processVariantAnalysis(variantAnalysisSubmission, variantAnalysisResponse);
// TODO: Remove once we have a proper notification
void showAndLogInformationMessage('Variant analysis submitted for processing');
void logger.log(`Variant analysis:\n${JSON.stringify(variantAnalysis, null, 2)}`);
void logger.log(`Variant analysis:\n${JSON.stringify(processedVariantAnalysis, null, 2)}`);
return { variantAnalysis };
void commands.executeCommand('codeQL.monitorVariantAnalysis', processedVariantAnalysis);
return { variantAnalysis: processedVariantAnalysis };
} else {
const apiResponse = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, controllerRepo, base64Pack, dryRun);

View File

@@ -0,0 +1,16 @@
import { VariantAnalysis } from './variant-analysis';
export type VariantAnalysisMonitorStatus =
| 'InProgress'
| 'CompletedSuccessfully'
| 'CompletedUnsuccessfully'
| 'Failed'
| 'Cancelled'
| 'TimedOut';
export interface VariantAnalysisMonitorResult {
status: VariantAnalysisMonitorStatus;
error?: string;
scannedReposDownloaded?: number[],
variantAnalysis?: VariantAnalysis
}

View File

@@ -0,0 +1,85 @@
import * as vscode from 'vscode';
import { Credentials } from '../authentication';
import { Logger } from '../logging';
import * as ghApiClient from './gh-api/gh-api-client';
import { VariantAnalysis, VariantAnalysisStatus } from './shared/variant-analysis';
import {
VariantAnalysis as VariantAnalysisApiResponse
} from './gh-api/variant-analysis';
import { VariantAnalysisMonitorResult } from './shared/variant-analysis-monitor-result';
import { processFailureReason } from './variant-analysis-processor';
export class VariantAnalysisMonitor {
// With a sleep of 5 seconds, the maximum number of attempts takes
// us to just over 2 days worth of monitoring.
public static maxAttemptCount = 17280;
public static sleepTime = 5000;
constructor(
private readonly extensionContext: vscode.ExtensionContext,
private readonly logger: Logger
) {
}
public async monitorVariantAnalysis(
variantAnalysis: VariantAnalysis,
cancellationToken: vscode.CancellationToken
): Promise<VariantAnalysisMonitorResult> {
const credentials = await Credentials.initialize(this.extensionContext);
if (!credentials) {
throw Error('Error authenticating with GitHub');
}
let variantAnalysisSummary: VariantAnalysisApiResponse;
let attemptCount = 0;
const scannedReposDownloaded: number[] = [];
while (attemptCount <= VariantAnalysisMonitor.maxAttemptCount) {
await this.sleep(VariantAnalysisMonitor.sleepTime);
if (cancellationToken && cancellationToken.isCancellationRequested) {
return { status: 'Cancelled', error: 'Variant Analysis was canceled.' };
}
variantAnalysisSummary = await ghApiClient.getVariantAnalysis(
credentials,
variantAnalysis.controllerRepoId,
variantAnalysis.id
);
if (variantAnalysisSummary.failure_reason) {
variantAnalysis.status = VariantAnalysisStatus.Failed;
variantAnalysis.failureReason = processFailureReason(variantAnalysisSummary.failure_reason);
return {
status: 'Failed',
error: `Variant Analysis has failed: ${variantAnalysisSummary.failure_reason}`,
variantAnalysis: variantAnalysis
};
}
void this.logger.log('****** Retrieved variant analysis' + JSON.stringify(variantAnalysisSummary));
if (variantAnalysisSummary.scanned_repositories) {
variantAnalysisSummary.scanned_repositories.forEach(scannedRepo => {
if (!scannedReposDownloaded.includes(scannedRepo.repository.id) && scannedRepo.analysis_status === 'succeeded') {
scannedReposDownloaded.push(scannedRepo.repository.id);
}
});
}
if (variantAnalysisSummary.status === 'completed') {
break;
}
attemptCount++;
}
return { status: 'CompletedSuccessfully', scannedReposDownloaded: scannedReposDownloaded };
}
private async sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

View File

@@ -0,0 +1,150 @@
import {
VariantAnalysis as ApiVariantAnalysis,
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository,
VariantAnalysisSkippedRepositories as ApiVariantAnalysisSkippedRepositories,
VariantAnalysisRepoStatus as ApiVariantAnalysisRepoStatus,
VariantAnalysisFailureReason as ApiVariantAnalysisFailureReason,
VariantAnalysisStatus as ApiVariantAnalysisStatus,
VariantAnalysisSkippedRepositoryGroup as ApiVariantAnalysisSkippedRepositoryGroup,
VariantAnalysisNotFoundRepositoryGroup as ApiVariantAnalysisNotFoundRepositoryGroup
} from './gh-api/variant-analysis';
import {
VariantAnalysis,
VariantAnalysisFailureReason,
VariantAnalysisScannedRepository,
VariantAnalysisSkippedRepositories,
VariantAnalysisStatus,
VariantAnalysisRepoStatus,
VariantAnalysisSubmission,
VariantAnalysisSkippedRepositoryGroup
} from './shared/variant-analysis';
export function processVariantAnalysis(
submission: VariantAnalysisSubmission,
response: ApiVariantAnalysis
): VariantAnalysis {
let scannedRepos: VariantAnalysisScannedRepository[] = [];
let skippedRepos: VariantAnalysisSkippedRepositories = {};
if (response.scanned_repositories) {
scannedRepos = processScannedRepositories(response.scanned_repositories as ApiVariantAnalysisScannedRepository[]);
}
if (response.skipped_repositories) {
skippedRepos = processSkippedRepositories(response.skipped_repositories as ApiVariantAnalysisSkippedRepositories);
}
const variantAnalysis: VariantAnalysis = {
id: response.id,
controllerRepoId: response.controller_repo.id,
query: {
name: submission.query.name,
filePath: submission.query.filePath,
language: submission.query.language
},
databases: submission.databases,
status: processApiStatus(response.status),
actionsWorkflowRunId: response.actions_workflow_run_id,
scannedRepos: scannedRepos,
skippedRepos: skippedRepos
};
if (response.failure_reason) {
variantAnalysis.failureReason = processFailureReason(response.failure_reason);
}
return variantAnalysis;
}
function processScannedRepositories(
scannedRepos: ApiVariantAnalysisScannedRepository[]
): VariantAnalysisScannedRepository[] {
return scannedRepos.map(scannedRepo => {
return {
repository: {
id: scannedRepo.repository.id,
fullName: scannedRepo.repository.full_name,
private: scannedRepo.repository.private,
},
analysisStatus: processApiRepoStatus(scannedRepo.analysis_status),
resultCount: scannedRepo.result_count,
artifactSizeInBytes: scannedRepo.artifact_size_in_bytes,
failureMessage: scannedRepo.failure_message
};
});
}
function processSkippedRepositories(
skippedRepos: ApiVariantAnalysisSkippedRepositories
): VariantAnalysisSkippedRepositories {
return {
accessMismatchRepos: processRepoGroup(skippedRepos.access_mismatch_repos),
notFoundRepos: processNotFoundRepoGroup(skippedRepos.not_found_repo_nwos),
noCodeqlDbRepos: processRepoGroup(skippedRepos.no_codeql_db_repos),
overLimitRepos: processRepoGroup(skippedRepos.over_limit_repos)
};
}
function processRepoGroup(repoGroup: ApiVariantAnalysisSkippedRepositoryGroup): VariantAnalysisSkippedRepositoryGroup {
const repos = repoGroup.repositories.map(repo => {
return {
id: repo.id,
fullName: repo.full_name
};
});
return {
repositoryCount: repoGroup.repository_count,
repositories: repos
};
}
function processNotFoundRepoGroup(repoGroup: ApiVariantAnalysisNotFoundRepositoryGroup): VariantAnalysisSkippedRepositoryGroup {
const repo_full_names = repoGroup.repository_full_names.map(nwo => {
return {
fullName: nwo
};
});
return {
repositoryCount: repoGroup.repository_count,
repositories: repo_full_names
};
}
function processApiRepoStatus(analysisStatus: ApiVariantAnalysisRepoStatus): VariantAnalysisRepoStatus {
switch (analysisStatus) {
case 'pending':
return VariantAnalysisRepoStatus.Pending;
case 'in_progress':
return VariantAnalysisRepoStatus.InProgress;
case 'succeeded':
return VariantAnalysisRepoStatus.Succeeded;
case 'failed':
return VariantAnalysisRepoStatus.Failed;
case 'canceled':
return VariantAnalysisRepoStatus.Canceled;
case 'timed_out':
return VariantAnalysisRepoStatus.TimedOut;
}
}
function processApiStatus(status: ApiVariantAnalysisStatus): VariantAnalysisStatus {
switch (status) {
case 'in_progress':
return VariantAnalysisStatus.InProgress;
case 'completed':
return VariantAnalysisStatus.Succeeded;
}
}
export function processFailureReason(failureReason: ApiVariantAnalysisFailureReason): VariantAnalysisFailureReason {
switch (failureReason) {
case 'no_repos_queried':
return VariantAnalysisFailureReason.NoReposQueried;
case 'internal_error':
return VariantAnalysisFailureReason.InternalError;
}
}

View File

@@ -15,9 +15,12 @@ import * as config from '../../../config';
import { UserCancellationException } from '../../../commandRunner';
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
import { lte } from 'semver';
import { VariantAnalysis } from '../../../remote-queries/gh-api/variant-analysis';
import {
VariantAnalysis as VariantAnalysisApiResponse
} from '../../../remote-queries/gh-api/variant-analysis';
import { Repository } from '../../../remote-queries/gh-api/repository';
import { VariantAnalysisStatus } from '../../../remote-queries/shared/variant-analysis';
import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
describe('Remote queries', function() {
const baseDir = path.join(__dirname, '../../../../src/vscode-tests/cli-integration');
@@ -285,70 +288,52 @@ describe('Remote queries', function() {
});
describe('when live results are enabled', () => {
let mockApiResponse: VariantAnalysisApiResponse;
let mockSubmitVariantAnalysis: sinon.SinonStub;
beforeEach(() => {
liveResultsStub.returns(true);
mockApiResponse = createMockApiResponse('in_progress');
mockSubmitVariantAnalysis = sandbox.stub(ghApiClient, 'submitVariantAnalysis').resolves(mockApiResponse);
});
const dummyVariantAnalysis: VariantAnalysis = {
id: 123,
controller_repo: {
id: 64,
name: 'pickles',
full_name: 'github/pickles',
private: false,
},
actor_id: 27,
query_language: 'javascript',
query_pack_url: 'https://example.com/foo',
status: 'in_progress',
};
it('should run a variant analysis that is part of a qlpack', async () => {
const submitVariantAnalysisStub = sandbox.stub(ghApiClient, 'submitVariantAnalysis').resolves(dummyVariantAnalysis);
const fileUri = getFile('data-remote-qlpack/in-pack.ql');
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, token);
expect(querySubmissionResult).to.be.ok;
const variantAnalysis = querySubmissionResult!.variantAnalysis!;
expect(variantAnalysis.id).to.be.equal(dummyVariantAnalysis.id);
expect(variantAnalysis.id).to.be.equal(mockApiResponse.id);
expect(variantAnalysis.status).to.be.equal(VariantAnalysisStatus.InProgress);
expect(getRepositoryFromNwoStub).to.have.been.calledOnce;
expect(submitVariantAnalysisStub).to.have.been.calledOnce;
expect(mockSubmitVariantAnalysis).to.have.been.calledOnce;
});
it('should run a remote query that is not part of a qlpack', async () => {
const submitVariantAnalysisStub = sandbox.stub(ghApiClient, 'submitVariantAnalysis').resolves(dummyVariantAnalysis);
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, token);
expect(querySubmissionResult).to.be.ok;
const variantAnalysis = querySubmissionResult!.variantAnalysis!;
expect(variantAnalysis.id).to.be.equal(dummyVariantAnalysis.id);
expect(variantAnalysis.id).to.be.equal(mockApiResponse.id);
expect(variantAnalysis.status).to.be.equal(VariantAnalysisStatus.InProgress);
expect(getRepositoryFromNwoStub).to.have.been.calledOnce;
expect(submitVariantAnalysisStub).to.have.been.calledOnce;
expect(mockSubmitVariantAnalysis).to.have.been.calledOnce;
});
it('should run a remote query that is nested inside a qlpack', async () => {
const submitVariantAnalysisStub = sandbox.stub(ghApiClient, 'submitVariantAnalysis').resolves(dummyVariantAnalysis);
const fileUri = getFile('data-remote-qlpack-nested/subfolder/in-pack.ql');
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, token);
expect(querySubmissionResult).to.be.ok;
const variantAnalysis = querySubmissionResult!.variantAnalysis!;
expect(variantAnalysis.id).to.be.equal(dummyVariantAnalysis.id);
expect(variantAnalysis.id).to.be.equal(mockApiResponse.id);
expect(variantAnalysis.status).to.be.equal(VariantAnalysisStatus.InProgress);
expect(getRepositoryFromNwoStub).to.have.been.calledOnce;
expect(submitVariantAnalysisStub).to.have.been.calledOnce;
expect(mockSubmitVariantAnalysis).to.have.been.calledOnce;
});
it('should cancel a run before uploading', async () => {

View File

@@ -0,0 +1,165 @@
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 { VariantAnalysisMonitor } from '../../../remote-queries/variant-analysis-monitor';
import {
VariantAnalysis as VariantAnalysisApiResponse,
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository,
VariantAnalysisFailureReason
} from '../../../remote-queries/gh-api/variant-analysis';
import { createFailedMockApiResponse, createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
import { VariantAnalysisStatus } from '../../../remote-queries/shared/variant-analysis';
import { createMockScannedRepos } from '../../factories/remote-queries/gh-api/scanned-repositories';
import { processFailureReason } from '../../../remote-queries/variant-analysis-processor';
import { Credentials } from '../../../authentication';
describe('Variant Analysis Monitor', async function() {
let sandbox: sinon.SinonSandbox;
let mockGetVariantAnalysis: sinon.SinonStub;
let cancellationToken: CancellationToken;
let variantAnalysisMonitor: VariantAnalysisMonitor;
let variantAnalysis: any;
beforeEach(async () => {
sandbox = sinon.createSandbox();
sandbox.stub(logger, 'log');
sandbox.stub(config, 'isVariantAnalysisLiveResultsEnabled').returns(false);
cancellationToken = {
isCancellationRequested: false
} as unknown as CancellationToken;
variantAnalysis = {
id: 123,
controllerRepoId: 1,
};
try {
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
variantAnalysisMonitor = new VariantAnalysisMonitor(extension.ctx, logger);
} catch (e) {
fail(e as Error);
}
limitNumberOfAttemptsToMonitor();
});
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 variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
} catch (error: any) {
expect(error.message).to.equal('Error authenticating with GitHub');
}
});
});
describe('when credentials are valid', async () => {
beforeEach(async () => {
const mockCredentials = {
getOctokit: () => Promise.resolve({
request: mockGetVariantAnalysis
})
} as unknown as Credentials;
sandbox.stub(Credentials, 'initialize').resolves(mockCredentials);
});
it('should return early if variant analysis is cancelled', async () => {
cancellationToken.isCancellationRequested = true;
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
expect(result).to.eql({ status: 'Cancelled', error: 'Variant Analysis was canceled.' });
});
describe('when the variant analysis fails', async () => {
let mockFailedApiResponse: VariantAnalysisApiResponse;
beforeEach(async function() {
mockFailedApiResponse = createFailedMockApiResponse('in_progress');
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockFailedApiResponse);
});
it('should mark as failed locally and stop monitoring', async () => {
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
variantAnalysis = result.variantAnalysis;
expect(mockGetVariantAnalysis.calledOnce).to.be.true;
expect(result.status).to.eql('Failed');
expect(result.error).to.eql(`Variant Analysis has failed: ${mockFailedApiResponse.failure_reason}`);
expect(variantAnalysis.status).to.equal(VariantAnalysisStatus.Failed);
expect(variantAnalysis.failureReason).to.equal(processFailureReason(mockFailedApiResponse.failure_reason as VariantAnalysisFailureReason));
});
});
describe('when the variant analysis completes', async () => {
let mockApiResponse: VariantAnalysisApiResponse;
let scannedRepos: ApiVariantAnalysisScannedRepository[];
describe('when there are successfully scanned repos', async () => {
beforeEach(async function() {
scannedRepos = createMockScannedRepos(['pending', 'in_progress', 'succeeded']);
mockApiResponse = createMockApiResponse('completed', scannedRepos);
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockApiResponse);
});
it('should succeed and return a list of scanned repo ids', async () => {
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
const scannedRepoIds = scannedRepos.filter(r => r.analysis_status == 'succeeded').map(r => r.repository.id);
expect(result.status).to.equal('CompletedSuccessfully');
expect(result.scannedReposDownloaded).to.eql(scannedRepoIds);
});
});
describe('when there are only in progress repos', async () => {
let scannedRepos: ApiVariantAnalysisScannedRepository[];
beforeEach(async function() {
scannedRepos = createMockScannedRepos(['pending', 'in_progress']);
mockApiResponse = createMockApiResponse('in_progress', scannedRepos);
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockApiResponse);
});
it('should succeed and return an empty list of scanned repo ids', async () => {
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
expect(result.status).to.equal('CompletedSuccessfully');
expect(result.scannedReposDownloaded).to.eql([]);
});
});
describe('when there are no repos to scan', async () => {
beforeEach(async function() {
scannedRepos = [];
mockApiResponse = createMockApiResponse('completed', scannedRepos);
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockApiResponse);
});
it('should succeed and return an empty list of scanned repo ids', async () => {
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationToken);
expect(result.status).to.equal('CompletedSuccessfully');
expect(result.scannedReposDownloaded).to.eql([]);
});
});
});
});
function limitNumberOfAttemptsToMonitor() {
VariantAnalysisMonitor.maxAttemptCount = 3;
VariantAnalysisMonitor.sleepTime = 1;
}
});

View File

@@ -0,0 +1,116 @@
import { expect } from 'chai';
import {
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository,
} from '../../../remote-queries/gh-api/variant-analysis';
import {
VariantAnalysisQueryLanguage,
VariantAnalysisScannedRepository,
VariantAnalysisRepoStatus
} from '../../../remote-queries/shared/variant-analysis';
import { processVariantAnalysis } from '../../../remote-queries/variant-analysis-processor';
import { createMockScannedRepos } from '../../factories/remote-queries/gh-api/scanned-repositories';
import { createMockSkippedRepos } from '../../factories/remote-queries/gh-api/skipped-repositories';
import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
import { createMockSubmission } from '../../factories/remote-queries/shared/variant-analysis-submission';
describe('Variant Analysis processor', function() {
const scannedRepos = createMockScannedRepos();
const skippedRepos = createMockSkippedRepos();
const mockApiResponse = createMockApiResponse('completed', scannedRepos, skippedRepos);
const mockSubmission = createMockSubmission();
it('should process an API response and return a variant analysis', () => {
const result = processVariantAnalysis(mockSubmission, mockApiResponse);
const { access_mismatch_repos, no_codeql_db_repos, not_found_repo_nwos, over_limit_repos } = skippedRepos;
expect(result).to.eql({
'id': 123,
'controllerRepoId': 456,
'query': {
'filePath': 'query-file-path',
'language': VariantAnalysisQueryLanguage.Javascript,
'name': 'query-name',
},
'databases': {
'repositories': ['1', '2', '3'],
},
'status': 'succeeded',
'actionsWorkflowRunId': 456,
'scannedRepos': [
transformScannedRepo(VariantAnalysisRepoStatus.Succeeded, scannedRepos[0]),
transformScannedRepo(VariantAnalysisRepoStatus.Pending, scannedRepos[1]),
transformScannedRepo(VariantAnalysisRepoStatus.InProgress, scannedRepos[2]),
],
'skippedRepos': {
'accessMismatchRepos': {
'repositories': [
{
'fullName': access_mismatch_repos.repositories[0].full_name,
'id': access_mismatch_repos.repositories[0].id
},
{
'fullName': access_mismatch_repos.repositories[1].full_name,
'id': access_mismatch_repos.repositories[1].id
}
],
'repositoryCount': access_mismatch_repos.repository_count
},
'noCodeqlDbRepos': {
'repositories': [
{
'fullName': no_codeql_db_repos.repositories[0].full_name,
'id': no_codeql_db_repos.repositories[0].id
},
{
'fullName': no_codeql_db_repos.repositories[1].full_name,
'id': no_codeql_db_repos.repositories[1].id,
}
],
'repositoryCount': 2
},
'notFoundRepos': {
'repositories': [
{
'fullName': not_found_repo_nwos.repository_full_names[0]
},
{
'fullName': not_found_repo_nwos.repository_full_names[1]
}
],
'repositoryCount': not_found_repo_nwos.repository_count
},
'overLimitRepos': {
'repositories': [
{
'fullName': over_limit_repos.repositories[0].full_name,
'id': over_limit_repos.repositories[0].id
},
{
'fullName': over_limit_repos.repositories[1].full_name,
'id': over_limit_repos.repositories[1].id
}
],
'repositoryCount': over_limit_repos.repository_count
}
}
});
});
function transformScannedRepo(
status: VariantAnalysisRepoStatus,
scannedRepo: ApiVariantAnalysisScannedRepository
): VariantAnalysisScannedRepository {
return {
'analysisStatus': status,
'artifactSizeInBytes': scannedRepo.artifact_size_in_bytes,
'failureMessage': scannedRepo.failure_message,
'repository': {
'fullName': scannedRepo.repository.full_name,
'id': scannedRepo.repository.id,
'private': scannedRepo.repository.private,
},
'resultCount': scannedRepo.result_count
};
}
});

View File

@@ -0,0 +1,30 @@
import { faker } from '@faker-js/faker';
import {
VariantAnalysisRepoStatus,
VariantAnalysisScannedRepository
} from '../../../../remote-queries/gh-api/variant-analysis';
export function createMockScannedRepo(
name: string,
isPrivate: boolean,
analysisStatus: VariantAnalysisRepoStatus,
): VariantAnalysisScannedRepository {
return {
repository: {
id: faker.datatype.number(),
name: name,
full_name: 'github/' + name,
private: isPrivate,
},
analysis_status: analysisStatus,
result_count: faker.datatype.number(),
artifact_size_in_bytes: faker.datatype.number()
};
}
export function createMockScannedRepos(
statuses: VariantAnalysisRepoStatus[] = ['succeeded', 'pending', 'in_progress']
): VariantAnalysisScannedRepository[] {
return statuses.map(status => createMockScannedRepo(`mona-${status}`, false, status));
}

View File

@@ -0,0 +1,45 @@
import { faker } from '@faker-js/faker';
import {
VariantAnalysisNotFoundRepositoryGroup,
VariantAnalysisSkippedRepositories,
VariantAnalysisSkippedRepositoryGroup
} from '../../../../remote-queries/gh-api/variant-analysis';
export function createMockSkippedRepos(): VariantAnalysisSkippedRepositories {
return {
access_mismatch_repos: createMockSkippedRepoGroup(),
no_codeql_db_repos: createMockSkippedRepoGroup(),
not_found_repo_nwos: createMockNotFoundSkippedRepoGroup(),
over_limit_repos: createMockSkippedRepoGroup()
};
}
export function createMockSkippedRepoGroup(): VariantAnalysisSkippedRepositoryGroup {
return {
repository_count: 2,
repositories: [
{
id: faker.datatype.number(),
name: faker.random.word(),
full_name: 'github/' + faker.random.word(),
private: true
},
{
id: faker.datatype.number(),
name: faker.random.word(),
full_name: 'github/' + faker.random.word(),
private: false
}
]
};
}
export function createMockNotFoundSkippedRepoGroup(): VariantAnalysisNotFoundRepositoryGroup {
const repoName1 = 'github/' + faker.random.word();
const repoName2 = 'github/' + faker.random.word();
return {
repository_count: 2,
repository_full_names: [repoName1, repoName2]
};
}

View File

@@ -0,0 +1,48 @@
import {
VariantAnalysis as VariantAnalysisApiResponse,
VariantAnalysisScannedRepository,
VariantAnalysisSkippedRepositories,
VariantAnalysisStatus,
} from '../../../../remote-queries/gh-api/variant-analysis';
import {
VariantAnalysisQueryLanguage
} from '../../../../remote-queries/shared/variant-analysis';
import { createMockScannedRepos } from './scanned-repositories';
import { createMockSkippedRepos } from './skipped-repositories';
export function createMockApiResponse(
status: VariantAnalysisStatus = 'in_progress',
scannedRepos: VariantAnalysisScannedRepository[] = createMockScannedRepos(),
skippedRepos: VariantAnalysisSkippedRepositories = createMockSkippedRepos()
): VariantAnalysisApiResponse {
const variantAnalysis: VariantAnalysisApiResponse = {
id: 123,
controller_repo: {
id: 456,
name: 'pickles',
full_name: 'github/pickles',
private: false,
},
actor_id: 123,
query_language: VariantAnalysisQueryLanguage.Javascript,
query_pack_url: 'https://example.com/foo',
status: status,
actions_workflow_run_id: 456,
scanned_repositories: scannedRepos,
skipped_repositories: skippedRepos
};
return variantAnalysis;
}
export function createFailedMockApiResponse(
status: VariantAnalysisStatus = 'in_progress',
scannedRepos: VariantAnalysisScannedRepository[] = createMockScannedRepos(),
skippedRepos: VariantAnalysisSkippedRepositories = createMockSkippedRepos(),
): VariantAnalysisApiResponse {
const variantAnalysis = createMockApiResponse(status, scannedRepos, skippedRepos);
variantAnalysis.status = status;
variantAnalysis.failure_reason = 'internal_error';
return variantAnalysis;
}

View File

@@ -0,0 +1,18 @@
import { VariantAnalysisQueryLanguage, VariantAnalysisSubmission } from '../../../../remote-queries/shared/variant-analysis';
export function createMockSubmission(): VariantAnalysisSubmission {
return {
startTime: 1234,
controllerRepoId: 5678,
actionRepoRef: 'repo-ref',
query: {
name: 'query-name',
filePath: 'query-file-path',
language: VariantAnalysisQueryLanguage.Javascript,
pack: 'base64-encoded-string',
},
databases: {
repositories: ['1', '2', '3'],
}
};
}