From 459d6a9bc8724b45b99ab4b427c24032c16ed13f Mon Sep 17 00:00:00 2001 From: Nick Rolfe Date: Wed, 16 Apr 2025 12:07:14 +0100 Subject: [PATCH] Add command for running a query suite This uses a new query-server command for running multiple queries, so that a single evaluator log will be produced for the entire run. To avoid too much code duplication, I have updated a lot of the code paths involved in running local queries to work with multiple query paths. This also required some refactoring to explicitly associate an output basename (used to produce the .bqrs, .csv, etc. paths) with each query, where before those output filenames were hard-coded. --- extensions/ql-vscode/package.json | 13 ++ .../ql-vscode/src/codeql-cli/cli-version.ts | 1 + extensions/ql-vscode/src/common/commands.ts | 1 + .../ql-vscode/src/compare/compare-view.ts | 19 ++- .../src/compare/interpreted-results.ts | 6 +- .../ql-vscode/src/debugger/debug-protocol.ts | 1 + .../ql-vscode/src/debugger/debug-session.ts | 35 +++-- .../ql-vscode/src/debugger/debugger-ui.ts | 13 +- .../ast-viewer/ast-builder.ts | 8 +- .../contextual/location-finder.ts | 14 +- .../contextual/query-resolver.ts | 11 +- .../contextual/template-provider.ts | 8 +- .../src/local-queries/local-queries.ts | 111 +++++++++++++- .../src/local-queries/local-query-run.ts | 81 ++++++----- .../src/local-queries/query-output-dir.ts | 30 ++-- .../src/local-queries/results-view.ts | 21 ++- .../ql-vscode/src/local-queries/run-query.ts | 20 +-- .../ql-vscode/src/model-editor/generate.ts | 10 +- .../src/model-editor/model-editor-queries.ts | 11 +- .../src/model-editor/suggestion-queries.ts | 10 +- .../history-item-label-provider.ts | 2 +- .../query-history/query-history-manager.ts | 55 ++++++- ...query-history-local-query-domain-mapper.ts | 2 +- .../query-history-local-query-dto-mapper.ts | 1 + .../store/query-history-local-query-dto.ts | 5 +- .../store/query-history-store.ts | 2 +- extensions/ql-vscode/src/query-results.ts | 8 +- .../ql-vscode/src/query-server/messages.ts | 24 +++- .../src/query-server/query-runner.ts | 43 ++++-- .../src/query-server/query-server-client.ts | 8 ++ .../ql-vscode/src/query-server/run-queries.ts | 136 ++++++++++++++++-- .../ql-vscode/src/run-queries-shared.ts | 63 +++++--- .../codeql-pack.lock.yml | 26 ++++ .../debugger/debug-controller.ts | 27 ++-- .../cli-integration/debugger/debugger.test.ts | 33 +++-- .../cli-integration/queries.test.ts | 20 ++- .../ast-viewer/ast-builder.test.ts | 3 +- .../external-api-usage-query.test.ts | 34 +++-- .../model-editor/generate.test.ts | 25 ++-- .../model-editor/suggestion-queries.test.ts | 24 ++-- .../store/query-history-store.test.ts | 4 +- .../no-workspace/query-results.test.ts | 6 +- .../no-workspace/run-queries.test.ts | 11 +- 43 files changed, 737 insertions(+), 249 deletions(-) create mode 100644 extensions/ql-vscode/test/data-extensions/pack-using-extensions/codeql-pack.lock.yml diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index 0c691154962..91914c60c9c 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -571,6 +571,10 @@ "command": "codeQL.runQueries", "title": "CodeQL: Run Queries in Selected Files" }, + { + "command": "codeQL.runQuerySuite", + "title": "CodeQL: Run Selected Query Suite" + }, { "command": "codeQL.quickEval", "title": "CodeQL: Quick Evaluation" @@ -1361,6 +1365,11 @@ "group": "9_qlCommands", "when": "resourceScheme != codeql-zip-archive" }, + { + "command": "codeQL.runQuerySuite", + "group": "9_qlCommands", + "when": "resourceScheme != codeql-zip-archive && resourceExtname == .qls && !explorerResourceIsFolder && !listMultiSelection && config.codeQL.canary" + }, { "command": "codeQL.runVariantAnalysisContextExplorer", "group": "9_qlCommands", @@ -1458,6 +1467,10 @@ "command": "codeQL.runQueries", "when": "false" }, + { + "command": "codeQL.runQuerySuite", + "when": "false" + }, { "command": "codeQL.quickEval", "when": "editorLangId == ql" diff --git a/extensions/ql-vscode/src/codeql-cli/cli-version.ts b/extensions/ql-vscode/src/codeql-cli/cli-version.ts index 94f1169c30a..5d3f7305688 100644 --- a/extensions/ql-vscode/src/codeql-cli/cli-version.ts +++ b/extensions/ql-vscode/src/codeql-cli/cli-version.ts @@ -13,6 +13,7 @@ export interface CliFeatures { featuresInVersionResult?: boolean; mrvaPackCreate?: boolean; generateSummarySymbolMap?: boolean; + queryServerRunQueries?: boolean; } export interface VersionAndFeatures { diff --git a/extensions/ql-vscode/src/common/commands.ts b/extensions/ql-vscode/src/common/commands.ts index 2fd8a1995d4..fcaf2eb72d8 100644 --- a/extensions/ql-vscode/src/common/commands.ts +++ b/extensions/ql-vscode/src/common/commands.ts @@ -138,6 +138,7 @@ export type LocalQueryCommands = { "codeQLQueries.createQuery": () => Promise; "codeQL.runLocalQueryFromFileTab": (uri: Uri) => Promise; "codeQL.runQueries": ExplorerSelectionCommandFunction; + "codeQL.runQuerySuite": ExplorerSelectionCommandFunction; "codeQL.quickEval": (uri: Uri) => Promise; "codeQL.quickEvalCount": (uri: Uri) => Promise; "codeQL.quickEvalContextEditor": (uri: Uri) => Promise; diff --git a/extensions/ql-vscode/src/compare/compare-view.ts b/extensions/ql-vscode/src/compare/compare-view.ts index c4c25df546a..93538f52e6b 100644 --- a/extensions/ql-vscode/src/compare/compare-view.ts +++ b/extensions/ql-vscode/src/compare/compare-view.ts @@ -70,22 +70,20 @@ export class CompareView extends AbstractWebview< selectedResultSetName?: string, ) { const [fromSchemas, toSchemas] = await Promise.all([ - this.cliServer.bqrsInfo( - from.completedQuery.query.resultsPaths.resultsPath, - ), - this.cliServer.bqrsInfo(to.completedQuery.query.resultsPaths.resultsPath), + this.cliServer.bqrsInfo(from.completedQuery.query.resultsPath), + this.cliServer.bqrsInfo(to.completedQuery.query.resultsPath), ]); const [fromSchemaNames, toSchemaNames] = await Promise.all([ getResultSetNames( fromSchemas, from.completedQuery.query.metadata, - from.completedQuery.query.resultsPaths.interpretedResultsPath, + from.completedQuery.query.interpretedResultsPath, ), getResultSetNames( toSchemas, to.completedQuery.query.metadata, - to.completedQuery.query.resultsPaths.interpretedResultsPath, + to.completedQuery.query.interpretedResultsPath, ), ]); @@ -101,15 +99,14 @@ export class CompareView extends AbstractWebview< schemaNames: fromSchemaNames, metadata: from.completedQuery.query.metadata, interpretedResultsPath: - from.completedQuery.query.resultsPaths.interpretedResultsPath, + from.completedQuery.query.interpretedResultsPath, }, to, toInfo: { schemas: toSchemas, schemaNames: toSchemaNames, metadata: to.completedQuery.query.metadata, - interpretedResultsPath: - to.completedQuery.query.resultsPaths.interpretedResultsPath, + interpretedResultsPath: to.completedQuery.query.interpretedResultsPath, }, commonResultSetNames, }; @@ -392,12 +389,12 @@ export class CompareView extends AbstractWebview< this.getResultSet( fromInfo.schemas, fromResultSetName, - from.completedQuery.query.resultsPaths.resultsPath, + from.completedQuery.query.resultsPath, ), this.getResultSet( toInfo.schemas, toResultSetName, - to.completedQuery.query.resultsPaths.resultsPath, + to.completedQuery.query.resultsPath, ), ]); diff --git a/extensions/ql-vscode/src/compare/interpreted-results.ts b/extensions/ql-vscode/src/compare/interpreted-results.ts index d5ca255ca4d..f98913a15e8 100644 --- a/extensions/ql-vscode/src/compare/interpreted-results.ts +++ b/extensions/ql-vscode/src/compare/interpreted-results.ts @@ -36,11 +36,9 @@ export async function compareInterpretedResults( const [fromResultSet, toResultSet, sourceLocationPrefix] = await Promise.all([ getInterpretedResults( - fromQuery.completedQuery.query.resultsPaths.interpretedResultsPath, - ), - getInterpretedResults( - toQuery.completedQuery.query.resultsPaths.interpretedResultsPath, + fromQuery.completedQuery.query.interpretedResultsPath, ), + getInterpretedResults(toQuery.completedQuery.query.interpretedResultsPath), database.getSourceLocationPrefix(cliServer), ]); diff --git a/extensions/ql-vscode/src/debugger/debug-protocol.ts b/extensions/ql-vscode/src/debugger/debug-protocol.ts index 44e4fcf3b39..c24d72e412b 100644 --- a/extensions/ql-vscode/src/debugger/debug-protocol.ts +++ b/extensions/ql-vscode/src/debugger/debug-protocol.ts @@ -39,6 +39,7 @@ export interface EvaluationCompletedEvent extends Event { resultType: QueryResultType; message: string | undefined; evaluationTime: number; + outputBaseName: string; }; } diff --git a/extensions/ql-vscode/src/debugger/debug-session.ts b/extensions/ql-vscode/src/debugger/debug-session.ts index 1a51df30a30..64100a7831f 100644 --- a/extensions/ql-vscode/src/debugger/debug-session.ts +++ b/extensions/ql-vscode/src/debugger/debug-session.ts @@ -16,7 +16,7 @@ import type { BaseLogger, LogOptions } from "../common/logging"; import { queryServerLogger } from "../common/logging/vscode"; import { QueryResultType } from "../query-server/messages"; import type { - CoreQueryResults, + CoreQueryResult, CoreQueryRun, QueryRunner, } from "../query-server"; @@ -25,6 +25,7 @@ import type * as CodeQLProtocol from "./debug-protocol"; import type { QuickEvalContext } from "../run-queries-shared"; import { getErrorMessage } from "../common/helpers-pure"; import { DisposableObject } from "../common/disposable-object"; +import { basename } from "path"; // More complete implementations of `Event` for certain events, because the classes from // `@vscode/debugadapter` make it more difficult to provide some of the message values. @@ -107,9 +108,9 @@ class EvaluationCompletedEvent public readonly event = "codeql-evaluation-completed"; public readonly body: CodeQLProtocol.EvaluationCompletedEvent["body"]; - constructor(results: CoreQueryResults) { + constructor(result: CoreQueryResult) { super("codeql-evaluation-completed"); - this.body = results; + this.body = result; } } @@ -143,6 +144,7 @@ const QUERY_THREAD_NAME = "Evaluation thread"; class RunningQuery extends DisposableObject { private readonly tokenSource = this.push(new CancellationTokenSource()); public readonly queryRun: CoreQueryRun; + private readonly queryPath: string; public constructor( queryRunner: QueryRunner, @@ -154,21 +156,25 @@ class RunningQuery extends DisposableObject { ) { super(); + this.queryPath = config.query; // Create the query run, which will give us some information about the query even before the // evaluation has completed. this.queryRun = queryRunner.createQueryRun( config.database, - { - queryPath: config.query, - quickEvalPosition: quickEvalContext?.quickEvalPosition, - quickEvalCountOnly: quickEvalContext?.quickEvalCount, - }, + [ + { + queryPath: this.queryPath, + outputBaseName: "results", + quickEvalPosition: quickEvalContext?.quickEvalPosition, + quickEvalCountOnly: quickEvalContext?.quickEvalCount, + }, + ], true, config.additionalPacks, config.extensionPacks, config.additionalRunQueryArgs, queryStorageDir, - undefined, + basename(config.query), undefined, ); } @@ -208,7 +214,7 @@ class RunningQuery extends DisposableObject { progressStart.body.cancellable = true; this.sendEvent(progressStart); try { - return await this.queryRun.evaluate( + const completedQuery = await this.queryRun.evaluate( (p) => { const progressUpdate = new ProgressUpdateEvent( this.queryRun.id, @@ -220,6 +226,14 @@ class RunningQuery extends DisposableObject { this.tokenSource.token, this.logger, ); + return ( + completedQuery.results.get(this.queryPath) ?? { + resultType: QueryResultType.OTHER_ERROR, + message: "Missing query results", + evaluationTime: 0, + outputBaseName: "unknown", + } + ); } finally { this.sendEvent(new ProgressEndEvent(this.queryRun.id)); } @@ -229,6 +243,7 @@ class RunningQuery extends DisposableObject { resultType: QueryResultType.OTHER_ERROR, message, evaluationTime: 0, + outputBaseName: "unknown", }; } } diff --git a/extensions/ql-vscode/src/debugger/debugger-ui.ts b/extensions/ql-vscode/src/debugger/debugger-ui.ts index 6eb9a2d9fc7..8d401897055 100644 --- a/extensions/ql-vscode/src/debugger/debugger-ui.ts +++ b/extensions/ql-vscode/src/debugger/debugger-ui.ts @@ -8,7 +8,7 @@ import { debug, Uri, CancellationTokenSource } from "vscode"; import type { DebuggerCommands } from "../common/commands"; import type { DatabaseManager } from "../databases/local-databases"; import { DisposableObject } from "../common/disposable-object"; -import type { CoreQueryResults } from "../query-server"; +import type { CoreQueryResult } from "../query-server"; import { getQuickEvalContext, saveBeforeStart, @@ -134,8 +134,15 @@ class QLDebugAdapterTracker body: EvaluationCompletedEvent["body"], ): Promise { if (this.localQueryRun !== undefined) { - const results: CoreQueryResults = body; - await this.localQueryRun.complete(results, (_) => {}); + const results: CoreQueryResult = body; + await this.localQueryRun.complete( + { + results: new Map([ + [this.configuration.query, results], + ]), + }, + (_) => {}, + ); this.localQueryRun = undefined; } } diff --git a/extensions/ql-vscode/src/language-support/ast-viewer/ast-builder.ts b/extensions/ql-vscode/src/language-support/ast-viewer/ast-builder.ts index fd2203c0615..a8fd32a3276 100644 --- a/extensions/ql-vscode/src/language-support/ast-viewer/ast-builder.ts +++ b/extensions/ql-vscode/src/language-support/ast-viewer/ast-builder.ts @@ -7,7 +7,6 @@ import type { import type { DatabaseItem } from "../../databases/local-databases"; import type { ChildAstItem, AstItem } from "./ast-viewer"; import type { Uri } from "vscode"; -import type { QueryOutputDir } from "../../local-queries/query-output-dir"; import { fileRangeFromURI } from "../contextual/file-range-from-uri"; import { mapUrlValue } from "../../common/bqrs-raw-results-mapper"; @@ -17,15 +16,12 @@ import { mapUrlValue } from "../../common/bqrs-raw-results-mapper"; */ export class AstBuilder { private roots: AstItem[] | undefined; - private bqrsPath: string; constructor( - outputDir: QueryOutputDir, + private readonly bqrsPath: string, private cli: CodeQLCliServer, public db: DatabaseItem, public fileName: Uri, - ) { - this.bqrsPath = outputDir.bqrsPath; - } + ) {} async getRoots(): Promise { if (!this.roots) { diff --git a/extensions/ql-vscode/src/language-support/contextual/location-finder.ts b/extensions/ql-vscode/src/language-support/contextual/location-finder.ts index 01b8a5bbe76..0d3c25de93d 100644 --- a/extensions/ql-vscode/src/language-support/contextual/location-finder.ts +++ b/extensions/ql-vscode/src/language-support/contextual/location-finder.ts @@ -21,7 +21,6 @@ import { } from "./query-resolver"; import type { CancellationToken, LocationLink } from "vscode"; import { Uri } from "vscode"; -import type { QueryOutputDir } from "../../local-queries/query-output-dir"; import type { QueryRunner } from "../../query-server"; import { QueryResultType } from "../../query-server/messages"; import { fileRangeFromURI } from "./file-range-from-uri"; @@ -84,9 +83,15 @@ export async function getLocationsForUriString( token, templates, ); - if (results.resultType === QueryResultType.SUCCESS) { + const queryResult = results.results.get(query); + if (queryResult?.resultType === QueryResultType.SUCCESS) { links.push( - ...(await getLinksFromResults(results.outputDir, cli, db, filter)), + ...(await getLinksFromResults( + results.outputDir.getBqrsPath(queryResult.outputBaseName), + cli, + db, + filter, + )), ); } } @@ -94,13 +99,12 @@ export async function getLocationsForUriString( } async function getLinksFromResults( - outputDir: QueryOutputDir, + bqrsPath: string, cli: CodeQLCliServer, db: DatabaseItem, filter: (srcFile: string, destFile: string) => boolean, ): Promise { const localLinks: FullLocationLink[] = []; - const bqrsPath = outputDir.bqrsPath; const info = await cli.bqrsInfo(bqrsPath); const selectInfo = info["result-sets"].find( (schema) => schema.name === SELECT_QUERY_NAME, diff --git a/extensions/ql-vscode/src/language-support/contextual/query-resolver.ts b/extensions/ql-vscode/src/language-support/contextual/query-resolver.ts index 4624fa6f383..0fe2a08d1fc 100644 --- a/extensions/ql-vscode/src/language-support/contextual/query-resolver.ts +++ b/extensions/ql-vscode/src/language-support/contextual/query-resolver.ts @@ -14,6 +14,7 @@ import type { CancellationToken } from "vscode"; import type { ProgressCallback } from "../../common/vscode/progress"; import type { CoreCompletedQuery, QueryRunner } from "../../query-server"; import { createLockFileForStandardQuery } from "../../local-queries/standard-queries"; +import { basename } from "path"; /** * This wil try to determine the qlpacks for a given database. If it can't find a matching @@ -80,13 +81,19 @@ export async function runContextualQuery( const { cleanup } = await createLockFileForStandardQuery(cli, query); const queryRun = qs.createQueryRun( db.databaseUri.fsPath, - { queryPath: query, quickEvalPosition: undefined }, + [ + { + queryPath: query, + outputBaseName: "results", + quickEvalPosition: undefined, + }, + ], false, getOnDiskWorkspaceFolders(), undefined, {}, queryStorageDir, - undefined, + basename(query), templates, ); void extLogger.log( diff --git a/extensions/ql-vscode/src/language-support/contextual/template-provider.ts b/extensions/ql-vscode/src/language-support/contextual/template-provider.ts index 19927bd8903..5fb75379001 100644 --- a/extensions/ql-vscode/src/language-support/contextual/template-provider.ts +++ b/extensions/ql-vscode/src/language-support/contextual/template-provider.ts @@ -209,8 +209,14 @@ export class TemplatePrintAstProvider { ? await this.cache.get(fileUri.toString(), progress, token) : await this.getAst(fileUri.toString(), progress, token); + const queryResults = Array.from(completedQuery.results.values()); + if (queryResults.length !== 1) { + throw new Error( + `Expected exactly one query result, but found ${queryResults.length}.`, + ); + } return new AstBuilder( - completedQuery.outputDir, + completedQuery.outputDir.getBqrsPath(queryResults[0].outputBaseName), this.cli, this.dbm.findDatabaseItem(Uri.file(completedQuery.dbPath))!, fileUri, diff --git a/extensions/ql-vscode/src/local-queries/local-queries.ts b/extensions/ql-vscode/src/local-queries/local-queries.ts index 2961586650b..4ada6cf5303 100644 --- a/extensions/ql-vscode/src/local-queries/local-queries.ts +++ b/extensions/ql-vscode/src/local-queries/local-queries.ts @@ -19,7 +19,11 @@ import { basename } from "path"; import { showBinaryChoiceDialog } from "../common/vscode/dialog"; import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders"; import { displayQuickQuery } from "./quick-query"; -import type { CoreCompletedQuery, QueryRunner } from "../query-server"; +import type { + CoreCompletedQuery, + CoreQueryTarget, + QueryRunner, +} from "../query-server"; import type { QueryHistoryManager } from "../query-history/query-history-manager"; import type { DatabaseQuickPickItem, @@ -37,6 +41,7 @@ import { createTimestampFile, getQuickEvalContext, saveBeforeStart, + validateQuerySuiteUri, validateQueryUri, } from "../run-queries-shared"; import type { CompletedLocalQueryInfo } from "../query-results"; @@ -107,6 +112,7 @@ export class LocalQueries extends DisposableObject { "codeQL.runQueries": createMultiSelectionCommand( this.runQueries.bind(this), ), + "codeQL.runQuerySuite": this.runQuerySuite.bind(this), "codeQL.quickEval": this.quickEval.bind(this), "codeQL.quickEvalCount": this.quickEvalCount.bind(this), "codeQL.quickEvalContextEditor": this.quickEval.bind(this), @@ -239,6 +245,94 @@ export class LocalQueries extends DisposableObject { ); } + private async runQuerySuite(fileUri: Uri): Promise { + await withProgress( + async (progress, token) => { + const suitePath = validateQuerySuiteUri(fileUri); + const databaseItem = await this.databaseUI.getDatabaseItem(progress); + if (databaseItem === undefined) { + throw new Error("Can't run query suite without a selected database"); + } + const selectedQuery: SelectedQuery = { + queryPath: suitePath, + }; + const additionalPacks = getOnDiskWorkspaceFolders(); + const extensionPacks = + await this.getDefaultExtensionPacks(additionalPacks); + const queries = await this.cliServer.resolveQueriesInSuite( + suitePath, + additionalPacks, + ); + if ( + !(await showBinaryChoiceDialog( + `You are about to run ${basename(suitePath)}, which contains ${queries.length} queries. Do you want to continue?`, + )) + ) { + return; + } + const queryTargets: CoreQueryTarget[] = []; + queries.forEach((query, index) => { + queryTargets.push({ + queryPath: query, + outputBaseName: `${index.toString().padStart(3, "0")}-${basename(query)}`, + quickEvalPosition: undefined, + quickEvalCountOnly: false, + }); + }); + const coreQueryRun = this.queryRunner.createQueryRun( + databaseItem.databaseUri.fsPath, + queryTargets, + true, + additionalPacks, + extensionPacks, + {}, + this.queryStorageDir, + basename(suitePath), + undefined, + ); + // handle cancellation from the history view. + const source = new CancellationTokenSource(); + try { + token.onCancellationRequested(() => source.cancel()); + + const localQueryRun = await this.createLocalQueryRun( + selectedQuery, + databaseItem, + coreQueryRun.outputDir, + source, + ); + + try { + const results = await coreQueryRun.evaluate( + progress, + source.token, + localQueryRun.logger, + ); + + await localQueryRun.complete(results, progress); + + return results; + } catch (e) { + const err = asError(e); + await localQueryRun.fail(err); + + if (token.isCancellationRequested) { + throw new UserCancellationException(err.message, true); + } else { + throw e; + } + } + } finally { + source.dispose(); + } + }, + { + title: "Running query suite", + cancellable: true, + }, + ); + } + private async quickEval(uri: Uri): Promise { await withProgress( async (progress, token) => { @@ -452,17 +546,20 @@ export class LocalQueries extends DisposableObject { const coreQueryRun = this.queryRunner.createQueryRun( databaseItem.databaseUri.fsPath, - { - queryPath: selectedQuery.queryPath, - quickEvalPosition: selectedQuery.quickEval?.quickEvalPosition, - quickEvalCountOnly: selectedQuery.quickEval?.quickEvalCount, - }, + [ + { + queryPath: selectedQuery.queryPath, + outputBaseName: "results", + quickEvalPosition: selectedQuery.quickEval?.quickEvalPosition, + quickEvalCountOnly: selectedQuery.quickEval?.quickEvalCount, + }, + ], true, additionalPacks, extensionPacks, {}, this.queryStorageDir, - undefined, + basename(selectedQuery.queryPath), templates, ); diff --git a/extensions/ql-vscode/src/local-queries/local-query-run.ts b/extensions/ql-vscode/src/local-queries/local-query-run.ts index 5ea2463caef..cbe151a6601 100644 --- a/extensions/ql-vscode/src/local-queries/local-query-run.ts +++ b/extensions/ql-vscode/src/local-queries/local-query-run.ts @@ -4,7 +4,7 @@ import { showAndLogExceptionWithTelemetry, showAndLogWarningMessage, } from "../common/logging"; -import type { CoreQueryResults } from "../query-server"; +import type { CoreQueryResult, CoreQueryResults } from "../query-server"; import type { QueryHistoryManager } from "../query-history/query-history-manager"; import type { DatabaseItem } from "../databases/local-databases"; import type { @@ -29,7 +29,7 @@ import type { Disposable } from "../common/disposable-object"; import type { ProgressCallback } from "../common/vscode/progress"; import { progressUpdate } from "../common/vscode/progress"; -function formatResultMessage(result: CoreQueryResults): string { +function formatResultMessage(result: CoreQueryResult): string { switch (result.resultType) { case QueryResultType.CANCELLATION: return `cancelled after ${Math.round( @@ -86,7 +86,9 @@ export class LocalQueryRun { progress: ProgressCallback, ): Promise { const evalLogPaths = await this.summarizeEvalLog( - results.resultType, + Array.from(results.results.values()).every( + (result) => result.resultType === QueryResultType.SUCCESS, + ), this.outputDir, this.logger, progress, @@ -95,9 +97,12 @@ export class LocalQueryRun { this.queryInfo.setEvaluatorLogPaths(evalLogPaths); } progress(progressUpdate(1, 4, "Getting completed query info")); - const queryWithResults = await this.getCompletedQueryInfo(results); + const queriesWithResults = await this.getCompletedQueryInfo(results); progress(progressUpdate(2, 4, "Updating query history")); - this.queryHistoryManager.completeQuery(this.queryInfo, queryWithResults); + this.queryHistoryManager.completeQueries( + this.queryInfo, + queriesWithResults, + ); progress(progressUpdate(3, 4, "Showing results")); await this.localQueries.showResultsForCompletedQuery( this.queryInfo as CompletedLocalQueryInfo, @@ -116,7 +121,7 @@ export class LocalQueryRun { */ public async fail(err: Error): Promise { const evalLogPaths = await this.summarizeEvalLog( - QueryResultType.OTHER_ERROR, + false, this.outputDir, this.logger, (_) => {}, @@ -136,7 +141,7 @@ export class LocalQueryRun { * Generate summaries of the structured evaluator log. */ private async summarizeEvalLog( - resultType: QueryResultType, + runSuccessful: boolean, outputDir: QueryOutputDir, logger: BaseLogger, progress: ProgressCallback, @@ -152,7 +157,7 @@ export class LocalQueryRun { } } else { // Raw evaluator log was not found. Notify the user, unless we know why it wasn't found. - if (resultType === QueryResultType.SUCCESS) { + if (runSuccessful) { void showAndLogWarningMessage( extLogger, `Failed to write structured evaluator log to ${outputDir.evalLogPath}.`, @@ -168,41 +173,43 @@ export class LocalQueryRun { } /** - * Gets a `QueryWithResults` containing information about the evaluation of the query and its + * Gets a `QueryWithResults` containing information about the evaluation of the queries and their * result, in the form expected by the query history UI. */ private async getCompletedQueryInfo( results: CoreQueryResults, - ): Promise { - // Read the query metadata if possible, to use in the UI. - const metadata = await tryGetQueryMetadata( - this.cliServer, - this.queryInfo.initialInfo.queryPath, - ); - const query = new QueryEvaluationInfo( - this.outputDir.querySaveDir, - this.dbItem.databaseUri.fsPath, - await this.dbItem.hasMetadataFile(), - this.queryInfo.initialInfo.quickEvalPosition, - metadata, - ); + ): Promise { + const infos: QueryWithResults[] = []; + for (const [queryPath, result] of results.results) { + // Read the query metadata if possible, to use in the UI. + const metadata = await tryGetQueryMetadata(this.cliServer, queryPath); + const query = new QueryEvaluationInfo( + this.outputDir.querySaveDir, + result.outputBaseName, + this.dbItem.databaseUri.fsPath, + await this.dbItem.hasMetadataFile(), + undefined, + metadata, + ); - if (results.resultType !== QueryResultType.SUCCESS) { - const message = results.message - ? redactableError`Failed to run query: ${results.message}` - : redactableError`Failed to run query`; - void showAndLogExceptionWithTelemetry( - extLogger, - telemetryListener, + if (result.resultType !== QueryResultType.SUCCESS) { + const message = result.message + ? redactableError`Failed to run query: ${result.message}` + : redactableError`Failed to run query`; + void showAndLogExceptionWithTelemetry( + extLogger, + telemetryListener, + message, + ); + } + const message = formatResultMessage(result); + const successful = result.resultType === QueryResultType.SUCCESS; + infos.push({ + query, message, - ); + successful, + }); } - const message = formatResultMessage(results); - const successful = results.resultType === QueryResultType.SUCCESS; - return { - query, - message, - successful, - }; + return infos; } } diff --git a/extensions/ql-vscode/src/local-queries/query-output-dir.ts b/extensions/ql-vscode/src/local-queries/query-output-dir.ts index a049849d54c..00be58078b7 100644 --- a/extensions/ql-vscode/src/local-queries/query-output-dir.ts +++ b/extensions/ql-vscode/src/local-queries/query-output-dir.ts @@ -30,10 +30,6 @@ function findQueryEvalLogEndSummaryFile(resultPath: string): string { export class QueryOutputDir { constructor(public readonly querySaveDir: string) {} - get dilPath() { - return join(this.querySaveDir, "results.dil"); - } - /** * Get the path that the compiled query is if it exists. Note that it only exists when using the legacy query server. */ @@ -41,10 +37,6 @@ export class QueryOutputDir { return join(this.querySaveDir, "compiledQuery.qlo"); } - get csvPath() { - return join(this.querySaveDir, "results.csv"); - } - get logPath() { return findQueryLogFile(this.querySaveDir); } @@ -69,7 +61,25 @@ export class QueryOutputDir { return findQueryEvalLogEndSummaryFile(this.querySaveDir); } - get bqrsPath() { - return join(this.querySaveDir, "results.bqrs"); + getBqrsPath(outputBaseName: string): string { + return join(this.querySaveDir, `${outputBaseName}.bqrs`); + } + + getInterpretedResultsPath( + metadataKind: string | undefined, + outputBaseName: string, + ): string { + return join( + this.querySaveDir, + `${outputBaseName}-${metadataKind === "graph" ? "graph" : `interpreted.sarif`}`, + ); + } + + getCsvPath(outputBaseName: string): string { + return join(this.querySaveDir, `${outputBaseName}.csv`); + } + + getDilPath(outputBaseName: string): string { + return join(this.querySaveDir, `${outputBaseName}.dil`); } } diff --git a/extensions/ql-vscode/src/local-queries/results-view.ts b/extensions/ql-vscode/src/local-queries/results-view.ts index 4e28e9f9c73..00eae1138aa 100644 --- a/extensions/ql-vscode/src/local-queries/results-view.ts +++ b/extensions/ql-vscode/src/local-queries/results-view.ts @@ -556,10 +556,14 @@ export class ResultsView extends AbstractWebview< await this.postMessage({ t: "setState", interpretation: interpretationPage, - origResultsPaths: fullQuery.completedQuery.query.resultsPaths, + origResultsPaths: { + resultsPath: fullQuery.completedQuery.query.resultsPath, + interpretedResultsPath: + fullQuery.completedQuery.query.interpretedResultsPath, + }, resultsPath: this.convertPathToWebviewUri( panel, - fullQuery.completedQuery.query.resultsPaths.resultsPath, + fullQuery.completedQuery.query.resultsPath, ), parsedResultSets, sortedResultsMap, @@ -704,10 +708,14 @@ export class ResultsView extends AbstractWebview< await this.postMessage({ t: "setState", interpretation: this._interpretation, - origResultsPaths: results.completedQuery.query.resultsPaths, + origResultsPaths: { + resultsPath: results.completedQuery.query.resultsPath, + interpretedResultsPath: + results.completedQuery.query.interpretedResultsPath, + }, resultsPath: this.convertPathToWebviewUri( panel, - results.completedQuery.query.resultsPaths.resultsPath, + results.completedQuery.query.resultsPath, ), parsedResultSets, sortedResultsMap, @@ -842,7 +850,10 @@ export class ResultsView extends AbstractWebview< }; await this._getInterpretedResults( query.metadata, - query.resultsPaths, + { + resultsPath: query.resultsPath, + interpretedResultsPath: query.interpretedResultsPath, + }, sourceInfo, sourceLocationPrefix, sortState, diff --git a/extensions/ql-vscode/src/local-queries/run-query.ts b/extensions/ql-vscode/src/local-queries/run-query.ts index 1f06c656b56..06ed7037280 100644 --- a/extensions/ql-vscode/src/local-queries/run-query.ts +++ b/extensions/ql-vscode/src/local-queries/run-query.ts @@ -33,17 +33,20 @@ export async function runQuery({ // Create a query run to execute const queryRun = queryRunner.createQueryRun( databaseItem.databaseUri.fsPath, - { - queryPath, - quickEvalPosition: undefined, - quickEvalCountOnly: false, - }, + [ + { + queryPath, + outputBaseName: "results", + quickEvalPosition: undefined, + quickEvalCountOnly: false, + }, + ], false, additionalPacks, extensionPacks, {}, queryStorageDir, - undefined, + basename(queryPath), undefined, ); @@ -54,13 +57,14 @@ export async function runQuery({ try { const completedQuery = await queryRun.evaluate(progress, token, teeLogger); + const result = completedQuery.results.get(queryPath); - if (completedQuery.resultType !== QueryResultType.SUCCESS) { + if (result?.resultType !== QueryResultType.SUCCESS) { void showAndLogExceptionWithTelemetry( extLogger, telemetryListener, redactableError`Failed to run ${basename(queryPath)} query: ${ - completedQuery.message ?? "No message" + result?.message ?? "No message" }`, ); return; diff --git a/extensions/ql-vscode/src/model-editor/generate.ts b/extensions/ql-vscode/src/model-editor/generate.ts index 9f4b20c13da..157ed78b78d 100644 --- a/extensions/ql-vscode/src/model-editor/generate.ts +++ b/extensions/ql-vscode/src/model-editor/generate.ts @@ -91,6 +91,14 @@ async function runSingleGenerateQuery( if (!completedQuery) { return undefined; } + const queryResults = Array.from(completedQuery.results.values()); + if (queryResults.length !== 1) { + throw new Error( + `Expected exactly one query result, but got ${queryResults.length}`, + ); + } - return cliServer.bqrsDecodeAll(completedQuery.outputDir.bqrsPath); + return cliServer.bqrsDecodeAll( + completedQuery.outputDir.getBqrsPath(queryResults[0].outputBaseName), + ); } diff --git a/extensions/ql-vscode/src/model-editor/model-editor-queries.ts b/extensions/ql-vscode/src/model-editor/model-editor-queries.ts index 0e7ddd48c4e..f1e7429afbd 100644 --- a/extensions/ql-vscode/src/model-editor/model-editor-queries.ts +++ b/extensions/ql-vscode/src/model-editor/model-editor-queries.ts @@ -172,10 +172,19 @@ export async function runModelEditorQueries( maxStep: externalApiQueriesProgressMaxStep, }); + const queryResults = Array.from(completedQuery.results.values()); + if (queryResults.length !== 1) { + throw new Error( + `Expected exactly one query result, but got ${queryResults.length}`, + ); + } + const bqrsChunk = await readQueryResults({ cliServer, logger, - bqrsPath: completedQuery.outputDir.bqrsPath, + bqrsPath: completedQuery.outputDir.getBqrsPath( + queryResults[0].outputBaseName, + ), }); if (!bqrsChunk) { return; diff --git a/extensions/ql-vscode/src/model-editor/suggestion-queries.ts b/extensions/ql-vscode/src/model-editor/suggestion-queries.ts index 37ce78fd3a5..f325fc2a4d5 100644 --- a/extensions/ql-vscode/src/model-editor/suggestion-queries.ts +++ b/extensions/ql-vscode/src/model-editor/suggestion-queries.ts @@ -109,7 +109,15 @@ export async function runSuggestionsQuery( maxStep, }); - const bqrs = await cliServer.bqrsDecodeAll(completedQuery.outputDir.bqrsPath); + const queryResults = Array.from(completedQuery.results.values()); + if (queryResults.length !== 1) { + throw new Error( + `Expected exactly one query result, but got ${queryResults.length}`, + ); + } + const bqrs = await cliServer.bqrsDecodeAll( + completedQuery.outputDir.getBqrsPath(queryResults[0].outputBaseName), + ); progress({ message: "Finalizing results", diff --git a/extensions/ql-vscode/src/query-history/history-item-label-provider.ts b/extensions/ql-vscode/src/query-history/history-item-label-provider.ts index 89f0f89a027..8787e025daf 100644 --- a/extensions/ql-vscode/src/query-history/history-item-label-provider.ts +++ b/extensions/ql-vscode/src/query-history/history-item-label-provider.ts @@ -115,7 +115,7 @@ export class HistoryItemLabelProvider { startTime: item.startTime, queryName: item.getQueryName(), databaseName: item.databaseName, - resultCount: `(${resultCount} results)`, + resultCount: resultCount === -1 ? "" : `(${resultCount} results)`, status: message, queryFileBasename: item.getQueryFileName(), queryLanguage: this.getLanguageLabel(item), diff --git a/extensions/ql-vscode/src/query-history/query-history-manager.ts b/extensions/ql-vscode/src/query-history/query-history-manager.ts index 3074ceb3bb9..bf6823abac1 100644 --- a/extensions/ql-vscode/src/query-history/query-history-manager.ts +++ b/extensions/ql-vscode/src/query-history/query-history-manager.ts @@ -23,7 +23,8 @@ import { URLSearchParams } from "url"; import { DisposableObject } from "../common/disposable-object"; import { ONE_HOUR_IN_MS, TWO_HOURS_IN_MS } from "../common/time"; import { assertNever, getErrorMessage } from "../common/helpers-pure"; -import type { CompletedLocalQueryInfo, LocalQueryInfo } from "../query-results"; +import type { CompletedLocalQueryInfo } from "../query-results"; +import { LocalQueryInfo } from "../query-results"; import type { QueryHistoryInfo } from "./query-history-info"; import { getActionsWorkflowRunUrl, @@ -348,8 +349,37 @@ export class QueryHistoryManager extends DisposableObject { }; } - public completeQuery(info: LocalQueryInfo, results: QueryWithResults): void { - info.completeThisQuery(results); + public completeQueries( + info: LocalQueryInfo, + results: QueryWithResults[], + ): void { + let first = true; + // Sorting results by the output/results basename should produce a deterministic order. + results.sort((a, b) => { + const aPath = a.query.outputBaseName; + const bPath = b.query.outputBaseName; + return aPath.localeCompare(bPath); + }); + for (const result of results) { + if (first) { + // This is the first query, so we can just update the existing info. + info.completeThisQuery(result); + first = false; + } else { + // For other queries in the same run, we'll add new entries to the history pane. In the long + // term, it would be better if we could have a single entry containing sub-entries for each + // query. + const clonedInfo = new LocalQueryInfo( + info.initialInfo, + undefined, + info.failureReason, + undefined, + info.evaluatorLogPaths, + ); + clonedInfo.completeThisQuery(result); + this.addQuery(clonedInfo); + } + } this._onDidCompleteQuery.fire(info); } @@ -555,6 +585,23 @@ export class QueryHistoryManager extends DisposableObject { }), ); + await Promise.all( + this.treeDataProvider.allHistory.map(async (item) => { + // Remove any local queries whose directories no longer exist. This can happen when running + // a query suite, which produces multiple queries in the history pane that all share the + // same underlying directory, which we may have just deleted above. (Ideally, there would be + // a first-class concept of a local multi-query run in this pane that would group them all + // together, but doing it this way at least avoids cluttering the history pane with entries + // that can no longer be viewed). + if (item.t === "local") { + const dir = item.completedQuery?.query.querySaveDir; + if (dir && !(await pathExists(dir))) { + this.treeDataProvider.remove(item); + } + } + }), + ); + await this.writeQueryHistory(); const current = this.treeDataProvider.getCurrent(); if (current !== undefined) { @@ -942,7 +989,7 @@ export class QueryHistoryManager extends DisposableObject { if (hasInterpretedResults) { await tryOpenExternalFile( this.app.commands, - query.resultsPaths.interpretedResultsPath, + query.interpretedResultsPath, ); } else { const label = this.labelProvider.getLabel(item); diff --git a/extensions/ql-vscode/src/query-history/store/query-history-local-query-domain-mapper.ts b/extensions/ql-vscode/src/query-history/store/query-history-local-query-domain-mapper.ts index 5f691e60785..61fe2e0bc4b 100644 --- a/extensions/ql-vscode/src/query-history/store/query-history-local-query-domain-mapper.ts +++ b/extensions/ql-vscode/src/query-history/store/query-history-local-query-domain-mapper.ts @@ -118,6 +118,6 @@ function mapQueryEvaluationInfoToDto( databaseHasMetadataFile: queryEvaluationInfo.databaseHasMetadataFile, quickEvalPosition: queryEvaluationInfo.quickEvalPosition, metadata: queryEvaluationInfo.metadata, - resultsPaths: queryEvaluationInfo.resultsPaths, + outputBaseName: queryEvaluationInfo.outputBaseName, }; } diff --git a/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto-mapper.ts b/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto-mapper.ts index 7afe4b907ad..aa42dd8c1a0 100644 --- a/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto-mapper.ts +++ b/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto-mapper.ts @@ -104,6 +104,7 @@ function mapQueryEvaluationInfoToDomainModel( ): QueryEvaluationInfo { return new QueryEvaluationInfo( evaluationInfo.querySaveDir, + evaluationInfo.outputBaseName ?? "results", evaluationInfo.dbItemPath, evaluationInfo.databaseHasMetadataFile, evaluationInfo.quickEvalPosition, diff --git a/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto.ts b/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto.ts index b9a2f3448fa..2a6b3c78ea0 100644 --- a/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto.ts +++ b/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto.ts @@ -86,7 +86,10 @@ export interface QueryEvaluationInfoDto { databaseHasMetadataFile: boolean; quickEvalPosition?: PositionDto; metadata?: QueryMetadataDto; - resultsPaths: { + outputBaseName?: string; + + // Superceded by outputBaseName + resultsPaths?: { resultsPath: string; interpretedResultsPath: string; }; diff --git a/extensions/ql-vscode/src/query-history/store/query-history-store.ts b/extensions/ql-vscode/src/query-history/store/query-history-store.ts index 279c17c4dc0..0b54908ceba 100644 --- a/extensions/ql-vscode/src/query-history/store/query-history-store.ts +++ b/extensions/ql-vscode/src/query-history/store/query-history-store.ts @@ -61,7 +61,7 @@ export async function readQueryHistoryFromFile( // to see if they exist on disk. return true; } - const resultsPath = q.completedQuery?.query.resultsPaths.resultsPath; + const resultsPath = q.completedQuery?.query.resultsPath; return !!resultsPath && (await pathExists(resultsPath)); }, ); diff --git a/extensions/ql-vscode/src/query-results.ts b/extensions/ql-vscode/src/query-results.ts index 69a99837b52..3e81762bc08 100644 --- a/extensions/ql-vscode/src/query-results.ts +++ b/extensions/ql-vscode/src/query-results.ts @@ -64,7 +64,7 @@ export class CompletedQueryInfo implements QueryWithResults { * sarif file. */ public interpretedResultsSortState: InterpretedResultsSortState | undefined, - public resultCount: number = 0, + public resultCount: number = -1, /** * Map from result set name to SortedResultSetInfo. @@ -78,11 +78,11 @@ export class CompletedQueryInfo implements QueryWithResults { getResultsPath(selectedTable: string, useSorted = true): string { if (!useSorted) { - return this.query.resultsPaths.resultsPath; + return this.query.resultsPath; } return ( this.sortedResultsInfo[selectedTable]?.resultsPath || - this.query.resultsPaths.resultsPath + this.query.resultsPath ); } @@ -102,7 +102,7 @@ export class CompletedQueryInfo implements QueryWithResults { }; await server.sortBqrs( - this.query.resultsPaths.resultsPath, + this.query.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], diff --git a/extensions/ql-vscode/src/query-server/messages.ts b/extensions/ql-vscode/src/query-server/messages.ts index 44e0d515458..4edacdc4e54 100644 --- a/extensions/ql-vscode/src/query-server/messages.ts +++ b/extensions/ql-vscode/src/query-server/messages.ts @@ -130,13 +130,29 @@ export interface RunQueryParams { extensionPacks?: string[]; } -interface RunQueryResult { +export interface RunQueryResult { resultType: QueryResultType; message?: string; expectedDbschemeName?: string; evaluationTime: number; } +export interface RunQueryInputOutput { + queryPath: string; + outputPath: string; + dilPath: string; +} + +export interface RunQueriesParams { + inputOutputPaths: RunQueryInputOutput[]; + db: string; + additionalPacks: string[]; + externalInputs: Record; + singletonExternalInputs: Record; + logPath?: string; + extensionPacks?: string[]; +} + interface UpgradeParams { db: string; additionalPacks: string[]; @@ -196,6 +212,12 @@ export const runQuery = new RequestType< void >("evaluation/runQuery"); +export const runQueries = new RequestType< + WithProgressId, + Record, + void +>("evaluation/runQueries"); + export const registerDatabases = new RequestType< WithProgressId, RegisterDatabasesResult, diff --git a/extensions/ql-vscode/src/query-server/query-runner.ts b/extensions/ql-vscode/src/query-server/query-runner.ts index 7fbb3446575..08b9f1507dc 100644 --- a/extensions/ql-vscode/src/query-server/query-runner.ts +++ b/extensions/ql-vscode/src/query-server/query-runner.ts @@ -20,18 +20,25 @@ import { upgradeDatabase, } from "./messages"; import type { BaseLogger, Logger } from "../common/logging"; -import { basename, join } from "path"; +import { join } from "path"; import { nanoid } from "nanoid"; import type { QueryServerClient } from "./query-server-client"; import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders"; import { compileAndRunQueryAgainstDatabaseCore } from "./run-queries"; export interface CoreQueryTarget { - /** The full path to the query. */ + /** Path to the query source file. */ queryPath: string; + + /** + * Base name to use for output files, without extension. For example, "foo" will result in the + * BQRS file being written to "/foo.bqrs". + */ + outputBaseName: string; + /** * Optional position of text to be used as QuickEval target. This need not be in the same file as - * `query`. + * `queryPath`. */ quickEvalPosition?: Position; /** @@ -40,14 +47,25 @@ export interface CoreQueryTarget { quickEvalCountOnly?: boolean; } -export interface CoreQueryResults { +export interface CoreQueryResult { readonly resultType: QueryResultType; readonly message: string | undefined; readonly evaluationTime: number; + + /** + * The base name of the output file. Append '.bqrs' and join with the output directory to get the + * path to the BQRS. + */ + readonly outputBaseName: string; +} + +export interface CoreQueryResults { + /** A map from query path to its results. */ + readonly results: Map; } export interface CoreQueryRun { - readonly queryTarget: CoreQueryTarget; + readonly queryTargets: CoreQueryTarget[]; readonly dbPath: string; readonly id: string; readonly outputDir: QueryOutputDir; @@ -126,7 +144,7 @@ export class QueryRunner { public async compileAndRunQueryAgainstDatabaseCore( dbPath: string, - query: CoreQueryTarget, + queries: CoreQueryTarget[], additionalPacks: string[], extensionPacks: string[] | undefined, additionalRunQueryArgs: Record, @@ -142,7 +160,7 @@ export class QueryRunner { return await compileAndRunQueryAgainstDatabaseCore( this.qs, dbPath, - query, + queries, generateEvalLog, additionalPacks, extensionPacks, @@ -213,19 +231,20 @@ export class QueryRunner { */ public createQueryRun( dbPath: string, - query: CoreQueryTarget, + queries: CoreQueryTarget[], generateEvalLog: boolean, additionalPacks: string[], extensionPacks: string[] | undefined, additionalRunQueryArgs: Record, queryStorageDir: string, - id = `${basename(query.queryPath)}-${nanoid()}`, + queryBasename: string, templates: Record | undefined, ): CoreQueryRun { + const id = `${queryBasename}-${nanoid()}`; const outputDir = new QueryOutputDir(join(queryStorageDir, id)); return { - queryTarget: query, + queryTargets: queries, dbPath, id, outputDir, @@ -238,10 +257,10 @@ export class QueryRunner { id, outputDir, dbPath, - queryTarget: query, + queryTargets: queries, ...(await this.compileAndRunQueryAgainstDatabaseCore( dbPath, - query, + queries, additionalPacks, extensionPacks, additionalRunQueryArgs, diff --git a/extensions/ql-vscode/src/query-server/query-server-client.ts b/extensions/ql-vscode/src/query-server/query-server-client.ts index c342e3b4996..d52af580dea 100644 --- a/extensions/ql-vscode/src/query-server/query-server-client.ts +++ b/extensions/ql-vscode/src/query-server/query-server-client.ts @@ -95,6 +95,14 @@ export class QueryServerClient extends DisposableObject { return this.opts.logger; } + /** + * Whether this query server supports the 'evaluation/runQueries' method for running multiple + * queries at once. + */ + async supportsRunQueriesMethod(): Promise { + return (await this.cliServer.getFeatures()).queryServerRunQueries === true; + } + /** Stops the query server by disposing of the current server process. */ private stopQueryServer(): void { if (this.serverProcess !== undefined) { diff --git a/extensions/ql-vscode/src/query-server/run-queries.ts b/extensions/ql-vscode/src/query-server/run-queries.ts index 593979118e7..5a35144728f 100644 --- a/extensions/ql-vscode/src/query-server/run-queries.ts +++ b/extensions/ql-vscode/src/query-server/run-queries.ts @@ -1,10 +1,19 @@ import type { CancellationToken } from "vscode"; import type { ProgressCallback } from "../common/vscode/progress"; -import type { RunQueryParams } from "./messages"; -import { runQuery } from "./messages"; +import type { + RunQueryParams, + RunQueryResult, + RunQueriesParams, + RunQueryInputOutput, +} from "./messages"; +import { runQueries, runQuery } from "./messages"; import type { QueryOutputDir } from "../local-queries/query-output-dir"; import type { QueryServerClient } from "./query-server-client"; -import type { CoreQueryResults, CoreQueryTarget } from "./query-runner"; +import type { + CoreQueryResult, + CoreQueryResults, + CoreQueryTarget, +} from "./query-runner"; import type { BaseLogger } from "../common/logging"; /** @@ -24,7 +33,7 @@ import type { BaseLogger } from "../common/logging"; export async function compileAndRunQueryAgainstDatabaseCore( qs: QueryServerClient, dbPath: string, - query: CoreQueryTarget, + targets: CoreQueryTarget[], generateEvalLog: boolean, additionalPacks: string[], extensionPacks: string[] | undefined, @@ -35,12 +44,36 @@ export async function compileAndRunQueryAgainstDatabaseCore( templates: Record | undefined, logger: BaseLogger, ): Promise { - const target = - query.quickEvalPosition !== undefined + if (targets.length > 1) { + // We are running a batch of multiple queries; use the new query server API for that. + if (targets.some((target) => target.quickEvalPosition !== undefined)) { + throw new Error( + "Quick evaluation is not supported when running multiple queries.", + ); + } + return compileAndRunQueriesAgainstDatabaseCore( + qs, + dbPath, + targets, + generateEvalLog, + additionalPacks, + extensionPacks, + additionalRunQueryArgs, + outputDir, + progress, + token, + templates, + logger, + ); + } + + const target = targets[0]; + const compilationTarget = + target.quickEvalPosition !== undefined ? { quickEval: { - quickEvalPos: query.quickEvalPosition, - countOnly: query.quickEvalCountOnly, + quickEvalPos: target.quickEvalPosition, + countOnly: target.quickEvalCountOnly, }, } : { query: {} }; @@ -51,11 +84,11 @@ export async function compileAndRunQueryAgainstDatabaseCore( additionalPacks, externalInputs: {}, singletonExternalInputs: templates || {}, - outputPath: outputDir.bqrsPath, - queryPath: query.queryPath, - dilPath: outputDir.dilPath, + queryPath: target.queryPath, + outputPath: outputDir.getBqrsPath(target.outputBaseName), + dilPath: outputDir.getDilPath(target.outputBaseName), logPath: evalLogPath, - target, + target: compilationTarget, extensionPacks, // Add any additional arguments without interpretation. ...additionalRunQueryArgs, @@ -67,10 +100,83 @@ export async function compileAndRunQueryAgainstDatabaseCore( // properly will require a change in the query server. qs.activeQueryLogger = logger; const result = await qs.sendRequest(runQuery, queryToRun, token, progress); + return { + results: new Map([ + [ + target.queryPath, + { + resultType: result.resultType, + message: result.message, + evaluationTime: result.evaluationTime, + outputBaseName: target.outputBaseName, + }, + ], + ]), + }; +} + +async function compileAndRunQueriesAgainstDatabaseCore( + qs: QueryServerClient, + dbPath: string, + targets: CoreQueryTarget[], + generateEvalLog: boolean, + additionalPacks: string[], + extensionPacks: string[] | undefined, + additionalRunQueryArgs: Record, + outputDir: QueryOutputDir, + progress: ProgressCallback, + token: CancellationToken, + templates: Record | undefined, + logger: BaseLogger, +): Promise { + if (!(await qs.supportsRunQueriesMethod())) { + throw new Error( + "The CodeQL CLI does not support the 'evaluation/runQueries' query-server command. Please update to the latest version.", + ); + } + const inputOutputPaths: RunQueryInputOutput[] = targets.map((target) => { + return { + queryPath: target.queryPath, + outputPath: outputDir.getBqrsPath(target.outputBaseName), + dilPath: outputDir.getDilPath(target.outputBaseName), + }; + }); + const evalLogPath = generateEvalLog ? outputDir.evalLogPath : undefined; + const queriesToRun: RunQueriesParams = { + db: dbPath, + additionalPacks, + externalInputs: {}, + singletonExternalInputs: templates || {}, + inputOutputPaths, + logPath: evalLogPath, + extensionPacks, + // Add any additional arguments without interpretation. + ...additionalRunQueryArgs, + }; + + // Update the active query logger every time there is a new request to compile. + // This isn't ideal because in situations where there are queries running + // in parallel, each query's log messages are interleaved. Fixing this + // properly will require a change in the query server. + qs.activeQueryLogger = logger; + const queryResults: Record = await qs.sendRequest( + runQueries, + queriesToRun, + token, + progress, + ); + const coreQueryResults = new Map(); + targets.forEach((target) => { + const queryResult = queryResults[target.queryPath]; + coreQueryResults.set(target.queryPath, { + resultType: queryResult.resultType, + message: queryResult.message, + evaluationTime: queryResult.evaluationTime, + outputBaseName: target.outputBaseName, + }); + }); return { - resultType: result.resultType, - message: result.message, - evaluationTime: result.evaluationTime, + results: coreQueryResults, }; } diff --git a/extensions/ql-vscode/src/run-queries-shared.ts b/extensions/ql-vscode/src/run-queries-shared.ts index dac447ee20b..990d3d8293d 100644 --- a/extensions/ql-vscode/src/run-queries-shared.ts +++ b/extensions/ql-vscode/src/run-queries-shared.ts @@ -65,6 +65,7 @@ export class QueryEvaluationInfo extends QueryOutputDir { */ constructor( querySaveDir: string, + public readonly outputBaseName: string, public readonly dbItemPath: string, public readonly databaseHasMetadataFile: boolean, public readonly quickEvalPosition?: Position, @@ -73,23 +74,30 @@ export class QueryEvaluationInfo extends QueryOutputDir { super(querySaveDir); } - get resultsPaths() { - return { - resultsPath: this.bqrsPath, - interpretedResultsPath: join( - this.querySaveDir, - this.metadata?.kind === "graph" - ? "graphResults" - : "interpretedResults.sarif", - ), - }; + get resultsPath() { + return this.getBqrsPath(this.outputBaseName); } + + get interpretedResultsPath() { + return this.getInterpretedResultsPath( + this.metadata?.kind, + this.outputBaseName, + ); + } + + get csvPath() { + return this.getCsvPath(this.outputBaseName); + } + + get dilPath() { + return this.getDilPath(this.outputBaseName); + } + getSortedResultSetPath(resultSetName: string) { const hasher = createHash("sha256"); hasher.update(resultSetName); - return join( - this.querySaveDir, - `sortedResults-${hasher.digest("hex")}.bqrs`, + return this.getBqrsPath( + `${this.outputBaseName}-sorted-${hasher.digest("hex")}`, ); } @@ -127,7 +135,7 @@ export class QueryEvaluationInfo extends QueryOutputDir { * Holds if this query actually has produced interpreted results. */ async hasInterpretedResults(): Promise { - return pathExists(this.resultsPaths.interpretedResultsPath); + return pathExists(this.interpretedResultsPath); } /** @@ -205,7 +213,7 @@ export class QueryEvaluationInfo extends QueryOutputDir { let nextOffset: number | undefined = 0; do { const chunk: DecodedBqrsChunk = await cliServer.bqrsDecode( - this.resultsPaths.resultsPath, + this.resultsPath, resultSet, { pageSize: 100, @@ -243,9 +251,9 @@ export class QueryEvaluationInfo extends QueryOutputDir { * If the query has no result sets, then return undefined. */ async chooseResultSet(cliServer: CodeQLCliServer) { - const resultSets = ( - await cliServer.bqrsInfo(this.resultsPaths.resultsPath) - )["result-sets"]; + const resultSets = (await cliServer.bqrsInfo(this.resultsPath))[ + "result-sets" + ]; if (!resultSets.length) { return undefined; } @@ -284,7 +292,7 @@ export class QueryEvaluationInfo extends QueryOutputDir { } await cliServer.generateResultsCsv( ensureMetadataIsComplete(this.metadata), - this.resultsPaths.resultsPath, + this.resultsPath, this.csvPath, sourceInfo, ); @@ -348,6 +356,23 @@ export function validateQueryPath( } } +/** + * Validates that the specified URI represents a QL query suite (QLS), and returns the file system + * path to that suite. + */ +export function validateQuerySuiteUri(suiteUri: Uri): string { + if (suiteUri.scheme !== "file") { + throw new Error("Can only run queries that are on disk."); + } + const suitePath = suiteUri.fsPath; + if (!suitePath.endsWith(".qls")) { + throw new Error( + 'The selected resource is not a CodeQL query suite; It should have the extension ".qls".', + ); + } + return suitePath; +} + export interface QuickEvalContext { quickEvalPosition: Position; quickEvalText: string; diff --git a/extensions/ql-vscode/test/data-extensions/pack-using-extensions/codeql-pack.lock.yml b/extensions/ql-vscode/test/data-extensions/pack-using-extensions/codeql-pack.lock.yml new file mode 100644 index 00000000000..61121d6d0cf --- /dev/null +++ b/extensions/ql-vscode/test/data-extensions/pack-using-extensions/codeql-pack.lock.yml @@ -0,0 +1,26 @@ +--- +lockVersion: 1.0.0 +dependencies: + codeql/dataflow: + version: 2.0.7 + codeql/javascript-all: + version: 2.6.3 + codeql/mad: + version: 1.0.23 + codeql/regex: + version: 1.0.23 + codeql/ssa: + version: 1.1.2 + codeql/threat-models: + version: 1.0.23 + codeql/tutorial: + version: 1.0.23 + codeql/typetracking: + version: 2.0.7 + codeql/util: + version: 2.0.10 + codeql/xml: + version: 1.0.23 + codeql/yaml: + version: 1.0.23 +compiled: false diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debug-controller.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debug-controller.ts index 35435b1a797..1161282bedf 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debug-controller.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debug-controller.ts @@ -83,14 +83,17 @@ class Tracker implements DebugAdapterTracker { kind: "evaluationCompleted", started: this.started!, results: { - ...this.started!, - ...this.completed!, + id: this.started!.id, + results: new Map([[this.queryPath!, this.completed!]]), outputDir: new QueryOutputDir(this.started!.outputDir), - queryTarget: { - queryPath: this.queryPath!, - quickEvalPosition: - this.started!.quickEvalContext?.quickEvalPosition, - }, + queryTargets: [ + { + queryPath: this.queryPath!, + outputBaseName: "results", + quickEvalPosition: + this.started!.quickEvalContext?.quickEvalPosition, + }, + ], dbPath: this.database!, }, }); @@ -350,15 +353,19 @@ class DebugController public async expectSucceeded(): Promise { const event = await this.expectCompleted(); - if (event.results.resultType !== QueryResultType.SUCCESS) { - expect(event.results.message).toBe("success"); + const results = Array.from(event.results.results.values()); + expect(results.length).toBe(1); + if (results[0].resultType !== QueryResultType.SUCCESS) { + expect(results[0].message).toBe("success"); } return event; } public async expectFailed(): Promise { const event = await this.expectCompleted(); - expect(event.results.resultType).not.toEqual(QueryResultType.SUCCESS); + const results = Array.from(event.results.results.values()); + expect(results.length).toBe(1); + expect(results[0].resultType).not.toEqual(QueryResultType.SUCCESS); return event; } diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debugger.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debugger.test.ts index d92a6a15cf5..76509030a4e 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debugger.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debugger.test.ts @@ -10,10 +10,10 @@ import { import { describeWithCodeQL } from "../../cli"; import { withDebugController } from "./debug-controller"; import type { CodeQLCliServer } from "../../../../src/codeql-cli/cli"; -import type { QueryOutputDir } from "../../../../src/local-queries/query-output-dir"; import { createVSCodeCommandManager } from "../../../../src/common/vscode/commands"; import type { AllCommands } from "../../../../src/common/commands"; import { getDataFolderFilePath } from "../utils"; +import type { CoreCompletedQuery } from "../../../../src/query-server"; async function selectForQuickEval( path: string, @@ -30,10 +30,15 @@ async function selectForQuickEval( } async function getResultCount( - outputDir: QueryOutputDir, + completedQuery: CoreCompletedQuery, cli: CodeQLCliServer, ): Promise { - const info = await cli.bqrsInfo(outputDir.bqrsPath, 100); + const results = Array.from(completedQuery.results.values()); + expect(results.length).toBe(1); + const info = await cli.bqrsInfo( + completedQuery.outputDir.getBqrsPath(results[0].outputBaseName), + 100, + ); const resultSet = info["result-sets"][0]; return resultSet.rows; } @@ -104,8 +109,9 @@ describeWithCodeQL()("Debugger", () => { expect(result.started.quickEvalContext!.quickEvalText).toBe( "InterestingNumber", ); - expect(result.results.queryTarget.quickEvalPosition).toBeDefined(); - expect(await getResultCount(result.results.outputDir, cli)).toBe(8); + expect(result.results.queryTargets.length).toBe(1); + expect(result.results.queryTargets[0].quickEvalPosition).toBeDefined(); + expect(await getResultCount(result.results, cli)).toBe(8); await controller.expectStopped(); }); }); @@ -122,8 +128,9 @@ describeWithCodeQL()("Debugger", () => { expect(result.started.quickEvalContext!.quickEvalText).toBe( "InterestingNumber", ); - expect(result.results.queryTarget.quickEvalPosition).toBeDefined(); - expect(await getResultCount(result.results.outputDir, cli)).toBe(0); + expect(result.results.queryTargets.length).toBe(1); + expect(result.results.queryTargets[0].quickEvalPosition).toBeDefined(); + expect(await getResultCount(result.results, cli)).toBe(0); await controller.expectStopped(); }); }); @@ -141,8 +148,9 @@ describeWithCodeQL()("Debugger", () => { expect(result.started.quickEvalContext!.quickEvalText).toBe( "InterestingNumber", ); - expect(result.results.queryTarget.quickEvalPosition).toBeDefined(); - expect(await getResultCount(result.results.outputDir, cli)).toBe(8); + expect(result.results.queryTargets.length).toBe(1); + expect(result.results.queryTargets[0].quickEvalPosition).toBeDefined(); + expect(await getResultCount(result.results, cli)).toBe(8); await controller.expectStopped(); }); }); @@ -165,8 +173,9 @@ describeWithCodeQL()("Debugger", () => { expect(result.started.quickEvalContext!.quickEvalText).toBe( "getBigIntValue", ); - expect(result.results.queryTarget.quickEvalPosition).toBeDefined(); - expect(await getResultCount(result.results.outputDir, cli)).toBe(8); + expect(result.results.queryTargets.length).toBe(1); + expect(result.results.queryTargets[0].quickEvalPosition).toBeDefined(); + expect(await getResultCount(result.results, cli)).toBe(8); await controller.expectStopped(); }); }); @@ -218,7 +227,7 @@ describeWithCodeQL()("Debugger", () => { await controller.expectSessionClosed(); // Expect the number of results to be the same as if we had run the simple query, not the quick eval query. - expect(await getResultCount(result.results.outputDir, cli)).toBe(2); + expect(await getResultCount(result.results, cli)).toBe(2); }); }); }); diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts index 8f3f3216827..239b1ca0435 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts @@ -162,7 +162,7 @@ describeWithCodeQL()("Queries", () => { async function runQueryWithExtensions() { console.log("Calling compileAndRunQuery"); - const result = await compileAndRunQuery( + const completedQuery = await compileAndRunQuery( mode, appCommandManager, localQueries, @@ -176,12 +176,14 @@ describeWithCodeQL()("Queries", () => { console.log("Completed compileAndRunQuery"); // Check that query was successful - expect(result.resultType).toBe(QueryResultType.SUCCESS); + const results = Array.from(completedQuery.results.values()); + expect(results.length).toBe(1); + expect(results[0].resultType).toBe(QueryResultType.SUCCESS); console.log("Loading query results"); // Load query results const chunk = await qs.cliServer.bqrsDecode( - result.outputDir.bqrsPath, + completedQuery.outputDir.getBqrsPath(results[0].outputBaseName), SELECT_QUERY_NAME, { // there should only be one result @@ -198,7 +200,7 @@ describeWithCodeQL()("Queries", () => { describe.each(MODES)("running queries (%s)", (mode) => { it("should run a query", async () => { - const result = await compileAndRunQuery( + const completedQuery = await compileAndRunQuery( mode, appCommandManager, localQueries, @@ -211,13 +213,15 @@ describeWithCodeQL()("Queries", () => { ); // just check that the query was successful - expect(result.resultType).toBe(QueryResultType.SUCCESS); + const results = Array.from(completedQuery.results.values()); + expect(results.length).toBe(1); + expect(results[0].resultType).toBe(QueryResultType.SUCCESS); }); // Asserts a fix for bug https://github.com/github/vscode-codeql/issues/733 it("should restart the database and run a query", async () => { await appCommandManager.execute("codeQL.restartQueryServer"); - const result = await compileAndRunQuery( + const completedQuery = await compileAndRunQuery( mode, appCommandManager, localQueries, @@ -229,7 +233,9 @@ describeWithCodeQL()("Queries", () => { undefined, ); - expect(result.resultType).toBe(QueryResultType.SUCCESS); + const results = Array.from(completedQuery.results.values()); + expect(results.length).toBe(1); + expect(results[0].resultType).toBe(QueryResultType.SUCCESS); }); }); diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/language-support/ast-viewer/ast-builder.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/language-support/ast-viewer/ast-builder.test.ts index fd033de5f06..9fc96e98597 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/language-support/ast-viewer/ast-builder.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/language-support/ast-viewer/ast-builder.test.ts @@ -2,7 +2,6 @@ import { readFileSync } from "fs-extra"; import type { CodeQLCliServer } from "../../../../../src/codeql-cli/cli"; import { Uri } from "vscode"; -import { QueryOutputDir } from "../../../../../src/local-queries/query-output-dir"; import { mockDatabaseItem, mockedObject } from "../../../utils/mocking.helpers"; import path from "path"; import { AstBuilder } from "../../../../../src/language-support"; @@ -141,7 +140,7 @@ describe("AstBuilder", () => { function createAstBuilder() { return new AstBuilder( - new QueryOutputDir("/a/b/c"), + path.normalize("/a/b/c/results.bqrs"), mockCli, mockDatabaseItem({ resolveSourceFile: undefined, diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/external-api-usage-query.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/external-api-usage-query.test.ts index 7a4fcd6f531..bdc407502cb 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/external-api-usage-query.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/external-api-usage-query.test.ts @@ -30,15 +30,13 @@ describe("runModelEditorQueries", () => { > = jest.spyOn(log, "showAndLogExceptionWithTelemetry"); const outputDir = new QueryOutputDir(join((await file()).path, "1")); - + const queryPath = "/a/b/c/ApplicationModeEndpoints.ql"; const options = { cliServer: mockedObject({ resolveQlpacks: jest.fn().mockResolvedValue({ "my/extensions": "/a/b/c/", }), - resolveQueriesInSuite: jest - .fn() - .mockResolvedValue(["/a/b/c/ApplicationModeEndpoints.ql"]), + resolveQueriesInSuite: jest.fn().mockResolvedValue([queryPath]), packPacklist: jest .fn() .mockResolvedValue([ @@ -50,7 +48,9 @@ describe("runModelEditorQueries", () => { queryRunner: mockedObject({ createQueryRun: jest.fn().mockReturnValue({ evaluate: jest.fn().mockResolvedValue({ - resultType: QueryResultType.CANCELLATION, + results: new Map([ + [queryPath, { resultType: QueryResultType.CANCELLATION }], + ]), }), outputDir, }), @@ -88,15 +88,13 @@ describe("runModelEditorQueries", () => { it("should run query for random language", async () => { const outputDir = new QueryOutputDir(join((await file()).path, "1")); - + const queryPath = "/a/b/c/ApplicationModeEndpoints.ql"; const options = { cliServer: mockedObject({ resolveQlpacks: jest.fn().mockResolvedValue({ "my/extensions": "/a/b/c/", }), - resolveQueriesInSuite: jest - .fn() - .mockResolvedValue(["/a/b/c/ApplicationModeEndpoints.ql"]), + resolveQueriesInSuite: jest.fn().mockResolvedValue([queryPath]), packPacklist: jest .fn() .mockResolvedValue([ @@ -122,6 +120,9 @@ describe("runModelEditorQueries", () => { createQueryRun: jest.fn().mockReturnValue({ evaluate: jest.fn().mockResolvedValue({ resultType: QueryResultType.SUCCESS, + results: new Map([ + [queryPath, { resultType: QueryResultType.SUCCESS }], + ]), outputDir, }), outputDir, @@ -156,17 +157,20 @@ describe("runModelEditorQueries", () => { expect(options.cliServer.resolveQlpacks).toHaveBeenCalledWith([], true); expect(options.queryRunner.createQueryRun).toHaveBeenCalledWith( "/a/b/c/src.zip", - { - queryPath: expect.stringMatching(/\S*ModeEndpoints\.ql/), - quickEvalPosition: undefined, - quickEvalCountOnly: false, - }, + [ + { + outputBaseName: "results", + queryPath: expect.stringMatching(/\S*ModeEndpoints\.ql/), + quickEvalPosition: undefined, + quickEvalCountOnly: false, + }, + ], false, [], ["my/extensions"], {}, "/tmp/queries", - undefined, + "ApplicationModeEndpoints.ql", undefined, ); }); diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/generate.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/generate.test.ts index 173f901a7ee..7686e66d912 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/generate.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/generate.test.ts @@ -28,12 +28,10 @@ describe("runGenerateQueries", () => { const outputDir = new QueryOutputDir(join(queryStorageDir, "1")); const onResults = jest.fn(); - + const queryPath = "/a/b/c/GenerateModel.ql"; const options = { cliServer: mockedObject({ - resolveQueriesInSuite: jest - .fn() - .mockResolvedValue(["/a/b/c/GenerateModel.ql"]), + resolveQueriesInSuite: jest.fn().mockResolvedValue([queryPath]), bqrsDecodeAll: jest.fn().mockResolvedValue({ sourceModel: { columns: [ @@ -101,7 +99,9 @@ describe("runGenerateQueries", () => { queryRunner: mockedObject({ createQueryRun: jest.fn().mockReturnValue({ evaluate: jest.fn().mockResolvedValue({ - resultType: QueryResultType.SUCCESS, + results: new Map([ + [queryPath, { resultType: QueryResultType.SUCCESS }], + ]), outputDir, }), outputDir, @@ -221,17 +221,20 @@ describe("runGenerateQueries", () => { expect(options.queryRunner.createQueryRun).toHaveBeenCalledTimes(1); expect(options.queryRunner.createQueryRun).toHaveBeenCalledWith( "/a/b/c/src.zip", - { - queryPath: "/a/b/c/GenerateModel.ql", - quickEvalPosition: undefined, - quickEvalCountOnly: false, - }, + [ + { + outputBaseName: "results", + queryPath: "/a/b/c/GenerateModel.ql", + quickEvalPosition: undefined, + quickEvalCountOnly: false, + }, + ], false, [], undefined, {}, "/tmp/queries", - undefined, + "GenerateModel.ql", undefined, ); }); diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/suggestion-queries.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/suggestion-queries.test.ts index e5c71658717..eeb41f869e1 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/suggestion-queries.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/suggestion-queries.test.ts @@ -146,9 +146,8 @@ describe("runSuggestionsQuery", () => { .mockResolvedValueOnce(mockInputSuggestions) .mockResolvedValueOnce(mockOutputSuggestions); - const resolveQueriesInSuite = jest - .fn() - .mockResolvedValue(["/a/b/c/FrameworkModeAccessPathSuggestions.ql"]); + const queryPath = "/a/b/c/FrameworkModeAccessPathSuggestions.ql"; + const resolveQueriesInSuite = jest.fn().mockResolvedValue([queryPath]); const options = { parseResults, @@ -173,7 +172,9 @@ describe("runSuggestionsQuery", () => { queryRunner: mockedObject({ createQueryRun: jest.fn().mockReturnValue({ evaluate: jest.fn().mockResolvedValue({ - resultType: QueryResultType.SUCCESS, + results: new Map([ + [queryPath, { resultType: QueryResultType.SUCCESS }], + ]), outputDir, }), outputDir, @@ -206,17 +207,20 @@ describe("runSuggestionsQuery", () => { expect(options.cliServer.resolveQlpacks).toHaveBeenCalledWith([], true); expect(options.queryRunner.createQueryRun).toHaveBeenCalledWith( "/a/b/c/src.zip", - { - queryPath: expect.stringMatching(/\S*AccessPathSuggestions\.ql/), - quickEvalPosition: undefined, - quickEvalCountOnly: false, - }, + [ + { + queryPath: expect.stringMatching(/\S*AccessPathSuggestions\.ql/), + outputBaseName: "results", + quickEvalPosition: undefined, + quickEvalCountOnly: false, + }, + ], false, [], ["my/extensions"], {}, "/tmp/queries", - undefined, + "FrameworkModeAccessPathSuggestions.ql", undefined, ); expect(options.cliServer.resolveQueriesInSuite).toHaveBeenCalledTimes(1); diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/store/query-history-store.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/store/query-history-store.test.ts index 34c092987cc..e8ae5a82975 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/store/query-history-store.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/store/query-history-store.test.ts @@ -237,12 +237,12 @@ describe("write and read", () => { dbPath = "/a/b/c", ): QueryWithResults { // pretend that the results path exists - const resultsPath = join(queryPath, "results.bqrs"); mkdirpSync(queryPath); - writeFileSync(resultsPath, "", "utf8"); + writeFileSync(join(queryPath, "results.bqrs"), "", "utf8"); const queryEvalInfo = new QueryEvaluationInfo( queryPath, + "results", Uri.file(dbPath).fsPath, true, undefined, diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-results.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-results.test.ts index a2bc725849e..fc31fc909e3 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-results.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-results.test.ts @@ -127,7 +127,7 @@ describe("query-results", () => { const expectedResultsPath = join(queryPath, "results.bqrs"); const expectedSortedResultsPath = join( queryPath, - "sortedResults-cc8589f226adc134f87f2438e10075e0667571c72342068e2281e0b3b65e1092.bqrs", + "results-sorted-cc8589f226adc134f87f2438e10075e0667571c72342068e2281e0b3b65e1092.bqrs", ); expect(spy).toHaveBeenCalledWith( expectedResultsPath, @@ -419,12 +419,12 @@ describe("query-results", () => { dbPath = "/a/b/c", ): QueryWithResults { // pretend that the results path exists - const resultsPath = join(queryPath, "results.bqrs"); mkdirpSync(queryPath); - writeFileSync(resultsPath, "", "utf8"); + writeFileSync(join(queryPath, "results.bqrs"), "", "utf8"); const queryEvalInfo = new QueryEvaluationInfo( queryPath, + "results", Uri.file(dbPath).fsPath, true, undefined, diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/run-queries.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/run-queries.test.ts index 45e0063b1bb..8437249e815 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/run-queries.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/run-queries.test.ts @@ -28,12 +28,10 @@ describe("run-queries", () => { const saveDir = "query-save-dir"; const queryEvalInfo = createMockQueryEvaluationInfo(true, saveDir); - expect(queryEvalInfo.dilPath).toBe(join(saveDir, "results.dil")); - expect(queryEvalInfo.resultsPaths.resultsPath).toBe( - join(saveDir, "results.bqrs"), - ); - expect(queryEvalInfo.resultsPaths.interpretedResultsPath).toBe( - join(saveDir, "interpretedResults.sarif"), + expect(queryEvalInfo.dilPath).toBe(join(saveDir, "foo.dil")); + expect(queryEvalInfo.resultsPath).toBe(join(saveDir, "foo.bqrs")); + expect(queryEvalInfo.interpretedResultsPath).toBe( + join(saveDir, "foo-interpreted.sarif"), ); expect(queryEvalInfo.dbItemPath).toBe(Uri.file("/abc").fsPath); }); @@ -215,6 +213,7 @@ describe("run-queries", () => { ) { return new QueryEvaluationInfo( saveDir, + "foo", Uri.parse("file:///abc").fsPath, databaseHasMetadataFile, undefined,