diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index b2ce7a2ad1c..8499201c88e 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -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.'); } diff --git a/extensions/ql-vscode/src/remote-queries/remote-queries-api.ts b/extensions/ql-vscode/src/remote-queries/remote-queries-api.ts new file mode 100644 index 00000000000..300574c1d19 --- /dev/null +++ b/extensions/ql-vscode/src/remote-queries/remote-queries-api.ts @@ -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 { + 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 + }; +} diff --git a/extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts b/extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts index 68ef20a67e5..325313ca727 100644 --- a/extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts +++ b/extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts @@ -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()); this.remoteQueryRemovedEventEmitter = this.push(new EventEmitter()); @@ -122,18 +121,42 @@ export class RemoteQueriesManager extends DisposableObject { ): Promise { const credentials = await Credentials.initialize(this.ctx); - const querySubmission = await runRemoteQuery(this.cliServer, credentials, uri || window.activeTextEditor?.document.uri, progress, token, this.variantAnalysisManager); - - if (querySubmission?.query) { - const query = querySubmission.query; - 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); + const { + actionBranch, + base64Pack, + repoSelection, + queryFile, + queryMetadata, + controllerRepo, + queryStartTime, + language, + } = await prepareRemoteQueryRun(this.cliServer, credentials, uri, progress, token); + + 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 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( diff --git a/extensions/ql-vscode/src/remote-queries/remote-query.ts b/extensions/ql-vscode/src/remote-queries/remote-query.ts index db838bec96b..c388fb69907 100644 --- a/extensions/ql-vscode/src/remote-queries/remote-query.ts +++ b/extensions/ql-vscode/src/remote-queries/remote-query.ts @@ -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 { + 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, + }; +} diff --git a/extensions/ql-vscode/src/remote-queries/run-remote-query.ts b/extensions/ql-vscode/src/remote-queries/run-remote-query.ts index 777b373f456..012bcf9e511 100644 --- a/extensions/ql-vscode/src/remote-queries/run-remote-query.ts +++ b/extensions/ql-vscode/src/remote-queries/run-remote-query.ts @@ -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 { - 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 { - 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 { - 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); } diff --git a/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts b/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts index ad295ecfe47..63a127444d4 100644 --- a/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts +++ b/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts @@ -1,30 +1,36 @@ import * as path from 'path'; import * as ghApiClient from './gh-api/gh-api-client'; -import { CancellationToken, commands, EventEmitter, ExtensionContext, window } from 'vscode'; +import { CancellationToken, commands, 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 { cancelVariantAnalysis } from './gh-api/gh-actions-api-client'; +import { ProgressCallback, UserCancellationException } from '../commandRunner'; +import { CodeQLCliServer } from '../cli'; export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager { private static readonly REPO_STATES_FILENAME = 'repo_states.json'; @@ -47,6 +53,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 ) { @@ -58,6 +65,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 { + 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 @@ -165,7 +231,7 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA this._onVariantAnalysisStatusUpdated.fire(variantAnalysis); } - public async onVariantAnalysisSubmitted(variantAnalysis: VariantAnalysis): Promise { + private async onVariantAnalysisSubmitted(variantAnalysis: VariantAnalysis): Promise { await this.setVariantAnalysis(variantAnalysis); await this.prepareStorageDirectory(variantAnalysis.id); diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/run-remote-query.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/remote-queries-manager.test.ts similarity index 68% rename from extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/run-remote-query.test.ts rename to extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/remote-queries-manager.test.ts index 4ef10a245a4..9595d4b52bd 100644 --- a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/run-remote-query.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/remote-queries-manager.test.ts @@ -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(); diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts index 5be436480b4..f259d6b27b1 100644 --- a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts @@ -1,6 +1,6 @@ import * as sinon from 'sinon'; -import { expect } from 'chai'; -import { CancellationTokenSource, commands, extensions } from 'vscode'; +import { assert, expect } from 'chai'; +import { CancellationTokenSource, commands, 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'; @@ -25,13 +25,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; @@ -46,7 +54,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'); @@ -63,7 +71,7 @@ describe('Variant Analysis Manager', async function() { const extension = await extensions.getExtension>('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); } @@ -73,6 +81,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({}); diff --git a/extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/remote-queries-api.test.ts b/extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/remote-queries-api.test.ts new file mode 100644 index 00000000000..4e343734a44 --- /dev/null +++ b/extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/remote-queries-api.test.ts @@ -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) + ); + }); +}); diff --git a/extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/run-remote-query.test.ts b/extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/run-remote-query.test.ts deleted file mode 100644 index ee2fe13cf54..00000000000 --- a/extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/run-remote-query.test.ts +++ /dev/null @@ -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) - ); - }); - }); -});