Skip to content

Commit 8011481

Browse files
authored
Merge pull request #2996 from github/koesie10/query-save-dir
Fix results directory and evaluator log for cancelled queries
2 parents 1ab198f + 18646ab commit 8011481

12 files changed

+132
-53
lines changed

extensions/ql-vscode/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- The "Install Pack Dependencies" will now only list CodeQL packs located in the workspace. [#2960](https://github.com/github/vscode-codeql/pull/2960)
77
- Fix a bug where the "View Query Log" action for a query history item was not working. [#2984](https://github.com/github/vscode-codeql/pull/2984)
88
- Add a command to sort items in the databases view by language. [#2993](https://github.com/github/vscode-codeql/pull/2993)
9+
- Fix not being able to open the results directory or evaluator log for a cancelled local query run. [#2996](https://github.com/github/vscode-codeql/pull/2996)
910

1011
## 1.9.2 - 12 October 2023
1112

extensions/ql-vscode/src/local-queries/local-queries.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -362,11 +362,15 @@ export class LocalQueries extends DisposableObject {
362362
);
363363
}
364364

365-
const initialInfo = await createInitialQueryInfo(selectedQuery, {
366-
databaseUri: dbItem.databaseUri.toString(),
367-
name: dbItem.name,
368-
language: tryGetQueryLanguage(dbItem.language),
369-
});
365+
const initialInfo = await createInitialQueryInfo(
366+
selectedQuery,
367+
{
368+
databaseUri: dbItem.databaseUri.toString(),
369+
name: dbItem.name,
370+
language: tryGetQueryLanguage(dbItem.language),
371+
},
372+
outputDir,
373+
);
370374

371375
// When cancellation is requested from the query history view, we just stop the debug session.
372376
const queryInfo = new LocalQueryInfo(initialInfo, tokenSource);

extensions/ql-vscode/src/local-queries/local-query-run.ts

+9
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ export class LocalQueryRun {
9797
* Updates the UI in the case where query evaluation throws an exception.
9898
*/
9999
public async fail(err: Error): Promise<void> {
100+
const evalLogPaths = await this.summarizeEvalLog(
101+
QueryResultType.OTHER_ERROR,
102+
this.outputDir,
103+
this.logger,
104+
);
105+
if (evalLogPaths !== undefined) {
106+
this.queryInfo.setEvaluatorLogPaths(evalLogPaths);
107+
}
108+
100109
err.message = `Error running query: ${err.message}`;
101110
this.queryInfo.failureReason = err.message;
102111
await this.queryHistoryManager.refreshTreeView();

extensions/ql-vscode/src/query-history/query-history-manager.ts

+36-44
Original file line numberDiff line numberDiff line change
@@ -702,32 +702,15 @@ export class QueryHistoryManager extends DisposableObject {
702702
}
703703
}
704704

705-
async getQueryHistoryItemDirectory(
706-
queryHistoryItem: QueryHistoryInfo,
707-
): Promise<string> {
708-
if (queryHistoryItem.t === "local") {
709-
if (queryHistoryItem.completedQuery) {
710-
return queryHistoryItem.completedQuery.query.querySaveDir;
711-
}
712-
} else if (queryHistoryItem.t === "variant-analysis") {
713-
return this.variantAnalysisManager.getVariantAnalysisStorageLocation(
714-
queryHistoryItem.variantAnalysis.id,
715-
);
716-
} else {
717-
assertNever(queryHistoryItem);
718-
}
719-
720-
throw new Error("Unable to get query directory");
721-
}
722-
723705
async handleOpenQueryDirectory(item: QueryHistoryInfo) {
724706
let externalFilePath: string | undefined;
725707
if (item.t === "local") {
726-
if (item.completedQuery) {
727-
externalFilePath = join(
728-
item.completedQuery.query.querySaveDir,
729-
"timestamp",
730-
);
708+
const querySaveDir =
709+
item.initialInfo.outputDir?.querySaveDir ??
710+
item.completedQuery?.query.querySaveDir;
711+
712+
if (querySaveDir) {
713+
externalFilePath = join(querySaveDir, "timestamp");
731714
}
732715
} else if (item.t === "variant-analysis") {
733716
externalFilePath = join(
@@ -761,37 +744,51 @@ export class QueryHistoryManager extends DisposableObject {
761744
`Failed to open ${externalFilePath}: ${getErrorMessage(e)}`,
762745
);
763746
}
747+
} else {
748+
this.warnNoQueryDir();
764749
}
765750
}
766751

767-
private warnNoEvalLogs() {
752+
private warnNoQueryDir() {
768753
void showAndLogWarningMessage(
769754
this.app.logger,
770-
`Evaluator log, summary, and viewer are not available for this run. Perhaps it failed before evaluation?`,
755+
`Results directory is not available for this run.`,
771756
);
772757
}
773758

774-
private warnInProgressEvalLogSummary() {
759+
private warnNoEvalLogs() {
775760
void showAndLogWarningMessage(
776761
this.app.logger,
777-
'The evaluator log summary is still being generated for this run. Please try again later. The summary generation process is tracked in the "CodeQL Extension Log" view.',
762+
`Evaluator log, summary, and viewer are not available for this run. Perhaps it failed before evaluation?`,
778763
);
779764
}
780765

781-
private warnInProgressEvalLogViewer() {
782-
void showAndLogWarningMessage(
783-
this.app.logger,
784-
"The viewer's data is still being generated for this run. Please try again or re-run the query.",
785-
);
766+
private async warnNoEvalLogSummary(item: LocalQueryInfo) {
767+
const evalLogLocation =
768+
item.evalLogLocation ?? item.initialInfo.outputDir?.evalLogPath;
769+
770+
// Summary log file doesn't exist.
771+
if (evalLogLocation && (await pathExists(evalLogLocation))) {
772+
// If raw log does exist, then the summary log is still being generated.
773+
void showAndLogWarningMessage(
774+
this.app.logger,
775+
'The evaluator log summary is still being generated for this run. Please try again later. The summary generation process is tracked in the "CodeQL Extension Log" view.',
776+
);
777+
} else {
778+
this.warnNoEvalLogs();
779+
}
786780
}
787781

788782
async handleShowEvalLog(item: QueryHistoryInfo) {
789783
if (item.t !== "local") {
790784
return;
791785
}
792786

793-
if (item.evalLogLocation) {
794-
await tryOpenExternalFile(this.app.commands, item.evalLogLocation);
787+
const evalLogLocation =
788+
item.evalLogLocation ?? item.initialInfo.outputDir?.evalLogPath;
789+
790+
if (evalLogLocation && (await pathExists(evalLogLocation))) {
791+
await tryOpenExternalFile(this.app.commands, evalLogLocation);
795792
} else {
796793
this.warnNoEvalLogs();
797794
}
@@ -802,18 +799,13 @@ export class QueryHistoryManager extends DisposableObject {
802799
return;
803800
}
804801

805-
if (item.evalLogSummaryLocation) {
806-
await tryOpenExternalFile(this.app.commands, item.evalLogSummaryLocation);
802+
// If the summary file location wasn't saved, display error
803+
if (!item.evalLogSummaryLocation) {
804+
await this.warnNoEvalLogSummary(item);
807805
return;
808806
}
809807

810-
// Summary log file doesn't exist.
811-
if (item.evalLogLocation && (await pathExists(item.evalLogLocation))) {
812-
// If raw log does exist, then the summary log is still being generated.
813-
this.warnInProgressEvalLogSummary();
814-
} else {
815-
this.warnNoEvalLogs();
816-
}
808+
await tryOpenExternalFile(this.app.commands, item.evalLogSummaryLocation);
817809
}
818810

819811
async handleShowEvalLogViewer(item: QueryHistoryInfo) {
@@ -823,7 +815,7 @@ export class QueryHistoryManager extends DisposableObject {
823815

824816
// If the JSON summary file location wasn't saved, display error
825817
if (item.jsonEvalLogSummaryLocation === undefined) {
826-
this.warnInProgressEvalLogViewer();
818+
await this.warnNoEvalLogSummary(item);
827819
return;
828820
}
829821

extensions/ql-vscode/src/query-history/store/query-history-local-query-domain-mapper.ts

+5
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ function mapInitialQueryInfoToDto(
109109
},
110110
start: localQueryInitialInfo.start,
111111
id: localQueryInitialInfo.id,
112+
outputDir: localQueryInitialInfo.outputDir
113+
? {
114+
querySaveDir: localQueryInitialInfo.outputDir.querySaveDir,
115+
}
116+
: undefined,
112117
};
113118
}
114119

extensions/ql-vscode/src/query-history/store/query-history-local-query-dto-mapper.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
CompletedQueryInfo,
44
InitialQueryInfo,
55
} from "../../query-results";
6-
import { QueryEvaluationInfo } from "../../run-queries-shared";
6+
import { QueryEvaluationInfo, QueryOutputDir } from "../../run-queries-shared";
77
import {
88
CompletedQueryInfoDto,
99
QueryEvaluationInfoDto,
@@ -26,7 +26,10 @@ export function mapLocalQueryItemToDomainModel(
2626
localQuery: QueryHistoryLocalQueryDto,
2727
): LocalQueryInfo {
2828
return new LocalQueryInfo(
29-
mapInitialQueryInfoToDomainModel(localQuery.initialInfo),
29+
mapInitialQueryInfoToDomainModel(
30+
localQuery.initialInfo,
31+
localQuery.completedQuery?.query?.querySaveDir,
32+
),
3033
undefined,
3134
localQuery.failureReason,
3235
localQuery.completedQuery &&
@@ -72,7 +75,14 @@ function mapCompletedQueryInfoToDomainModel(
7275

7376
function mapInitialQueryInfoToDomainModel(
7477
initialInfo: InitialQueryInfoDto,
78+
// The completedQuerySaveDir is a migration to support old query items that don't have
79+
// the querySaveDir in the initialInfo. It should be removed once all query
80+
// items have the querySaveDir in the initialInfo.
81+
completedQuerySaveDir?: string,
7582
): InitialQueryInfo {
83+
const querySaveDir =
84+
initialInfo.outputDir?.querySaveDir ?? completedQuerySaveDir;
85+
7686
return {
7787
userSpecifiedLabel: initialInfo.userSpecifiedLabel,
7888
queryText: initialInfo.queryText,
@@ -90,6 +100,7 @@ function mapInitialQueryInfoToDomainModel(
90100
},
91101
start: new Date(initialInfo.start),
92102
id: initialInfo.id,
103+
outputDir: querySaveDir ? new QueryOutputDir(querySaveDir) : undefined,
93104
};
94105
}
95106

extensions/ql-vscode/src/query-history/store/query-history-local-query-dto.ts

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export interface InitialQueryInfoDto {
2424
databaseInfo: DatabaseInfoDto;
2525
start: Date;
2626
id: string;
27+
outputDir?: QueryOutputDirDto; // Undefined for backwards compatibility
28+
}
29+
30+
interface QueryOutputDirDto {
31+
querySaveDir: string;
2732
}
2833

2934
interface DatabaseInfoDto {

extensions/ql-vscode/src/query-results.ts

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { QueryStatus } from "./query-history/query-status";
1919
import {
2020
EvaluatorLogPaths,
2121
QueryEvaluationInfo,
22+
QueryOutputDir,
2223
QueryWithResults,
2324
} from "./run-queries-shared";
2425
import { formatLegacyMessage } from "./query-server/legacy";
@@ -47,6 +48,7 @@ export interface InitialQueryInfo {
4748
readonly databaseInfo: DatabaseInfo;
4849
readonly start: Date;
4950
readonly id: string; // unique id for this query.
51+
readonly outputDir?: QueryOutputDir; // If missing, we do not have a query save dir. The query may have been cancelled. This is only for backwards compatibility.
5052
}
5153

5254
export class CompletedQueryInfo implements QueryWithResults {

extensions/ql-vscode/src/run-queries-shared.ts

+3
Original file line numberDiff line numberDiff line change
@@ -562,11 +562,13 @@ async function convertToQlPath(filePath: string): Promise<string> {
562562
*
563563
* @param selectedQuery The query to run, including any quickeval info.
564564
* @param databaseInfo The database to run the query against.
565+
* @param outputDir The output directory for this query.
565566
* @returns The initial information for the query to be run.
566567
*/
567568
export async function createInitialQueryInfo(
568569
selectedQuery: SelectedQuery,
569570
databaseInfo: DatabaseInfo,
571+
outputDir: QueryOutputDir,
570572
): Promise<InitialQueryInfo> {
571573
const isQuickEval = selectedQuery.quickEval !== undefined;
572574
const isQuickEvalCount =
@@ -587,6 +589,7 @@ export async function createInitialQueryInfo(
587589
: {
588590
queryText: await readFile(selectedQuery.queryPath, "utf8"),
589591
}),
592+
outputDir,
590593
};
591594
}
592595

extensions/ql-vscode/test/factories/query-history/local-query-history-item.ts

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { faker } from "@faker-js/faker";
22
import { InitialQueryInfo, LocalQueryInfo } from "../../../src/query-results";
33
import {
44
QueryEvaluationInfo,
5+
QueryOutputDir,
56
QueryWithResults,
67
} from "../../../src/run-queries-shared";
78
import { CancellationTokenSource } from "vscode";
@@ -18,6 +19,7 @@ export function createMockLocalQueryInfo({
1819
hasMetadata = false,
1920
queryWithResults = undefined,
2021
language = undefined,
22+
outputDir = new QueryOutputDir("/a/b/c"),
2123
}: {
2224
startTime?: Date;
2325
resultCount?: number;
@@ -27,6 +29,7 @@ export function createMockLocalQueryInfo({
2729
hasMetadata?: boolean;
2830
queryWithResults?: QueryWithResults | undefined;
2931
language?: QueryLanguage;
32+
outputDir?: QueryOutputDir | undefined;
3033
}): LocalQueryInfo {
3134
const cancellationToken = {
3235
dispose: () => {
@@ -48,6 +51,7 @@ export function createMockLocalQueryInfo({
4851
start: startTime,
4952
id: faker.number.int().toString(),
5053
userSpecifiedLabel,
54+
outputDir,
5155
} as InitialQueryInfo;
5256

5357
const localQuery = new LocalQueryInfo(initialQueryInfo, cancellationToken);

extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/store/query-history-store.test.ts

+40-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
LocalQueryInfo,
99
InitialQueryInfo,
1010
} from "../../../../../src/query-results";
11-
import { QueryWithResults } from "../../../../../src/run-queries-shared";
11+
import {
12+
QueryOutputDir,
13+
QueryWithResults,
14+
} from "../../../../../src/run-queries-shared";
1215
import { DatabaseInfo } from "../../../../../src/common/interface-types";
1316
import { CancellationTokenSource, Uri } from "vscode";
1417
import { tmpDir } from "../../../../../src/tmp-dir";
@@ -130,6 +133,38 @@ describe("write and read", () => {
130133
expect(allHistoryActual.length).toEqual(expectedHistory.length);
131134
});
132135

136+
it("should read query output dir from completed query if not present", async () => {
137+
const historyPath = join(tmpDir.name, "workspace-query-history.json");
138+
139+
const queryItem = createMockFullQueryInfo(
140+
"a",
141+
createMockQueryWithResults(
142+
`${queryPath}-a`,
143+
false,
144+
false,
145+
"/a/b/c/a",
146+
false,
147+
),
148+
false,
149+
null,
150+
);
151+
152+
// write and read
153+
await writeQueryHistoryToFile([queryItem], historyPath);
154+
const actual = await readQueryHistoryFromFile(historyPath);
155+
156+
expect(actual).toHaveLength(1);
157+
158+
expect(actual[0].t).toEqual("local");
159+
160+
if (actual[0].t === "local") {
161+
expect(actual[0].initialInfo.outputDir?.querySaveDir).not.toBeUndefined();
162+
expect(actual[0].initialInfo.outputDir?.querySaveDir).toEqual(
163+
queryItem.completedQuery?.query?.querySaveDir,
164+
);
165+
}
166+
});
167+
133168
it("should remove remote queries from the history", async () => {
134169
const path = join(tmpDir.name, "query-history-with-remote.json");
135170
await writeFile(
@@ -205,6 +240,9 @@ describe("write and read", () => {
205240
dbName = "a",
206241
queryWithResults?: QueryWithResults,
207242
isFail = false,
243+
outputDir: QueryOutputDir | null = new QueryOutputDir(
244+
"/path/to/output/dir",
245+
),
208246
): LocalQueryInfo {
209247
const fqi = new LocalQueryInfo(
210248
{
@@ -218,6 +256,7 @@ describe("write and read", () => {
218256
isQuickQuery: false,
219257
isQuickEval: false,
220258
id: `some-id-${dbName}`,
259+
outputDir: outputDir ? outputDir : undefined,
221260
} as InitialQueryInfo,
222261
{
223262
dispose: () => {

0 commit comments

Comments
 (0)