Merge pull request #1545 from github/elenatanasoiu/monitor-variant-analysis
Implement monitoring for variant analysis live results
This commit is contained in:
17
extensions/ql-vscode/package-lock.json
generated
17
extensions/ql-vscode/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user