Merge remote-tracking branch 'origin/main' into koesie10/copy-repo-list

This commit is contained in:
Koen Vlaswinkel
2022-11-11 15:23:42 +01:00
10 changed files with 635 additions and 586 deletions

View File

@@ -33,6 +33,7 @@ import {
CliConfigListener,
DistributionConfigListener,
isCanary,
isVariantAnalysisLiveResultsEnabled,
joinOrderWarningThreshold,
MAX_QUERIES,
QueryHistoryConfigListener,
@@ -489,13 +490,13 @@ async function activateWithInstalledDistribution(
const variantAnalysisStorageDir = path.join(ctx.globalStorageUri.fsPath, 'variant-analyses');
await fs.ensureDir(variantAnalysisStorageDir);
const variantAnalysisResultsManager = new VariantAnalysisResultsManager(cliServer, logger);
const variantAnalysisManager = new VariantAnalysisManager(ctx, variantAnalysisStorageDir, variantAnalysisResultsManager);
const variantAnalysisManager = new VariantAnalysisManager(ctx, cliServer, variantAnalysisStorageDir, variantAnalysisResultsManager);
ctx.subscriptions.push(variantAnalysisManager);
ctx.subscriptions.push(variantAnalysisResultsManager);
ctx.subscriptions.push(workspace.registerTextDocumentContentProvider('codeql-variant-analysis', createVariantAnalysisContentProvider(variantAnalysisManager)));
void logger.log('Initializing remote queries manager.');
const rqm = new RemoteQueriesManager(ctx, cliServer, queryStorageDir, logger, variantAnalysisManager);
const rqm = new RemoteQueriesManager(ctx, cliServer, queryStorageDir, logger);
ctx.subscriptions.push(rqm);
void logger.log('Initializing query history.');
@@ -906,11 +907,20 @@ async function activateWithInstalledDistribution(
step: 0,
message: 'Getting credentials'
});
await rqm.runRemoteQuery(
uri || window.activeTextEditor?.document.uri,
progress,
token
);
if (isVariantAnalysisLiveResultsEnabled()) {
await variantAnalysisManager.runVariantAnalysis(
uri || window.activeTextEditor?.document.uri,
progress,
token
);
} else {
await rqm.runRemoteQuery(
uri || window.activeTextEditor?.document.uri,
progress,
token
);
}
} else {
throw new Error('Variant analysis requires the CodeQL Canary version to run.');
}

View File

@@ -0,0 +1,87 @@
import * as os from 'os';
import { Credentials } from '../authentication';
import { RepositorySelection } from './repository-selection';
import { Repository } from './shared/repository';
import { RemoteQueriesResponse } from './gh-api/remote-queries';
import * as ghApiClient from './gh-api/gh-api-client';
import { showAndLogErrorMessage, showAndLogInformationMessage } from '../helpers';
import { getErrorMessage } from '../pure/helpers-pure';
import { pluralize } from '../pure/word';
export async function runRemoteQueriesApiRequest(
credentials: Credentials,
ref: string,
language: string,
repoSelection: RepositorySelection,
controllerRepo: Repository,
queryPackBase64: string,
): Promise<void | RemoteQueriesResponse> {
try {
const response = await ghApiClient.submitRemoteQueries(credentials, {
ref,
language,
repositories: repoSelection.repositories,
repositoryLists: repoSelection.repositoryLists,
repositoryOwners: repoSelection.owners,
queryPack: queryPackBase64,
controllerRepoId: controllerRepo.id,
});
const { popupMessage, logMessage } = parseResponse(controllerRepo, response);
void showAndLogInformationMessage(popupMessage, { fullMessage: logMessage });
return response;
} catch (error: any) {
if (error.status === 404) {
void showAndLogErrorMessage(`Controller repository was not found. Please make sure it's a valid repo name.${eol}`);
} else {
void showAndLogErrorMessage(getErrorMessage(error));
}
}
}
const eol = os.EOL;
const eol2 = os.EOL + os.EOL;
// exported for testing only
export function parseResponse(controllerRepo: Repository, response: RemoteQueriesResponse) {
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/${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/${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;
logMessage += `${eol2}Some repositories could not be scheduled.`;
if (invalid_repositories?.length) {
logMessage += `${eol2}${pluralize(invalid_repositories.length, 'repository', 'repositories')} invalid and could not be found:${eol}${invalid_repositories.join(', ')}`;
}
if (repositories_without_database?.length) {
logMessage += `${eol2}${pluralize(repositories_without_database.length, 'repository', 'repositories')} did not have a CodeQL database available:${eol}${repositories_without_database.join(', ')}`;
logMessage += `${eol}For each public repository that has not yet been added to the database service, we will try to create a database next time the store is updated.`;
}
if (private_repositories?.length) {
logMessage += `${eol2}${pluralize(private_repositories.length, 'repository', 'repositories')} not public:${eol}${private_repositories.join(', ')}`;
logMessage += `${eol}When using a public controller repository, only public repositories can be queried.`;
}
if (cutoff_repositories_count) {
logMessage += `${eol2}${pluralize(cutoff_repositories_count, 'repository', 'repositories')} over the limit for a single request`;
if (cutoff_repositories) {
logMessage += `:${eol}${cutoff_repositories.join(', ')}`;
if (cutoff_repositories_count !== cutoff_repositories.length) {
const moreRepositories = cutoff_repositories_count - cutoff_repositories.length;
logMessage += `${eol}...${eol}And another ${pluralize(moreRepositories, 'repository', 'repositories')}.`;
}
} else {
logMessage += '.';
}
logMessage += `${eol}Repositories were selected based on how recently they had been updated.`;
}
}
return {
popupMessage,
logMessage
};
}

View File

@@ -1,4 +1,4 @@
import { CancellationToken, commands, EventEmitter, ExtensionContext, Uri, env, window } from 'vscode';
import { CancellationToken, commands, EventEmitter, ExtensionContext, Uri, env } from 'vscode';
import { nanoid } from 'nanoid';
import * as path from 'path';
import * as fs from 'fs-extra';
@@ -9,9 +9,11 @@ import { CodeQLCliServer } from '../cli';
import { ProgressCallback } from '../commandRunner';
import { createTimestampFile, showAndLogErrorMessage, showAndLogInformationMessage, showInformationMessageWithAction } from '../helpers';
import { Logger } from '../logging';
import { runRemoteQuery } from './run-remote-query';
import {
prepareRemoteQueryRun,
} from './run-remote-query';
import { RemoteQueriesView } from './remote-queries-view';
import { RemoteQuery } from './remote-query';
import { buildRemoteQueryEntity, RemoteQuery } from './remote-query';
import { RemoteQueriesMonitor } from './remote-queries-monitor';
import { getRemoteQueryIndex, getRepositoriesMetadata, RepositoriesMetadata } from './gh-api/gh-actions-api-client';
import { RemoteQueryResultIndex } from './remote-query-result-index';
@@ -22,7 +24,7 @@ import { assertNever } from '../pure/helpers-pure';
import { QueryStatus } from '../query-status';
import { DisposableObject } from '../pure/disposable-object';
import { AnalysisResults } from './shared/analysis-result';
import { VariantAnalysisManager } from './variant-analysis-manager';
import { runRemoteQueriesApiRequest } from './remote-queries-api';
const autoDownloadMaxSize = 300 * 1024;
const autoDownloadMaxCount = 100;
@@ -57,7 +59,6 @@ export class RemoteQueriesManager extends DisposableObject {
private readonly remoteQueriesMonitor: RemoteQueriesMonitor;
private readonly analysesResultsManager: AnalysesResultsManager;
private readonly variantAnalysisManager: VariantAnalysisManager;
private readonly view: RemoteQueriesView;
constructor(
@@ -65,13 +66,11 @@ export class RemoteQueriesManager extends DisposableObject {
private readonly cliServer: CodeQLCliServer,
private readonly storagePath: string,
logger: Logger,
variantAnalysisManager: VariantAnalysisManager,
) {
super();
this.analysesResultsManager = new AnalysesResultsManager(ctx, cliServer, storagePath, logger);
this.view = new RemoteQueriesView(ctx, logger, this.analysesResultsManager);
this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger);
this.variantAnalysisManager = variantAnalysisManager;
this.remoteQueryAddedEventEmitter = this.push(new EventEmitter<NewQueryEvent>());
this.remoteQueryRemovedEventEmitter = this.push(new EventEmitter<RemovedQueryEvent>());
@@ -122,18 +121,42 @@ export class RemoteQueriesManager extends DisposableObject {
): Promise<void> {
const credentials = await Credentials.initialize(this.ctx);
const querySubmission = await runRemoteQuery(this.cliServer, credentials, uri || window.activeTextEditor?.document.uri, progress, token, this.variantAnalysisManager);
const {
actionBranch,
base64Pack,
repoSelection,
queryFile,
queryMetadata,
controllerRepo,
queryStartTime,
language,
} = await prepareRemoteQueryRun(this.cliServer, credentials, uri, progress, token);
if (querySubmission?.query) {
const query = querySubmission.query;
const queryId = this.createQueryId();
const apiResponse = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, controllerRepo, base64Pack);
await this.prepareStorageDirectory(queryId);
await this.storeJsonFile(queryId, 'query.json', query);
this.remoteQueryAddedEventEmitter.fire({ queryId, query });
void commands.executeCommand('codeQL.monitorRemoteQuery', queryId, query);
if (!apiResponse) {
return;
}
const workflowRunId = apiResponse.workflow_run_id;
const repositoryCount = apiResponse.repositories_queried.length;
const query = await buildRemoteQueryEntity(
queryFile,
queryMetadata,
controllerRepo,
queryStartTime,
workflowRunId,
language,
repositoryCount
);
const queryId = this.createQueryId();
await this.prepareStorageDirectory(queryId);
await this.storeJsonFile(queryId, 'query.json', query);
this.remoteQueryAddedEventEmitter.fire({ queryId, query });
void commands.executeCommand('codeQL.monitorRemoteQuery', queryId, query);
}
public async monitorRemoteQuery(

View File

@@ -1,12 +1,44 @@
import { Repository } from './repository';
import * as fs from 'fs-extra';
import { Repository as RemoteRepository } from './repository';
import { QueryMetadata } from '../pure/interface-types';
import { getQueryName } from './run-remote-query';
import { Repository } from './shared/repository';
export interface RemoteQuery {
queryName: string;
queryFilePath: string;
queryText: string;
language: string;
controllerRepository: Repository;
controllerRepository: RemoteRepository;
executionStartTime: number; // Use number here since it needs to be serialized and desserialized.
actionsWorkflowRunId: number;
repositoryCount: number;
}
export async function buildRemoteQueryEntity(
queryFilePath: string,
queryMetadata: QueryMetadata | undefined,
controllerRepo: Repository,
queryStartTime: number,
workflowRunId: number,
language: string,
repositoryCount: number
): Promise<RemoteQuery> {
const queryName = getQueryName(queryMetadata, queryFilePath);
const queryText = await fs.readFile(queryFilePath, 'utf8');
const [owner, name] = controllerRepo.fullName.split('/');
return {
queryName,
queryFilePath,
queryText,
language,
controllerRepository: {
owner,
name,
},
executionStartTime: queryStartTime,
actionsWorkflowRunId: workflowRunId,
repositoryCount,
};
}

View File

@@ -1,37 +1,26 @@
import { CancellationToken, commands, Uri, window } from 'vscode';
import { CancellationToken, Uri, window } from 'vscode';
import * as path from 'path';
import * as yaml from 'js-yaml';
import * as fs from 'fs-extra';
import * as os from 'os';
import * as tmp from 'tmp-promise';
import {
askForLanguage,
findLanguage,
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
showAndLogInformationMessage,
tryGetQueryMetadata,
tmpDir,
} from '../helpers';
import { Credentials } from '../authentication';
import * as cli from '../cli';
import { logger } from '../logging';
import { getActionBranch, getRemoteControllerRepo, isVariantAnalysisLiveResultsEnabled, setRemoteControllerRepo } from '../config';
import { getActionBranch, getRemoteControllerRepo, setRemoteControllerRepo } from '../config';
import { ProgressCallback, UserCancellationException } from '../commandRunner';
import { 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 { pluralize } from '../pure/word';
import { REPO_REGEX } from '../pure/helpers-pure';
import * as ghApiClient from './gh-api/gh-api-client';
import { RemoteQueriesResponse } from './gh-api/remote-queries';
import { getRepositorySelection, isValidSelection, RepositorySelection } from './repository-selection';
import { parseVariantAnalysisQueryLanguage, VariantAnalysisSubmission } from './shared/variant-analysis';
import { Repository } from './shared/repository';
import { processVariantAnalysis } from './variant-analysis-processor';
import { VariantAnalysisManager } from './variant-analysis-manager';
import { CodeQLCliServer } from '../cli';
export interface QlPack {
name: string;
@@ -275,175 +264,6 @@ export async function prepareRemoteQueryRun(
};
}
export async function runRemoteQuery(
cliServer: CodeQLCliServer,
credentials: Credentials,
uri: Uri | undefined,
progress: ProgressCallback,
token: CancellationToken,
variantAnalysisManager: VariantAnalysisManager,
): Promise<void | RemoteQuerySubmissionResult> {
if (!(await cliServer.cliConstraints.supportsRemoteQueries())) {
throw new Error(`Variant analysis is not supported by this version of CodeQL. Please upgrade to v${cli.CliVersionConstraint.CLI_VERSION_REMOTE_QUERIES
} or later.`);
}
const {
actionBranch,
base64Pack,
repoSelection,
queryFile,
queryMetadata,
controllerRepo,
queryStartTime,
language,
} = await prepareRemoteQueryRun(cliServer, credentials, uri, progress, token);
if (isVariantAnalysisLiveResultsEnabled()) {
const queryName = getQueryName(queryMetadata, queryFile);
const variantAnalysisLanguage = parseVariantAnalysisQueryLanguage(language);
if (variantAnalysisLanguage === undefined) {
throw new UserCancellationException(`Found unsupported language: ${language}`);
}
const queryText = await fs.readFile(queryFile, 'utf8');
const variantAnalysisSubmission: VariantAnalysisSubmission = {
startTime: queryStartTime,
actionRepoRef: actionBranch,
controllerRepoId: controllerRepo.id,
query: {
name: queryName,
filePath: queryFile,
pack: base64Pack,
language: variantAnalysisLanguage,
text: queryText,
},
databases: {
repositories: repoSelection.repositories,
repositoryLists: repoSelection.repositoryLists,
repositoryOwners: repoSelection.owners
}
};
const variantAnalysisResponse = await ghApiClient.submitVariantAnalysis(
credentials,
variantAnalysisSubmission
);
const processedVariantAnalysis = processVariantAnalysis(variantAnalysisSubmission, variantAnalysisResponse);
await variantAnalysisManager.onVariantAnalysisSubmitted(processedVariantAnalysis);
void logger.log(`Variant analysis:\n${JSON.stringify(processedVariantAnalysis, null, 2)}`);
void showAndLogInformationMessage(`Variant analysis ${processedVariantAnalysis.query.name} submitted for processing`);
void commands.executeCommand('codeQL.openVariantAnalysisView', processedVariantAnalysis.id);
void commands.executeCommand('codeQL.monitorVariantAnalysis', processedVariantAnalysis);
return { variantAnalysis: processedVariantAnalysis };
} else {
const apiResponse = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, controllerRepo, base64Pack);
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 };
}
}
async function runRemoteQueriesApiRequest(
credentials: Credentials,
ref: string,
language: string,
repoSelection: RepositorySelection,
controllerRepo: Repository,
queryPackBase64: string,
): Promise<void | RemoteQueriesResponse> {
try {
const response = await ghApiClient.submitRemoteQueries(credentials, {
ref,
language,
repositories: repoSelection.repositories,
repositoryLists: repoSelection.repositoryLists,
repositoryOwners: repoSelection.owners,
queryPack: queryPackBase64,
controllerRepoId: controllerRepo.id,
});
const { popupMessage, logMessage } = parseResponse(controllerRepo, response);
void showAndLogInformationMessage(popupMessage, { fullMessage: logMessage });
return response;
} catch (error: any) {
if (error.status === 404) {
void showAndLogErrorMessage(`Controller repository was not found. Please make sure it's a valid repo name.${eol}`);
} else {
void showAndLogErrorMessage(getErrorMessage(error));
}
}
}
const eol = os.EOL;
const eol2 = os.EOL + os.EOL;
// exported for testing only
export function parseResponse(controllerRepo: Repository, response: RemoteQueriesResponse) {
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/${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/${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;
logMessage += `${eol2}Some repositories could not be scheduled.`;
if (invalid_repositories?.length) {
logMessage += `${eol2}${pluralize(invalid_repositories.length, 'repository', 'repositories')} invalid and could not be found:${eol}${invalid_repositories.join(', ')}`;
}
if (repositories_without_database?.length) {
logMessage += `${eol2}${pluralize(repositories_without_database.length, 'repository', 'repositories')} did not have a CodeQL database available:${eol}${repositories_without_database.join(', ')}`;
logMessage += `${eol}For each public repository that has not yet been added to the database service, we will try to create a database next time the store is updated.`;
}
if (private_repositories?.length) {
logMessage += `${eol2}${pluralize(private_repositories.length, 'repository', 'repositories')} not public:${eol}${private_repositories.join(', ')}`;
logMessage += `${eol}When using a public controller repository, only public repositories can be queried.`;
}
if (cutoff_repositories_count) {
logMessage += `${eol2}${pluralize(cutoff_repositories_count, 'repository', 'repositories')} over the limit for a single request`;
if (cutoff_repositories) {
logMessage += `:${eol}${cutoff_repositories.join(', ')}`;
if (cutoff_repositories_count !== cutoff_repositories.length) {
const moreRepositories = cutoff_repositories_count - cutoff_repositories.length;
logMessage += `${eol}...${eol}And another ${pluralize(moreRepositories, 'repository', 'repositories')}.`;
}
} else {
logMessage += '.';
}
logMessage += `${eol}Repositories were selected based on how recently they had been updated.`;
}
}
return {
popupMessage,
logMessage
};
}
/**
* Updates the default suite of the query pack. This is used to ensure
* only the specified query is run.
@@ -468,35 +288,7 @@ async function ensureNameAndSuite(queryPackDir: string, packRelativePath: string
await fs.writeFile(packPath, yaml.dump(qlpack));
}
async function buildRemoteQueryEntity(
queryFilePath: string,
queryMetadata: QueryMetadata | undefined,
controllerRepo: Repository,
queryStartTime: number,
workflowRunId: number,
language: string,
repositoryCount: number
): Promise<RemoteQuery> {
const queryName = getQueryName(queryMetadata, queryFilePath);
const queryText = await fs.readFile(queryFilePath, 'utf8');
const [owner, name] = controllerRepo.fullName.split('/');
return {
queryName,
queryFilePath,
queryText,
language,
controllerRepository: {
owner,
name,
},
executionStartTime: queryStartTime,
actionsWorkflowRunId: workflowRunId,
repositoryCount,
};
}
function getQueryName(queryMetadata: QueryMetadata | undefined, queryFilePath: string): string {
export 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);
}

View File

@@ -1,31 +1,37 @@
import * as path from 'path';
import * as ghApiClient from './gh-api/gh-api-client';
import { CancellationToken, commands, env, EventEmitter, ExtensionContext, window } from 'vscode';
import { CancellationToken, commands, env, EventEmitter, ExtensionContext, Uri, window } from 'vscode';
import { DisposableObject } from '../pure/disposable-object';
import { Credentials } from '../authentication';
import { VariantAnalysisMonitor } from './variant-analysis-monitor';
import {
isVariantAnalysisComplete,
isVariantAnalysisComplete, parseVariantAnalysisQueryLanguage,
VariantAnalysis,
VariantAnalysisQueryLanguage,
VariantAnalysisRepositoryTask,
VariantAnalysisScannedRepository,
VariantAnalysisScannedRepositoryDownloadStatus,
VariantAnalysisScannedRepositoryResult,
VariantAnalysisScannedRepositoryState
VariantAnalysisScannedRepositoryState, VariantAnalysisSubmission
} from './shared/variant-analysis';
import { getErrorMessage } from '../pure/helpers-pure';
import { VariantAnalysisView } from './variant-analysis-view';
import { VariantAnalysisViewManager } from './variant-analysis-view-manager';
import { VariantAnalysisResultsManager } from './variant-analysis-results-manager';
import { getControllerRepo } from './run-remote-query';
import { processUpdatedVariantAnalysis, processVariantAnalysisRepositoryTask } from './variant-analysis-processor';
import { getControllerRepo, getQueryName, prepareRemoteQueryRun } from './run-remote-query';
import {
processUpdatedVariantAnalysis,
processVariantAnalysis,
processVariantAnalysisRepositoryTask
} from './variant-analysis-processor';
import PQueue from 'p-queue';
import { createTimestampFile, showAndLogErrorMessage, showAndLogInformationMessage } from '../helpers';
import * as fs from 'fs-extra';
import * as os from 'os';
import { cancelVariantAnalysis } from './gh-api/gh-actions-api-client';
import { ProgressCallback, UserCancellationException } from '../commandRunner';
import { CodeQLCliServer } from '../cli';
export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager<VariantAnalysisView> {
private static readonly REPO_STATES_FILENAME = 'repo_states.json';
@@ -48,6 +54,7 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
constructor(
private readonly ctx: ExtensionContext,
private readonly cliServer: CodeQLCliServer,
private readonly storagePath: string,
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager
) {
@@ -59,6 +66,65 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
this.variantAnalysisResultsManager.onResultLoaded(this.onRepoResultLoaded.bind(this));
}
public async runVariantAnalysis(
uri: Uri | undefined,
progress: ProgressCallback,
token: CancellationToken,
): Promise<void> {
const credentials = await Credentials.initialize(this.ctx);
const {
actionBranch,
base64Pack,
repoSelection,
queryFile,
queryMetadata,
controllerRepo,
queryStartTime,
language,
} = await prepareRemoteQueryRun(this.cliServer, credentials, uri, progress, token);
const queryName = getQueryName(queryMetadata, queryFile);
const variantAnalysisLanguage = parseVariantAnalysisQueryLanguage(language);
if (variantAnalysisLanguage === undefined) {
throw new UserCancellationException(`Found unsupported language: ${language}`);
}
const queryText = await fs.readFile(queryFile, 'utf8');
const variantAnalysisSubmission: VariantAnalysisSubmission = {
startTime: queryStartTime,
actionRepoRef: actionBranch,
controllerRepoId: controllerRepo.id,
query: {
name: queryName,
filePath: queryFile,
pack: base64Pack,
language: variantAnalysisLanguage,
text: queryText,
},
databases: {
repositories: repoSelection.repositories,
repositoryLists: repoSelection.repositoryLists,
repositoryOwners: repoSelection.owners
}
};
const variantAnalysisResponse = await ghApiClient.submitVariantAnalysis(
credentials,
variantAnalysisSubmission
);
const processedVariantAnalysis = processVariantAnalysis(variantAnalysisSubmission, variantAnalysisResponse);
await this.onVariantAnalysisSubmitted(processedVariantAnalysis);
void showAndLogInformationMessage(`Variant analysis ${processedVariantAnalysis.query.name} submitted for processing`);
void commands.executeCommand('codeQL.openVariantAnalysisView', processedVariantAnalysis.id);
void commands.executeCommand('codeQL.monitorVariantAnalysis', processedVariantAnalysis);
}
public async rehydrateVariantAnalysis(variantAnalysis: VariantAnalysis) {
if (!(await this.variantAnalysisRecordExists(variantAnalysis.id))) {
// In this case, the variant analysis was deleted from disk, most likely because
@@ -166,7 +232,7 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
this._onVariantAnalysisStatusUpdated.fire(variantAnalysis);
}
public async onVariantAnalysisSubmitted(variantAnalysis: VariantAnalysis): Promise<void> {
private async onVariantAnalysisSubmitted(variantAnalysis: VariantAnalysis): Promise<void> {
await this.setVariantAnalysis(variantAnalysis);
await this.prepareStorageDirectory(variantAnalysis.id);

View File

@@ -1,31 +1,24 @@
import { assert, expect } from 'chai';
import * as path from 'path';
import * as sinon from 'sinon';
import { CancellationTokenSource, ExtensionContext, extensions, QuickPickItem, Uri, window } from 'vscode';
import { CancellationTokenSource, commands, ExtensionContext, extensions, QuickPickItem, Uri, window } from 'vscode';
import * as os from 'os';
import * as yaml from 'js-yaml';
import { QlPack, runRemoteQuery } from '../../../remote-queries/run-remote-query';
import { Credentials } from '../../../authentication';
import { QlPack } from '../../../remote-queries/run-remote-query';
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 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';
import { createMockExtensionContext } from '../../no-workspace';
import { VariantAnalysisManager } from '../../../remote-queries/variant-analysis-manager';
import { OutputChannelLogger } from '../../../logging';
import { VariantAnalysisResultsManager } from '../../../remote-queries/variant-analysis-results-manager';
import { RemoteQueriesSubmission } from '../../../remote-queries/shared/remote-queries';
import { readBundledPack } from '../../utils/bundled-pack-helpers';
import { RemoteQueriesManager } from '../../../remote-queries/remote-queries-manager';
import { Credentials } from '../../../authentication';
describe('Remote queries', function() {
const baseDir = path.join(__dirname, '../../../../src/vscode-tests/cli-integration');
@@ -36,16 +29,13 @@ describe('Remote queries', function() {
this.timeout(3 * 60 * 1000);
let cli: CodeQLCliServer;
let credentials: Credentials = {} as unknown as Credentials;
let cancellationTokenSource: CancellationTokenSource;
let progress: sinon.SinonSpy;
let showQuickPickSpy: sinon.SinonStub;
let getRepositoryFromNwoStub: sinon.SinonStub;
let liveResultsStub: sinon.SinonStub;
let ctx: ExtensionContext;
let logger: any;
let variantAnalysisManager: VariantAnalysisManager;
let variantAnalysisResultsManager: VariantAnalysisResultsManager;
let remoteQueriesManager: RemoteQueriesManager;
// use `function` so we have access to `this`
beforeEach(async function() {
@@ -60,15 +50,13 @@ describe('Remote queries', function() {
ctx = createMockExtensionContext();
logger = new OutputChannelLogger('test-logger');
variantAnalysisResultsManager = new VariantAnalysisResultsManager(cli, logger);
variantAnalysisManager = new VariantAnalysisManager(ctx, 'fake-storage-dir', variantAnalysisResultsManager);
remoteQueriesManager = new RemoteQueriesManager(ctx, cli, 'fake-storage-dir', logger);
if (!(await cli.cliConstraints.supportsRemoteQueries())) {
console.log(`Remote queries are not supported on CodeQL CLI v${CliVersionConstraint.CLI_VERSION_REMOTE_QUERIES
}. Skipping this test.`);
this.skip();
}
credentials = {} as unknown as Credentials;
cancellationTokenSource = new CancellationTokenSource();
@@ -90,17 +78,25 @@ describe('Remote queries', function() {
await setRemoteControllerRepo('github/vscode-codeql');
await setRemoteRepositoryLists({ 'vscode-codeql': ['github/vscode-codeql'] });
liveResultsStub = sandbox.stub(config, 'isVariantAnalysisLiveResultsEnabled').returns(false);
const mockCredentials = {
getOctokit: () => Promise.resolve({
request: undefined,
})
} as unknown as Credentials;
sandbox.stub(Credentials, 'initialize').resolves(mockCredentials);
});
afterEach(async () => {
sandbox.restore();
});
describe('when live results are not enabled', () => {
describe('runRemoteQuery', () => {
let mockSubmitRemoteQueries: sinon.SinonStub;
let executeCommandSpy: sinon.SinonStub;
beforeEach(() => {
executeCommandSpy = sandbox.stub(commands, 'executeCommand').callThrough();
mockSubmitRemoteQueries = sandbox.stub(ghApiClient, 'submitRemoteQueries').resolves({
workflow_run_id: 20,
repositories_queried: ['octodemo/hello-world-1'],
@@ -110,10 +106,10 @@ describe('Remote queries', function() {
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, progress, cancellationTokenSource.token, variantAnalysisManager);
expect(querySubmissionResult).to.be.ok;
await remoteQueriesManager.runRemoteQuery(fileUri, progress, cancellationTokenSource.token);
expect(mockSubmitRemoteQueries).to.have.been.calledOnce;
expect(executeCommandSpy).to.have.been.calledWith('codeQL.monitorRemoteQuery', sinon.match.string, sinon.match.has('queryFilePath', fileUri.fsPath));
const request: RemoteQueriesSubmission = mockSubmitRemoteQueries.getCall(0).lastArg;
@@ -155,10 +151,10 @@ describe('Remote queries', function() {
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, progress, cancellationTokenSource.token, variantAnalysisManager);
expect(querySubmissionResult).to.be.ok;
await remoteQueriesManager.runRemoteQuery(fileUri, progress, cancellationTokenSource.token);
expect(mockSubmitRemoteQueries).to.have.been.calledOnce;
expect(executeCommandSpy).to.have.been.calledWith('codeQL.monitorRemoteQuery', sinon.match.string, sinon.match.has('queryFilePath', fileUri.fsPath));
const request: RemoteQueriesSubmission = mockSubmitRemoteQueries.getCall(0).lastArg;
@@ -203,10 +199,10 @@ describe('Remote queries', function() {
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, progress, cancellationTokenSource.token, variantAnalysisManager);
expect(querySubmissionResult).to.be.ok;
await remoteQueriesManager.runRemoteQuery(fileUri, progress, cancellationTokenSource.token);
expect(mockSubmitRemoteQueries).to.have.been.calledOnce;
expect(executeCommandSpy).to.have.been.calledWith('codeQL.monitorRemoteQuery', sinon.match.string, sinon.match.has('queryFilePath', fileUri.fsPath));
const request: RemoteQueriesSubmission = mockSubmitRemoteQueries.getCall(0).lastArg;
@@ -250,72 +246,7 @@ describe('Remote queries', function() {
it('should cancel a run before uploading', async () => {
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
const promise = runRemoteQuery(cli, credentials, fileUri, progress, cancellationTokenSource.token, variantAnalysisManager);
cancellationTokenSource.cancel();
try {
await promise;
assert.fail('should have thrown');
} catch (e) {
expect(e).to.be.instanceof(UserCancellationException);
}
});
});
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);
});
it('should run a variant analysis that is part of a qlpack', async () => {
const fileUri = getFile('data-remote-qlpack/in-pack.ql');
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, progress, cancellationTokenSource.token, variantAnalysisManager);
expect(querySubmissionResult).to.be.ok;
const variantAnalysis = querySubmissionResult!.variantAnalysis!;
expect(variantAnalysis.id).to.be.equal(mockApiResponse.id);
expect(variantAnalysis.status).to.be.equal(VariantAnalysisStatus.InProgress);
expect(getRepositoryFromNwoStub).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 fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, progress, cancellationTokenSource.token, variantAnalysisManager);
expect(querySubmissionResult).to.be.ok;
const variantAnalysis = querySubmissionResult!.variantAnalysis!;
expect(variantAnalysis.id).to.be.equal(mockApiResponse.id);
expect(variantAnalysis.status).to.be.equal(VariantAnalysisStatus.InProgress);
expect(getRepositoryFromNwoStub).to.have.been.calledOnce;
expect(mockSubmitVariantAnalysis).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');
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, progress, cancellationTokenSource.token, variantAnalysisManager);
expect(querySubmissionResult).to.be.ok;
const variantAnalysis = querySubmissionResult!.variantAnalysis!;
expect(variantAnalysis.id).to.be.equal(mockApiResponse.id);
expect(variantAnalysis.status).to.be.equal(VariantAnalysisStatus.InProgress);
expect(getRepositoryFromNwoStub).to.have.been.calledOnce;
expect(mockSubmitVariantAnalysis).to.have.been.calledOnce;
});
it('should cancel a run before uploading', async () => {
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
const promise = runRemoteQuery(cli, credentials, fileUri, progress, cancellationTokenSource.token, variantAnalysisManager);
const promise = remoteQueriesManager.runRemoteQuery(fileUri, progress, cancellationTokenSource.token);
cancellationTokenSource.cancel();

View File

@@ -1,6 +1,6 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { CancellationTokenSource, commands, env, extensions } from 'vscode';
import { assert, expect } from 'chai';
import { CancellationTokenSource, commands, env, extensions, QuickPickItem, Uri, window } from 'vscode';
import { CodeQLExtensionInterface } from '../../../extension';
import { logger } from '../../../logging';
import * as config from '../../../config';
@@ -11,7 +11,7 @@ import * as fs from 'fs-extra';
import * as path from 'path';
import { VariantAnalysisManager } from '../../../remote-queries/variant-analysis-manager';
import { CodeQLCliServer } from '../../../cli';
import { CliVersionConstraint, CodeQLCliServer } from '../../../cli';
import { storagePath } from '../global.helper';
import { VariantAnalysisResultsManager } from '../../../remote-queries/variant-analysis-results-manager';
import { createMockVariantAnalysis } from '../../factories/remote-queries/shared/variant-analysis';
@@ -28,13 +28,21 @@ import {
} from '../../../remote-queries/shared/variant-analysis';
import { createTimestampFile } from '../../../helpers';
import { createMockVariantAnalysisRepoTask } from '../../factories/remote-queries/gh-api/variant-analysis-repo-task';
import { VariantAnalysisRepoTask } from '../../../remote-queries/gh-api/variant-analysis';
import {
VariantAnalysis as VariantAnalysisApiResponse,
VariantAnalysisRepoTask
} from '../../../remote-queries/gh-api/variant-analysis';
import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
import { UserCancellationException } from '../../../commandRunner';
import { Repository } from '../../../remote-queries/gh-api/repository';
import { setRemoteControllerRepo, setRemoteRepositoryLists } from '../../../config';
describe('Variant Analysis Manager', async function() {
let sandbox: sinon.SinonSandbox;
let pathExistsStub: sinon.SinonStub;
let readJsonStub: sinon.SinonStub;
let outputJsonStub: sinon.SinonStub;
let writeFileStub: sinon.SinonStub;
let cli: CodeQLCliServer;
let cancellationTokenSource: CancellationTokenSource;
let variantAnalysisManager: VariantAnalysisManager;
@@ -49,7 +57,7 @@ describe('Variant Analysis Manager', async function() {
sandbox.stub(logger, 'log');
sandbox.stub(config, 'isVariantAnalysisLiveResultsEnabled').returns(false);
sandbox.stub(fs, 'mkdirSync');
sandbox.stub(fs, 'writeFile');
writeFileStub = sandbox.stub(fs, 'writeFile');
pathExistsStub = sandbox.stub(fs, 'pathExists').callThrough();
readJsonStub = sandbox.stub(fs, 'readJson').callThrough();
outputJsonStub = sandbox.stub(fs, 'outputJson');
@@ -66,7 +74,7 @@ describe('Variant Analysis Manager', async function() {
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
cli = extension.cliServer;
variantAnalysisResultsManager = new VariantAnalysisResultsManager(cli, logger);
variantAnalysisManager = new VariantAnalysisManager(extension.ctx, storagePath, variantAnalysisResultsManager);
variantAnalysisManager = new VariantAnalysisManager(extension.ctx, cli, storagePath, variantAnalysisResultsManager);
} catch (e) {
fail(e as Error);
}
@@ -76,6 +84,108 @@ describe('Variant Analysis Manager', async function() {
sandbox.restore();
});
describe('runVariantAnalysis', function() {
// up to 3 minutes per test
this.timeout(3 * 60 * 1000);
let progress: sinon.SinonSpy;
let showQuickPickSpy: sinon.SinonStub;
let mockGetRepositoryFromNwo: sinon.SinonStub;
let mockSubmitVariantAnalysis: sinon.SinonStub;
let mockApiResponse: VariantAnalysisApiResponse;
let executeCommandSpy: sinon.SinonStub;
const baseDir = path.join(__dirname, '../../../../src/vscode-tests/cli-integration');
function getFile(file: string): Uri {
return Uri.file(path.join(baseDir, file));
}
beforeEach(async function() {
if (!(await cli.cliConstraints.supportsRemoteQueries())) {
console.log(`Remote queries are not supported on CodeQL CLI v${CliVersionConstraint.CLI_VERSION_REMOTE_QUERIES
}. Skipping this test.`);
this.skip();
}
writeFileStub.callThrough();
progress = sandbox.spy();
// Should not have asked for a language
showQuickPickSpy = sandbox.stub(window, 'showQuickPick')
.onFirstCall().resolves({ repositories: ['github/vscode-codeql'] } as unknown as QuickPickItem)
.onSecondCall().resolves('javascript' as unknown as QuickPickItem);
executeCommandSpy = sandbox.stub(commands, 'executeCommand').callThrough();
cancellationTokenSource = new CancellationTokenSource();
const dummyRepository: Repository = {
id: 123,
name: 'vscode-codeql',
full_name: 'github/vscode-codeql',
private: false,
};
mockGetRepositoryFromNwo = sandbox.stub(ghApiClient, 'getRepositoryFromNwo').resolves(dummyRepository);
mockApiResponse = createMockApiResponse('in_progress');
mockSubmitVariantAnalysis = sandbox.stub(ghApiClient, 'submitVariantAnalysis').resolves(mockApiResponse);
// always run in the vscode-codeql repo
await setRemoteControllerRepo('github/vscode-codeql');
await setRemoteRepositoryLists({ 'vscode-codeql': ['github/vscode-codeql'] });
});
it('should run a variant analysis that is part of a qlpack', async () => {
const fileUri = getFile('data-remote-qlpack/in-pack.ql');
await variantAnalysisManager.runVariantAnalysis(fileUri, progress, cancellationTokenSource.token);
expect(executeCommandSpy).to.have.been.calledWith('codeQL.monitorVariantAnalysis', sinon.match.has('id', mockApiResponse.id).and(sinon.match.has('status', VariantAnalysisStatus.InProgress)));
expect(showQuickPickSpy).to.have.been.calledOnce;
expect(mockGetRepositoryFromNwo).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 fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
await variantAnalysisManager.runVariantAnalysis(fileUri, progress, cancellationTokenSource.token);
expect(executeCommandSpy).to.have.been.calledWith('codeQL.monitorVariantAnalysis', sinon.match.has('id', mockApiResponse.id).and(sinon.match.has('status', VariantAnalysisStatus.InProgress)));
expect(mockGetRepositoryFromNwo).to.have.been.calledOnce;
expect(mockSubmitVariantAnalysis).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');
await variantAnalysisManager.runVariantAnalysis(fileUri, progress, cancellationTokenSource.token);
expect(executeCommandSpy).to.have.been.calledWith('codeQL.monitorVariantAnalysis', sinon.match.has('id', mockApiResponse.id).and(sinon.match.has('status', VariantAnalysisStatus.InProgress)));
expect(mockGetRepositoryFromNwo).to.have.been.calledOnce;
expect(mockSubmitVariantAnalysis).to.have.been.calledOnce;
});
it('should cancel a run before uploading', async () => {
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
const promise = variantAnalysisManager.runVariantAnalysis(fileUri, progress, cancellationTokenSource.token);
cancellationTokenSource.cancel();
try {
await promise;
assert.fail('should have thrown');
} catch (e) {
expect(e).to.be.instanceof(UserCancellationException);
}
});
});
describe('rehydrateVariantAnalysis', () => {
const variantAnalysis = createMockVariantAnalysis({});

View File

@@ -0,0 +1,243 @@
import { expect } from 'chai';
import * as os from 'os';
import { parseResponse } from '../../../remote-queries/remote-queries-api';
import { Repository } from '../../../remote-queries/shared/repository';
describe('parseResponse', () => {
const controllerRepository: Repository = {
id: 123,
fullName: 'org/name',
private: true
};
it('should parse a successful response', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
});
expect(result.popupMessage).to.equal('Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).');
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d'].join(os.EOL),
);
});
it('should parse a response with invalid repos', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
invalid_repositories: ['e/f', 'g/h'],
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d',
'',
'Some repositories could not be scheduled.',
'',
'2 repositories invalid and could not be found:',
'e/f, g/h'].join(os.EOL)
);
});
it('should parse a response with repos w/o databases', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
repositories_without_database: ['e/f', 'g/h'],
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d',
'',
'Some repositories could not be scheduled.',
'',
'2 repositories did not have a CodeQL database available:',
'e/f, g/h',
'For each public repository that has not yet been added to the database service, we will try to create a database next time the store is updated.'].join(os.EOL)
);
});
it('should parse a response with private repos', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
private_repositories: ['e/f', 'g/h'],
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d',
'',
'Some repositories could not be scheduled.',
'',
'2 repositories not public:',
'e/f, g/h',
'When using a public controller repository, only public repositories can be queried.'].join(os.EOL)
);
});
it('should parse a response with cutoff repos and cutoff repos count', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
cutoff_repositories: ['e/f', 'g/h'],
cutoff_repositories_count: 2,
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d',
'',
'Some repositories could not be scheduled.',
'',
'2 repositories over the limit for a single request:',
'e/f, g/h',
'Repositories were selected based on how recently they had been updated.'].join(os.EOL)
);
});
it('should parse a response with cutoff repos count but not cutoff repos', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
cutoff_repositories_count: 2,
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d',
'',
'Some repositories could not be scheduled.',
'',
'2 repositories over the limit for a single request.',
'Repositories were selected based on how recently they had been updated.'].join(os.EOL)
);
});
it('should parse a response with invalid repos and repos w/o databases', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
invalid_repositories: ['e/f', 'g/h'],
repositories_without_database: ['i/j', 'k/l'],
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d',
'',
'Some repositories could not be scheduled.',
'',
'2 repositories invalid and could not be found:',
'e/f, g/h',
'',
'2 repositories did not have a CodeQL database available:',
'i/j, k/l',
'For each public repository that has not yet been added to the database service, we will try to create a database next time the store is updated.'].join(os.EOL)
);
});
it('should parse a response with one repo of each category, and not pluralize "repositories"', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b'],
errors: {
private_repositories: ['e/f'],
cutoff_repositories: ['i/j'],
cutoff_repositories_count: 1,
invalid_repositories: ['m/n'],
repositories_without_database: ['q/r'],
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 1 repository. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
[
'Successfully scheduled runs on 1 repository. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b',
'',
'Some repositories could not be scheduled.',
'',
'1 repository invalid and could not be found:',
'm/n',
'',
'1 repository did not have a CodeQL database available:',
'q/r',
'For each public repository that has not yet been added to the database service, we will try to create a database next time the store is updated.',
'',
'1 repository not public:',
'e/f',
'When using a public controller repository, only public repositories can be queried.',
'',
'1 repository over the limit for a single request:',
'i/j',
'Repositories were selected based on how recently they had been updated.',
].join(os.EOL)
);
});
});

View File

@@ -1,245 +0,0 @@
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(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
});
expect(result.popupMessage).to.equal('Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).');
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d'].join(os.EOL),
);
});
it('should parse a response with invalid repos', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
invalid_repositories: ['e/f', 'g/h'],
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d',
'',
'Some repositories could not be scheduled.',
'',
'2 repositories invalid and could not be found:',
'e/f, g/h'].join(os.EOL)
);
});
it('should parse a response with repos w/o databases', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
repositories_without_database: ['e/f', 'g/h'],
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d',
'',
'Some repositories could not be scheduled.',
'',
'2 repositories did not have a CodeQL database available:',
'e/f, g/h',
'For each public repository that has not yet been added to the database service, we will try to create a database next time the store is updated.'].join(os.EOL)
);
});
it('should parse a response with private repos', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
private_repositories: ['e/f', 'g/h'],
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d',
'',
'Some repositories could not be scheduled.',
'',
'2 repositories not public:',
'e/f, g/h',
'When using a public controller repository, only public repositories can be queried.'].join(os.EOL)
);
});
it('should parse a response with cutoff repos and cutoff repos count', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
cutoff_repositories: ['e/f', 'g/h'],
cutoff_repositories_count: 2,
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d',
'',
'Some repositories could not be scheduled.',
'',
'2 repositories over the limit for a single request:',
'e/f, g/h',
'Repositories were selected based on how recently they had been updated.'].join(os.EOL)
);
});
it('should parse a response with cutoff repos count but not cutoff repos', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
cutoff_repositories_count: 2,
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d',
'',
'Some repositories could not be scheduled.',
'',
'2 repositories over the limit for a single request.',
'Repositories were selected based on how recently they had been updated.'].join(os.EOL)
);
});
it('should parse a response with invalid repos and repos w/o databases', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b', 'c/d'],
errors: {
invalid_repositories: ['e/f', 'g/h'],
repositories_without_database: ['i/j', 'k/l'],
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 2 repositories. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
['Successfully scheduled runs on 2 repositories. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b, c/d',
'',
'Some repositories could not be scheduled.',
'',
'2 repositories invalid and could not be found:',
'e/f, g/h',
'',
'2 repositories did not have a CodeQL database available:',
'i/j, k/l',
'For each public repository that has not yet been added to the database service, we will try to create a database next time the store is updated.'].join(os.EOL)
);
});
it('should parse a response with one repo of each category, and not pluralize "repositories"', () => {
const result = parseResponse(controllerRepository, {
workflow_run_id: 123,
repositories_queried: ['a/b'],
errors: {
private_repositories: ['e/f'],
cutoff_repositories: ['i/j'],
cutoff_repositories_count: 1,
invalid_repositories: ['m/n'],
repositories_without_database: ['q/r'],
}
});
expect(result.popupMessage).to.equal(
['Successfully scheduled runs on 1 repository. [Click here to see the progress](https://github.com/org/name/actions/runs/123).',
'',
'Some repositories could not be scheduled. See extension log for details.'].join(os.EOL)
);
expect(result.logMessage).to.equal(
[
'Successfully scheduled runs on 1 repository. See https://github.com/org/name/actions/runs/123.',
'',
'Repositories queried:',
'a/b',
'',
'Some repositories could not be scheduled.',
'',
'1 repository invalid and could not be found:',
'm/n',
'',
'1 repository did not have a CodeQL database available:',
'q/r',
'For each public repository that has not yet been added to the database service, we will try to create a database next time the store is updated.',
'',
'1 repository not public:',
'e/f',
'When using a public controller repository, only public repositories can be queried.',
'',
'1 repository over the limit for a single request:',
'i/j',
'Repositories were selected based on how recently they had been updated.',
].join(os.EOL)
);
});
});
});