Merge pull request #2271 from github/koesie10/data-extension-editor-cli-tests

Split and add tests for external API usages query
This commit is contained in:
Koen Vlaswinkel
2023-04-11 11:46:29 +02:00
committed by GitHub
5 changed files with 358 additions and 92 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,17 +13,14 @@ import {
ToDataExtensionsEditorMessage,
} from "../pure/interface-types";
import { ProgressUpdate } from "../progress";
import { extLogger, TeeLogger } from "../common";
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
import { qlpackOfDatabase } from "../contextual/queryResolver";
import { file } from "tmp-promise";
import { readFile, writeFile } from "fs-extra";
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
import { QueryRunner } from "../queryRunner";
import {
getOnDiskWorkspaceFolders,
showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
} from "../helpers";
import { extLogger } from "../common";
import { readFile, writeFile } from "fs-extra";
import { load as loadYaml } from "js-yaml";
import { DatabaseItem, DatabaseManager } from "../local-databases";
import { CodeQLCliServer } from "../cli";
import { asError, assertNever, getErrorMessage } from "../pure/helpers-pure";
@@ -34,6 +31,7 @@ import { ResolvableLocationValue } from "../pure/bqrs-cli-types";
import { showResolvableLocation } from "../interface-utils";
import { decodeBqrsToExternalApiUsages } from "./bqrs";
import { redactableError } from "../pure/errors";
import { readQueryResults, runQuery } from "./external-api-usage-query";
import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
import { ExternalApiUsage } from "./external-api-usage";
import { ModeledMethod } from "./modeled-method";
@@ -192,22 +190,36 @@ export class DataExtensionsEditorView extends AbstractWebview<
}
protected async loadExternalApiUsages(): Promise<void> {
const cancellationTokenSource = new CancellationTokenSource();
try {
const queryResult = await this.runQuery();
const queryResult = await runQuery({
cliServer: this.cliServer,
queryRunner: this.queryRunner,
databaseItem: this.databaseItem,
queryStorageDir: this.queryStorageDir,
logger: extLogger,
progress: (progressUpdate: ProgressUpdate) => {
void this.showProgress(progressUpdate, 1500);
},
token: cancellationTokenSource.token,
});
if (!queryResult) {
await this.clearProgress();
return;
}
await this.showProgress({
message: "Loading results",
message: "Decoding results",
step: 1100,
maxStep: 1500,
});
const bqrsPath = queryResult.outputDir.bqrsPath;
const bqrsChunk = await this.getResults(bqrsPath);
const bqrsChunk = await readQueryResults({
cliServer: this.cliServer,
bqrsPath: queryResult.outputDir.bqrsPath,
logger: extLogger,
});
if (!bqrsChunk) {
await this.clearProgress();
return;
@@ -322,83 +334,6 @@ export class DataExtensionsEditorView extends AbstractWebview<
await this.clearProgress();
}
private async runQuery(): Promise<CoreCompletedQuery | undefined> {
const qlpacks = await qlpackOfDatabase(this.cliServer, this.databaseItem);
const packsToSearch = [qlpacks.dbschemePack];
if (qlpacks.queryPack) {
packsToSearch.push(qlpacks.queryPack);
}
const suiteFile = (
await file({
postfix: ".qls",
})
).path;
const suiteYaml = [];
for (const qlpack of packsToSearch) {
suiteYaml.push({
from: qlpack,
queries: ".",
include: {
id: `${this.databaseItem.language}/telemetry/fetch-external-apis`,
},
});
}
await writeFile(suiteFile, dumpYaml(suiteYaml), "utf8");
const queries = await this.cliServer.resolveQueriesInSuite(
suiteFile,
getOnDiskWorkspaceFolders(),
);
if (queries.length !== 1) {
void extLogger.log(`Expected exactly one query, got ${queries.length}`);
return;
}
const query = queries[0];
const tokenSource = new CancellationTokenSource();
const queryRun = this.queryRunner.createQueryRun(
this.databaseItem.databaseUri.fsPath,
{ queryPath: query, quickEvalPosition: undefined },
false,
getOnDiskWorkspaceFolders(),
undefined,
this.queryStorageDir,
undefined,
undefined,
);
return queryRun.evaluate(
(update) => this.showProgress(update, 1500),
tokenSource.token,
new TeeLogger(this.queryRunner.logger, queryRun.outputDir.logPath),
);
}
private async getResults(bqrsPath: string) {
const bqrsInfo = await this.cliServer.bqrsInfo(bqrsPath);
if (bqrsInfo["result-sets"].length !== 1) {
void extLogger.log(
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
);
return undefined;
}
const resultSet = bqrsInfo["result-sets"][0];
await this.showProgress({
message: "Decoding results",
step: 1200,
maxStep: 1500,
});
return this.cliServer.bqrsDecode(bqrsPath, resultSet.name);
}
/*
* Progress in this class is a bit weird. Most of the progress is based on running the query.
* Query progress is always between 0 and 1000. However, we still have some steps that need

View File

@@ -0,0 +1,109 @@
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
import { qlpackOfDatabase } from "../contextual/queryResolver";
import { file } from "tmp-promise";
import { writeFile } from "fs-extra";
import { dump as dumpYaml } from "js-yaml";
import { getOnDiskWorkspaceFolders } from "../helpers";
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: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveQueriesInSuite">;
queryRunner: Pick<QueryRunner, "createQueryRun" | "logger">;
databaseItem: Pick<DatabaseItem, "contents" | "databaseUri" | "language">;
queryStorageDir: string;
logger: Logger;
progress: ProgressCallback;
token: CancellationToken;
};
export async function runQuery({
cliServer,
queryRunner,
databaseItem,
queryStorageDir,
logger,
progress,
token,
}: RunQueryOptions): Promise<CoreCompletedQuery | undefined> {
const qlpacks = await qlpackOfDatabase(cliServer, databaseItem);
const packsToSearch = [qlpacks.dbschemePack];
if (qlpacks.queryPack) {
packsToSearch.push(qlpacks.queryPack);
}
const suiteFile = (
await file({
postfix: ".qls",
})
).path;
const suiteYaml = [];
for (const qlpack of packsToSearch) {
suiteYaml.push({
from: qlpack,
queries: ".",
include: {
id: `${databaseItem.language}/telemetry/fetch-external-apis`,
},
});
}
await writeFile(suiteFile, dumpYaml(suiteYaml), "utf8");
const queries = await cliServer.resolveQueriesInSuite(
suiteFile,
getOnDiskWorkspaceFolders(),
);
if (queries.length !== 1) {
void logger.log(`Expected exactly one query, got ${queries.length}`);
return;
}
const query = queries[0];
const queryRun = queryRunner.createQueryRun(
databaseItem.databaseUri.fsPath,
{ queryPath: query, quickEvalPosition: undefined },
false,
getOnDiskWorkspaceFolders(),
undefined,
queryStorageDir,
undefined,
undefined,
);
return queryRun.evaluate(
progress,
token,
new TeeLogger(queryRunner.logger, queryRun.outputDir.logPath),
);
}
export type GetResultsOptions = {
cliServer: Pick<CodeQLCliServer, "bqrsInfo" | "bqrsDecode">;
bqrsPath: string;
logger: Logger;
};
export async function readQueryResults({
cliServer,
bqrsPath,
logger,
}: GetResultsOptions) {
const bqrsInfo = await cliServer.bqrsInfo(bqrsPath);
if (bqrsInfo["result-sets"].length !== 1) {
void logger.log(
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
);
return undefined;
}
const resultSet = bqrsInfo["result-sets"][0];
return cliServer.bqrsDecode(bqrsPath, resultSet.name);
}

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 {
readQueryResults,
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 readQueryResults(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 readQueryResults(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 readQueryResults(options);
expect(result).toEqual(decodedResultSet);
expect(options.cliServer.bqrsInfo).toHaveBeenCalledWith(options.bqrsPath);
expect(options.cliServer.bqrsDecode).toHaveBeenCalledWith(
options.bqrsPath,
"#select",
);
});
});