import fs from "node:fs"; import path from "node:path"; import * as ghcache from "@actions/cache"; import { createPackageName, type CommandOptions, type PackageName, } from "ts-apt"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import winston from "winston"; import { Cache, CacheKey, CACHE_DEFAULT_DIRNAME, MANIFEST_ALL_FILENAME, MANIFEST_MAIN_FILENAME, } from "../src/cache.js"; import { Manifest, ManifestEntry } from "../src/manifest.js"; import { ActionPackageNames } from "../src/packages.js"; // Cache resolves its directory from os.homedir(), so point it at a temp dir. const { HOME_DIR } = await vi.hoisted(async () => { const nodeFs = await import("node:fs"); const nodeOs = await import("node:os"); const nodePath = await import("node:path"); return { HOME_DIR: nodeFs.mkdtempSync( nodePath.join(nodeOs.tmpdir(), "cache-restore-test-"), ), }; }); vi.mock("node:os", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, homedir: () => HOME_DIR }; }); vi.mock("@actions/cache", () => ({ restoreCache: vi.fn(), saveCache: vi.fn(), isFeatureAvailable: vi.fn(), })); const CACHE_DIR = path.join(HOME_DIR, CACHE_DEFAULT_DIRNAME); const CACHE_ID = 42; const REQUESTED_PACKAGE = createPackageName("app", "1.0"); // Install order from apt, deliberately not alphabetical. const INSTALL_ORDER = [ createPackageName("zlib1g", "1.2"), createPackageName("libfoo", "2.0"), REQUESTED_PACKAGE, ]; // Packages without files (e.g. metapackages) get no archive. const METAPACKAGE = createPackageName("meta", "1.0"); const KEY = new CacheKey( "v1", "1", "x86_64", ActionPackageNames.fromInput(REQUESTED_PACKAGE.serialize()), ); const archivePath = (packageName: PackageName) => path.join(CACHE_DIR, packageName.serialize() + ".tar"); const filesOf = (packageName: PackageName) => [ `usr/lib/${packageName.name}.so`, ]; const entryOf = (packageName: PackageName) => new ManifestEntry(packageName, filesOf(packageName)); const MANIFEST = new Manifest( new Date(0), [...INSTALL_ORDER.map(entryOf), new ManifestEntry(METAPACKAGE)], KEY, ); async function writeCache(archivedPackages = INSTALL_ORDER): Promise { fs.mkdirSync(CACHE_DIR, { recursive: true }); await new Manifest( MANIFEST.created, [entryOf(REQUESTED_PACKAGE)], KEY, ).writeToFile(path.join(CACHE_DIR, MANIFEST_MAIN_FILENAME)); await MANIFEST.writeToFile(path.join(CACHE_DIR, MANIFEST_ALL_FILENAME)); archivedPackages.forEach((pkg) => fs.writeFileSync(archivePath(pkg), "")); } /** Archive operations observed through the command runner. */ interface TarCalls { created: [archive: string, files: string[]][]; extracted: string[]; /** Number of commands run that extracted archives. */ extractCommands?: number; } const argValue = (args: string[], flag: string) => args.find((arg) => arg.startsWith(`${flag}=`))?.slice(flag.length + 1); /** Records sudo tar calls, including the file list piped to tar create. */ function recordTar(calls: TarCalls) { return vi.fn( async (_command: string, args: string[] = [], options: CommandOptions = {}) => { const archive = argValue(args, "--file") ?? ""; if (args.includes("--create")) { calls.created.push([archive, String(options.input ?? "").split("\0")]); } // Extraction runs as `sh -c