Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Add some tests for downloadDependencyCaches related to feature pref…
…ixes
  • Loading branch information
mbg committed Nov 9, 2025
commit 48a56f6b9314d73d14ed3df19a834441cabcbb37
163 changes: 163 additions & 0 deletions src/dependency-caching.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as fs from "fs";
import path from "path";

import * as actionsCache from "@actions/cache";
import * as glob from "@actions/glob";
import test from "ava";
import * as sinon from "sinon";

Expand All @@ -15,6 +17,9 @@ import {
internal,
CSHARP_BASE_PATTERNS,
CSHARP_EXTRA_PATTERNS,
downloadDependencyCaches,
CacheHitKind,
cacheKey,
} from "./dependency-caching";
import { Feature } from "./feature-flags";
import { KnownLanguage } from "./languages";
Expand Down Expand Up @@ -168,6 +173,164 @@ test("checkHashPatterns - returns patterns when patterns match", async (t) => {
});
});

type RestoreCacheFunc = (
paths: string[],
primaryKey: string,
restoreKeys: string[] | undefined,
) => Promise<string | undefined>;

/**
* Constructs a function that `actionsCache.restoreCache` can be stubbed with.
*
* @param mockCacheKeys The keys of caches that we want to exist in the Actions cache.
*
* @returns Returns a function that `actionsCache.restoreCache` can be stubbed with.
*/
function makeMockCacheCheck(mockCacheKeys: string[]): RestoreCacheFunc {
return async (
_paths: string[],
primaryKey: string,
restoreKeys: string[] | undefined,
) => {
// The behaviour here mirrors what the real `restoreCache` would do:
// - Starting with the primary restore key, check all caches for a match:
// even for the primary restore key, this only has to be a prefix match.
// - If the primary restore key doesn't prefix-match any cache, then proceed
// in the same way for each restore key in turn.
for (const restoreKey of [primaryKey, ...(restoreKeys || [])]) {
for (const mockCacheKey of mockCacheKeys) {
if (mockCacheKey.startsWith(restoreKey)) {
return mockCacheKey;
}
}
}
// Only if no restore key matches any cache key prefix, there is no matching
// cache and we return `undefined`.
return undefined;
};
}

test("downloadDependencyCaches - does not restore caches with feature keys if no features are enabled", async (t) => {
process.env["RUNNER_OS"] = "Linux";

const codeql = createStubCodeQL({});
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);

sinon.stub(glob, "hashFiles").resolves("abcdef");

const keyWithFeature = await cacheKey(
codeql,
createFeatures([Feature.CsharpNewCacheKey]),
KnownLanguage.csharp,
// Patterns don't matter here because we have stubbed `hashFiles` to always return a specific hash above.
[],
);

const restoreCacheStub = sinon
.stub(actionsCache, "restoreCache")
.callsFake(makeMockCacheCheck([keyWithFeature]));

const makePatternCheckStub = sinon.stub(internal, "makePatternCheck");
makePatternCheckStub
.withArgs(CSHARP_BASE_PATTERNS)
.resolves(CSHARP_BASE_PATTERNS);
makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined);

const results = await downloadDependencyCaches(
codeql,
createFeatures([]),
[KnownLanguage.csharp],
logger,
);
t.is(results.length, 1);
t.is(results[0].language, KnownLanguage.csharp);
t.is(results[0].hit_kind, CacheHitKind.Miss);
t.assert(restoreCacheStub.calledOnce);
});

test("downloadDependencyCaches - restores caches with feature keys if features are enabled", async (t) => {
process.env["RUNNER_OS"] = "Linux";

const codeql = createStubCodeQL({});
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
const features = createFeatures([Feature.CsharpNewCacheKey]);

sinon.stub(glob, "hashFiles").resolves("abcdef");

const keyWithFeature = await cacheKey(
codeql,
features,
KnownLanguage.csharp,
// Patterns don't matter here because we have stubbed `hashFiles` to always return a specific hash above.
[],
);

const restoreCacheStub = sinon
.stub(actionsCache, "restoreCache")
.callsFake(makeMockCacheCheck([keyWithFeature]));

const makePatternCheckStub = sinon.stub(internal, "makePatternCheck");
makePatternCheckStub
.withArgs(CSHARP_BASE_PATTERNS)
.resolves(CSHARP_BASE_PATTERNS);
makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined);

const results = await downloadDependencyCaches(
codeql,
features,
[KnownLanguage.csharp],
logger,
);
t.is(results.length, 1);
t.is(results[0].language, KnownLanguage.csharp);
t.is(results[0].hit_kind, CacheHitKind.Exact);
t.assert(restoreCacheStub.calledOnce);
});

test("downloadDependencyCaches - restores caches with feature keys if features are enabled for partial matches", async (t) => {
process.env["RUNNER_OS"] = "Linux";

const codeql = createStubCodeQL({});
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
const features = createFeatures([Feature.CsharpNewCacheKey]);

const hashFilesStub = sinon.stub(glob, "hashFiles");
hashFilesStub.onFirstCall().resolves("abcdef");
hashFilesStub.onSecondCall().resolves("123456");

const keyWithFeature = await cacheKey(
codeql,
features,
KnownLanguage.csharp,
// Patterns don't matter here because we have stubbed `hashFiles` to always return a specific hash above.
[],
);

const restoreCacheStub = sinon
.stub(actionsCache, "restoreCache")
.callsFake(makeMockCacheCheck([keyWithFeature]));

const makePatternCheckStub = sinon.stub(internal, "makePatternCheck");
makePatternCheckStub
.withArgs(CSHARP_BASE_PATTERNS)
.resolves(CSHARP_BASE_PATTERNS);
makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined);

const results = await downloadDependencyCaches(
codeql,
features,
[KnownLanguage.csharp],
logger,
);
t.is(results.length, 1);
t.is(results[0].language, KnownLanguage.csharp);
t.is(results[0].hit_kind, CacheHitKind.Partial);
t.assert(restoreCacheStub.calledOnce);
});

test("getFeaturePrefix - returns empty string if no features are enabled", async (t) => {
const codeql = createStubCodeQL({});
const features = createFeatures([]);
Expand Down
12 changes: 6 additions & 6 deletions src/dependency-caching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { createCacheKeyHash, getTotalCacheSize } from "./caching-utils";
import { CodeQL } from "./codeql";
import { Config } from "./config-utils";
import { EnvVar } from "./environment";
import { Feature, FeatureEnablement, Features } from "./feature-flags";
import { Feature, FeatureEnablement } from "./feature-flags";
import { KnownLanguage, Language } from "./languages";
import { Logger } from "./logging";
import { getErrorMessage, getRequiredEnvParam } from "./util";
Expand Down Expand Up @@ -236,7 +236,7 @@ export async function checkHashPatterns(
*/
export async function downloadDependencyCaches(
codeql: CodeQL,
features: Features,
features: FeatureEnablement,
languages: Language[],
logger: Logger,
): Promise<DependencyCacheRestoreStatusReport> {
Expand Down Expand Up @@ -335,7 +335,7 @@ export type DependencyCacheUploadStatusReport = DependencyCacheUploadStatus[];
*/
export async function uploadDependencyCaches(
codeql: CodeQL,
features: Features,
features: FeatureEnablement,
config: Config,
logger: Logger,
): Promise<DependencyCacheUploadStatusReport> {
Expand Down Expand Up @@ -438,9 +438,9 @@ export async function uploadDependencyCaches(
*
* @returns A cache key capturing information about the project(s) being analyzed in the specified language.
*/
async function cacheKey(
export async function cacheKey(
codeql: CodeQL,
features: Features,
features: FeatureEnablement,
language: Language,
patterns: string[],
): Promise<string> {
Expand Down Expand Up @@ -509,7 +509,7 @@ export async function getFeaturePrefix(
*/
async function cachePrefix(
codeql: CodeQL,
features: Features,
features: FeatureEnablement,
language: Language,
): Promise<string> {
const runnerOs = getRequiredEnvParam("RUNNER_OS");
Expand Down
Loading