Files
codeql-action/src/trap-caching.test.ts
2024-05-24 09:48:41 +01:00

278 lines
8.3 KiB
TypeScript

import * as fs from "fs";
import * as path from "path";
import * as cache from "@actions/cache";
import test from "ava";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import * as apiClient from "./api-client";
import {
setCodeQL,
getTrapCachingExtractorConfigArgs,
getTrapCachingExtractorConfigArgsForLang,
} from "./codeql";
import * as configUtils from "./config-utils";
import { Feature } from "./feature-flags";
import { Language } from "./languages";
import { getRunnerLogger } from "./logging";
import {
createFeatures,
createTestConfig,
getRecordingLogger,
makeVersionInfo,
setupTests,
} from "./testing-utils";
import {
cleanupTrapCaches,
downloadTrapCaches,
getLanguagesSupportingCaching,
uploadTrapCaches,
} from "./trap-caching";
import * as util from "./util";
setupTests(test);
const stubCodeql = setCodeQL({
async getVersion() {
return makeVersionInfo("2.10.3");
},
async betterResolveLanguages() {
return {
extractors: {
[Language.javascript]: [
{
extractor_root: "some_root",
extractor_options: {
trap: {
properties: {
cache: {
properties: {
dir: {
title: "Cache directory",
},
bound: {
title: "Cache bound",
},
write: {
title: "Cache write",
},
},
},
},
},
},
},
],
[Language.cpp]: [
{
extractor_root: "other_root",
},
],
},
};
},
});
const testConfigWithoutTmpDir = createTestConfig({
languages: [Language.javascript, Language.cpp],
trapCaches: {
javascript: "/some/cache/dir",
},
});
function getTestConfigWithTempDir(tempDir: string): configUtils.Config {
return createTestConfig({
languages: [Language.javascript, Language.ruby],
tempDir,
dbLocation: path.resolve(tempDir, "codeql_databases"),
trapCaches: {
javascript: path.resolve(tempDir, "jsCache"),
ruby: path.resolve(tempDir, "rubyCache"),
},
});
}
test("check flags for JS, analyzing default branch", async (t) => {
await util.withTmpDir(async (tmpDir) => {
const config = getTestConfigWithTempDir(tmpDir);
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true);
const result = await getTrapCachingExtractorConfigArgsForLang(
config,
Language.javascript,
);
t.deepEqual(result, [
`-O=javascript.trap.cache.dir=${path.resolve(tmpDir, "jsCache")}`,
"-O=javascript.trap.cache.bound=1024",
"-O=javascript.trap.cache.write=true",
]);
});
});
test("check flags for all, not analyzing default branch", async (t) => {
await util.withTmpDir(async (tmpDir) => {
const config = getTestConfigWithTempDir(tmpDir);
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(false);
const result = await getTrapCachingExtractorConfigArgs(config);
t.deepEqual(result, [
`-O=javascript.trap.cache.dir=${path.resolve(tmpDir, "jsCache")}`,
"-O=javascript.trap.cache.bound=1024",
"-O=javascript.trap.cache.write=false",
`-O=ruby.trap.cache.dir=${path.resolve(tmpDir, "rubyCache")}`,
"-O=ruby.trap.cache.bound=1024",
"-O=ruby.trap.cache.write=false",
]);
});
});
test("get languages that support TRAP caching", async (t) => {
const loggedMessages = [];
const logger = getRecordingLogger(loggedMessages);
const languagesSupportingCaching = await getLanguagesSupportingCaching(
stubCodeql,
[Language.javascript, Language.cpp],
logger,
);
t.deepEqual(languagesSupportingCaching, [Language.javascript]);
});
test("upload cache key contains right fields", async (t) => {
const loggedMessages = [];
const logger = getRecordingLogger(loggedMessages);
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true);
sinon.stub(util, "tryGetFolderBytes").resolves(999_999_999);
const stubSave = sinon.stub(cache, "saveCache");
process.env.GITHUB_SHA = "somesha";
await uploadTrapCaches(stubCodeql, testConfigWithoutTmpDir, logger);
t.assert(
stubSave.calledOnceWith(
sinon.match.array.contains(["/some/cache/dir"]),
sinon
.match("somesha")
.and(sinon.match("2.10.3"))
.and(sinon.match("javascript")),
),
);
});
test("download cache looks for the right key and creates dir", async (t) => {
await util.withTmpDir(async (tmpDir) => {
const loggedMessages = [];
const logger = getRecordingLogger(loggedMessages);
sinon.stub(actionsUtil, "getTemporaryDirectory").returns(tmpDir);
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(false);
const stubRestore = sinon.stub(cache, "restoreCache").resolves("found");
const eventFile = path.resolve(tmpDir, "event.json");
process.env.GITHUB_EVENT_NAME = "pull_request";
process.env.GITHUB_EVENT_PATH = eventFile;
fs.writeFileSync(
eventFile,
JSON.stringify({
pull_request: {
base: {
sha: "somesha",
},
},
}),
);
await downloadTrapCaches(
stubCodeql,
[Language.javascript, Language.cpp],
logger,
);
t.assert(
stubRestore.calledOnceWith(
sinon.match.array.contains([
path.resolve(tmpDir, "trapCaches", "javascript"),
]),
sinon
.match("somesha")
.and(sinon.match("2.10.3"))
.and(sinon.match("javascript")),
),
);
t.assert(fs.existsSync(path.resolve(tmpDir, "trapCaches", "javascript")));
});
});
test("cleanup removes only old CodeQL TRAP caches", async (t) => {
await util.withTmpDir(async (tmpDir) => {
// This config specifies that we are analyzing JavaScript and Ruby, but not Swift.
const config = getTestConfigWithTempDir(tmpDir);
sinon.stub(actionsUtil, "getRef").resolves("refs/heads/main");
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true);
const listStub = sinon.stub(apiClient, "listActionsCaches").resolves([
// Should be kept, since it's not relevant to CodeQL. In reality, the API shouldn't return
// this in the first place, but this is a defensive check.
{
id: 1,
key: "some-other-key",
created_at: "2024-05-23T14:25:00Z",
size_in_bytes: 100 * 1024 * 1024,
},
// Should be kept, since it's the newest TRAP cache for JavaScript
{
id: 2,
key: "codeql-trap-1-2.0.0-javascript-newest",
created_at: "2024-04-23T14:25:00Z",
size_in_bytes: 50 * 1024 * 1024,
},
// Should be cleaned up
{
id: 3,
key: "codeql-trap-1-2.0.0-javascript-older",
created_at: "2024-03-22T14:25:00Z",
size_in_bytes: 200 * 1024 * 1024,
},
// Should be cleaned up
{
id: 4,
key: "codeql-trap-1-2.0.0-javascript-oldest",
created_at: "2024-02-21T14:25:00Z",
size_in_bytes: 300 * 1024 * 1024,
},
// Should be kept, since it's the newest TRAP cache for Ruby
{
id: 5,
key: "codeql-trap-1-2.0.0-ruby-newest",
created_at: "2024-02-20T14:25:00Z",
size_in_bytes: 300 * 1024 * 1024,
},
// Should be kept, since we aren't analyzing Swift
{
id: 6,
key: "codeql-trap-1-2.0.0-swift-newest",
created_at: "2024-02-22T14:25:00Z",
size_in_bytes: 300 * 1024 * 1024,
},
// Should be kept, since we aren't analyzing Swift
{
id: 7,
key: "codeql-trap-1-2.0.0-swift-older",
created_at: "2024-02-21T14:25:00Z",
size_in_bytes: 300 * 1024 * 1024,
},
]);
const deleteStub = sinon.stub(apiClient, "deleteActionsCache").resolves();
const statusReport = await cleanupTrapCaches(
config,
createFeatures([Feature.CleanupTrapCaches]),
getRunnerLogger(true),
);
t.is(listStub.callCount, 1);
t.assert(listStub.calledWithExactly("codeql-trap", "refs/heads/main"));
t.deepEqual(statusReport, {
trap_cache_cleanup_size_bytes: 500 * 1024 * 1024,
});
t.is(deleteStub.callCount, 2);
t.assert(deleteStub.calledWithExactly(3));
t.assert(deleteStub.calledWithExactly(4));
});
});