Merge remote-tracking branch 'origin/main' into koesie10/copy-repo-list
This commit is contained in:
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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({});
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user