Extract creation of lock file to more generic function

This commit is contained in:
Koen Vlaswinkel
2023-07-19 14:29:34 +02:00
parent 32656c1cb8
commit 57bcfbbe29
3 changed files with 193 additions and 69 deletions

View File

@@ -1,6 +1,3 @@
import { promises } from "fs-extra";
import { basename, dirname, resolve } from "path";
import { getOnDiskWorkspaceFolders } from "../../common/vscode/workspace-folders";
import { QlPacksForLanguage } from "../../databases/qlpack";
import {
@@ -17,7 +14,7 @@ import { TeeLogger } from "../../common/logging";
import { CancellationToken } from "vscode";
import { ProgressCallback } from "../../common/vscode/progress";
import { CoreCompletedQuery, QueryRunner } from "../../query-server";
import { QLPACK_FILENAMES } from "../../common/ql";
import { createLockFileForStandardQuery } from "../../local-queries/standard-queries";
export async function resolveQueries(
cli: CodeQLCliServer,
@@ -30,64 +27,6 @@ export async function resolveQueries(
});
}
async function resolveContextualQuery(
cli: CodeQLCliServer,
query: string,
): Promise<{ packPath: string; createdTempLockFile: boolean }> {
// Contextual queries now live within the standard library packs.
// This simplifies distribution (you don't need the standard query pack to use the AST viewer),
// but if the library pack doesn't have a lockfile, we won't be able to find
// other pack dependencies of the library pack.
// Work out the enclosing pack.
const packContents = await cli.packPacklist(query, false);
const packFilePath = packContents.find((p) =>
QLPACK_FILENAMES.includes(basename(p)),
);
if (packFilePath === undefined) {
// Should not happen; we already resolved this query.
throw new Error(
`Could not find a CodeQL pack file for the pack enclosing the contextual query ${query}`,
);
}
const packPath = dirname(packFilePath);
const lockFilePath = packContents.find((p) =>
["codeql-pack.lock.yml", "qlpack.lock.yml"].includes(basename(p)),
);
let createdTempLockFile = false;
if (!lockFilePath) {
// No lock file, likely because this library pack is in the package cache.
// Create a lock file so that we can resolve dependencies and library path
// for the contextual query.
void extLogger.log(
`Library pack ${packPath} is missing a lock file; creating a temporary lock file`,
);
await cli.packResolveDependencies(packPath);
createdTempLockFile = true;
// Clear CLI server pack cache before installing dependencies,
// so that it picks up the new lock file, not the previously cached pack.
void extLogger.log("Clearing the CodeQL CLI server's pack cache");
await cli.clearCache();
// Install dependencies.
void extLogger.log(
`Installing package dependencies for library pack ${packPath}`,
);
await cli.packInstall(packPath);
}
return { packPath, createdTempLockFile };
}
async function removeTemporaryLockFile(packPath: string) {
const tempLockFilePath = resolve(packPath, "codeql-pack.lock.yml");
void extLogger.log(
`Deleting temporary package lock file at ${tempLockFilePath}`,
);
// It's fine if the file doesn't exist.
await promises.rm(resolve(packPath, "codeql-pack.lock.yml"), {
force: true,
});
}
export async function runContextualQuery(
query: string,
db: DatabaseItem,
@@ -98,10 +37,7 @@ export async function runContextualQuery(
token: CancellationToken,
templates: Record<string, string>,
): Promise<CoreCompletedQuery> {
const { packPath, createdTempLockFile } = await resolveContextualQuery(
cli,
query,
);
const { cleanup } = await createLockFileForStandardQuery(cli, query);
const queryRun = qs.createQueryRun(
db.databaseUri.fsPath,
{ queryPath: query, quickEvalPosition: undefined },
@@ -120,8 +56,6 @@ export async function runContextualQuery(
token,
new TeeLogger(qs.logger, queryRun.outputDir.logPath),
);
if (createdTempLockFile) {
await removeTemporaryLockFile(packPath);
}
await cleanup?.();
return results;
}

View File

@@ -0,0 +1,77 @@
import { CodeQLCliServer } from "../codeql-cli/cli";
import { QLPACK_FILENAMES, QLPACK_LOCK_FILENAMES } from "../common/ql";
import { basename, dirname, resolve } from "path";
import { extLogger } from "../common/logging/vscode";
import { promises } from "fs-extra";
import { BaseLogger } from "../common/logging";
export type LockFileForStandardQueryResult = {
cleanup?: () => Promise<void>;
};
/**
* Create a temporary query suite for a given query living within the standard library packs.
*
* This will create a lock file so the CLI can run the query without having the ql submodule.
*/
export async function createLockFileForStandardQuery(
cli: CodeQLCliServer,
queryPath: string,
logger: BaseLogger = extLogger,
): Promise<LockFileForStandardQueryResult> {
// These queries live within the standard library packs.
// This simplifies distribution (you don't need the standard query pack to use the AST viewer),
// but if the library pack doesn't have a lockfile, we won't be able to find
// other pack dependencies of the library pack.
// Work out the enclosing pack.
const packContents = await cli.packPacklist(queryPath, false);
const packFilePath = packContents.find((p) =>
QLPACK_FILENAMES.includes(basename(p)),
);
if (packFilePath === undefined) {
// Should not happen; we already resolved this query.
throw new Error(
`Could not find a CodeQL pack file for the pack enclosing the contextual query ${queryPath}`,
);
}
const packPath = dirname(packFilePath);
const lockFilePath = packContents.find((p) =>
QLPACK_LOCK_FILENAMES.includes(basename(p)),
);
let cleanup: (() => Promise<void>) | undefined = undefined;
if (!lockFilePath) {
// No lock file, likely because this library pack is in the package cache.
// Create a lock file so that we can resolve dependencies and library path
// for the contextual query.
void logger.log(
`Library pack ${packPath} is missing a lock file; creating a temporary lock file`,
);
await cli.packResolveDependencies(packPath);
cleanup = async () => {
const tempLockFilePath = resolve(packPath, "codeql-pack.lock.yml");
void logger.log(
`Deleting temporary package lock file at ${tempLockFilePath}`,
);
// It's fine if the file doesn't exist.
await promises.rm(resolve(packPath, "codeql-pack.lock.yml"), {
force: true,
});
};
// Clear CLI server pack cache before installing dependencies,
// so that it picks up the new lock file, not the previously cached pack.
void logger.log("Clearing the CodeQL CLI server's pack cache");
await cli.clearCache();
// Install dependencies.
void logger.log(
`Installing package dependencies for library pack ${packPath}`,
);
await cli.packInstall(packPath);
}
return { cleanup };
}

View File

@@ -0,0 +1,113 @@
import { mockedObject } from "../../utils/mocking.helpers";
import { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
import { dir, DirectoryResult } from "tmp-promise";
import { join } from "path";
import { createLockFileForStandardQuery } from "../../../../src/local-queries/standard-queries";
import { outputFile, pathExists } from "fs-extra";
describe("createLockFileForStandardQuery", () => {
let tmpDir: DirectoryResult;
let packPath: string;
let qlpackPath: string;
let queryPath: string;
const packPacklist = jest.fn();
const packResolveDependencies = jest.fn();
const clearCache = jest.fn();
const packInstall = jest.fn();
const mockCli = mockedObject<CodeQLCliServer>({
packPacklist,
packResolveDependencies,
clearCache,
packInstall,
});
beforeEach(async () => {
tmpDir = await dir({
unsafeCleanup: true,
});
packPath = join(tmpDir.path, "a", "b");
qlpackPath = join(packPath, "qlpack.yml");
queryPath = join(packPath, "d", "e", "query.ql");
packPacklist.mockResolvedValue([qlpackPath, queryPath]);
});
afterEach(async () => {
await tmpDir.cleanup();
});
describe("when the lock file exists", () => {
let lockfilePath: string;
beforeEach(async () => {
lockfilePath = join(packPath, "qlpack.lock.yml");
packPacklist.mockResolvedValue([qlpackPath, lockfilePath, queryPath]);
});
it("does not resolve or install dependencies", async () => {
expect(await createLockFileForStandardQuery(mockCli, queryPath)).toEqual({
cleanup: undefined,
});
expect(packResolveDependencies).not.toHaveBeenCalled();
expect(clearCache).not.toHaveBeenCalled();
expect(packInstall).not.toHaveBeenCalled();
});
it("does not resolve or install dependencies with a codeql-pack.lock.yml", async () => {
lockfilePath = join(packPath, "codeql-pack.lock.yml");
packPacklist.mockResolvedValue([qlpackPath, lockfilePath, queryPath]);
expect(await createLockFileForStandardQuery(mockCli, queryPath)).toEqual({
cleanup: undefined,
});
expect(packResolveDependencies).not.toHaveBeenCalled();
expect(clearCache).not.toHaveBeenCalled();
expect(packInstall).not.toHaveBeenCalled();
});
});
describe("when the lock file does not exist", () => {
it("resolves and installs dependencies", async () => {
expect(await createLockFileForStandardQuery(mockCli, queryPath)).toEqual({
cleanup: expect.any(Function),
});
expect(packResolveDependencies).toHaveBeenCalledWith(packPath);
expect(clearCache).toHaveBeenCalledWith();
expect(packInstall).toHaveBeenCalledWith(packPath);
});
it("cleans up the lock file using the cleanup function", async () => {
const { cleanup } = await createLockFileForStandardQuery(
mockCli,
queryPath,
);
expect(cleanup).not.toBeUndefined();
const lockfilePath = join(packPath, "codeql-pack.lock.yml");
await outputFile(lockfilePath, "lock file contents");
await cleanup?.();
expect(await pathExists(lockfilePath)).toBe(false);
});
it("does not fail when cleaning up a non-existing lock file", async () => {
const { cleanup } = await createLockFileForStandardQuery(
mockCli,
queryPath,
);
expect(cleanup).not.toBeUndefined();
await cleanup?.();
});
});
});