diff --git a/src/dependency-caching.test.ts b/src/dependency-caching.test.ts index 58aedf5b0..eefb8504c 100644 --- a/src/dependency-caching.test.ts +++ b/src/dependency-caching.test.ts @@ -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"; @@ -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"; @@ -168,6 +173,164 @@ test("checkHashPatterns - returns patterns when patterns match", async (t) => { }); }); +type RestoreCacheFunc = ( + paths: string[], + primaryKey: string, + restoreKeys: string[] | undefined, +) => Promise; + +/** + * 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([]); diff --git a/src/dependency-caching.ts b/src/dependency-caching.ts index 15c0e502c..220f1d5ba 100644 --- a/src/dependency-caching.ts +++ b/src/dependency-caching.ts @@ -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"; @@ -236,7 +236,7 @@ export async function checkHashPatterns( */ export async function downloadDependencyCaches( codeql: CodeQL, - features: Features, + features: FeatureEnablement, languages: Language[], logger: Logger, ): Promise { @@ -335,7 +335,7 @@ export type DependencyCacheUploadStatusReport = DependencyCacheUploadStatus[]; */ export async function uploadDependencyCaches( codeql: CodeQL, - features: Features, + features: FeatureEnablement, config: Config, logger: Logger, ): Promise { @@ -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 { @@ -509,7 +509,7 @@ export async function getFeaturePrefix( */ async function cachePrefix( codeql: CodeQL, - features: Features, + features: FeatureEnablement, language: Language, ): Promise { const runnerOs = getRequiredEnvParam("RUNNER_OS");