From b1a5f14471eb0679ca7ebb98f30cdd47558931c3 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Tue, 8 Nov 2022 16:48:44 +0100 Subject: [PATCH 1/3] Split flows for running remote queries This removes the `runRemoteQuery` method and instead moves all logic specific to remote queries/variant analysis to the remote queries manager and variant analysis manager respectively. This will make it easier to completely remove the remote queries manager in the future. --- extensions/ql-vscode/src/extension.ts | 24 +++- .../remote-queries/remote-queries-manager.ts | 58 ++++++--- .../src/remote-queries/run-remote-query.ts | 106 +-------------- .../variant-analysis-manager.ts | 80 +++++++++++- ...test.ts => remote-queries-manager.test.ts} | 104 +++------------ .../variant-analysis-manager.test.ts | 122 +++++++++++++++++- 6 files changed, 270 insertions(+), 224 deletions(-) rename extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/{run-remote-query.test.ts => remote-queries-manager.test.ts} (69%) 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-manager.ts b/extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts index 68ef20a67e5..53b68459b90 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,7 +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 { + buildRemoteQueryEntity, + prepareRemoteQueryRun, + runRemoteQueriesApiRequest, +} from './run-remote-query'; import { RemoteQueriesView } from './remote-queries-view'; import { RemoteQuery } from './remote-query'; import { RemoteQueriesMonitor } from './remote-queries-monitor'; @@ -22,7 +26,6 @@ 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'; const autoDownloadMaxSize = 300 * 1024; const autoDownloadMaxCount = 100; @@ -57,7 +60,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 +67,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()); @@ -119,21 +119,47 @@ export class RemoteQueriesManager extends DisposableObject { uri: Uri | undefined, progress: ProgressCallback, token: CancellationToken - ): Promise { + ): Promise { 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); + + const apiResponse = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, controllerRepo, base64Pack); + + if (!apiResponse) { + return undefined; + } - if (querySubmission?.query) { - const query = querySubmission.query; - const queryId = this.createQueryId(); + const workflowRunId = apiResponse.workflow_run_id; + const repositoryCount = apiResponse.repositories_queried.length; + const query = await buildRemoteQueryEntity( + queryFile, + queryMetadata, + controllerRepo, + queryStartTime, + workflowRunId, + language, + repositoryCount + ); - await this.prepareStorageDirectory(queryId); - await this.storeJsonFile(queryId, 'query.json', query); + const queryId = this.createQueryId(); - this.remoteQueryAddedEventEmitter.fire({ queryId, query }); - void commands.executeCommand('codeQL.monitorRemoteQuery', queryId, query); - } + await this.prepareStorageDirectory(queryId); + await this.storeJsonFile(queryId, 'query.json', query); + + this.remoteQueryAddedEventEmitter.fire({ queryId, query }); + void commands.executeCommand('codeQL.monitorRemoteQuery', queryId, query); + + return query; } public async monitorRemoteQuery( 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 8b58c4a4b05..746bcd71d6c 100644 --- a/extensions/ql-vscode/src/remote-queries/run-remote-query.ts +++ b/extensions/ql-vscode/src/remote-queries/run-remote-query.ts @@ -1,4 +1,4 @@ -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'; @@ -16,22 +16,17 @@ import { 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 * 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; @@ -262,98 +257,7 @@ 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( +export async function runRemoteQueriesApiRequest( credentials: Credentials, ref: string, language: string, @@ -455,7 +359,7 @@ async function ensureNameAndSuite(queryPackDir: string, packRelativePath: string await fs.writeFile(packPath, yaml.dump(qlpack)); } -async function buildRemoteQueryEntity( +export async function buildRemoteQueryEntity( queryFilePath: string, queryMetadata: QueryMetadata | undefined, controllerRepo: Repository, @@ -483,7 +387,7 @@ async function buildRemoteQueryEntity( }; } -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..0b633468352 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,67 @@ 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); + + return 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 +233,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 69% 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..d62326d67d0 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 @@ -5,27 +5,20 @@ import { CancellationTokenSource, ExtensionContext, extensions, QuickPickItem, U 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,14 +78,19 @@ 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; beforeEach(() => { @@ -110,7 +103,7 @@ 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); + const querySubmissionResult = await remoteQueriesManager.runRemoteQuery(fileUri, progress, cancellationTokenSource.token); expect(querySubmissionResult).to.be.ok; expect(mockSubmitRemoteQueries).to.have.been.calledOnce; @@ -155,7 +148,7 @@ 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); + const querySubmissionResult = await remoteQueriesManager.runRemoteQuery(fileUri, progress, cancellationTokenSource.token); expect(querySubmissionResult).to.be.ok; expect(mockSubmitRemoteQueries).to.have.been.calledOnce; @@ -203,7 +196,7 @@ 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); + const querySubmissionResult = await remoteQueriesManager.runRemoteQuery(fileUri, progress, cancellationTokenSource.token); expect(querySubmissionResult).to.be.ok; expect(mockSubmitRemoteQueries).to.have.been.calledOnce; @@ -250,72 +243,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..0bc29da0999 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; + + 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); + + 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'); + + const variantAnalysis = await variantAnalysisManager.runVariantAnalysis(fileUri, progress, cancellationTokenSource.token); + expect(variantAnalysis).to.be.ok; + expect(variantAnalysis.id).to.be.equal(mockApiResponse.id); + expect(variantAnalysis.status).to.be.equal(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'); + + const variantAnalysis = await variantAnalysisManager.runVariantAnalysis(fileUri, progress, cancellationTokenSource.token); + expect(variantAnalysis).to.be.ok; + expect(variantAnalysis.id).to.be.equal(mockApiResponse.id); + expect(variantAnalysis.status).to.be.equal(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'); + + const variantAnalysis = await variantAnalysisManager.runVariantAnalysis(fileUri, progress, cancellationTokenSource.token); + expect(variantAnalysis).to.be.ok; + expect(variantAnalysis.id).to.be.equal(mockApiResponse.id); + expect(variantAnalysis.status).to.be.equal(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({}); From 7649f20d4d2ddc472ebda853be7a5e4a16c59d3f Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Fri, 11 Nov 2022 14:28:09 +0100 Subject: [PATCH 2/3] Remove unused return values for `run` methods The `runRemoteQuery` and `runVariantAnalysis` were returning values which were only used in tests. This removes them and replaces the tests by expectations on the commands called by the methods. --- .../remote-queries/remote-queries-manager.ts | 6 ++--- .../variant-analysis-manager.ts | 4 +--- .../remote-queries-manager.test.ts | 17 +++++++------ .../variant-analysis-manager.test.ts | 24 +++++++++---------- 4 files changed, 25 insertions(+), 26 deletions(-) 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 53b68459b90..8e72910754d 100644 --- a/extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts +++ b/extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts @@ -119,7 +119,7 @@ export class RemoteQueriesManager extends DisposableObject { uri: Uri | undefined, progress: ProgressCallback, token: CancellationToken - ): Promise { + ): Promise { const credentials = await Credentials.initialize(this.ctx); const { @@ -136,7 +136,7 @@ export class RemoteQueriesManager extends DisposableObject { const apiResponse = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, controllerRepo, base64Pack); if (!apiResponse) { - return undefined; + return; } const workflowRunId = apiResponse.workflow_run_id; @@ -158,8 +158,6 @@ export class RemoteQueriesManager extends DisposableObject { this.remoteQueryAddedEventEmitter.fire({ queryId, query }); void commands.executeCommand('codeQL.monitorRemoteQuery', queryId, query); - - return query; } public async monitorRemoteQuery( 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 0b633468352..63a127444d4 100644 --- a/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts +++ b/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts @@ -69,7 +69,7 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA uri: Uri | undefined, progress: ProgressCallback, token: CancellationToken, - ): Promise { + ): Promise { const credentials = await Credentials.initialize(this.ctx); const { @@ -122,8 +122,6 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA void commands.executeCommand('codeQL.openVariantAnalysisView', processedVariantAnalysis.id); void commands.executeCommand('codeQL.monitorVariantAnalysis', processedVariantAnalysis); - - return processedVariantAnalysis; } public async rehydrateVariantAnalysis(variantAnalysis: VariantAnalysis) { diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/remote-queries-manager.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/remote-queries-manager.test.ts index d62326d67d0..9595d4b52bd 100644 --- a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/remote-queries-manager.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/remote-queries-manager.test.ts @@ -1,7 +1,7 @@ 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'; @@ -92,8 +92,11 @@ describe('Remote queries', function() { 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'], @@ -103,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 remoteQueriesManager.runRemoteQuery(fileUri, progress, cancellationTokenSource.token); - 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; @@ -148,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 remoteQueriesManager.runRemoteQuery(fileUri, progress, cancellationTokenSource.token); - 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; @@ -196,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 remoteQueriesManager.runRemoteQuery(fileUri, progress, cancellationTokenSource.token); - 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; 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 0bc29da0999..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 @@ -90,6 +90,7 @@ describe('Variant Analysis Manager', async function() { 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 { @@ -111,6 +112,8 @@ describe('Variant Analysis Manager', async function() { .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 = { @@ -132,10 +135,9 @@ describe('Variant Analysis Manager', async function() { it('should run a variant analysis that is part of a qlpack', async () => { const fileUri = getFile('data-remote-qlpack/in-pack.ql'); - const variantAnalysis = await variantAnalysisManager.runVariantAnalysis(fileUri, progress, cancellationTokenSource.token); - expect(variantAnalysis).to.be.ok; - expect(variantAnalysis.id).to.be.equal(mockApiResponse.id); - expect(variantAnalysis.status).to.be.equal(VariantAnalysisStatus.InProgress); + 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; @@ -146,10 +148,9 @@ describe('Variant Analysis Manager', async 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 variantAnalysis = await variantAnalysisManager.runVariantAnalysis(fileUri, progress, cancellationTokenSource.token); - expect(variantAnalysis).to.be.ok; - expect(variantAnalysis.id).to.be.equal(mockApiResponse.id); - expect(variantAnalysis.status).to.be.equal(VariantAnalysisStatus.InProgress); + 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; @@ -158,10 +159,9 @@ describe('Variant Analysis Manager', async 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 variantAnalysis = await variantAnalysisManager.runVariantAnalysis(fileUri, progress, cancellationTokenSource.token); - expect(variantAnalysis).to.be.ok; - expect(variantAnalysis.id).to.be.equal(mockApiResponse.id); - expect(variantAnalysis.status).to.be.equal(VariantAnalysisStatus.InProgress); + 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; From 65641e3776a8ede701ad178c87244d2c9f70ee27 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Fri, 11 Nov 2022 14:35:02 +0100 Subject: [PATCH 3/3] Move remote queries specific code out of run-remote-query This moves some of the code that is specific to remote queries out of the `run-remote-query.ts` file and instead places it in separate files that only deal with remote queries, rather than also dealing with variant analyses. --- .../src/remote-queries/remote-queries-api.ts | 87 +++++++ .../remote-queries/remote-queries-manager.ts | 5 +- .../src/remote-queries/remote-query.ts | 36 ++- .../src/remote-queries/run-remote-query.ts | 116 +-------- .../remote-queries/remote-queries-api.test.ts | 243 +++++++++++++++++ .../remote-queries/run-remote-query.test.ts | 245 ------------------ 6 files changed, 368 insertions(+), 364 deletions(-) create mode 100644 extensions/ql-vscode/src/remote-queries/remote-queries-api.ts create mode 100644 extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/remote-queries-api.test.ts delete mode 100644 extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/run-remote-query.test.ts 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 8e72910754d..325313ca727 100644 --- a/extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts +++ b/extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts @@ -10,12 +10,10 @@ import { ProgressCallback } from '../commandRunner'; import { createTimestampFile, showAndLogErrorMessage, showAndLogInformationMessage, showInformationMessageWithAction } from '../helpers'; import { Logger } from '../logging'; import { - buildRemoteQueryEntity, prepareRemoteQueryRun, - runRemoteQueriesApiRequest, } 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'; @@ -26,6 +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 { runRemoteQueriesApiRequest } from './remote-queries-api'; const autoDownloadMaxSize = 300 * 1024; const autoDownloadMaxCount = 100; 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 746bcd71d6c..4013c156b05 100644 --- a/extensions/ql-vscode/src/remote-queries/run-remote-query.ts +++ b/extensions/ql-vscode/src/remote-queries/run-remote-query.ts @@ -2,14 +2,11 @@ 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'; @@ -19,13 +16,10 @@ import { logger } from '../logging'; import { getActionBranch, getRemoteControllerRepo, setRemoteControllerRepo } from '../config'; import { ProgressCallback, UserCancellationException } from '../commandRunner'; import { RequestError } from '@octokit/types/dist-types'; -import { RemoteQuery } from './remote-query'; 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 { getRepositorySelection, isValidSelection } from './repository-selection'; import { Repository } from './shared/repository'; export interface QlPack { @@ -257,84 +251,6 @@ export async function prepareRemoteQueryRun( }; } -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 - }; -} - /** * Updates the default suite of the query pack. This is used to ensure * only the specified query is run. @@ -359,34 +275,6 @@ async function ensureNameAndSuite(queryPackDir: string, packRelativePath: string await fs.writeFile(packPath, yaml.dump(qlpack)); } -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, - }; -} - 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/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) - ); - }); - }); -});