Skip to content

Commit 5ce3b22

Browse files
committed
Add tests for external API query
This adds tests for the external API query and retrieving of results. It does not use the "real" CLI integration, but instead mocks the CLI server and query runner. To make mocking easier and require less type casting, I've narrowed some of the arguments of some other functions. They now use `Pick` to only require the properties they need.
1 parent c245f33 commit 5ce3b22

File tree

5 files changed

+246
-12
lines changed

5 files changed

+246
-12
lines changed

extensions/ql-vscode/src/contextual/queryResolver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import { redactableError } from "../pure/errors";
2121
import { QLPACK_FILENAMES } from "../pure/ql";
2222

2323
export async function qlpackOfDatabase(
24-
cli: CodeQLCliServer,
25-
db: DatabaseItem,
24+
cli: Pick<CodeQLCliServer, "resolveQlpacks">,
25+
db: Pick<DatabaseItem, "contents">,
2626
): Promise<QlPacksForLanguage> {
2727
if (db.contents === undefined) {
2828
throw new Error("Database is invalid and cannot infer QLPack.");

extensions/ql-vscode/src/data-extensions-editor/data-extensions-editor-view.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { decodeBqrsToExternalApiUsages } from "./bqrs";
1313
import { redactableError } from "../pure/errors";
1414
import { asError, getErrorMessage } from "../pure/helpers-pure";
1515
import { getResults, runQuery } from "./external-api-usage-query";
16+
import { extLogger } from "../common";
1617

1718
export class DataExtensionsEditorView extends AbstractWebview<
1819
ToDataExtensionsEditorMessage,
@@ -77,6 +78,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
7778
queryRunner: this.queryRunner,
7879
databaseItem: this.databaseItem,
7980
queryStorageDir: this.queryStorageDir,
81+
logger: extLogger,
8082
progress: (progressUpdate: ProgressUpdate) => {
8183
void this.showProgress(progressUpdate, 1500);
8284
},
@@ -96,6 +98,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
9698
const bqrsChunk = await getResults({
9799
cliServer: this.cliServer,
98100
bqrsPath: queryResult.outputDir.bqrsPath,
101+
logger: extLogger,
99102
});
100103
if (!bqrsChunk) {
101104
await this.clearProgress();

extensions/ql-vscode/src/data-extensions-editor/external-api-usage-query.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ import { file } from "tmp-promise";
44
import { writeFile } from "fs-extra";
55
import { dump } from "js-yaml";
66
import { getOnDiskWorkspaceFolders } from "../helpers";
7-
import { extLogger, TeeLogger } from "../common";
7+
import { Logger, TeeLogger } from "../common";
88
import { CancellationToken } from "vscode";
99
import { CodeQLCliServer } from "../cli";
1010
import { DatabaseItem } from "../local-databases";
1111
import { ProgressCallback } from "../progress";
1212

1313
export type RunQueryOptions = {
14-
cliServer: CodeQLCliServer;
15-
queryRunner: QueryRunner;
16-
databaseItem: DatabaseItem;
14+
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveQueriesInSuite">;
15+
queryRunner: Pick<QueryRunner, "createQueryRun" | "logger">;
16+
databaseItem: Pick<DatabaseItem, "contents" | "databaseUri" | "language">;
1717
queryStorageDir: string;
18+
logger: Logger;
1819

1920
progress: ProgressCallback;
2021
token: CancellationToken;
@@ -25,6 +26,7 @@ export async function runQuery({
2526
queryRunner,
2627
databaseItem,
2728
queryStorageDir,
29+
logger,
2830
progress,
2931
token,
3032
}: RunQueryOptions): Promise<CoreCompletedQuery | undefined> {
@@ -58,7 +60,7 @@ export async function runQuery({
5860
);
5961

6062
if (queries.length !== 1) {
61-
void extLogger.log(`Expected exactly one query, got ${queries.length}`);
63+
void logger.log(`Expected exactly one query, got ${queries.length}`);
6264
return;
6365
}
6466

@@ -83,20 +85,27 @@ export async function runQuery({
8385
}
8486

8587
export type GetResultsOptions = {
86-
cliServer: CodeQLCliServer;
88+
cliServer: Pick<CodeQLCliServer, "bqrsInfo" | "bqrsDecode">;
8789
bqrsPath: string;
90+
logger: Logger;
8891
};
8992

90-
export async function getResults({ cliServer, bqrsPath }: GetResultsOptions) {
93+
export async function getResults({
94+
cliServer,
95+
bqrsPath,
96+
logger,
97+
}: GetResultsOptions) {
9198
const bqrsInfo = await cliServer.bqrsInfo(bqrsPath);
9299
if (bqrsInfo["result-sets"].length !== 1) {
93-
void extLogger.log(
100+
void logger.log(
94101
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
95102
);
96103
return undefined;
97104
}
98105

99106
const resultSet = bqrsInfo["result-sets"][0];
100107

101-
return cliServer.bqrsDecode(bqrsPath, resultSet.name);
108+
const result = await cliServer.bqrsDecode(bqrsPath, resultSet.name);
109+
void logger.log(JSON.stringify(result));
110+
return result;
102111
}

extensions/ql-vscode/src/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ function findStandardQueryPack(
478478
}
479479

480480
export async function getQlPackForDbscheme(
481-
cliServer: CodeQLCliServer,
481+
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
482482
dbschemePath: string,
483483
): Promise<QlPacksForLanguage> {
484484
const qlpacks = await cliServer.resolveQlpacks(getOnDiskWorkspaceFolders());
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import {
2+
getResults,
3+
runQuery,
4+
} from "../../../../src/data-extensions-editor/external-api-usage-query";
5+
import { createMockLogger } from "../../../__mocks__/loggerMock";
6+
import type { Uri } from "vscode";
7+
import { DatabaseKind } from "../../../../src/local-databases";
8+
import * as queryResolver from "../../../../src/contextual/queryResolver";
9+
import { file } from "tmp-promise";
10+
import { QueryResultType } from "../../../../src/pure/new-messages";
11+
import { readFile } from "fs-extra";
12+
import { load } from "js-yaml";
13+
14+
function createMockUri(path = "/a/b/c/foo"): Uri {
15+
return {
16+
scheme: "file",
17+
authority: "",
18+
path,
19+
query: "",
20+
fragment: "",
21+
fsPath: path,
22+
with: jest.fn(),
23+
toJSON: jest.fn(),
24+
};
25+
}
26+
27+
describe("runQuery", () => {
28+
it("runs the query", async () => {
29+
jest.spyOn(queryResolver, "qlpackOfDatabase").mockResolvedValue({
30+
dbschemePack: "codeql/java-all",
31+
dbschemePackIsLibraryPack: false,
32+
queryPack: "codeql/java-queries",
33+
});
34+
35+
const logPath = (await file()).path;
36+
37+
const options = {
38+
cliServer: {
39+
resolveQlpacks: jest
40+
.fn()
41+
.mockRejectedValue(
42+
new Error("Did not expect mocked method to be called"),
43+
),
44+
resolveQueriesInSuite: jest
45+
.fn()
46+
.mockResolvedValue([
47+
"/home/github/codeql/java/ql/src/Telemetry/FetchExternalAPIs.ql",
48+
]),
49+
},
50+
queryRunner: {
51+
createQueryRun: jest.fn().mockReturnValue({
52+
evaluate: jest.fn().mockResolvedValue({
53+
resultType: QueryResultType.SUCCESS,
54+
}),
55+
outputDir: {
56+
logPath,
57+
},
58+
}),
59+
logger: createMockLogger(),
60+
},
61+
logger: createMockLogger(),
62+
databaseItem: {
63+
databaseUri: createMockUri("/a/b/c/src.zip"),
64+
contents: {
65+
kind: DatabaseKind.Database,
66+
name: "foo",
67+
datasetUri: createMockUri(),
68+
},
69+
language: "java",
70+
},
71+
queryStorageDir: "/tmp/queries",
72+
progress: jest.fn(),
73+
token: {
74+
isCancellationRequested: false,
75+
onCancellationRequested: jest.fn(),
76+
},
77+
};
78+
const result = await runQuery(options);
79+
80+
expect(result?.resultType).toEqual(QueryResultType.SUCCESS);
81+
82+
expect(options.cliServer.resolveQueriesInSuite).toHaveBeenCalledWith(
83+
expect.anything(),
84+
[],
85+
);
86+
const suiteFile = options.cliServer.resolveQueriesInSuite.mock.calls[0][0];
87+
const suiteFileContents = await readFile(suiteFile, "utf8");
88+
const suiteYaml = load(suiteFileContents);
89+
expect(suiteYaml).toEqual([
90+
{
91+
from: "codeql/java-all",
92+
queries: ".",
93+
include: {
94+
id: "java/telemetry/fetch-external-apis",
95+
},
96+
},
97+
{
98+
from: "codeql/java-queries",
99+
queries: ".",
100+
include: {
101+
id: "java/telemetry/fetch-external-apis",
102+
},
103+
},
104+
]);
105+
106+
expect(options.queryRunner.createQueryRun).toHaveBeenCalledWith(
107+
"/a/b/c/src.zip",
108+
{
109+
queryPath:
110+
"/home/github/codeql/java/ql/src/Telemetry/FetchExternalAPIs.ql",
111+
quickEvalPosition: undefined,
112+
},
113+
false,
114+
[],
115+
undefined,
116+
"/tmp/queries",
117+
undefined,
118+
undefined,
119+
);
120+
});
121+
});
122+
123+
describe("getResults", () => {
124+
const options = {
125+
cliServer: {
126+
bqrsInfo: jest.fn(),
127+
bqrsDecode: jest.fn(),
128+
},
129+
bqrsPath: "/tmp/results.bqrs",
130+
logger: createMockLogger(),
131+
};
132+
133+
it("returns undefined when there are no results", async () => {
134+
options.cliServer.bqrsInfo.mockResolvedValue({
135+
"result-sets": [],
136+
});
137+
138+
expect(await getResults(options)).toBeUndefined();
139+
expect(options.logger.log).toHaveBeenCalledWith(
140+
expect.stringMatching(/Expected exactly one result set/),
141+
);
142+
});
143+
144+
it("returns undefined when there are multiple result sets", async () => {
145+
options.cliServer.bqrsInfo.mockResolvedValue({
146+
"result-sets": [
147+
{
148+
name: "#select",
149+
rows: 10,
150+
columns: [
151+
{ name: "apiName", kind: "s" },
152+
{ name: "supported", kind: "b" },
153+
{ name: "usage", kind: "e" },
154+
],
155+
},
156+
{
157+
name: "#select2",
158+
rows: 10,
159+
columns: [
160+
{ name: "apiName", kind: "s" },
161+
{ name: "supported", kind: "b" },
162+
{ name: "usage", kind: "e" },
163+
],
164+
},
165+
],
166+
});
167+
168+
expect(await getResults(options)).toBeUndefined();
169+
expect(options.logger.log).toHaveBeenCalledWith(
170+
expect.stringMatching(/Expected exactly one result set/),
171+
);
172+
});
173+
174+
it("gets the result set", async () => {
175+
options.cliServer.bqrsInfo.mockResolvedValue({
176+
"result-sets": [
177+
{
178+
name: "#select",
179+
rows: 10,
180+
columns: [
181+
{ name: "apiName", kind: "s" },
182+
{ name: "supported", kind: "b" },
183+
{ name: "usage", kind: "e" },
184+
],
185+
},
186+
],
187+
"compatible-query-kinds": ["Table", "Tree", "Graph"],
188+
});
189+
const decodedResultSet = {
190+
columns: [
191+
{ name: "apiName", kind: "String" },
192+
{ name: "supported", kind: "Boolean" },
193+
{ name: "usage", kind: "Entity" },
194+
],
195+
tuples: [
196+
[
197+
"java.io.PrintStream#println(String)",
198+
true,
199+
{
200+
label: "println(...)",
201+
url: {
202+
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
203+
startLine: 29,
204+
startColumn: 9,
205+
endLine: 29,
206+
endColumn: 49,
207+
},
208+
},
209+
],
210+
],
211+
};
212+
options.cliServer.bqrsDecode.mockResolvedValue(decodedResultSet);
213+
214+
const result = await getResults(options);
215+
expect(result).toEqual(decodedResultSet);
216+
expect(options.cliServer.bqrsInfo).toHaveBeenCalledWith(options.bqrsPath);
217+
expect(options.cliServer.bqrsDecode).toHaveBeenCalledWith(
218+
options.bqrsPath,
219+
"#select",
220+
);
221+
});
222+
});

0 commit comments

Comments
 (0)