Skip to content

Commit 57bcfbb

Browse files
committed
Extract creation of lock file to more generic function
1 parent 32656c1 commit 57bcfbb

File tree

3 files changed

+193
-69
lines changed

3 files changed

+193
-69
lines changed
Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import { promises } from "fs-extra";
2-
import { basename, dirname, resolve } from "path";
3-
41
import { getOnDiskWorkspaceFolders } from "../../common/vscode/workspace-folders";
52
import { QlPacksForLanguage } from "../../databases/qlpack";
63
import {
@@ -17,7 +14,7 @@ import { TeeLogger } from "../../common/logging";
1714
import { CancellationToken } from "vscode";
1815
import { ProgressCallback } from "../../common/vscode/progress";
1916
import { CoreCompletedQuery, QueryRunner } from "../../query-server";
20-
import { QLPACK_FILENAMES } from "../../common/ql";
17+
import { createLockFileForStandardQuery } from "../../local-queries/standard-queries";
2118

2219
export async function resolveQueries(
2320
cli: CodeQLCliServer,
@@ -30,64 +27,6 @@ export async function resolveQueries(
3027
});
3128
}
3229

33-
async function resolveContextualQuery(
34-
cli: CodeQLCliServer,
35-
query: string,
36-
): Promise<{ packPath: string; createdTempLockFile: boolean }> {
37-
// Contextual queries now live within the standard library packs.
38-
// This simplifies distribution (you don't need the standard query pack to use the AST viewer),
39-
// but if the library pack doesn't have a lockfile, we won't be able to find
40-
// other pack dependencies of the library pack.
41-
42-
// Work out the enclosing pack.
43-
const packContents = await cli.packPacklist(query, false);
44-
const packFilePath = packContents.find((p) =>
45-
QLPACK_FILENAMES.includes(basename(p)),
46-
);
47-
if (packFilePath === undefined) {
48-
// Should not happen; we already resolved this query.
49-
throw new Error(
50-
`Could not find a CodeQL pack file for the pack enclosing the contextual query ${query}`,
51-
);
52-
}
53-
const packPath = dirname(packFilePath);
54-
const lockFilePath = packContents.find((p) =>
55-
["codeql-pack.lock.yml", "qlpack.lock.yml"].includes(basename(p)),
56-
);
57-
let createdTempLockFile = false;
58-
if (!lockFilePath) {
59-
// No lock file, likely because this library pack is in the package cache.
60-
// Create a lock file so that we can resolve dependencies and library path
61-
// for the contextual query.
62-
void extLogger.log(
63-
`Library pack ${packPath} is missing a lock file; creating a temporary lock file`,
64-
);
65-
await cli.packResolveDependencies(packPath);
66-
createdTempLockFile = true;
67-
// Clear CLI server pack cache before installing dependencies,
68-
// so that it picks up the new lock file, not the previously cached pack.
69-
void extLogger.log("Clearing the CodeQL CLI server's pack cache");
70-
await cli.clearCache();
71-
// Install dependencies.
72-
void extLogger.log(
73-
`Installing package dependencies for library pack ${packPath}`,
74-
);
75-
await cli.packInstall(packPath);
76-
}
77-
return { packPath, createdTempLockFile };
78-
}
79-
80-
async function removeTemporaryLockFile(packPath: string) {
81-
const tempLockFilePath = resolve(packPath, "codeql-pack.lock.yml");
82-
void extLogger.log(
83-
`Deleting temporary package lock file at ${tempLockFilePath}`,
84-
);
85-
// It's fine if the file doesn't exist.
86-
await promises.rm(resolve(packPath, "codeql-pack.lock.yml"), {
87-
force: true,
88-
});
89-
}
90-
9130
export async function runContextualQuery(
9231
query: string,
9332
db: DatabaseItem,
@@ -98,10 +37,7 @@ export async function runContextualQuery(
9837
token: CancellationToken,
9938
templates: Record<string, string>,
10039
): Promise<CoreCompletedQuery> {
101-
const { packPath, createdTempLockFile } = await resolveContextualQuery(
102-
cli,
103-
query,
104-
);
40+
const { cleanup } = await createLockFileForStandardQuery(cli, query);
10541
const queryRun = qs.createQueryRun(
10642
db.databaseUri.fsPath,
10743
{ queryPath: query, quickEvalPosition: undefined },
@@ -120,8 +56,6 @@ export async function runContextualQuery(
12056
token,
12157
new TeeLogger(qs.logger, queryRun.outputDir.logPath),
12258
);
123-
if (createdTempLockFile) {
124-
await removeTemporaryLockFile(packPath);
125-
}
59+
await cleanup?.();
12660
return results;
12761
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { CodeQLCliServer } from "../codeql-cli/cli";
2+
import { QLPACK_FILENAMES, QLPACK_LOCK_FILENAMES } from "../common/ql";
3+
import { basename, dirname, resolve } from "path";
4+
import { extLogger } from "../common/logging/vscode";
5+
import { promises } from "fs-extra";
6+
import { BaseLogger } from "../common/logging";
7+
8+
export type LockFileForStandardQueryResult = {
9+
cleanup?: () => Promise<void>;
10+
};
11+
12+
/**
13+
* Create a temporary query suite for a given query living within the standard library packs.
14+
*
15+
* This will create a lock file so the CLI can run the query without having the ql submodule.
16+
*/
17+
export async function createLockFileForStandardQuery(
18+
cli: CodeQLCliServer,
19+
queryPath: string,
20+
logger: BaseLogger = extLogger,
21+
): Promise<LockFileForStandardQueryResult> {
22+
// These queries live within the standard library packs.
23+
// This simplifies distribution (you don't need the standard query pack to use the AST viewer),
24+
// but if the library pack doesn't have a lockfile, we won't be able to find
25+
// other pack dependencies of the library pack.
26+
27+
// Work out the enclosing pack.
28+
const packContents = await cli.packPacklist(queryPath, false);
29+
const packFilePath = packContents.find((p) =>
30+
QLPACK_FILENAMES.includes(basename(p)),
31+
);
32+
if (packFilePath === undefined) {
33+
// Should not happen; we already resolved this query.
34+
throw new Error(
35+
`Could not find a CodeQL pack file for the pack enclosing the contextual query ${queryPath}`,
36+
);
37+
}
38+
const packPath = dirname(packFilePath);
39+
const lockFilePath = packContents.find((p) =>
40+
QLPACK_LOCK_FILENAMES.includes(basename(p)),
41+
);
42+
43+
let cleanup: (() => Promise<void>) | undefined = undefined;
44+
45+
if (!lockFilePath) {
46+
// No lock file, likely because this library pack is in the package cache.
47+
// Create a lock file so that we can resolve dependencies and library path
48+
// for the contextual query.
49+
void logger.log(
50+
`Library pack ${packPath} is missing a lock file; creating a temporary lock file`,
51+
);
52+
await cli.packResolveDependencies(packPath);
53+
54+
cleanup = async () => {
55+
const tempLockFilePath = resolve(packPath, "codeql-pack.lock.yml");
56+
void logger.log(
57+
`Deleting temporary package lock file at ${tempLockFilePath}`,
58+
);
59+
// It's fine if the file doesn't exist.
60+
await promises.rm(resolve(packPath, "codeql-pack.lock.yml"), {
61+
force: true,
62+
});
63+
};
64+
65+
// Clear CLI server pack cache before installing dependencies,
66+
// so that it picks up the new lock file, not the previously cached pack.
67+
void logger.log("Clearing the CodeQL CLI server's pack cache");
68+
await cli.clearCache();
69+
// Install dependencies.
70+
void logger.log(
71+
`Installing package dependencies for library pack ${packPath}`,
72+
);
73+
await cli.packInstall(packPath);
74+
}
75+
76+
return { cleanup };
77+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { mockedObject } from "../../utils/mocking.helpers";
2+
import { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
3+
import { dir, DirectoryResult } from "tmp-promise";
4+
import { join } from "path";
5+
import { createLockFileForStandardQuery } from "../../../../src/local-queries/standard-queries";
6+
import { outputFile, pathExists } from "fs-extra";
7+
8+
describe("createLockFileForStandardQuery", () => {
9+
let tmpDir: DirectoryResult;
10+
let packPath: string;
11+
let qlpackPath: string;
12+
let queryPath: string;
13+
14+
const packPacklist = jest.fn();
15+
const packResolveDependencies = jest.fn();
16+
const clearCache = jest.fn();
17+
const packInstall = jest.fn();
18+
19+
const mockCli = mockedObject<CodeQLCliServer>({
20+
packPacklist,
21+
packResolveDependencies,
22+
clearCache,
23+
packInstall,
24+
});
25+
26+
beforeEach(async () => {
27+
tmpDir = await dir({
28+
unsafeCleanup: true,
29+
});
30+
31+
packPath = join(tmpDir.path, "a", "b");
32+
qlpackPath = join(packPath, "qlpack.yml");
33+
queryPath = join(packPath, "d", "e", "query.ql");
34+
35+
packPacklist.mockResolvedValue([qlpackPath, queryPath]);
36+
});
37+
38+
afterEach(async () => {
39+
await tmpDir.cleanup();
40+
});
41+
42+
describe("when the lock file exists", () => {
43+
let lockfilePath: string;
44+
45+
beforeEach(async () => {
46+
lockfilePath = join(packPath, "qlpack.lock.yml");
47+
48+
packPacklist.mockResolvedValue([qlpackPath, lockfilePath, queryPath]);
49+
});
50+
51+
it("does not resolve or install dependencies", async () => {
52+
expect(await createLockFileForStandardQuery(mockCli, queryPath)).toEqual({
53+
cleanup: undefined,
54+
});
55+
56+
expect(packResolveDependencies).not.toHaveBeenCalled();
57+
expect(clearCache).not.toHaveBeenCalled();
58+
expect(packInstall).not.toHaveBeenCalled();
59+
});
60+
61+
it("does not resolve or install dependencies with a codeql-pack.lock.yml", async () => {
62+
lockfilePath = join(packPath, "codeql-pack.lock.yml");
63+
64+
packPacklist.mockResolvedValue([qlpackPath, lockfilePath, queryPath]);
65+
66+
expect(await createLockFileForStandardQuery(mockCli, queryPath)).toEqual({
67+
cleanup: undefined,
68+
});
69+
70+
expect(packResolveDependencies).not.toHaveBeenCalled();
71+
expect(clearCache).not.toHaveBeenCalled();
72+
expect(packInstall).not.toHaveBeenCalled();
73+
});
74+
});
75+
76+
describe("when the lock file does not exist", () => {
77+
it("resolves and installs dependencies", async () => {
78+
expect(await createLockFileForStandardQuery(mockCli, queryPath)).toEqual({
79+
cleanup: expect.any(Function),
80+
});
81+
82+
expect(packResolveDependencies).toHaveBeenCalledWith(packPath);
83+
expect(clearCache).toHaveBeenCalledWith();
84+
expect(packInstall).toHaveBeenCalledWith(packPath);
85+
});
86+
87+
it("cleans up the lock file using the cleanup function", async () => {
88+
const { cleanup } = await createLockFileForStandardQuery(
89+
mockCli,
90+
queryPath,
91+
);
92+
expect(cleanup).not.toBeUndefined();
93+
94+
const lockfilePath = join(packPath, "codeql-pack.lock.yml");
95+
96+
await outputFile(lockfilePath, "lock file contents");
97+
98+
await cleanup?.();
99+
100+
expect(await pathExists(lockfilePath)).toBe(false);
101+
});
102+
103+
it("does not fail when cleaning up a non-existing lock file", async () => {
104+
const { cleanup } = await createLockFileForStandardQuery(
105+
mockCli,
106+
queryPath,
107+
);
108+
expect(cleanup).not.toBeUndefined();
109+
110+
await cleanup?.();
111+
});
112+
});
113+
});

0 commit comments

Comments
 (0)