Merge branch 'main' into henrymercer/breakdown-overlay-disabled-reason

# Conflicts:
#	src/config-utils.test.ts
This commit is contained in:
Henry Mercer
2026-03-04 17:54:35 +01:00
50 changed files with 4732 additions and 3901 deletions
+67 -64
View File
@@ -30,65 +30,68 @@ import {
setupTests(test);
test("writeOverlayChangesFile generates correct changes file", async (t) => {
await withTmpDir(async (tmpDir) => {
const dbLocation = path.join(tmpDir, "db");
await fs.promises.mkdir(dbLocation, { recursive: true });
const sourceRoot = path.join(tmpDir, "src");
await fs.promises.mkdir(sourceRoot, { recursive: true });
const tempDir = path.join(tmpDir, "temp");
await fs.promises.mkdir(tempDir, { recursive: true });
test.serial(
"writeOverlayChangesFile generates correct changes file",
async (t) => {
await withTmpDir(async (tmpDir) => {
const dbLocation = path.join(tmpDir, "db");
await fs.promises.mkdir(dbLocation, { recursive: true });
const sourceRoot = path.join(tmpDir, "src");
await fs.promises.mkdir(sourceRoot, { recursive: true });
const tempDir = path.join(tmpDir, "temp");
await fs.promises.mkdir(tempDir, { recursive: true });
const logger = getRunnerLogger(true);
const config = createTestConfig({ dbLocation });
const logger = getRunnerLogger(true);
const config = createTestConfig({ dbLocation });
// Mock the getFileOidsUnderPath function to return base OIDs
const baseOids = {
"unchanged.js": "aaa111",
"modified.js": "bbb222",
"deleted.js": "ccc333",
};
const getFileOidsStubForBase = sinon
.stub(gitUtils, "getFileOidsUnderPath")
.resolves(baseOids);
// Mock the getFileOidsUnderPath function to return base OIDs
const baseOids = {
"unchanged.js": "aaa111",
"modified.js": "bbb222",
"deleted.js": "ccc333",
};
const getFileOidsStubForBase = sinon
.stub(gitUtils, "getFileOidsUnderPath")
.resolves(baseOids);
// Write the base database OIDs file
await writeBaseDatabaseOidsFile(config, sourceRoot);
getFileOidsStubForBase.restore();
// Write the base database OIDs file
await writeBaseDatabaseOidsFile(config, sourceRoot);
getFileOidsStubForBase.restore();
// Mock the getFileOidsUnderPath function to return overlay OIDs
const currentOids = {
"unchanged.js": "aaa111",
"modified.js": "ddd444", // Changed OID
"added.js": "eee555", // New file
};
const getFileOidsStubForOverlay = sinon
.stub(gitUtils, "getFileOidsUnderPath")
.resolves(currentOids);
// Mock the getFileOidsUnderPath function to return overlay OIDs
const currentOids = {
"unchanged.js": "aaa111",
"modified.js": "ddd444", // Changed OID
"added.js": "eee555", // New file
};
const getFileOidsStubForOverlay = sinon
.stub(gitUtils, "getFileOidsUnderPath")
.resolves(currentOids);
// Write the overlay changes file, which uses the mocked overlay OIDs
// and the base database OIDs file
const getTempDirStub = sinon
.stub(actionsUtil, "getTemporaryDirectory")
.returns(tempDir);
const changesFilePath = await writeOverlayChangesFile(
config,
sourceRoot,
logger,
);
getFileOidsStubForOverlay.restore();
getTempDirStub.restore();
// Write the overlay changes file, which uses the mocked overlay OIDs
// and the base database OIDs file
const getTempDirStub = sinon
.stub(actionsUtil, "getTemporaryDirectory")
.returns(tempDir);
const changesFilePath = await writeOverlayChangesFile(
config,
sourceRoot,
logger,
);
getFileOidsStubForOverlay.restore();
getTempDirStub.restore();
const fileContent = await fs.promises.readFile(changesFilePath, "utf-8");
const parsedContent = JSON.parse(fileContent) as { changes: string[] };
const fileContent = await fs.promises.readFile(changesFilePath, "utf-8");
const parsedContent = JSON.parse(fileContent) as { changes: string[] };
t.deepEqual(
parsedContent.changes.sort(),
["added.js", "deleted.js", "modified.js"],
"Should identify added, deleted, and modified files",
);
});
});
t.deepEqual(
parsedContent.changes.sort(),
["added.js", "deleted.js", "modified.js"],
"Should identify added, deleted, and modified files",
);
});
},
);
interface DownloadOverlayBaseDatabaseTestCase {
overlayDatabaseMode: OverlayDatabaseMode;
@@ -206,14 +209,14 @@ const testDownloadOverlayBaseDatabaseFromCache = test.macro({
title: (_, title) => `downloadOverlayBaseDatabaseFromCache: ${title}`,
});
test(
test.serial(
testDownloadOverlayBaseDatabaseFromCache,
"returns stats when successful",
{},
true,
);
test(
test.serial(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when mode is OverlayDatabaseMode.OverlayBase",
{
@@ -222,7 +225,7 @@ test(
false,
);
test(
test.serial(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when mode is OverlayDatabaseMode.None",
{
@@ -231,7 +234,7 @@ test(
false,
);
test(
test.serial(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when caching is disabled",
{
@@ -240,7 +243,7 @@ test(
false,
);
test(
test.serial(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined in test mode",
{
@@ -249,7 +252,7 @@ test(
false,
);
test(
test.serial(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when cache miss",
{
@@ -258,7 +261,7 @@ test(
false,
);
test(
test.serial(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when download fails",
{
@@ -267,7 +270,7 @@ test(
false,
);
test(
test.serial(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when downloaded database is invalid",
{
@@ -276,7 +279,7 @@ test(
false,
);
test(
test.serial(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when downloaded database doesn't have an overlayBaseSpecifier",
{
@@ -285,7 +288,7 @@ test(
false,
);
test(
test.serial(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when resolving database metadata fails",
{
@@ -294,7 +297,7 @@ test(
false,
);
test(
test.serial(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when filesystem error occurs",
{
@@ -303,7 +306,7 @@ test(
false,
);
test("overlay-base database cache keys remain stable", async (t) => {
test.serial("overlay-base database cache keys remain stable", async (t) => {
const logger = getRunnerLogger(true);
const config = createTestConfig({ languages: ["python", "javascript"] });
const codeQlVersion = "2.23.0";
+95 -86
View File
@@ -72,101 +72,110 @@ test("getCacheKey rounds disk space down to nearest 10 GiB", async (t) => {
);
});
test("shouldSkipOverlayAnalysis returns false when no cached status exists", async (t) => {
await withTmpDir(async (tmpDir) => {
process.env["RUNNER_TEMP"] = tmpDir;
const codeql = mockCodeQLVersion("2.20.0");
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
test.serial(
"shouldSkipOverlayAnalysis returns false when no cached status exists",
async (t) => {
await withTmpDir(async (tmpDir) => {
process.env["RUNNER_TEMP"] = tmpDir;
const codeql = mockCodeQLVersion("2.20.0");
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
sinon.stub(actionsCache, "restoreCache").resolves(undefined);
sinon.stub(actionsCache, "restoreCache").resolves(undefined);
const result = await shouldSkipOverlayAnalysis(
codeql,
["javascript"],
makeDiskUsage(50),
logger,
);
const result = await shouldSkipOverlayAnalysis(
codeql,
["javascript"],
makeDiskUsage(50),
logger,
);
t.false(result);
t.true(
messages.some(
(m) =>
m.type === "debug" &&
typeof m.message === "string" &&
m.message.includes("No overlay status found in Actions cache."),
),
);
});
});
test("shouldSkipOverlayAnalysis returns true when cached status indicates failed build", async (t) => {
await withTmpDir(async (tmpDir) => {
process.env["RUNNER_TEMP"] = tmpDir;
const codeql = mockCodeQLVersion("2.20.0");
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
const status = {
attemptedToBuildOverlayBaseDatabase: true,
builtOverlayBaseDatabase: false,
};
// Stub restoreCache to write the status file and return a key
sinon.stub(actionsCache, "restoreCache").callsFake(async (paths) => {
const statusFile = paths[0];
await fs.promises.mkdir(path.dirname(statusFile), { recursive: true });
await fs.promises.writeFile(statusFile, JSON.stringify(status));
return "found-key";
t.false(result);
t.true(
messages.some(
(m) =>
m.type === "debug" &&
typeof m.message === "string" &&
m.message.includes("No overlay status found in Actions cache."),
),
);
});
},
);
const result = await shouldSkipOverlayAnalysis(
codeql,
["javascript"],
makeDiskUsage(50),
logger,
);
test.serial(
"shouldSkipOverlayAnalysis returns true when cached status indicates failed build",
async (t) => {
await withTmpDir(async (tmpDir) => {
process.env["RUNNER_TEMP"] = tmpDir;
const codeql = mockCodeQLVersion("2.20.0");
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
t.true(result);
});
});
const status = {
attemptedToBuildOverlayBaseDatabase: true,
builtOverlayBaseDatabase: false,
};
test("shouldSkipOverlayAnalysis returns false when cached status indicates successful build", async (t) => {
await withTmpDir(async (tmpDir) => {
process.env["RUNNER_TEMP"] = tmpDir;
const codeql = mockCodeQLVersion("2.20.0");
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
// Stub restoreCache to write the status file and return a key
sinon.stub(actionsCache, "restoreCache").callsFake(async (paths) => {
const statusFile = paths[0];
await fs.promises.mkdir(path.dirname(statusFile), { recursive: true });
await fs.promises.writeFile(statusFile, JSON.stringify(status));
return "found-key";
});
const status = {
attemptedToBuildOverlayBaseDatabase: true,
builtOverlayBaseDatabase: true,
};
const result = await shouldSkipOverlayAnalysis(
codeql,
["javascript"],
makeDiskUsage(50),
logger,
);
sinon.stub(actionsCache, "restoreCache").callsFake(async (paths) => {
const statusFile = paths[0];
await fs.promises.mkdir(path.dirname(statusFile), { recursive: true });
await fs.promises.writeFile(statusFile, JSON.stringify(status));
return "found-key";
t.true(result);
});
},
);
const result = await shouldSkipOverlayAnalysis(
codeql,
["javascript"],
makeDiskUsage(50),
logger,
);
test.serial(
"shouldSkipOverlayAnalysis returns false when cached status indicates successful build",
async (t) => {
await withTmpDir(async (tmpDir) => {
process.env["RUNNER_TEMP"] = tmpDir;
const codeql = mockCodeQLVersion("2.20.0");
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
t.false(result);
t.true(
messages.some(
(m) =>
m.type === "debug" &&
typeof m.message === "string" &&
m.message.includes(
"Cached overlay status does not indicate a previous unsuccessful attempt",
),
),
);
});
});
const status = {
attemptedToBuildOverlayBaseDatabase: true,
builtOverlayBaseDatabase: true,
};
sinon.stub(actionsCache, "restoreCache").callsFake(async (paths) => {
const statusFile = paths[0];
await fs.promises.mkdir(path.dirname(statusFile), { recursive: true });
await fs.promises.writeFile(statusFile, JSON.stringify(status));
return "found-key";
});
const result = await shouldSkipOverlayAnalysis(
codeql,
["javascript"],
makeDiskUsage(50),
logger,
);
t.false(result);
t.true(
messages.some(
(m) =>
m.type === "debug" &&
typeof m.message === "string" &&
m.message.includes(
"Cached overlay status does not indicate a previous unsuccessful attempt",
),
),
);
});
},
);
+37 -1
View File
@@ -13,12 +13,17 @@ import * as path from "path";
import * as actionsCache from "@actions/cache";
import { getTemporaryDirectory } from "../actions-util";
import {
getTemporaryDirectory,
getWorkflowRunAttempt,
getWorkflowRunID,
} from "../actions-util";
import { type CodeQL } from "../codeql";
import { Logger } from "../logging";
import {
DiskUsage,
getErrorMessage,
getRequiredEnvParam,
waitForResultWithTimeLimit,
} from "../util";
@@ -38,12 +43,43 @@ function getStatusFilePath(languages: string[]): string {
);
}
/** Details of the job that recorded an overlay status. */
interface JobInfo {
/** The check run ID. This is optional since it is not always available. */
checkRunId?: number;
/** The workflow run ID. */
workflowRunId: number;
/** The workflow run attempt number. */
workflowRunAttempt: number;
/** The name of the job (from GITHUB_JOB). */
name: string;
}
/** Status of an overlay analysis for a group of languages. */
export interface OverlayStatus {
/** Whether the job attempted to build an overlay base database. */
attemptedToBuildOverlayBaseDatabase: boolean;
/** Whether the job successfully built an overlay base database. */
builtOverlayBaseDatabase: boolean;
/** Details of the job that recorded this status. */
job?: JobInfo;
}
/** Creates an `OverlayStatus` populated with the details of the current job. */
export function createOverlayStatus(
attributes: Omit<OverlayStatus, "job">,
checkRunId?: number,
): OverlayStatus {
const job: JobInfo = {
workflowRunId: getWorkflowRunID(),
workflowRunAttempt: getWorkflowRunAttempt(),
name: getRequiredEnvParam("GITHUB_JOB"),
checkRunId,
};
return {
...attributes,
job,
};
}
/**