364 lines
11 KiB
TypeScript
364 lines
11 KiB
TypeScript
import { join } from "path";
|
|
import { readFileSync } from "fs-extra";
|
|
import { Uri } from "vscode";
|
|
|
|
import {
|
|
Severity,
|
|
compileQuery,
|
|
registerDatabases,
|
|
deregisterDatabases,
|
|
} from "../../../src/pure/legacy-messages";
|
|
import * as config from "../../../src/config";
|
|
import { tmpDir } from "../../../src/helpers";
|
|
import { QueryServerClient } from "../../../src/legacy-query-server/queryserver-client";
|
|
import { CodeQLCliServer } from "../../../src/cli";
|
|
import { SELECT_QUERY_NAME } from "../../../src/contextual/locationFinder";
|
|
import {
|
|
QueryInProgress,
|
|
compileQuery as compileQueryLegacy,
|
|
} from "../../../src/legacy-query-server/run-queries";
|
|
import { LegacyQueryRunner } from "../../../src/legacy-query-server/legacyRunner";
|
|
import { DatabaseItem } from "../../../src/local-databases";
|
|
import { DeepPartial, mockedObject } from "../utils/mocking.helpers";
|
|
import { BqrsKind } from "../../../src/pure/bqrs-cli-types";
|
|
|
|
describe("run-queries", () => {
|
|
let isCanarySpy: jest.SpiedFunction<typeof config.isCanary>;
|
|
|
|
beforeEach(() => {
|
|
isCanarySpy = jest.spyOn(config, "isCanary").mockReturnValue(false);
|
|
});
|
|
|
|
it("should create a QueryEvaluationInfo", () => {
|
|
const saveDir = "query-save-dir";
|
|
const info = createMockQueryInfo(true, saveDir);
|
|
|
|
expect(info.queryEvalInfo.dilPath).toBe(join(saveDir, "results.dil"));
|
|
expect(info.queryEvalInfo.resultsPaths.resultsPath).toBe(
|
|
join(saveDir, "results.bqrs"),
|
|
);
|
|
expect(info.queryEvalInfo.resultsPaths.interpretedResultsPath).toBe(
|
|
join(saveDir, "interpretedResults.sarif"),
|
|
);
|
|
expect(info.dbItemPath).toBe(Uri.file("/abc").fsPath);
|
|
});
|
|
|
|
it("should check if interpreted results can be created", async () => {
|
|
const info = createMockQueryInfo(true);
|
|
|
|
// "1"
|
|
expect(info.queryEvalInfo.canHaveInterpretedResults()).toBe(true);
|
|
|
|
(info.queryEvalInfo as any).databaseHasMetadataFile = false;
|
|
// "2"
|
|
expect(info.queryEvalInfo.canHaveInterpretedResults()).toBe(false);
|
|
|
|
(info.queryEvalInfo as any).databaseHasMetadataFile = true;
|
|
info.metadata!.kind = undefined;
|
|
// "3"
|
|
expect(info.queryEvalInfo.canHaveInterpretedResults()).toBe(false);
|
|
|
|
info.metadata!.kind = "table";
|
|
// "4"
|
|
expect(info.queryEvalInfo.canHaveInterpretedResults()).toBe(false);
|
|
|
|
// Graphs are not interpreted unless canary is set
|
|
info.metadata!.kind = "graph";
|
|
// "5"
|
|
expect(info.queryEvalInfo.canHaveInterpretedResults()).toBe(false);
|
|
|
|
isCanarySpy.mockReturnValueOnce(true);
|
|
// "6"
|
|
expect(info.queryEvalInfo.canHaveInterpretedResults()).toBe(true);
|
|
});
|
|
|
|
[SELECT_QUERY_NAME, "other"].forEach((resultSetName) => {
|
|
it(`should export csv results for result set ${resultSetName}`, async () => {
|
|
const csvLocation = join(tmpDir.name, "test.csv");
|
|
const cliServer = createMockCliServer({
|
|
bqrsInfo: [
|
|
{ "result-sets": [{ name: resultSetName }, { name: "hucairz" }] },
|
|
],
|
|
bqrsDecode: [
|
|
{
|
|
columns: [{ kind: "NotString" as BqrsKind }, { kind: "String" }],
|
|
tuples: [
|
|
["a", "b"],
|
|
["c", "d"],
|
|
],
|
|
next: 1,
|
|
},
|
|
{
|
|
// just for fun, give a different set of columns here
|
|
// this won't happen with the real CLI, but it's a good test
|
|
columns: [
|
|
{ kind: "String" },
|
|
{ kind: "NotString" as BqrsKind },
|
|
{ kind: "StillNotString" as BqrsKind },
|
|
],
|
|
tuples: [["a", "b", "c"]],
|
|
},
|
|
],
|
|
});
|
|
const info = createMockQueryInfo();
|
|
const promise = info.queryEvalInfo.exportCsvResults(
|
|
cliServer,
|
|
csvLocation,
|
|
);
|
|
|
|
const result = await promise;
|
|
expect(result).toBe(true);
|
|
|
|
const csv = readFileSync(csvLocation, "utf8");
|
|
expect(csv).toBe('a,"b"\nc,"d"\n"a",b,c\n');
|
|
|
|
// now verify that we are using the expected result set
|
|
expect(cliServer.bqrsDecode).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
resultSetName,
|
|
expect.anything(),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("should export csv results with characters that need to be escaped", async () => {
|
|
const csvLocation = join(tmpDir.name, "test.csv");
|
|
const cliServer = createMockCliServer({
|
|
bqrsInfo: [
|
|
{ "result-sets": [{ name: SELECT_QUERY_NAME }, { name: "hucairz" }] },
|
|
],
|
|
bqrsDecode: [
|
|
{
|
|
columns: [{ kind: "NotString" as BqrsKind }, { kind: "String" }],
|
|
// We only escape string columns. In practice, we will only see quotes in strings, but
|
|
// it is a good test anyway.
|
|
tuples: [
|
|
['"a"', '"b"'],
|
|
["c,xxx", "d,yyy"],
|
|
['aaa " bbb', 'ccc " ddd'],
|
|
[true, false],
|
|
[123, 456],
|
|
[123.98, 456.99],
|
|
],
|
|
},
|
|
],
|
|
});
|
|
const info = createMockQueryInfo();
|
|
const promise = info.queryEvalInfo.exportCsvResults(cliServer, csvLocation);
|
|
|
|
const result = await promise;
|
|
expect(result).toBe(true);
|
|
|
|
const csv = readFileSync(csvLocation, "utf8");
|
|
expect(csv).toBe(
|
|
'"a","""b"""\nc,xxx,"d,yyy"\naaa " bbb,"ccc "" ddd"\ntrue,"false"\n123,"456"\n123.98,"456.99"\n',
|
|
);
|
|
|
|
// now verify that we are using the expected result set
|
|
expect(cliServer.bqrsDecode).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
SELECT_QUERY_NAME,
|
|
expect.anything(),
|
|
);
|
|
});
|
|
|
|
it("should handle csv exports for a query with no result sets", async () => {
|
|
const csvLocation = join(tmpDir.name, "test.csv");
|
|
const cliServer = createMockCliServer({
|
|
bqrsInfo: [{ "result-sets": [] }],
|
|
});
|
|
const info = createMockQueryInfo();
|
|
const result = await info.queryEvalInfo.exportCsvResults(
|
|
cliServer,
|
|
csvLocation,
|
|
);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
describe("compile", () => {
|
|
it("should compile", async () => {
|
|
const info = createMockQueryInfo();
|
|
const qs = createMockQueryServerClient();
|
|
const mockProgress = "progress-monitor";
|
|
const mockCancel = "cancel-token";
|
|
const mockQlProgram = {
|
|
dbschemePath: "",
|
|
libraryPath: [],
|
|
queryPath: "",
|
|
};
|
|
|
|
const results = await compileQueryLegacy(
|
|
qs as any,
|
|
mockQlProgram,
|
|
undefined,
|
|
info.queryEvalInfo,
|
|
mockProgress as any,
|
|
mockCancel as any,
|
|
qs.logger,
|
|
);
|
|
expect(results).toEqual([{ message: "err", severity: Severity.ERROR }]);
|
|
|
|
expect(qs.sendRequest).toHaveBeenCalledTimes(1);
|
|
expect(qs.sendRequest).toHaveBeenCalledWith(
|
|
compileQuery,
|
|
{
|
|
compilationOptions: {
|
|
computeNoLocationUrls: true,
|
|
failOnWarnings: false,
|
|
fastCompilation: false,
|
|
includeDilInQlo: true,
|
|
localChecking: false,
|
|
noComputeGetUrl: false,
|
|
noComputeToString: false,
|
|
computeDefaultStrings: true,
|
|
emitDebugInfo: true,
|
|
},
|
|
extraOptions: {
|
|
timeoutSecs: 5,
|
|
},
|
|
queryToCheck: mockQlProgram,
|
|
resultPath: info.queryEvalInfo.compileQueryPath,
|
|
target: { query: {} },
|
|
},
|
|
mockCancel,
|
|
mockProgress,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("register", () => {
|
|
it("should register", async () => {
|
|
const qs = createMockQueryServerClient();
|
|
const runner = new LegacyQueryRunner(qs);
|
|
const mockProgress = "progress-monitor";
|
|
const mockCancel = "cancel-token";
|
|
const datasetUri = Uri.file("dataset-uri");
|
|
|
|
const dbItem: DatabaseItem = {
|
|
contents: {
|
|
datasetUri,
|
|
},
|
|
} as any;
|
|
|
|
await runner.registerDatabase(
|
|
mockProgress as any,
|
|
mockCancel as any,
|
|
dbItem,
|
|
);
|
|
|
|
expect(qs.sendRequest).toHaveBeenCalledTimes(1);
|
|
expect(qs.sendRequest).toHaveBeenCalledWith(
|
|
registerDatabases,
|
|
{
|
|
databases: [
|
|
{
|
|
dbDir: datasetUri.fsPath,
|
|
workingSet: "default",
|
|
},
|
|
],
|
|
},
|
|
mockCancel,
|
|
mockProgress,
|
|
);
|
|
});
|
|
|
|
it("should deregister", async () => {
|
|
const qs = createMockQueryServerClient();
|
|
const runner = new LegacyQueryRunner(qs);
|
|
const mockProgress = "progress-monitor";
|
|
const mockCancel = "cancel-token";
|
|
const datasetUri = Uri.file("dataset-uri");
|
|
|
|
const dbItem: DatabaseItem = {
|
|
contents: {
|
|
datasetUri,
|
|
},
|
|
} as any;
|
|
|
|
await runner.deregisterDatabase(
|
|
mockProgress as any,
|
|
mockCancel as any,
|
|
dbItem,
|
|
);
|
|
|
|
expect(qs.sendRequest).toHaveBeenCalledTimes(1);
|
|
expect(qs.sendRequest).toHaveBeenCalledWith(
|
|
deregisterDatabases,
|
|
{
|
|
databases: [
|
|
{
|
|
dbDir: datasetUri.fsPath,
|
|
workingSet: "default",
|
|
},
|
|
],
|
|
},
|
|
mockCancel,
|
|
mockProgress,
|
|
);
|
|
});
|
|
});
|
|
|
|
let queryNum = 0;
|
|
function createMockQueryInfo(
|
|
databaseHasMetadataFile = true,
|
|
saveDir = `save-dir${queryNum++}`,
|
|
) {
|
|
return new QueryInProgress(
|
|
saveDir,
|
|
Uri.parse("file:///abc").fsPath,
|
|
databaseHasMetadataFile,
|
|
"my-scheme", // queryDbscheme,
|
|
undefined,
|
|
{
|
|
kind: "problem",
|
|
},
|
|
);
|
|
}
|
|
|
|
function createMockQueryServerClient(
|
|
cliServer?: CodeQLCliServer,
|
|
): QueryServerClient {
|
|
return mockedObject<QueryServerClient>({
|
|
config: {
|
|
timeoutSecs: 5,
|
|
},
|
|
sendRequest: jest.fn().mockResolvedValue({
|
|
messages: [
|
|
{ message: "err", severity: Severity.ERROR },
|
|
{ message: "warn", severity: Severity.WARNING },
|
|
],
|
|
}),
|
|
logger: {
|
|
log: jest.fn(),
|
|
},
|
|
cliServer,
|
|
});
|
|
}
|
|
|
|
// A type that represents the mocked methods of a CodeQLCliServer. Exclude any non-methods.
|
|
// This allows passing in an array of return values for a single method.
|
|
type MockedCLIMethods = {
|
|
[K in keyof CodeQLCliServer]: CodeQLCliServer[K] extends (
|
|
...args: any
|
|
) => any
|
|
? Array<DeepPartial<Awaited<ReturnType<CodeQLCliServer[K]>>>>
|
|
: never;
|
|
};
|
|
|
|
function createMockCliServer(
|
|
mockOperations: Partial<MockedCLIMethods>,
|
|
): CodeQLCliServer {
|
|
const mockedMethods: Record<string, jest.Mock> = {};
|
|
|
|
for (const [operation, returns] of Object.entries(mockOperations)) {
|
|
const fn = jest.fn();
|
|
returns.forEach((returnValue: any) => {
|
|
fn.mockResolvedValueOnce(returnValue);
|
|
});
|
|
mockedMethods[operation] = fn;
|
|
}
|
|
|
|
return mockedObject<CodeQLCliServer>(mockedMethods);
|
|
}
|
|
});
|