Merge pull request #1538 from github/robertbrignull/submit-variant-analysis

Implement submitting a live-results variant analysis
This commit is contained in:
Robert
2022-09-27 10:36:24 +01:00
committed by GitHub
7 changed files with 444 additions and 235 deletions

View File

@@ -6,6 +6,7 @@ import {
VariantAnalysisRepoTask,
VariantAnalysisSubmissionRequest
} from './variant-analysis';
import { Repository } from './repository';
export async function submitVariantAnalysis(
credentials: Credentials,
@@ -73,13 +74,13 @@ export async function getVariantAnalysisRepo(
return response.data;
}
export async function getRepositoryIdFromNwo(
export async function getRepositoryFromNwo(
credentials: Credentials,
owner: string,
repo: string
): Promise<number> {
): Promise<Repository> {
const octokit = await credentials.getOctokit();
const response = await octokit.rest.repos.get({ owner, repo });
return response.data.id;
return response.data as Repository;
}

View File

@@ -1,6 +1,8 @@
import { RemoteQuery } from './remote-query';
import { VariantAnalysis } from './shared/variant-analysis';
export interface RemoteQuerySubmissionResult {
queryDirPath?: string;
query?: RemoteQuery;
variantAnalysis?: VariantAnalysis;
}

View File

@@ -17,14 +17,17 @@ import {
import { Credentials } from '../authentication';
import * as cli from '../cli';
import { logger } from '../logging';
import { getActionBranch, getRemoteControllerRepo, setRemoteControllerRepo } from '../config';
import { getActionBranch, getRemoteControllerRepo, isVariantAnalysisLiveResultsEnabled, setRemoteControllerRepo } from '../config';
import { ProgressCallback, UserCancellationException } from '../commandRunner';
import { OctokitResponse } from '@octokit/types/dist-types';
import { OctokitResponse, RequestError } from '@octokit/types/dist-types';
import { RemoteQuery } from './remote-query';
import { RemoteQuerySubmissionResult } from './remote-query-submission-result';
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 { Repository } from './shared/repository';
export interface QlPack {
name: string;
@@ -210,31 +213,7 @@ export async function runRemoteQuery(
message: 'Determining controller repo'
});
// Get the controller repo from the config, if it exists.
// If it doesn't exist, prompt the user to enter it, and save that value to the config.
let controllerRepo: string | undefined;
controllerRepo = getRemoteControllerRepo();
if (!controllerRepo || !REPO_REGEX.test(controllerRepo)) {
void logger.log(controllerRepo ? 'Invalid controller repository name.' : 'No controller repository defined.');
controllerRepo = await window.showInputBox({
title: 'Controller repository in which to run the GitHub Actions workflow for this variant analysis',
placeHolder: '<owner>/<repo>',
prompt: 'Enter the name of a GitHub repository in the format <owner>/<repo>',
ignoreFocusOut: true,
});
if (!controllerRepo) {
void showAndLogErrorMessage('No controller repository entered.');
return;
} else if (!REPO_REGEX.test(controllerRepo)) { // Check if user entered invalid input
void showAndLogErrorMessage('Invalid repository format. Must be a valid GitHub repository in the format <owner>/<repo>.');
return;
}
void logger.log(`Setting the controller repository as: ${controllerRepo}`);
await setRemoteControllerRepo(controllerRepo);
}
void logger.log(`Using controller repository: ${controllerRepo}`);
const [owner, repo] = controllerRepo.split('/');
const controllerRepo = await getControllerRepo(credentials);
progress({
maxStep: 4,
@@ -259,31 +238,84 @@ export async function runRemoteQuery(
});
const actionBranch = getActionBranch();
const apiResponse = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, owner, repo, base64Pack, dryRun);
const queryStartTime = Date.now();
const queryMetadata = await tryGetQueryMetadata(cliServer, queryFile);
if (dryRun) {
return { queryDirPath: remoteQueryDir.path };
} else {
if (!apiResponse) {
return;
if (isVariantAnalysisLiveResultsEnabled()) {
const queryName = getQueryName(queryMetadata, queryFile);
const variantAnalysisLanguage = parseVariantAnalysisQueryLanguage(language);
if (variantAnalysisLanguage === undefined) {
throw new UserCancellationException(`Found unsupported language: ${language}`);
}
const workflowRunId = apiResponse.workflow_run_id;
const repositoryCount = apiResponse.repositories_queried.length;
const remoteQuery = await buildRemoteQueryEntity(
queryFile,
queryMetadata,
owner,
repo,
queryStartTime,
workflowRunId,
language,
repositoryCount);
const variantAnalysisSubmission: VariantAnalysisSubmission = {
startTime: queryStartTime,
actionRepoRef: actionBranch,
controllerRepoId: controllerRepo.id,
query: {
name: queryName,
filePath: queryFile,
pack: base64Pack,
language: variantAnalysisLanguage,
},
databases: {
repositories: repoSelection.repositories,
repositoryLists: repoSelection.repositoryLists,
repositoryOwners: repoSelection.owners
}
};
// don't return the path because it has been deleted
return { query: remoteQuery };
const variantAnalysisResponse = await ghApiClient.submitVariantAnalysis(
credentials,
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,
};
// 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)}`);
return { variantAnalysis };
} else {
const apiResponse = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, controllerRepo, base64Pack, dryRun);
if (dryRun) {
return { queryDirPath: remoteQueryDir.path };
} else {
if (!apiResponse) {
return;
}
const workflowRunId = apiResponse.workflow_run_id;
const repositoryCount = apiResponse.repositories_queried.length;
const remoteQuery = await buildRemoteQueryEntity(
queryFile,
queryMetadata,
controllerRepo,
queryStartTime,
workflowRunId,
language,
repositoryCount);
// don't return the path because it has been deleted
return { query: remoteQuery };
}
}
} finally {
@@ -301,8 +333,7 @@ async function runRemoteQueriesApiRequest(
ref: string,
language: string,
repoSelection: RepositorySelection,
owner: string,
repo: string,
controllerRepo: Repository,
queryPackBase64: string,
dryRun = false
): Promise<void | QueriesResponse> {
@@ -318,8 +349,7 @@ async function runRemoteQueriesApiRequest(
if (dryRun) {
void showAndLogInformationMessage('[DRY RUN] Would have sent request. See extension log for the payload.');
void logger.log(JSON.stringify({
owner,
repo,
controllerRepo,
data: {
...data,
queryPackBase64: queryPackBase64.substring(0, 100) + '... ' + queryPackBase64.length + ' bytes'
@@ -331,14 +361,13 @@ async function runRemoteQueriesApiRequest(
try {
const octokit = await credentials.getOctokit();
const response: OctokitResponse<QueriesResponse, number> = await octokit.request(
'POST /repos/:owner/:repo/code-scanning/codeql/queries',
'POST /repos/:controllerRepo/code-scanning/codeql/queries',
{
owner,
repo,
controllerRepo: controllerRepo.fullName,
data
}
);
const { popupMessage, logMessage } = parseResponse(owner, repo, response.data);
const { popupMessage, logMessage } = parseResponse(controllerRepo, response.data);
void showAndLogInformationMessage(popupMessage, { fullMessage: logMessage });
return response.data;
} catch (error: any) {
@@ -354,14 +383,14 @@ const eol = os.EOL;
const eol2 = os.EOL + os.EOL;
// exported for testing only
export function parseResponse(owner: string, repo: string, response: QueriesResponse) {
export function parseResponse(controllerRepo: Repository, response: QueriesResponse) {
const repositoriesQueried = response.repositories_queried;
const repositoryCount = repositoriesQueried.length;
const popupMessage = `Successfully scheduled runs on ${pluralize(repositoryCount, 'repository', 'repositories')}. [Click here to see the progress](https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}).`
const popupMessage = `Successfully scheduled runs on ${pluralize(repositoryCount, 'repository', 'repositories')}. [Click here to see the progress](https://github.com/${controllerRepo.fullName}/actions/runs/${response.workflow_run_id}).`
+ (response.errors ? `${eol2}Some repositories could not be scheduled. See extension log for details.` : '');
let logMessage = `Successfully scheduled runs on ${pluralize(repositoryCount, 'repository', 'repositories')}. See https://github.com/${owner}/${repo}/actions/runs/${response.workflow_run_id}.`;
let logMessage = `Successfully scheduled runs on ${pluralize(repositoryCount, 'repository', 'repositories')}. See https://github.com/${controllerRepo.fullName}/actions/runs/${response.workflow_run_id}.`;
logMessage += `${eol2}Repositories queried:${eol}${repositoriesQueried.join(', ')}`;
if (response.errors) {
const { invalid_repositories, repositories_without_database, private_repositories, cutoff_repositories, cutoff_repositories_count } = response.errors;
@@ -425,17 +454,15 @@ async function ensureNameAndSuite(queryPackDir: string, packRelativePath: string
async function buildRemoteQueryEntity(
queryFilePath: string,
queryMetadata: QueryMetadata | undefined,
controllerRepoOwner: string,
controllerRepoName: string,
controllerRepo: Repository,
queryStartTime: number,
workflowRunId: number,
language: string,
repositoryCount: number
): Promise<RemoteQuery> {
// The query name is either the name as specified in the query metadata, or the file name.
const queryName = queryMetadata?.name ?? path.basename(queryFilePath);
const queryName = getQueryName(queryMetadata, queryFilePath);
const queryText = await fs.readFile(queryFilePath, 'utf8');
const [owner, name] = controllerRepo.fullName.split('/');
return {
queryName,
@@ -443,11 +470,59 @@ async function buildRemoteQueryEntity(
queryText,
language,
controllerRepository: {
owner: controllerRepoOwner,
name: controllerRepoName,
owner,
name,
},
executionStartTime: queryStartTime,
actionsWorkflowRunId: workflowRunId,
repositoryCount,
};
}
function getQueryName(queryMetadata: QueryMetadata | undefined, queryFilePath: string): string {
// The query name is either the name as specified in the query metadata, or the file name.
return queryMetadata?.name ?? path.basename(queryFilePath);
}
async function getControllerRepo(credentials: Credentials): Promise<Repository> {
// Get the controller repo from the config, if it exists.
// If it doesn't exist, prompt the user to enter it, and save that value to the config.
let controllerRepoNwo: string | undefined;
controllerRepoNwo = getRemoteControllerRepo();
if (!controllerRepoNwo || !REPO_REGEX.test(controllerRepoNwo)) {
void logger.log(controllerRepoNwo ? 'Invalid controller repository name.' : 'No controller repository defined.');
controllerRepoNwo = await window.showInputBox({
title: 'Controller repository in which to run the GitHub Actions workflow for this variant analysis',
placeHolder: '<owner>/<repo>',
prompt: 'Enter the name of a GitHub repository in the format <owner>/<repo>',
ignoreFocusOut: true,
});
if (!controllerRepoNwo) {
throw new UserCancellationException('No controller repository entered.');
} else if (!REPO_REGEX.test(controllerRepoNwo)) { // Check if user entered invalid input
throw new UserCancellationException('Invalid repository format. Must be a valid GitHub repository in the format <owner>/<repo>.');
}
void logger.log(`Setting the controller repository as: ${controllerRepoNwo}`);
await setRemoteControllerRepo(controllerRepoNwo);
}
void logger.log(`Using controller repository: ${controllerRepoNwo}`);
const [owner, repo] = controllerRepoNwo.split('/');
try {
const controllerRepo = await ghApiClient.getRepositoryFromNwo(credentials, owner, repo);
void logger.log(`Controller repository ID: ${controllerRepo.id}`);
return {
id: controllerRepo.id,
fullName: controllerRepo.full_name,
private: controllerRepo.private,
};
} catch (e: any) {
if ((e as RequestError).status === 404) {
throw new Error(`Controller repository "${owner}/${repo}" not found`);
} else {
throw new Error(`Error getting controller repository "${owner}/${repo}": ${e.message}`);
}
}
}

View File

@@ -30,6 +30,10 @@ export enum VariantAnalysisQueryLanguage {
Ruby = 'ruby'
}
export function parseVariantAnalysisQueryLanguage(language: string): VariantAnalysisQueryLanguage | undefined {
return Object.values(VariantAnalysisQueryLanguage).find(x => x === language);
}
export enum VariantAnalysisStatus {
InProgress = 'inProgress',
Succeeded = 'succeeded',

View File

@@ -11,8 +11,13 @@ import { Credentials } from '../../../authentication';
import { CliVersionConstraint, CodeQLCliServer } from '../../../cli';
import { CodeQLExtensionInterface } from '../../../extension';
import { setRemoteControllerRepo, setRemoteRepositoryLists } from '../../../config';
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 { Repository } from '../../../remote-queries/gh-api/repository';
import { VariantAnalysisStatus } from '../../../remote-queries/shared/variant-analysis';
describe('Remote queries', function() {
const baseDir = path.join(__dirname, '../../../../src/vscode-tests/cli-integration');
@@ -27,6 +32,8 @@ describe('Remote queries', function() {
let token: CancellationToken;
let progress: sinon.SinonSpy;
let showQuickPickSpy: sinon.SinonStub;
let getRepositoryFromNwoStub: sinon.SinonStub;
let liveResultsStub: sinon.SinonStub;
// use `function` so we have access to `this`
beforeEach(async function() {
@@ -55,208 +62,309 @@ describe('Remote queries', function() {
.onFirstCall().resolves({ repositories: ['github/vscode-codeql'] } as unknown as QuickPickItem)
.onSecondCall().resolves('javascript' as unknown as QuickPickItem);
const dummyRepository: Repository = {
id: 123,
name: 'vscode-codeql',
full_name: 'github/vscode-codeql',
private: false,
};
getRepositoryFromNwoStub = sandbox.stub(ghApiClient, 'getRepositoryFromNwo').resolves(dummyRepository);
// always run in the vscode-codeql repo
await setRemoteControllerRepo('github/vscode-codeql');
await setRemoteRepositoryLists({ 'vscode-codeql': ['github/vscode-codeql'] });
liveResultsStub = sandbox.stub(config, 'isVariantAnalysisLiveResultsEnabled').returns(false);
});
afterEach(() => {
afterEach(async () => {
sandbox.restore();
});
it('should run a remote query that is part of a qlpack', async () => {
const fileUri = getFile('data-remote-qlpack/in-pack.ql');
describe('when live results are not enabled', () => {
it('should run a remote query that is part of a qlpack', async () => {
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 queryPackRootDir = querySubmissionResult!.queryDirPath!;
printDirectoryContents(queryPackRootDir);
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, token);
expect(querySubmissionResult).to.be.ok;
const queryPackRootDir = querySubmissionResult!.queryDirPath!;
printDirectoryContents(queryPackRootDir);
// to retrieve the list of repositories
expect(showQuickPickSpy).to.have.been.calledOnce;
// to retrieve the list of repositories
expect(showQuickPickSpy).to.have.been.calledOnce;
// check a few files that we know should exist and others that we know should not
expect(getRepositoryFromNwoStub).to.have.been.calledOnce;
// the tarball to deliver to the server
expect(fs.readdirSync(queryPackRootDir).find(f => f.startsWith('qlpack-') && f.endsWith('-generated.tgz'))).not.to.be.undefined;
// check a few files that we know should exist and others that we know should not
const queryPackDir = path.join(queryPackRootDir, 'query-pack');
printDirectoryContents(queryPackDir);
// the tarball to deliver to the server
expect(fs.readdirSync(queryPackRootDir).find(f => f.startsWith('qlpack-') && f.endsWith('-generated.tgz'))).not.to.be.undefined;
expect(fs.existsSync(path.join(queryPackDir, 'in-pack.ql'))).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'lib.qll'))).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'qlpack.yml'))).to.be.true;
const queryPackDir = path.join(queryPackRootDir, 'query-pack');
printDirectoryContents(queryPackDir);
// depending on the cli version, we should have one of these files
expect(
fs.existsSync(path.join(queryPackDir, 'qlpack.lock.yml')) ||
fs.existsSync(path.join(queryPackDir, 'codeql-pack.lock.yml'))
).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'not-in-pack.ql'))).to.be.false;
expect(fs.existsSync(path.join(queryPackDir, 'in-pack.ql'))).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'lib.qll'))).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'qlpack.yml'))).to.be.true;
// the compiled pack
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/codeql-remote/query/0.0.0/');
printDirectoryContents(compiledPackDir);
// depending on the cli version, we should have one of these files
expect(
fs.existsSync(path.join(queryPackDir, 'qlpack.lock.yml')) ||
fs.existsSync(path.join(queryPackDir, 'codeql-pack.lock.yml'))
).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'not-in-pack.ql'))).to.be.false;
expect(fs.existsSync(path.join(compiledPackDir, 'in-pack.ql'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'lib.qll'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
// should have generated a correct qlpack file
const qlpackContents: any = yaml.load(fs.readFileSync(path.join(compiledPackDir, 'qlpack.yml'), 'utf8'));
expect(qlpackContents.name).to.equal('codeql-remote/query');
// the compiled pack
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/codeql-remote/query/0.0.0/');
printDirectoryContents(compiledPackDir);
// depending on the cli version, we should have one of these files
expect(
fs.existsSync(path.join(compiledPackDir, 'qlpack.lock.yml')) ||
fs.existsSync(path.join(compiledPackDir, 'codeql-pack.lock.yml'))
).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'not-in-pack.ql'))).to.be.false;
verifyQlPack(path.join(compiledPackDir, 'qlpack.yml'), 'in-pack.ql', '0.0.0', await pathSerializationBroken());
expect(fs.existsSync(path.join(compiledPackDir, 'in-pack.ql'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'lib.qll'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
// should have generated a correct qlpack file
const qlpackContents: any = yaml.load(fs.readFileSync(path.join(compiledPackDir, 'qlpack.yml'), 'utf8'));
expect(qlpackContents.name).to.equal('codeql-remote/query');
const libraryDir = path.join(compiledPackDir, '.codeql/libraries/codeql');
const packNames = fs.readdirSync(libraryDir).sort();
// depending on the cli version, we should have one of these files
expect(
fs.existsSync(path.join(compiledPackDir, 'qlpack.lock.yml')) ||
fs.existsSync(path.join(compiledPackDir, 'codeql-pack.lock.yml'))
).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'not-in-pack.ql'))).to.be.false;
verifyQlPack(path.join(compiledPackDir, 'qlpack.yml'), 'in-pack.ql', '0.0.0', await pathSerializationBroken());
// check dependencies.
// 2.7.4 and earlier have ['javascript-all', 'javascript-upgrades']
// later only have ['javascript-all']. ensure this test can handle either
expect(packNames.length).to.be.lessThan(3).and.greaterThan(0);
expect(packNames[0]).to.deep.equal('javascript-all');
const libraryDir = path.join(compiledPackDir, '.codeql/libraries/codeql');
const packNames = fs.readdirSync(libraryDir).sort();
// check dependencies.
// 2.7.4 and earlier have ['javascript-all', 'javascript-upgrades']
// later only have ['javascript-all']. ensure this test can handle either
expect(packNames.length).to.be.lessThan(3).and.greaterThan(0);
expect(packNames[0]).to.deep.equal('javascript-all');
});
it('should run a remote query that is not part of a qlpack', async () => {
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 queryPackRootDir = querySubmissionResult!.queryDirPath!;
// to retrieve the list of repositories
// and a second time to ask for the language
expect(showQuickPickSpy).to.have.been.calledTwice;
expect(getRepositoryFromNwoStub).to.have.been.calledOnce;
// check a few files that we know should exist and others that we know should not
// the tarball to deliver to the server
printDirectoryContents(queryPackRootDir);
expect(fs.readdirSync(queryPackRootDir).find(f => f.startsWith('qlpack-') && f.endsWith('-generated.tgz'))).not.to.be.undefined;
const queryPackDir = path.join(queryPackRootDir, 'query-pack');
printDirectoryContents(queryPackDir);
expect(fs.existsSync(path.join(queryPackDir, 'in-pack.ql'))).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'qlpack.yml'))).to.be.true;
// depending on the cli version, we should have one of these files
expect(
fs.existsSync(path.join(queryPackDir, 'qlpack.lock.yml')) ||
fs.existsSync(path.join(queryPackDir, 'codeql-pack.lock.yml'))
).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'lib.qll'))).to.be.false;
expect(fs.existsSync(path.join(queryPackDir, 'not-in-pack.ql'))).to.be.false;
// the compiled pack
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/codeql-remote/query/0.0.0/');
printDirectoryContents(compiledPackDir);
expect(fs.existsSync(path.join(compiledPackDir, 'in-pack.ql'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
verifyQlPack(path.join(compiledPackDir, 'qlpack.yml'), 'in-pack.ql', '0.0.0', await pathSerializationBroken());
// depending on the cli version, we should have one of these files
expect(
fs.existsSync(path.join(compiledPackDir, 'qlpack.lock.yml')) ||
fs.existsSync(path.join(compiledPackDir, 'codeql-pack.lock.yml'))
).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'lib.qll'))).to.be.false;
expect(fs.existsSync(path.join(compiledPackDir, 'not-in-pack.ql'))).to.be.false;
// should have generated a correct qlpack file
const qlpackContents: any = yaml.load(fs.readFileSync(path.join(compiledPackDir, 'qlpack.yml'), 'utf8'));
expect(qlpackContents.name).to.equal('codeql-remote/query');
expect(qlpackContents.version).to.equal('0.0.0');
expect(qlpackContents.dependencies?.['codeql/javascript-all']).to.equal('*');
const libraryDir = path.join(compiledPackDir, '.codeql/libraries/codeql');
printDirectoryContents(libraryDir);
const packNames = fs.readdirSync(libraryDir).sort();
// check dependencies.
// 2.7.4 and earlier have ['javascript-all', 'javascript-upgrades']
// later only have ['javascript-all']. ensure this test can handle either
expect(packNames.length).to.be.lessThan(3).and.greaterThan(0);
expect(packNames[0]).to.deep.equal('javascript-all');
});
it('should run a remote query that is nested inside a qlpack', async () => {
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 queryPackRootDir = querySubmissionResult!.queryDirPath!;
// to retrieve the list of repositories
expect(showQuickPickSpy).to.have.been.calledOnce;
expect(getRepositoryFromNwoStub).to.have.been.calledOnce;
// check a few files that we know should exist and others that we know should not
// the tarball to deliver to the server
printDirectoryContents(queryPackRootDir);
expect(fs.readdirSync(queryPackRootDir).find(f => f.startsWith('qlpack-') && f.endsWith('-generated.tgz'))).not.to.be.undefined;
const queryPackDir = path.join(queryPackRootDir, 'query-pack');
printDirectoryContents(queryPackDir);
expect(fs.existsSync(path.join(queryPackDir, 'subfolder/in-pack.ql'))).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'qlpack.yml'))).to.be.true;
// depending on the cli version, we should have one of these files
expect(
fs.existsSync(path.join(queryPackDir, 'qlpack.lock.yml')) ||
fs.existsSync(path.join(queryPackDir, 'codeql-pack.lock.yml'))
).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'otherfolder/lib.qll'))).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'not-in-pack.ql'))).to.be.false;
// the compiled pack
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/codeql-remote/query/0.0.0/');
printDirectoryContents(compiledPackDir);
expect(fs.existsSync(path.join(compiledPackDir, 'otherfolder/lib.qll'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'subfolder/in-pack.ql'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
verifyQlPack(path.join(compiledPackDir, 'qlpack.yml'), 'subfolder/in-pack.ql', '0.0.0', await pathSerializationBroken());
// depending on the cli version, we should have one of these files
expect(
fs.existsSync(path.join(compiledPackDir, 'qlpack.lock.yml')) ||
fs.existsSync(path.join(compiledPackDir, 'codeql-pack.lock.yml'))
).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'not-in-pack.ql'))).to.be.false;
// should have generated a correct qlpack file
const qlpackContents: any = yaml.load(fs.readFileSync(path.join(compiledPackDir, 'qlpack.yml'), 'utf8'));
expect(qlpackContents.name).to.equal('codeql-remote/query');
expect(qlpackContents.version).to.equal('0.0.0');
expect(qlpackContents.dependencies?.['codeql/javascript-all']).to.equal('*');
const libraryDir = path.join(compiledPackDir, '.codeql/libraries/codeql');
printDirectoryContents(libraryDir);
const packNames = fs.readdirSync(libraryDir).sort();
// check dependencies.
// 2.7.4 and earlier have ['javascript-all', 'javascript-upgrades']
// later only have ['javascript-all']. ensure this test can handle either
expect(packNames.length).to.be.lessThan(3).and.greaterThan(0);
expect(packNames[0]).to.deep.equal('javascript-all');
});
it('should cancel a run before uploading', async () => {
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, token);
token.isCancellationRequested = true;
try {
await promise;
assert.fail('should have thrown');
} catch (e) {
expect(e).to.be.instanceof(UserCancellationException);
}
});
});
it('should run a remote query that is not part of a qlpack', async () => {
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
describe('when live results are enabled', () => {
beforeEach(() => {
liveResultsStub.returns(true);
});
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, token);
expect(querySubmissionResult).to.be.ok;
const queryPackRootDir = querySubmissionResult!.queryDirPath!;
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',
};
// to retrieve the list of repositories
// and a second time to ask for the language
expect(showQuickPickSpy).to.have.been.calledTwice;
it('should run a variant analysis that is part of a qlpack', async () => {
const submitVariantAnalysisStub = sandbox.stub(ghApiClient, 'submitVariantAnalysis').resolves(dummyVariantAnalysis);
// check a few files that we know should exist and others that we know should not
const fileUri = getFile('data-remote-qlpack/in-pack.ql');
// the tarball to deliver to the server
printDirectoryContents(queryPackRootDir);
expect(fs.readdirSync(queryPackRootDir).find(f => f.startsWith('qlpack-') && f.endsWith('-generated.tgz'))).not.to.be.undefined;
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.status).to.be.equal(VariantAnalysisStatus.InProgress);
const queryPackDir = path.join(queryPackRootDir, 'query-pack');
printDirectoryContents(queryPackDir);
expect(getRepositoryFromNwoStub).to.have.been.calledOnce;
expect(fs.existsSync(path.join(queryPackDir, 'in-pack.ql'))).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'qlpack.yml'))).to.be.true;
// depending on the cli version, we should have one of these files
expect(
fs.existsSync(path.join(queryPackDir, 'qlpack.lock.yml')) ||
fs.existsSync(path.join(queryPackDir, 'codeql-pack.lock.yml'))
).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'lib.qll'))).to.be.false;
expect(fs.existsSync(path.join(queryPackDir, 'not-in-pack.ql'))).to.be.false;
expect(submitVariantAnalysisStub).to.have.been.calledOnce;
});
// the compiled pack
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/codeql-remote/query/0.0.0/');
printDirectoryContents(compiledPackDir);
expect(fs.existsSync(path.join(compiledPackDir, 'in-pack.ql'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
verifyQlPack(path.join(compiledPackDir, 'qlpack.yml'), 'in-pack.ql', '0.0.0', await pathSerializationBroken());
it('should run a remote query that is not part of a qlpack', async () => {
const submitVariantAnalysisStub = sandbox.stub(ghApiClient, 'submitVariantAnalysis').resolves(dummyVariantAnalysis);
// depending on the cli version, we should have one of these files
expect(
fs.existsSync(path.join(compiledPackDir, 'qlpack.lock.yml')) ||
fs.existsSync(path.join(compiledPackDir, 'codeql-pack.lock.yml'))
).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'lib.qll'))).to.be.false;
expect(fs.existsSync(path.join(compiledPackDir, 'not-in-pack.ql'))).to.be.false;
// should have generated a correct qlpack file
const qlpackContents: any = yaml.load(fs.readFileSync(path.join(compiledPackDir, 'qlpack.yml'), 'utf8'));
expect(qlpackContents.name).to.equal('codeql-remote/query');
expect(qlpackContents.version).to.equal('0.0.0');
expect(qlpackContents.dependencies?.['codeql/javascript-all']).to.equal('*');
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
const libraryDir = path.join(compiledPackDir, '.codeql/libraries/codeql');
printDirectoryContents(libraryDir);
const packNames = fs.readdirSync(libraryDir).sort();
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.status).to.be.equal(VariantAnalysisStatus.InProgress);
// check dependencies.
// 2.7.4 and earlier have ['javascript-all', 'javascript-upgrades']
// later only have ['javascript-all']. ensure this test can handle either
expect(packNames.length).to.be.lessThan(3).and.greaterThan(0);
expect(packNames[0]).to.deep.equal('javascript-all');
});
expect(getRepositoryFromNwoStub).to.have.been.calledOnce;
it('should run a remote query that is nested inside a qlpack', async () => {
const fileUri = getFile('data-remote-qlpack-nested/subfolder/in-pack.ql');
expect(submitVariantAnalysisStub).to.have.been.calledOnce;
});
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, token);
expect(querySubmissionResult).to.be.ok;
const queryPackRootDir = querySubmissionResult!.queryDirPath!;
it('should run a remote query that is nested inside a qlpack', async () => {
const submitVariantAnalysisStub = sandbox.stub(ghApiClient, 'submitVariantAnalysis').resolves(dummyVariantAnalysis);
// to retrieve the list of repositories
expect(showQuickPickSpy).to.have.been.calledOnce;
const fileUri = getFile('data-remote-qlpack-nested/subfolder/in-pack.ql');
// check a few files that we know should exist and others that we know should not
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.status).to.be.equal(VariantAnalysisStatus.InProgress);
// the tarball to deliver to the server
printDirectoryContents(queryPackRootDir);
expect(fs.readdirSync(queryPackRootDir).find(f => f.startsWith('qlpack-') && f.endsWith('-generated.tgz'))).not.to.be.undefined;
expect(getRepositoryFromNwoStub).to.have.been.calledOnce;
const queryPackDir = path.join(queryPackRootDir, 'query-pack');
printDirectoryContents(queryPackDir);
expect(submitVariantAnalysisStub).to.have.been.calledOnce;
});
expect(fs.existsSync(path.join(queryPackDir, 'subfolder/in-pack.ql'))).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'qlpack.yml'))).to.be.true;
// depending on the cli version, we should have one of these files
expect(
fs.existsSync(path.join(queryPackDir, 'qlpack.lock.yml')) ||
fs.existsSync(path.join(queryPackDir, 'codeql-pack.lock.yml'))
).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'otherfolder/lib.qll'))).to.be.true;
expect(fs.existsSync(path.join(queryPackDir, 'not-in-pack.ql'))).to.be.false;
it('should cancel a run before uploading', async () => {
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
// the compiled pack
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/codeql-remote/query/0.0.0/');
printDirectoryContents(compiledPackDir);
expect(fs.existsSync(path.join(compiledPackDir, 'otherfolder/lib.qll'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'subfolder/in-pack.ql'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
verifyQlPack(path.join(compiledPackDir, 'qlpack.yml'), 'subfolder/in-pack.ql', '0.0.0', await pathSerializationBroken());
const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, token);
// depending on the cli version, we should have one of these files
expect(
fs.existsSync(path.join(compiledPackDir, 'qlpack.lock.yml')) ||
fs.existsSync(path.join(compiledPackDir, 'codeql-pack.lock.yml'))
).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'not-in-pack.ql'))).to.be.false;
// should have generated a correct qlpack file
const qlpackContents: any = yaml.load(fs.readFileSync(path.join(compiledPackDir, 'qlpack.yml'), 'utf8'));
expect(qlpackContents.name).to.equal('codeql-remote/query');
expect(qlpackContents.version).to.equal('0.0.0');
expect(qlpackContents.dependencies?.['codeql/javascript-all']).to.equal('*');
token.isCancellationRequested = true;
const libraryDir = path.join(compiledPackDir, '.codeql/libraries/codeql');
printDirectoryContents(libraryDir);
const packNames = fs.readdirSync(libraryDir).sort();
// check dependencies.
// 2.7.4 and earlier have ['javascript-all', 'javascript-upgrades']
// later only have ['javascript-all']. ensure this test can handle either
expect(packNames.length).to.be.lessThan(3).and.greaterThan(0);
expect(packNames[0]).to.deep.equal('javascript-all');
});
it('should cancel a run before uploading', async () => {
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, token);
token.isCancellationRequested = true;
try {
await promise;
assert.fail('should have thrown');
} catch (e) {
expect(e).to.be.instanceof(UserCancellationException);
}
try {
await promise;
assert.fail('should have thrown');
} catch (e) {
expect(e).to.be.instanceof(UserCancellationException);
}
});
});
function verifyQlPack(qlpackPath: string, queryPath: string, packVersion: string, pathSerializationBroken: boolean) {

View File

@@ -1,11 +1,18 @@
import { expect } from 'chai';
import * as os from 'os';
import { parseResponse } from '../../../remote-queries/run-remote-query';
import { Repository } from '../../../remote-queries/shared/repository';
describe('run-remote-query', () => {
describe('parseResponse', () => {
const controllerRepository: Repository = {
id: 123,
fullName: 'org/name',
private: true
};
it('should parse a successful response', () => {
const result = parseResponse('org', 'name', {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
});
@@ -20,7 +27,7 @@ describe('run-remote-query', () => {
});
it('should parse a response with invalid repos', () => {
const result = parseResponse('org', 'name', {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
@@ -47,7 +54,7 @@ describe('run-remote-query', () => {
});
it('should parse a response with repos w/o databases', () => {
const result = parseResponse('org', 'name', {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
@@ -75,7 +82,7 @@ describe('run-remote-query', () => {
});
it('should parse a response with private repos', () => {
const result = parseResponse('org', 'name', {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
@@ -103,7 +110,7 @@ describe('run-remote-query', () => {
});
it('should parse a response with cutoff repos and cutoff repos count', () => {
const result = parseResponse('org', 'name', {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
@@ -132,7 +139,7 @@ describe('run-remote-query', () => {
});
it('should parse a response with cutoff repos count but not cutoff repos', () => {
const result = parseResponse('org', 'name', {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
@@ -159,7 +166,7 @@ describe('run-remote-query', () => {
});
it('should parse a response with invalid repos and repos w/o databases', () => {
const result = parseResponse('org', 'name', {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
@@ -191,7 +198,7 @@ describe('run-remote-query', () => {
});
it('should parse a response with one repo of each category, and not pluralize "repositories"', () => {
const result = parseResponse('org', 'name', {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b'],
errors: {

View File

@@ -0,0 +1,12 @@
import { expect } from 'chai';
import { parseVariantAnalysisQueryLanguage, VariantAnalysisQueryLanguage } from '../../src/remote-queries/shared/variant-analysis';
describe('parseVariantAnalysisQueryLanguage', () => {
it('parses a valid language', () => {
expect(parseVariantAnalysisQueryLanguage('javascript')).to.equal(VariantAnalysisQueryLanguage.Javascript);
});
it('returns undefined for an valid language', () => {
expect(parseVariantAnalysisQueryLanguage('rubbish')).to.not.exist;
});
});