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:
@@ -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.");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user