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.
This commit is contained in:
Koen Vlaswinkel
2023-04-05 13:50:48 +02:00
parent c245f338f5
commit 5ce3b22351
5 changed files with 246 additions and 12 deletions

View File

@@ -21,8 +21,8 @@ import { redactableError } from "../pure/errors";
import { QLPACK_FILENAMES } from "../pure/ql";
export async function qlpackOfDatabase(
cli: CodeQLCliServer,
db: DatabaseItem,
cli: Pick<CodeQLCliServer, "resolveQlpacks">,
db: Pick<DatabaseItem, "contents">,
): Promise<QlPacksForLanguage> {
if (db.contents === undefined) {
throw new Error("Database is invalid and cannot infer QLPack.");

View File

@@ -13,6 +13,7 @@ import { decodeBqrsToExternalApiUsages } from "./bqrs";
import { redactableError } from "../pure/errors";
import { asError, getErrorMessage } from "../pure/helpers-pure";
import { getResults, runQuery } from "./external-api-usage-query";
import { extLogger } from "../common";
export class DataExtensionsEditorView extends AbstractWebview<
ToDataExtensionsEditorMessage,
@@ -77,6 +78,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
queryRunner: this.queryRunner,
databaseItem: this.databaseItem,
queryStorageDir: this.queryStorageDir,
logger: extLogger,
progress: (progressUpdate: ProgressUpdate) => {
void this.showProgress(progressUpdate, 1500);
},
@@ -96,6 +98,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
const bqrsChunk = await getResults({
cliServer: this.cliServer,
bqrsPath: queryResult.outputDir.bqrsPath,
logger: extLogger,
});
if (!bqrsChunk) {
await this.clearProgress();

View File

@@ -4,17 +4,18 @@ import { file } from "tmp-promise";
import { writeFile } from "fs-extra";
import { dump } from "js-yaml";
import { getOnDiskWorkspaceFolders } from "../helpers";
import { extLogger, TeeLogger } from "../common";
import { Logger, TeeLogger } from "../common";
import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "../cli";
import { DatabaseItem } from "../local-databases";
import { ProgressCallback } from "../progress";
export type RunQueryOptions = {
cliServer: CodeQLCliServer;
queryRunner: QueryRunner;
databaseItem: DatabaseItem;
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveQueriesInSuite">;
queryRunner: Pick<QueryRunner, "createQueryRun" | "logger">;
databaseItem: Pick<DatabaseItem, "contents" | "databaseUri" | "language">;
queryStorageDir: string;
logger: Logger;
progress: ProgressCallback;
token: CancellationToken;
@@ -25,6 +26,7 @@ export async function runQuery({
queryRunner,
databaseItem,
queryStorageDir,
logger,
progress,
token,
}: RunQueryOptions): Promise<CoreCompletedQuery | undefined> {
@@ -58,7 +60,7 @@ export async function runQuery({
);
if (queries.length !== 1) {
void extLogger.log(`Expected exactly one query, got ${queries.length}`);
void logger.log(`Expected exactly one query, got ${queries.length}`);
return;
}
@@ -83,14 +85,19 @@ export async function runQuery({
}
export type GetResultsOptions = {
cliServer: CodeQLCliServer;
cliServer: Pick<CodeQLCliServer, "bqrsInfo" | "bqrsDecode">;
bqrsPath: string;
logger: Logger;
};
export async function getResults({ cliServer, bqrsPath }: GetResultsOptions) {
export async function getResults({
cliServer,
bqrsPath,
logger,
}: GetResultsOptions) {
const bqrsInfo = await cliServer.bqrsInfo(bqrsPath);
if (bqrsInfo["result-sets"].length !== 1) {
void extLogger.log(
void logger.log(
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
);
return undefined;
@@ -98,5 +105,7 @@ export async function getResults({ cliServer, bqrsPath }: GetResultsOptions) {
const resultSet = bqrsInfo["result-sets"][0];
return cliServer.bqrsDecode(bqrsPath, resultSet.name);
const result = await cliServer.bqrsDecode(bqrsPath, resultSet.name);
void logger.log(JSON.stringify(result));
return result;
}

View File

@@ -478,7 +478,7 @@ function findStandardQueryPack(
}
export async function getQlPackForDbscheme(
cliServer: CodeQLCliServer,
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
dbschemePath: string,
): Promise<QlPacksForLanguage> {
const qlpacks = await cliServer.resolveQlpacks(getOnDiskWorkspaceFolders());

View File

@@ -0,0 +1,222 @@
import {
getResults,
runQuery,
} from "../../../../src/data-extensions-editor/external-api-usage-query";
import { createMockLogger } from "../../../__mocks__/loggerMock";
import type { Uri } from "vscode";
import { DatabaseKind } from "../../../../src/local-databases";
import * as queryResolver from "../../../../src/contextual/queryResolver";
import { file } from "tmp-promise";
import { QueryResultType } from "../../../../src/pure/new-messages";
import { readFile } from "fs-extra";
import { load } from "js-yaml";
function createMockUri(path = "/a/b/c/foo"): Uri {
return {
scheme: "file",
authority: "",
path,
query: "",
fragment: "",
fsPath: path,
with: jest.fn(),
toJSON: jest.fn(),
};
}
describe("runQuery", () => {
it("runs the query", async () => {
jest.spyOn(queryResolver, "qlpackOfDatabase").mockResolvedValue({
dbschemePack: "codeql/java-all",
dbschemePackIsLibraryPack: false,
queryPack: "codeql/java-queries",
});
const logPath = (await file()).path;
const options = {
cliServer: {
resolveQlpacks: jest
.fn()
.mockRejectedValue(
new Error("Did not expect mocked method to be called"),
),
resolveQueriesInSuite: jest
.fn()
.mockResolvedValue([
"/home/github/codeql/java/ql/src/Telemetry/FetchExternalAPIs.ql",
]),
},
queryRunner: {
createQueryRun: jest.fn().mockReturnValue({
evaluate: jest.fn().mockResolvedValue({
resultType: QueryResultType.SUCCESS,
}),
outputDir: {
logPath,
},
}),
logger: createMockLogger(),
},
logger: createMockLogger(),
databaseItem: {
databaseUri: createMockUri("/a/b/c/src.zip"),
contents: {
kind: DatabaseKind.Database,
name: "foo",
datasetUri: createMockUri(),
},
language: "java",
},
queryStorageDir: "/tmp/queries",
progress: jest.fn(),
token: {
isCancellationRequested: false,
onCancellationRequested: jest.fn(),
},
};
const result = await runQuery(options);
expect(result?.resultType).toEqual(QueryResultType.SUCCESS);
expect(options.cliServer.resolveQueriesInSuite).toHaveBeenCalledWith(
expect.anything(),
[],
);
const suiteFile = options.cliServer.resolveQueriesInSuite.mock.calls[0][0];
const suiteFileContents = await readFile(suiteFile, "utf8");
const suiteYaml = load(suiteFileContents);
expect(suiteYaml).toEqual([
{
from: "codeql/java-all",
queries: ".",
include: {
id: "java/telemetry/fetch-external-apis",
},
},
{
from: "codeql/java-queries",
queries: ".",
include: {
id: "java/telemetry/fetch-external-apis",
},
},
]);
expect(options.queryRunner.createQueryRun).toHaveBeenCalledWith(
"/a/b/c/src.zip",
{
queryPath:
"/home/github/codeql/java/ql/src/Telemetry/FetchExternalAPIs.ql",
quickEvalPosition: undefined,
},
false,
[],
undefined,
"/tmp/queries",
undefined,
undefined,
);
});
});
describe("getResults", () => {
const options = {
cliServer: {
bqrsInfo: jest.fn(),
bqrsDecode: jest.fn(),
},
bqrsPath: "/tmp/results.bqrs",
logger: createMockLogger(),
};
it("returns undefined when there are no results", async () => {
options.cliServer.bqrsInfo.mockResolvedValue({
"result-sets": [],
});
expect(await getResults(options)).toBeUndefined();
expect(options.logger.log).toHaveBeenCalledWith(
expect.stringMatching(/Expected exactly one result set/),
);
});
it("returns undefined when there are multiple result sets", async () => {
options.cliServer.bqrsInfo.mockResolvedValue({
"result-sets": [
{
name: "#select",
rows: 10,
columns: [
{ name: "apiName", kind: "s" },
{ name: "supported", kind: "b" },
{ name: "usage", kind: "e" },
],
},
{
name: "#select2",
rows: 10,
columns: [
{ name: "apiName", kind: "s" },
{ name: "supported", kind: "b" },
{ name: "usage", kind: "e" },
],
},
],
});
expect(await getResults(options)).toBeUndefined();
expect(options.logger.log).toHaveBeenCalledWith(
expect.stringMatching(/Expected exactly one result set/),
);
});
it("gets the result set", async () => {
options.cliServer.bqrsInfo.mockResolvedValue({
"result-sets": [
{
name: "#select",
rows: 10,
columns: [
{ name: "apiName", kind: "s" },
{ name: "supported", kind: "b" },
{ name: "usage", kind: "e" },
],
},
],
"compatible-query-kinds": ["Table", "Tree", "Graph"],
});
const decodedResultSet = {
columns: [
{ name: "apiName", kind: "String" },
{ name: "supported", kind: "Boolean" },
{ name: "usage", kind: "Entity" },
],
tuples: [
[
"java.io.PrintStream#println(String)",
true,
{
label: "println(...)",
url: {
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
startLine: 29,
startColumn: 9,
endLine: 29,
endColumn: 49,
},
},
],
],
};
options.cliServer.bqrsDecode.mockResolvedValue(decodedResultSet);
const result = await getResults(options);
expect(result).toEqual(decodedResultSet);
expect(options.cliServer.bqrsInfo).toHaveBeenCalledWith(options.bqrsPath);
expect(options.cliServer.bqrsDecode).toHaveBeenCalledWith(
options.bqrsPath,
"#select",
);
});
});