Update packages, migrate to ESM

This commit is contained in:
Joshua Brooks
2026-05-20 19:31:39 +00:00
parent 27d5ce7f10
commit 80f777761d
13 changed files with 2503 additions and 2019 deletions

View File

@@ -1,25 +1,69 @@
import * as cache from "@actions/cache"; import { jest, test, expect, beforeEach, afterAll } from "@jest/globals";
import * as core from "@actions/core";
import { Events, RefKey } from "../src/constants"; // Mock @actions/core
import * as actionUtils from "../src/utils/actionUtils"; jest.unstable_mockModule("@actions/core", () => ({
import * as testUtils from "../src/utils/testUtils"; getInput: jest.fn((name: string, options?: { required?: boolean }) => {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.mock("@actions/core"); // Mock @actions/cache
jest.mock("@actions/cache"); jest.unstable_mockModule("@actions/cache", () => ({
restoreCache: jest.fn(),
saveCache: jest.fn(),
isFeatureAvailable: jest.fn(() => true),
ReserveCacheError: class ReserveCacheError extends Error {
constructor(message: string) {
super(message);
this.name = "ReserveCacheError";
}
}
}));
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, RefKey } = await import("../src/constants");
const actionUtils = await import("../src/utils/actionUtils");
const testUtils = await import("../src/utils/testUtils");
let pristineEnv: NodeJS.ProcessEnv; let pristineEnv: NodeJS.ProcessEnv;
beforeAll(() => {
pristineEnv = process.env;
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
return jest.requireActual("@actions/core").getInput(name, options);
});
});
beforeEach(() => { beforeEach(() => {
jest.resetModules(); pristineEnv = { ...process.env };
process.env = pristineEnv; jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[
`INPUT_${name.replace(/ /g, "_").toUpperCase()}`
] || "";
if (options && options.required && !val) {
throw new Error(
`Input required and not supplied: ${name}`
);
}
return val.trim();
}
);
delete process.env[Events.Key]; delete process.env[Events.Key];
delete process.env[RefKey]; delete process.env[RefKey];
}); });
@@ -47,74 +91,44 @@ test("isGhes returns false when server url is github.com", () => {
}); });
test("isExactKeyMatch with undefined cache key returns false", () => { test("isExactKeyMatch with undefined cache key returns false", () => {
const key = "linux-rust"; expect(actionUtils.isExactKeyMatch("linux-rust", undefined)).toBe(false);
const cacheKey = undefined;
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
}); });
test("isExactKeyMatch with empty cache key returns false", () => { test("isExactKeyMatch with empty cache key returns false", () => {
const key = "linux-rust"; expect(actionUtils.isExactKeyMatch("linux-rust", "")).toBe(false);
const cacheKey = "";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
}); });
test("isExactKeyMatch with different keys returns false", () => { test("isExactKeyMatch with different keys returns false", () => {
const key = "linux-rust"; expect(actionUtils.isExactKeyMatch("linux-rust", "linux-")).toBe(false);
const cacheKey = "linux-";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
}); });
test("isExactKeyMatch with different key accents returns false", () => { test("isExactKeyMatch with different key accents returns false", () => {
const key = "linux-áccent"; expect(actionUtils.isExactKeyMatch("linux-áccent", "linux-accent")).toBe(false);
const cacheKey = "linux-accent";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
}); });
test("isExactKeyMatch with same key returns true", () => { test("isExactKeyMatch with same key returns true", () => {
const key = "linux-rust"; expect(actionUtils.isExactKeyMatch("linux-rust", "linux-rust")).toBe(true);
const cacheKey = "linux-rust";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(true);
}); });
test("isExactKeyMatch with same key and different casing returns true", () => { test("isExactKeyMatch with same key and different casing returns true", () => {
const key = "linux-rust"; expect(actionUtils.isExactKeyMatch("linux-rust", "LINUX-RUST")).toBe(true);
const cacheKey = "LINUX-RUST";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(true);
}); });
test("logWarning logs a message with a warning prefix", () => { test("logWarning logs a message with a warning prefix", () => {
const message = "A warning occurred."; const message = "A warning occurred.";
const infoMock = jest.spyOn(core, "info");
actionUtils.logWarning(message); actionUtils.logWarning(message);
expect(core.info).toHaveBeenCalledWith(`[warning]${message}`);
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`);
}); });
test("isValidEvent returns false for event that does not have a branch or tag", () => { test("isValidEvent returns false for event that does not have a branch or tag", () => {
const event = "foo"; process.env[Events.Key] = "foo";
process.env[Events.Key] = event; expect(actionUtils.isValidEvent()).toBe(false);
const isValidEvent = actionUtils.isValidEvent();
expect(isValidEvent).toBe(false);
}); });
test("isValidEvent returns true for event that has a ref", () => { test("isValidEvent returns true for event that has a ref", () => {
const event = Events.Push; process.env[Events.Key] = Events.Push;
process.env[Events.Key] = event;
process.env[RefKey] = "ref/heads/feature"; process.env[RefKey] = "ref/heads/feature";
expect(actionUtils.isValidEvent()).toBe(true);
const isValidEvent = actionUtils.isValidEvent();
expect(isValidEvent).toBe(true);
}); });
test("getInputAsArray returns empty array if not required and missing", () => { test("getInputAsArray returns empty array if not required and missing", () => {
@@ -124,7 +138,7 @@ test("getInputAsArray returns empty array if not required and missing", () => {
test("getInputAsArray throws error if required and missing", () => { test("getInputAsArray throws error if required and missing", () => {
expect(() => expect(() =>
actionUtils.getInputAsArray("foo", { required: true }) actionUtils.getInputAsArray("foo", { required: true })
).toThrowError(); ).toThrow();
}); });
test("getInputAsArray handles single line correctly", () => { test("getInputAsArray handles single line correctly", () => {
@@ -180,7 +194,7 @@ test("getInputAsInt returns undefined if input is invalid or NaN", () => {
test("getInputAsInt throws if required and value missing", () => { test("getInputAsInt throws if required and value missing", () => {
expect(() => expect(() =>
actionUtils.getInputAsInt("undefined", { required: true }) actionUtils.getInputAsInt("undefined", { required: true })
).toThrowError(); ).toThrow();
}); });
test("getInputAsBool returns false if input not set", () => { test("getInputAsBool returns false if input not set", () => {
@@ -200,68 +214,65 @@ test("getInputAsBool returns false if input is invalid or NaN", () => {
test("getInputAsBool throws if required and value missing", () => { test("getInputAsBool throws if required and value missing", () => {
expect(() => expect(() =>
actionUtils.getInputAsBool("undefined2", { required: true }) actionUtils.getInputAsBool("undefined2", { required: true })
).toThrowError(); ).toThrow();
}); });
test("isCacheFeatureAvailable for ac enabled", () => { test("isCacheFeatureAvailable for ac enabled", () => {
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => true); (cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
expect(actionUtils.isCacheFeatureAvailable()).toBe(true); expect(actionUtils.isCacheFeatureAvailable()).toBe(true);
}); });
test("isCacheFeatureAvailable for ac disabled on GHES", () => { test("isCacheFeatureAvailable for ac disabled on GHES", () => {
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => false); (cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
const message = `Cache action is only supported on GHES version >= 3.5. If you are on version >=3.5 Please check with GHES admin if Actions cache service is enabled or not. const message = `Cache action is only supported on GHES version >= 3.5. If you are on version >=3.5 Please check with GHES admin if Actions cache service is enabled or not.
Otherwise please upgrade to GHES version >= 3.5 and If you are also using Github Connect, please unretire the actions/cache namespace before upgrade (see https://docs.github.com/en/enterprise-server@3.5/admin/github-actions/managing-access-to-actions-from-githubcom/enabling-automatic-access-to-githubcom-actions-using-github-connect#automatic-retirement-of-namespaces-for-actions-accessed-on-githubcom)`; Otherwise please upgrade to GHES version >= 3.5 and If you are also using Github Connect, please unretire the actions/cache namespace before upgrade (see https://docs.github.com/en/enterprise-server@3.5/admin/github-actions/managing-access-to-actions-from-githubcom/enabling-automatic-access-to-githubcom-actions-using-github-connect#automatic-retirement-of-namespaces-for-actions-accessed-on-githubcom)`;
const infoMock = jest.spyOn(core, "info");
try { try {
process.env["GITHUB_SERVER_URL"] = "http://example.com"; process.env["GITHUB_SERVER_URL"] = "http://example.com";
expect(actionUtils.isCacheFeatureAvailable()).toBe(false); expect(actionUtils.isCacheFeatureAvailable()).toBe(false);
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`); expect(core.info).toHaveBeenCalledWith(`[warning]${message}`);
} finally { } finally {
delete process.env["GITHUB_SERVER_URL"]; delete process.env["GITHUB_SERVER_URL"];
} }
}); });
test("isCacheFeatureAvailable for ac disabled on dotcom", () => { test("isCacheFeatureAvailable for ac disabled on dotcom", () => {
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => false); (cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
const message = const message =
"An internal error has occurred in cache backend. Please check https://www.githubstatus.com/ for any ongoing issue in actions."; "An internal error has occurred in cache backend. Please check https://www.githubstatus.com/ for any ongoing issue in actions.";
const infoMock = jest.spyOn(core, "info");
try { try {
process.env["GITHUB_SERVER_URL"] = "http://github.com"; process.env["GITHUB_SERVER_URL"] = "http://github.com";
expect(actionUtils.isCacheFeatureAvailable()).toBe(false); expect(actionUtils.isCacheFeatureAvailable()).toBe(false);
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`); expect(core.info).toHaveBeenCalledWith(`[warning]${message}`);
} finally { } finally {
delete process.env["GITHUB_SERVER_URL"]; delete process.env["GITHUB_SERVER_URL"];
} }
}); });
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is not defined", async () => { test("isGhes returns false when the GITHUB_SERVER_URL environment variable is not defined", () => {
delete process.env["GITHUB_SERVER_URL"]; delete process.env["GITHUB_SERVER_URL"];
expect(actionUtils.isGhes()).toBeFalsy(); expect(actionUtils.isGhes()).toBeFalsy();
}); });
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is set to github.com", async () => { test("isGhes returns false when the GITHUB_SERVER_URL environment variable is set to github.com", () => {
process.env["GITHUB_SERVER_URL"] = "https://github.com"; process.env["GITHUB_SERVER_URL"] = "https://github.com";
expect(actionUtils.isGhes()).toBeFalsy(); expect(actionUtils.isGhes()).toBeFalsy();
}); });
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL", async () => { test("isGhes returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL", () => {
process.env["GITHUB_SERVER_URL"] = "https://contoso.ghe.com"; process.env["GITHUB_SERVER_URL"] = "https://contoso.ghe.com";
expect(actionUtils.isGhes()).toBeFalsy(); expect(actionUtils.isGhes()).toBeFalsy();
}); });
test("isGhes returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix", async () => { test("isGhes returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix", () => {
process.env["GITHUB_SERVER_URL"] = "https://mock-github.localhost"; process.env["GITHUB_SERVER_URL"] = "https://mock-github.localhost";
expect(actionUtils.isGhes()).toBeFalsy(); expect(actionUtils.isGhes()).toBeFalsy();
}); });
test("isGhes returns true when the GITHUB_SERVER_URL environment variable is set to some other URL", async () => { test("isGhes returns true when the GITHUB_SERVER_URL environment variable is set to some other URL", () => {
process.env["GITHUB_SERVER_URL"] = "https://src.onpremise.fabrikam.com"; process.env["GITHUB_SERVER_URL"] = "https://src.onpremise.fabrikam.com";
expect(actionUtils.isGhes()).toBeTruthy(); expect(actionUtils.isGhes()).toBeTruthy();
}); });

View File

@@ -1,50 +1,69 @@
import * as cache from "@actions/cache"; import { jest, test, expect, beforeEach, afterEach } from "@jest/globals";
import * as core from "@actions/core";
import { Events, RefKey } from "../src/constants"; // Mock @actions/core
import { restoreRun } from "../src/restoreImpl"; jest.unstable_mockModule("@actions/core", () => ({
import * as actionUtils from "../src/utils/actionUtils"; getInput: jest.fn((name: string, options?: { required?: boolean }) => {
import * as testUtils from "../src/utils/testUtils"; const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
jest.mock("../src/utils/actionUtils"); if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
beforeAll(() => {
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isExactKeyMatch(key, cacheResult);
} }
); return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => { // Mock @actions/cache
const actualUtils = jest.requireActual("../src/utils/actionUtils"); jest.unstable_mockModule("@actions/cache", () => ({
return actualUtils.isValidEvent(); restoreCache: jest.fn(),
}); saveCache: jest.fn(),
isFeatureAvailable: jest.fn(() => true),
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation( ReserveCacheError: class ReserveCacheError extends Error {
(name, options) => { constructor(message: string) {
const actualUtils = jest.requireActual("../src/utils/actionUtils"); super(message);
return actualUtils.getInputAsArray(name, options); this.name = "ReserveCacheError";
} }
); }
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation( const core = await import("@actions/core");
(name, options) => { const cache = await import("@actions/cache");
const actualUtils = jest.requireActual("../src/utils/actionUtils"); const { Events, RefKey } = await import("../src/constants");
return actualUtils.getInputAsBool(name, options); const { restoreRun } = await import("../src/restoreImpl");
} const testUtils = await import("../src/utils/testUtils");
);
});
beforeEach(() => { beforeEach(() => {
jest.restoreAllMocks(); jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[
`INPUT_${name.replace(/ /g, "_").toUpperCase()}`
] || "";
if (options && options.required && !val) {
throw new Error(
`Input required and not supplied: ${name}`
);
}
return val.trim();
}
);
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env[Events.Key] = Events.Push; process.env[Events.Key] = Events.Push;
process.env[RefKey] = "refs/heads/feature-branch"; process.env[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
}); });
afterEach(() => { afterEach(() => {
@@ -62,19 +81,12 @@ test("restore with no cache found", async () => {
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
await restoreRun(); await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(cache.restoreCache).toHaveBeenCalledWith(
[path], [path],
key, key,
[], [],
@@ -84,12 +96,10 @@ test("restore with no cache found", async () => {
false false
); );
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledTimes(1); expect(core.saveState).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.info).toHaveBeenCalledWith(
expect(infoMock).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}` `Cache not found for input keys: ${key}`
); );
}); });
@@ -105,19 +115,12 @@ test("restore with restore keys and no cache found", async () => {
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
await restoreRun(); await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(cache.restoreCache).toHaveBeenCalledWith(
[path], [path],
key, key,
[restoreKey], [restoreKey],
@@ -127,12 +130,10 @@ test("restore with restore keys and no cache found", async () => {
false false
); );
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledTimes(1); expect(core.saveState).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.info).toHaveBeenCalledWith(
expect(infoMock).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}, ${restoreKey}` `Cache not found for input keys: ${key}, ${restoreKey}`
); );
}); });
@@ -146,20 +147,12 @@ test("restore with cache found for key", async () => {
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(key);
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(key);
});
await restoreRun(); await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(cache.restoreCache).toHaveBeenCalledWith(
[path], [path],
key, key,
[], [],
@@ -169,15 +162,13 @@ test("restore with cache found for key", async () => {
false false
); );
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", key); expect(core.saveState).toHaveBeenCalledWith("CACHE_RESULT", key);
expect(stateMock).toHaveBeenCalledTimes(2); expect(core.saveState).toHaveBeenCalledTimes(2);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true"); expect(core.info).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(failedMock).toHaveBeenCalledTimes(0);
}); });
test("restore with cache found for restore key", async () => { test("restore with cache found for restore key", async () => {
@@ -191,39 +182,20 @@ test("restore with cache found for restore key", async () => {
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(restoreKey);
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(restoreKey);
});
await restoreRun(); await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
[path], expect(core.saveState).toHaveBeenCalledWith("CACHE_RESULT", restoreKey);
key, expect(core.saveState).toHaveBeenCalledTimes(2);
[restoreKey], expect(core.setOutput).toHaveBeenCalledTimes(1);
{ expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
lookupOnly: false expect(core.info).toHaveBeenCalledWith(
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", restoreKey);
expect(stateMock).toHaveBeenCalledTimes(2);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(infoMock).toHaveBeenCalledWith(
`Cache restored from key: ${restoreKey}` `Cache restored from key: ${restoreKey}`
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
}); });
test("Fail restore when fail on cache miss is enabled and primary + restore keys not found", async () => { test("Fail restore when fail on cache miss is enabled and primary + restore keys not found", async () => {
@@ -237,35 +209,17 @@ test("Fail restore when fail on cache miss is enabled and primary + restore keys
failOnCacheMiss: true failOnCacheMiss: true
}); });
const failedMock = jest.spyOn(core, "setFailed"); (cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
await restoreRun(); await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
[path], expect(core.setOutput).toHaveBeenCalledTimes(0);
key, expect(core.setFailed).toHaveBeenCalledWith(
[restoreKey],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledWith(
`Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${key}` `Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${key}`
); );
expect(failedMock).toHaveBeenCalledTimes(1); expect(core.setFailed).toHaveBeenCalledTimes(1);
}); });
test("restore when fail on cache miss is enabled and primary key doesn't match restored key", async () => { test("restore when fail on cache miss is enabled and primary key doesn't match restored key", async () => {
@@ -279,40 +233,20 @@ test("restore when fail on cache miss is enabled and primary key doesn't match r
failOnCacheMiss: true failOnCacheMiss: true
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(restoreKey);
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(restoreKey);
});
await restoreRun(); await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
[path], expect(core.saveState).toHaveBeenCalledWith("CACHE_RESULT", restoreKey);
key, expect(core.saveState).toHaveBeenCalledTimes(2);
[restoreKey], expect(core.setOutput).toHaveBeenCalledTimes(1);
{ expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
lookupOnly: false expect(core.info).toHaveBeenCalledWith(
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", restoreKey);
expect(stateMock).toHaveBeenCalledTimes(2);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(infoMock).toHaveBeenCalledWith(
`Cache restored from key: ${restoreKey}` `Cache restored from key: ${restoreKey}`
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
}); });
test("restore with fail on cache miss disabled and no cache found", async () => { test("restore with fail on cache miss disabled and no cache found", async () => {
@@ -326,33 +260,15 @@ test("restore with fail on cache miss disabled and no cache found", async () =>
failOnCacheMiss: false failOnCacheMiss: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
await restoreRun(); await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
[path], expect(core.saveState).toHaveBeenCalledTimes(1);
key, expect(core.info).toHaveBeenCalledWith(
[restoreKey],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledTimes(1);
expect(infoMock).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}, ${restoreKey}` `Cache not found for input keys: ${key}, ${restoreKey}`
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
}); });

View File

@@ -1,51 +1,71 @@
import * as cache from "@actions/cache"; import { jest, test, expect, beforeEach, afterEach } from "@jest/globals";
import * as core from "@actions/core";
import { Events, Inputs, RefKey } from "../src/constants"; // Mock @actions/core
import { restoreImpl } from "../src/restoreImpl"; jest.unstable_mockModule("@actions/core", () => ({
import { StateProvider } from "../src/stateProvider"; getInput: jest.fn((name: string, options?: { required?: boolean }) => {
import * as actionUtils from "../src/utils/actionUtils"; const val =
import * as testUtils from "../src/utils/testUtils"; process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) {
jest.mock("../src/utils/actionUtils"); throw new Error(`Input required and not supplied: ${name}`);
beforeAll(() => {
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isExactKeyMatch(key, cacheResult);
} }
); return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => { // Mock @actions/cache
const actualUtils = jest.requireActual("../src/utils/actionUtils"); jest.unstable_mockModule("@actions/cache", () => ({
return actualUtils.isValidEvent(); restoreCache: jest.fn(),
}); saveCache: jest.fn(),
isFeatureAvailable: jest.fn(() => true),
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation( ReserveCacheError: class ReserveCacheError extends Error {
(name, options) => { constructor(message: string) {
const actualUtils = jest.requireActual("../src/utils/actionUtils"); super(message);
return actualUtils.getInputAsArray(name, options); this.name = "ReserveCacheError";
} }
); }
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation( const core = await import("@actions/core");
(name, options) => { const cache = await import("@actions/cache");
const actualUtils = jest.requireActual("../src/utils/actionUtils"); const { Events, Inputs, RefKey } = await import("../src/constants");
return actualUtils.getInputAsBool(name, options); const { restoreImpl } = await import("../src/restoreImpl");
} const { StateProvider } = await import("../src/stateProvider");
); const testUtils = await import("../src/utils/testUtils");
});
beforeEach(() => { beforeEach(() => {
jest.restoreAllMocks(); jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[
`INPUT_${name.replace(/ /g, "_").toUpperCase()}`
] || "";
if (options && options.required && !val) {
throw new Error(
`Input required and not supplied: ${name}`
);
}
return val.trim();
}
);
(core.getState as jest.Mock).mockReturnValue("");
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env[Events.Key] = Events.Push; process.env[Events.Key] = Events.Push;
process.env[RefKey] = "refs/heads/feature-branch"; process.env[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
}); });
afterEach(() => { afterEach(() => {
@@ -55,52 +75,37 @@ afterEach(() => {
}); });
test("restore with invalid event outputs warning", async () => { test("restore with invalid event outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const invalidEvent = "commit_comment"; const invalidEvent = "commit_comment";
process.env[Events.Key] = invalidEvent; process.env[Events.Key] = invalidEvent;
delete process.env[RefKey]; delete process.env[RefKey];
await restoreImpl(new StateProvider()); await restoreImpl(new StateProvider());
expect(logWarningMock).toHaveBeenCalledWith( expect(core.info).toHaveBeenCalledWith(
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.` `[warning]Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
}); });
test("restore without AC available should no-op", async () => { test("restore without AC available should no-op", async () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false); (cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => false
);
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
await restoreImpl(new StateProvider()); await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(0); expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false"); expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
}); });
test("restore on GHES without AC available should no-op", async () => { test("restore on GHES without AC available should no-op", async () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true); (cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => false
);
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
await restoreImpl(new StateProvider()); await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(0); expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false"); expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
}); });
test("restore on GHES with AC available ", async () => { test("restore on GHES with AC available ", async () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
const path = "node_modules"; const path = "node_modules";
const key = "node-test"; const key = "node-test";
testUtils.setInputs({ testUtils.setInputs({
@@ -109,20 +114,12 @@ test("restore on GHES with AC available ", async () => {
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(key);
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(key);
});
await restoreImpl(new StateProvider()); await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(cache.restoreCache).toHaveBeenCalledWith(
[path], [path],
key, key,
[], [],
@@ -132,32 +129,26 @@ test("restore on GHES with AC available ", async () => {
false false
); );
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true"); expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
expect(core.info).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledTimes(0);
}); });
test("restore with no path should fail", async () => { test("restore with no path should fail", async () => {
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
await restoreImpl(new StateProvider()); await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(0); expect(cache.restoreCache).toHaveBeenCalledTimes(0);
// this input isn't necessary for restore b/c tarball contains entries relative to workspace expect(core.setFailed).not.toHaveBeenCalledWith(
expect(failedMock).not.toHaveBeenCalledWith(
"Input required and not supplied: path" "Input required and not supplied: path"
); );
}); });
test("restore with no key", async () => { test("restore with no key", async () => {
testUtils.setInput(Inputs.Path, "node_modules"); testUtils.setInput(Inputs.Path, "node_modules");
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
await restoreImpl(new StateProvider()); await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(0); expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledWith( expect(core.setFailed).toHaveBeenCalledWith(
"Input required and not supplied: key" "Input required and not supplied: key"
); );
}); });
@@ -172,46 +163,30 @@ test("restore with too many keys should fail", async () => {
restoreKeys, restoreKeys,
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const failedMock = jest.spyOn(core, "setFailed"); (cache.restoreCache as jest.Mock).mockRejectedValue(
const restoreCacheMock = jest.spyOn(cache, "restoreCache"); new Error("Key Validation Error: Keys are limited to a maximum of 10.")
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
restoreKeys,
{
lookupOnly: false
},
false
); );
expect(failedMock).toHaveBeenCalledWith( await restoreImpl(new StateProvider());
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledWith(
`Key Validation Error: Keys are limited to a maximum of 10.` `Key Validation Error: Keys are limited to a maximum of 10.`
); );
}); });
test("restore with large key should fail", async () => { test("restore with large key should fail", async () => {
const path = "node_modules"; const path = "node_modules";
const key = "foo".repeat(512); // Over the 512 character limit const key = "foo".repeat(512);
testUtils.setInputs({ testUtils.setInputs({
path: path, path: path,
key, key,
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const failedMock = jest.spyOn(core, "setFailed"); (cache.restoreCache as jest.Mock).mockRejectedValue(
const restoreCacheMock = jest.spyOn(cache, "restoreCache"); new Error(`Key Validation Error: ${key} cannot be larger than 512 characters.`)
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[],
{
lookupOnly: false
},
false
); );
expect(failedMock).toHaveBeenCalledWith( await restoreImpl(new StateProvider());
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledWith(
`Key Validation Error: ${key} cannot be larger than 512 characters.` `Key Validation Error: ${key} cannot be larger than 512 characters.`
); );
}); });
@@ -224,20 +199,12 @@ test("restore with invalid key should fail", async () => {
key, key,
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const failedMock = jest.spyOn(core, "setFailed"); (cache.restoreCache as jest.Mock).mockRejectedValue(
const restoreCacheMock = jest.spyOn(cache, "restoreCache"); new Error(`Key Validation Error: ${key} cannot contain commas.`)
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[],
{
lookupOnly: false
},
false
); );
expect(failedMock).toHaveBeenCalledWith( await restoreImpl(new StateProvider());
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledWith(
`Key Validation Error: ${key} cannot contain commas.` `Key Validation Error: ${key} cannot contain commas.`
); );
}); });
@@ -251,32 +218,14 @@ test("restore with no cache found", async () => {
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
await restoreImpl(new StateProvider()); await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
[path], expect(core.setFailed).toHaveBeenCalledTimes(0);
key, expect(core.info).toHaveBeenCalledWith(
[],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}` `Cache not found for input keys: ${key}`
); );
}); });
@@ -292,32 +241,14 @@ test("restore with restore keys and no cache found", async () => {
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
await restoreImpl(new StateProvider()); await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
[path], expect(core.setFailed).toHaveBeenCalledTimes(0);
key, expect(core.info).toHaveBeenCalledWith(
[restoreKey],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}, ${restoreKey}` `Cache not found for input keys: ${key}, ${restoreKey}`
); );
}); });
@@ -331,35 +262,16 @@ test("restore with cache found for key", async () => {
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(key);
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(key);
});
await restoreImpl(new StateProvider()); await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
[path], expect(core.setOutput).toHaveBeenCalledTimes(1);
key, expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
[], expect(core.info).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
{ expect(core.setFailed).toHaveBeenCalledTimes(0);
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(failedMock).toHaveBeenCalledTimes(0);
}); });
test("restore with cache found for restore key", async () => { test("restore with cache found for restore key", async () => {
@@ -373,36 +285,18 @@ test("restore with cache found for restore key", async () => {
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(restoreKey);
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(restoreKey);
});
await restoreImpl(new StateProvider()); await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
[path], expect(core.setOutput).toHaveBeenCalledTimes(1);
key, expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
[restoreKey], expect(core.info).toHaveBeenCalledWith(
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(infoMock).toHaveBeenCalledWith(
`Cache restored from key: ${restoreKey}` `Cache restored from key: ${restoreKey}`
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
}); });
test("restore with lookup-only set", async () => { test("restore with lookup-only set", async () => {
@@ -414,20 +308,12 @@ test("restore with lookup-only set", async () => {
lookupOnly: true lookupOnly: true
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(key);
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(key);
});
await restoreImpl(new StateProvider()); await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(cache.restoreCache).toHaveBeenCalledWith(
[path], [path],
key, key,
[], [],
@@ -437,31 +323,27 @@ test("restore with lookup-only set", async () => {
false false
); );
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", key); expect(core.saveState).toHaveBeenCalledWith("CACHE_RESULT", key);
expect(stateMock).toHaveBeenCalledTimes(2); expect(core.saveState).toHaveBeenCalledTimes(2);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true"); expect(core.info).toHaveBeenCalledWith(
expect(infoMock).toHaveBeenCalledWith(
`Cache found and can be restored from key: ${key}` `Cache found and can be restored from key: ${key}`
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
}); });
test("restore failure with earlyExit should call process exit", async () => { test("restore failure with earlyExit should call process exit", async () => {
testUtils.setInput(Inputs.Path, "node_modules"); testUtils.setInput(Inputs.Path, "node_modules");
const failedMock = jest.spyOn(core, "setFailed"); const processExitMock = jest.spyOn(process, "exit").mockImplementation((() => {}) as any);
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
const processExitMock = jest.spyOn(process, "exit").mockImplementation();
// call restoreImpl with `earlyExit` set to true
await restoreImpl(new StateProvider(), true); await restoreImpl(new StateProvider(), true);
expect(restoreCacheMock).toHaveBeenCalledTimes(0); expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledWith( expect(core.setFailed).toHaveBeenCalledWith(
"Input required and not supplied: key" "Input required and not supplied: key"
); );
expect(processExitMock).toHaveBeenCalledWith(1); expect(processExitMock).toHaveBeenCalledWith(1);
processExitMock.mockRestore();
}); });

View File

@@ -1,51 +1,69 @@
import * as cache from "@actions/cache"; import { jest, test, expect, beforeEach, afterEach } from "@jest/globals";
import * as core from "@actions/core";
import { Events, RefKey } from "../src/constants"; // Mock @actions/core
import { restoreOnlyRun } from "../src/restoreImpl"; jest.unstable_mockModule("@actions/core", () => ({
import * as actionUtils from "../src/utils/actionUtils"; getInput: jest.fn((name: string, options?: { required?: boolean }) => {
import * as testUtils from "../src/utils/testUtils"; const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
jest.mock("../src/utils/actionUtils"); if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
beforeAll(() => {
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isExactKeyMatch(key, cacheResult);
} }
); return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => { // Mock @actions/cache
const actualUtils = jest.requireActual("../src/utils/actionUtils"); jest.unstable_mockModule("@actions/cache", () => ({
return actualUtils.isValidEvent(); restoreCache: jest.fn(),
}); saveCache: jest.fn(),
isFeatureAvailable: jest.fn(() => true),
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation( ReserveCacheError: class ReserveCacheError extends Error {
(name, options) => { constructor(message: string) {
const actualUtils = jest.requireActual("../src/utils/actionUtils"); super(message);
return actualUtils.getInputAsArray(name, options); this.name = "ReserveCacheError";
} }
); }
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation( const core = await import("@actions/core");
(name, options) => { const cache = await import("@actions/cache");
return jest const { Events, RefKey } = await import("../src/constants");
.requireActual("../src/utils/actionUtils") const { restoreOnlyRun } = await import("../src/restoreImpl");
.getInputAsBool(name, options); const testUtils = await import("../src/utils/testUtils");
}
);
});
beforeEach(() => { beforeEach(() => {
jest.restoreAllMocks(); jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[
`INPUT_${name.replace(/ /g, "_").toUpperCase()}`
] || "";
if (options && options.required && !val) {
throw new Error(
`Input required and not supplied: ${name}`
);
}
return val.trim();
}
);
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env[Events.Key] = Events.Push; process.env[Events.Key] = Events.Push;
process.env[RefKey] = "refs/heads/feature-branch"; process.env[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
}); });
afterEach(() => { afterEach(() => {
@@ -63,19 +81,12 @@ test("restore with no cache found", async () => {
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
await restoreOnlyRun(); await restoreOnlyRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(cache.restoreCache).toHaveBeenCalledWith(
[path], [path],
key, key,
[], [],
@@ -85,11 +96,10 @@ test("restore with no cache found", async () => {
false false
); );
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key); expect(core.setOutput).toHaveBeenCalledWith("cache-primary-key", key);
expect(outputMock).toHaveBeenCalledTimes(1); expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
expect(infoMock).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}` `Cache not found for input keys: ${key}`
); );
}); });
@@ -105,32 +115,14 @@ test("restore with restore keys and no cache found", async () => {
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
await restoreOnlyRun(); await restoreOnlyRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(core.setOutput).toHaveBeenCalledWith("cache-primary-key", key);
[path], expect(core.setFailed).toHaveBeenCalledTimes(0);
key, expect(core.info).toHaveBeenCalledWith(
[restoreKey],
{
lookupOnly: false
},
false
);
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}, ${restoreKey}` `Cache not found for input keys: ${key}, ${restoreKey}`
); );
}); });
@@ -144,36 +136,17 @@ test("restore with cache found for key", async () => {
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(key);
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(key);
});
await restoreOnlyRun(); await restoreOnlyRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(core.setOutput).toHaveBeenCalledWith("cache-primary-key", key);
[path], expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
key, expect(core.setOutput).toHaveBeenCalledWith("cache-matched-key", key);
[], expect(core.setOutput).toHaveBeenCalledTimes(3);
{ expect(core.info).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
lookupOnly: false expect(core.setFailed).toHaveBeenCalledTimes(0);
},
false
);
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
expect(outputMock).toHaveBeenCalledWith("cache-hit", "true");
expect(outputMock).toHaveBeenCalledWith("cache-matched-key", key);
expect(outputMock).toHaveBeenCalledTimes(3);
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(failedMock).toHaveBeenCalledTimes(0);
}); });
test("restore with cache found for restore key", async () => { test("restore with cache found for restore key", async () => {
@@ -187,36 +160,17 @@ test("restore with cache found for restore key", async () => {
enableCrossOsArchive: false enableCrossOsArchive: false
}); });
const infoMock = jest.spyOn(core, "info"); (cache.restoreCache as jest.Mock).mockResolvedValue(restoreKey);
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(restoreKey);
});
await restoreOnlyRun(); await restoreOnlyRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1); expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith( expect(core.setOutput).toHaveBeenCalledWith("cache-primary-key", key);
[path], expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
key, expect(core.setOutput).toHaveBeenCalledWith("cache-matched-key", restoreKey);
[restoreKey], expect(core.setOutput).toHaveBeenCalledTimes(3);
{ expect(core.info).toHaveBeenCalledWith(
lookupOnly: false
},
false
);
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
expect(outputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(outputMock).toHaveBeenCalledWith("cache-matched-key", restoreKey);
expect(outputMock).toHaveBeenCalledTimes(3);
expect(infoMock).toHaveBeenCalledWith(
`Cache restored from key: ${restoreKey}` `Cache restored from key: ${restoreKey}`
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
}); });

View File

@@ -1,70 +1,69 @@
import * as cache from "@actions/cache"; import { jest, test, expect, beforeEach, afterEach } from "@jest/globals";
import * as core from "@actions/core";
import { Events, Inputs, RefKey } from "../src/constants"; // Mock @actions/core
import { saveRun } from "../src/saveImpl"; jest.unstable_mockModule("@actions/core", () => ({
import * as actionUtils from "../src/utils/actionUtils"; getInput: jest.fn((name: string, options?: { required?: boolean }) => {
import * as testUtils from "../src/utils/testUtils"; const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
jest.mock("@actions/core"); if (options && options.required && !val) {
jest.mock("@actions/cache"); throw new Error(`Input required and not supplied: ${name}`);
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
return jest.requireActual("@actions/core").getInput(name, options);
});
jest.spyOn(core, "getState").mockImplementation(name => {
return jest.requireActual("@actions/core").getState(name);
});
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsArray(name, options);
} }
); return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.spyOn(actionUtils, "getInputAsInt").mockImplementation( // Mock @actions/cache
(name, options) => { jest.unstable_mockModule("@actions/cache", () => ({
return jest restoreCache: jest.fn(),
.requireActual("../src/utils/actionUtils") saveCache: jest.fn(),
.getInputAsInt(name, options); isFeatureAvailable: jest.fn(() => true),
ReserveCacheError: class ReserveCacheError extends Error {
constructor(message: string) {
super(message);
this.name = "ReserveCacheError";
} }
); }
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation( const core = await import("@actions/core");
(name, options) => { const cache = await import("@actions/cache");
return jest const { Events, Inputs, RefKey } = await import("../src/constants");
.requireActual("../src/utils/actionUtils") const { saveRun } = await import("../src/saveImpl");
.getInputAsBool(name, options); const testUtils = await import("../src/utils/testUtils");
}
);
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
return jest
.requireActual("../src/utils/actionUtils")
.isExactKeyMatch(key, cacheResult);
}
);
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
});
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[
`INPUT_${name.replace(/ /g, "_").toUpperCase()}`
] || "";
if (options && options.required && !val) {
throw new Error(
`Input required and not supplied: ${name}`
);
}
return val.trim();
}
);
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env[Events.Key] = Events.Push; process.env[Events.Key] = Events.Push;
process.env[RefKey] = "refs/heads/feature-branch"; process.env[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
}); });
afterEach(() => { afterEach(() => {
@@ -74,36 +73,24 @@ afterEach(() => {
}); });
test("save with valid inputs uploads a cache", async () => { test("save with valid inputs uploads a cache", async () => {
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-"; const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState") (core.getState as jest.Mock)
// Cache Entry State .mockReturnValueOnce(primaryKey)
.mockImplementationOnce(() => { .mockReturnValueOnce(savedCacheKey);
return primaryKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return savedCacheKey;
});
const inputPath = "node_modules"; const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath); testUtils.setInput(Inputs.Path, inputPath);
testUtils.setInput(Inputs.UploadChunkSize, "4000000"); testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = 4; const cacheId = 4;
const saveCacheMock = jest (cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
await saveRun(); await saveRun();
expect(saveCacheMock).toHaveBeenCalledTimes(1); expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith( expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath], [inputPath],
primaryKey, primaryKey,
{ {
@@ -112,5 +99,5 @@ test("save with valid inputs uploads a cache", async () => {
false false
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
}); });

View File

@@ -1,68 +1,71 @@
import * as cache from "@actions/cache"; import { jest, test, expect, beforeEach, afterEach } from "@jest/globals";
import * as core from "@actions/core";
import { Events, Inputs, RefKey } from "../src/constants"; // Mock @actions/core
import { saveImpl } from "../src/saveImpl"; jest.unstable_mockModule("@actions/core", () => ({
import { StateProvider } from "../src/stateProvider"; getInput: jest.fn((name: string, options?: { required?: boolean }) => {
import * as actionUtils from "../src/utils/actionUtils"; const val =
import * as testUtils from "../src/utils/testUtils"; process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) {
jest.mock("@actions/core"); throw new Error(`Input required and not supplied: ${name}`);
jest.mock("@actions/cache");
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
return jest.requireActual("@actions/core").getInput(name, options);
});
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsArray(name, options);
} }
); return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.spyOn(actionUtils, "getInputAsInt").mockImplementation( // Mock @actions/cache
(name, options) => { jest.unstable_mockModule("@actions/cache", () => ({
return jest restoreCache: jest.fn(),
.requireActual("../src/utils/actionUtils") saveCache: jest.fn(),
.getInputAsInt(name, options); isFeatureAvailable: jest.fn(() => true),
ReserveCacheError: class ReserveCacheError extends Error {
constructor(message: string) {
super(message);
this.name = "ReserveCacheError";
} }
); }
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation( const core = await import("@actions/core");
(name, options) => { const cache = await import("@actions/cache");
return jest const { Events, Inputs, RefKey } = await import("../src/constants");
.requireActual("../src/utils/actionUtils") const { saveImpl } = await import("../src/saveImpl");
.getInputAsBool(name, options); const { StateProvider } = await import("../src/stateProvider");
} const testUtils = await import("../src/utils/testUtils");
);
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
return jest
.requireActual("../src/utils/actionUtils")
.isExactKeyMatch(key, cacheResult);
}
);
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
});
beforeEach(() => { beforeEach(() => {
jest.restoreAllMocks(); jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[
`INPUT_${name.replace(/ /g, "_").toUpperCase()}`
] || "";
if (options && options.required && !val) {
throw new Error(
`Input required and not supplied: ${name}`
);
}
return val.trim();
}
);
(core.getState as jest.Mock).mockReturnValue("");
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env[Events.Key] = Events.Push; process.env[Events.Key] = Events.Push;
process.env[RefKey] = "refs/heads/feature-branch"; process.env[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
}); });
afterEach(() => { afterEach(() => {
@@ -72,99 +75,61 @@ afterEach(() => {
}); });
test("save with invalid event outputs warning", async () => { test("save with invalid event outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const invalidEvent = "commit_comment"; const invalidEvent = "commit_comment";
process.env[Events.Key] = invalidEvent; process.env[Events.Key] = invalidEvent;
delete process.env[RefKey]; delete process.env[RefKey];
await saveImpl(new StateProvider()); await saveImpl(new StateProvider());
expect(logWarningMock).toHaveBeenCalledWith( expect(core.info).toHaveBeenCalledWith(
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.` `[warning]Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
}); });
test("save with no primary key in state outputs warning", async () => { test("save with no primary key in state outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning"); (core.getState as jest.Mock).mockReturnValue("");
const failedMock = jest.spyOn(core, "setFailed");
const savedCacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return "";
})
// Cache Key State
.mockImplementationOnce(() => {
return savedCacheKey;
});
const saveCacheMock = jest.spyOn(cache, "saveCache");
await saveImpl(new StateProvider()); await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0); expect(cache.saveCache).toHaveBeenCalledTimes(0);
expect(logWarningMock).toHaveBeenCalledWith(`Key is not specified.`); expect(core.info).toHaveBeenCalledWith(`[warning]Key is not specified.`);
expect(logWarningMock).toHaveBeenCalledTimes(1); expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledTimes(0);
}); });
test("save without AC available should no-op", async () => { test("save without AC available should no-op", async () => {
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation( (cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
() => false
);
const saveCacheMock = jest.spyOn(cache, "saveCache");
await saveImpl(new StateProvider()); await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0); expect(cache.saveCache).toHaveBeenCalledTimes(0);
}); });
test("save on ghes without AC available should no-op", async () => { test("save on ghes without AC available should no-op", async () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true); (cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => false
);
const saveCacheMock = jest.spyOn(cache, "saveCache");
await saveImpl(new StateProvider()); await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0); expect(cache.saveCache).toHaveBeenCalledTimes(0);
}); });
test("save on GHES with AC available", async () => { test("save on GHES with AC available", async () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-"; const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState") (core.getState as jest.Mock)
// Cache Entry State .mockReturnValueOnce(primaryKey)
.mockImplementationOnce(() => { .mockReturnValueOnce(savedCacheKey);
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
const inputPath = "node_modules"; const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath); testUtils.setInput(Inputs.Path, inputPath);
testUtils.setInput(Inputs.UploadChunkSize, "4000000"); testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = 4; const cacheId = 4;
const saveCacheMock = jest (cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
await saveImpl(new StateProvider()); await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1); expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith( expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath], [inputPath],
primaryKey, primaryKey,
{ {
@@ -173,229 +138,135 @@ test("save on GHES with AC available", async () => {
false false
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
}); });
test("save with exact match returns early", async () => { test("save with exact match returns early", async () => {
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = primaryKey;
jest.spyOn(core, "getState") (core.getState as jest.Mock)
// Cache Entry State .mockReturnValueOnce(primaryKey)
.mockImplementationOnce(() => { .mockReturnValueOnce(primaryKey);
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
const saveCacheMock = jest.spyOn(cache, "saveCache");
await saveImpl(new StateProvider()); await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0); expect(cache.saveCache).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith( expect(core.info).toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.` `Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
}); });
test("save with missing input outputs warning", async () => { test("save with missing input outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-"; const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState") (core.getState as jest.Mock)
// Cache Entry State .mockReturnValueOnce(savedCacheKey)
.mockImplementationOnce(() => { .mockReturnValueOnce(primaryKey);
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
const saveCacheMock = jest.spyOn(cache, "saveCache");
await saveImpl(new StateProvider()); await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0); expect(cache.saveCache).toHaveBeenCalledTimes(0);
expect(logWarningMock).toHaveBeenCalledWith( expect(core.info).toHaveBeenCalledWith(
"Input required and not supplied: path" "[warning]Input required and not supplied: path"
); );
expect(logWarningMock).toHaveBeenCalledTimes(1); expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledTimes(0);
}); });
test("save with large cache outputs warning", async () => { test("save with large cache outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-"; const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState") (core.getState as jest.Mock)
// Cache Entry State .mockReturnValueOnce(savedCacheKey)
.mockImplementationOnce(() => { .mockReturnValueOnce(primaryKey);
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
const inputPath = "node_modules"; const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath); testUtils.setInput(Inputs.Path, inputPath);
const saveCacheMock = jest (cache.saveCache as jest.Mock).mockRejectedValue(
.spyOn(cache, "saveCache") new Error(
.mockImplementationOnce(() => { "Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
throw new Error( )
"Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache." );
);
});
await saveImpl(new StateProvider()); await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1); expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith( expect(core.info).toHaveBeenCalledWith(
[inputPath], "[warning]Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
primaryKey,
expect.anything(),
false
); );
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).toHaveBeenCalledWith(
"Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
);
expect(failedMock).toHaveBeenCalledTimes(0);
}); });
test("save with reserve cache failure outputs warning", async () => { test("save with reserve cache failure outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-"; const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState") (core.getState as jest.Mock)
// Cache Entry State .mockReturnValueOnce(savedCacheKey)
.mockImplementationOnce(() => { .mockReturnValueOnce(primaryKey);
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
const inputPath = "node_modules"; const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath); testUtils.setInput(Inputs.Path, inputPath);
const saveCacheMock = jest (cache.saveCache as jest.Mock).mockRejectedValue(
.spyOn(cache, "saveCache") new Error(
.mockImplementationOnce(() => { `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
const actualCache = jest.requireActual("@actions/cache"); )
const error = new actualCache.ReserveCacheError( );
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
);
throw error;
});
await saveImpl(new StateProvider()); await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1); expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith( expect(core.info).toHaveBeenCalledWith(
[inputPath], `[warning]Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
primaryKey,
expect.anything(),
false
); );
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(logWarningMock).toHaveBeenCalledWith(
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
}); });
test("save with server error outputs warning", async () => { test("save with server error outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-"; const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState") (core.getState as jest.Mock)
// Cache Entry State .mockReturnValueOnce(savedCacheKey)
.mockImplementationOnce(() => { .mockReturnValueOnce(primaryKey);
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
const inputPath = "node_modules"; const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath); testUtils.setInput(Inputs.Path, inputPath);
const saveCacheMock = jest (cache.saveCache as jest.Mock).mockRejectedValue(
.spyOn(cache, "saveCache") new Error("HTTP Error Occurred")
.mockImplementationOnce(() => { );
throw new Error("HTTP Error Occurred");
});
await saveImpl(new StateProvider()); await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1); expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith( expect(core.info).toHaveBeenCalledWith("[warning]HTTP Error Occurred");
[inputPath], expect(core.setFailed).toHaveBeenCalledTimes(0);
primaryKey,
expect.anything(),
false
);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
expect(failedMock).toHaveBeenCalledTimes(0);
}); });
test("save with valid inputs uploads a cache", async () => { test("save with valid inputs uploads a cache", async () => {
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-"; const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState") (core.getState as jest.Mock)
// Cache Entry State .mockReturnValueOnce(primaryKey)
.mockImplementationOnce(() => { .mockReturnValueOnce(savedCacheKey);
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
const inputPath = "node_modules"; const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath); testUtils.setInput(Inputs.Path, inputPath);
testUtils.setInput(Inputs.UploadChunkSize, "4000000"); testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = 4; const cacheId = 4;
const saveCacheMock = jest (cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
await saveImpl(new StateProvider()); await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1); expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith( expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath], [inputPath],
primaryKey, primaryKey,
{ {
@@ -404,5 +275,5 @@ test("save with valid inputs uploads a cache", async () => {
false false
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
}); });

View File

@@ -1,70 +1,69 @@
import * as cache from "@actions/cache"; import { jest, test, expect, beforeEach, afterEach } from "@jest/globals";
import * as core from "@actions/core";
import { Events, Inputs, RefKey } from "../src/constants"; // Mock @actions/core
import { saveOnlyRun } from "../src/saveImpl"; jest.unstable_mockModule("@actions/core", () => ({
import * as actionUtils from "../src/utils/actionUtils"; getInput: jest.fn((name: string, options?: { required?: boolean }) => {
import * as testUtils from "../src/utils/testUtils"; const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
jest.mock("@actions/core"); if (options && options.required && !val) {
jest.mock("@actions/cache"); throw new Error(`Input required and not supplied: ${name}`);
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
return jest.requireActual("@actions/core").getInput(name, options);
});
jest.spyOn(core, "setOutput").mockImplementation((key, value) => {
return jest.requireActual("@actions/core").getInput(key, value);
});
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsArray(name, options);
} }
); return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.spyOn(actionUtils, "getInputAsInt").mockImplementation( // Mock @actions/cache
(name, options) => { jest.unstable_mockModule("@actions/cache", () => ({
return jest restoreCache: jest.fn(),
.requireActual("../src/utils/actionUtils") saveCache: jest.fn(),
.getInputAsInt(name, options); isFeatureAvailable: jest.fn(() => true),
ReserveCacheError: class ReserveCacheError extends Error {
constructor(message: string) {
super(message);
this.name = "ReserveCacheError";
} }
); }
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation( const core = await import("@actions/core");
(name, options) => { const cache = await import("@actions/cache");
return jest const { Events, Inputs, RefKey } = await import("../src/constants");
.requireActual("../src/utils/actionUtils") const { saveOnlyRun } = await import("../src/saveImpl");
.getInputAsBool(name, options); const testUtils = await import("../src/utils/testUtils");
}
);
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
return jest
.requireActual("../src/utils/actionUtils")
.isExactKeyMatch(key, cacheResult);
}
);
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
});
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks();
(core.getInput as jest.Mock).mockImplementation(
(name: string, options?: { required?: boolean }) => {
const val =
process.env[
`INPUT_${name.replace(/ /g, "_").toUpperCase()}`
] || "";
if (options && options.required && !val) {
throw new Error(
`Input required and not supplied: ${name}`
);
}
return val.trim();
}
);
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
process.env[Events.Key] = Events.Push; process.env[Events.Key] = Events.Push;
process.env[RefKey] = "refs/heads/feature-branch"; process.env[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
}); });
afterEach(() => { afterEach(() => {
@@ -74,8 +73,6 @@ afterEach(() => {
}); });
test("save with valid inputs uploads a cache", async () => { test("save with valid inputs uploads a cache", async () => {
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const inputPath = "node_modules"; const inputPath = "node_modules";
@@ -84,16 +81,12 @@ test("save with valid inputs uploads a cache", async () => {
testUtils.setInput(Inputs.UploadChunkSize, "4000000"); testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = 4; const cacheId = 4;
const saveCacheMock = jest (cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
await saveOnlyRun(); await saveOnlyRun();
expect(saveCacheMock).toHaveBeenCalledTimes(1); expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith( expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath], [inputPath],
primaryKey, primaryKey,
{ {
@@ -102,12 +95,10 @@ test("save with valid inputs uploads a cache", async () => {
false false
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
}); });
test("save failing logs the warning message", async () => { test("save failing logs the warning message", async () => {
const warningMock = jest.spyOn(core, "warning");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const inputPath = "node_modules"; const inputPath = "node_modules";
@@ -116,16 +107,12 @@ test("save failing logs the warning message", async () => {
testUtils.setInput(Inputs.UploadChunkSize, "4000000"); testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = -1; const cacheId = -1;
const saveCacheMock = jest (cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
await saveOnlyRun(); await saveOnlyRun();
expect(saveCacheMock).toHaveBeenCalledTimes(1); expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith( expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath], [inputPath],
primaryKey, primaryKey,
{ {
@@ -134,6 +121,6 @@ test("save failing logs the warning message", async () => {
false false
); );
expect(warningMock).toHaveBeenCalledTimes(1); expect(core.warning).toHaveBeenCalledTimes(1);
expect(warningMock).toHaveBeenCalledWith("Cache save failed."); expect(core.warning).toHaveBeenCalledWith("Cache save failed.");
}); });

View File

@@ -1,22 +1,39 @@
import * as core from "@actions/core"; import { jest, test, expect, beforeEach, afterEach } from "@jest/globals";
import { Events, RefKey, State } from "../src/constants"; // Mock @actions/core
import { jest.unstable_mockModule("@actions/core", () => ({
IStateProvider, getInput: jest.fn((name: string, options?: { required?: boolean }) => {
NullStateProvider, const val =
StateProvider process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
} from "../src/stateProvider"; if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
return val.trim();
}),
setOutput: jest.fn(),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
saveState: jest.fn(),
getState: jest.fn(() => ""),
isDebug: jest.fn(() => false),
exportVariable: jest.fn(),
addPath: jest.fn(),
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
startGroup: jest.fn(),
endGroup: jest.fn()
}));
jest.mock("@actions/core"); const core = await import("@actions/core");
const { Events, RefKey, State } = await import("../src/constants");
const { NullStateProvider, StateProvider } = await import("../src/stateProvider");
import type { IStateProvider } from "../src/stateProvider";
beforeAll(() => { beforeEach(() => {
jest.spyOn(core, "getInput").mockImplementation((name, options) => { jest.clearAllMocks();
return jest.requireActual("@actions/core").getInput(name, options); (core.getState as jest.Mock).mockReturnValue("");
});
jest.spyOn(core, "setOutput").mockImplementation((key, value) => {
return jest.requireActual("@actions/core").setOutput(key, value);
});
}); });
afterEach(() => { afterEach(() => {
@@ -26,21 +43,10 @@ afterEach(() => {
test("StateProvider saves states", async () => { test("StateProvider saves states", async () => {
const states = new Map<string, string>(); const states = new Map<string, string>();
const getStateMock = jest (core.getState as jest.Mock).mockImplementation((key: string) => states.get(key) || "");
.spyOn(core, "getState") (core.saveState as jest.Mock).mockImplementation((key: string, value: string) => {
.mockImplementation(key => states.get(key) || ""); states.set(key, value);
});
const saveStateMock = jest
.spyOn(core, "saveState")
.mockImplementation((key, value) => {
states.set(key, value);
});
const setOutputMock = jest
.spyOn(core, "setOutput")
.mockImplementation((key, value) => {
return jest.requireActual("@actions/core").setOutput(key, value);
});
const cacheMatchedKey = "node-cache"; const cacheMatchedKey = "node-cache";
@@ -52,38 +58,19 @@ test("StateProvider saves states", async () => {
expect(stateValue).toBe("stateValue"); expect(stateValue).toBe("stateValue");
expect(cacheStateValue).toBe(cacheMatchedKey); expect(cacheStateValue).toBe(cacheMatchedKey);
expect(getStateMock).toHaveBeenCalledTimes(2); expect(core.getState).toHaveBeenCalledTimes(2);
expect(saveStateMock).toHaveBeenCalledTimes(2); expect(core.saveState).toHaveBeenCalledTimes(2);
expect(setOutputMock).toHaveBeenCalledTimes(0); expect(core.setOutput).toHaveBeenCalledTimes(0);
}); });
test("NullStateProvider saves outputs", async () => { test("NullStateProvider saves outputs", async () => {
const getStateMock = jest
.spyOn(core, "getState")
.mockImplementation(name =>
jest.requireActual("@actions/core").getState(name)
);
const setOutputMock = jest
.spyOn(core, "setOutput")
.mockImplementation((key, value) => {
return jest.requireActual("@actions/core").setOutput(key, value);
});
const saveStateMock = jest
.spyOn(core, "saveState")
.mockImplementation((key, value) => {
return jest.requireActual("@actions/core").saveState(key, value);
});
const cacheMatchedKey = "node-cache";
const nullStateProvider: IStateProvider = new NullStateProvider(); const nullStateProvider: IStateProvider = new NullStateProvider();
nullStateProvider.setState(State.CacheMatchedKey, "outputValue"); nullStateProvider.setState(State.CacheMatchedKey, "outputValue");
nullStateProvider.setState(State.CachePrimaryKey, cacheMatchedKey); nullStateProvider.setState(State.CachePrimaryKey, "node-cache");
nullStateProvider.getState("outputKey"); nullStateProvider.getState("outputKey");
nullStateProvider.getCacheState(); nullStateProvider.getCacheState();
expect(getStateMock).toHaveBeenCalledTimes(0); expect(core.getState).toHaveBeenCalledTimes(0);
expect(setOutputMock).toHaveBeenCalledTimes(2); expect(core.setOutput).toHaveBeenCalledTimes(2);
expect(saveStateMock).toHaveBeenCalledTimes(0); expect(core.saveState).toHaveBeenCalledTimes(0);
}); });

View File

@@ -1,23 +0,0 @@
require("nock").disableNetConnect();
module.exports = {
clearMocks: true,
moduleFileExtensions: ["js", "ts"],
testEnvironment: "node",
testMatch: ["**/*.test.ts"],
testRunner: "jest-circus/runner",
transform: {
"^.+\\.ts$": "ts-jest"
},
verbose: true
};
const processStdoutWrite = process.stdout.write.bind(process.stdout);
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
process.stdout.write = (str, encoding, cb) => {
// Core library will directly call process.stdout.write for commands
// We don't want :: commands to be executed by the runner during tests
if (!String(str).match(/^::/)) {
return processStdoutWrite(str, encoding, cb);
}
};

21
jest.config.ts Normal file
View File

@@ -0,0 +1,21 @@
export default {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
roots: ['<rootDir>'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
useESM: true,
diagnostics: {
ignoreCodes: [151002]
}
}
]
},
extensionsToTreatAsEsm: ['.ts'],
transformIgnorePatterns: ['node_modules/(?!(@actions)/)'],
verbose: true
}

2546
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,13 @@
{ {
"name": "cache", "name": "cache",
"version": "5.0.4", "version": "6.0.0",
"private": true, "private": true,
"description": "Cache dependencies and build outputs", "description": "Cache dependencies and build outputs",
"type": "module",
"main": "dist/restore/index.js", "main": "dist/restore/index.js",
"scripts": { "scripts": {
"build": "tsc && ncc build -o dist/restore src/restore.ts && ncc build -o dist/save src/save.ts && ncc build -o dist/restore-only src/restoreOnly.ts && ncc build -o dist/save-only src/saveOnly.ts", "build": "tsc && ncc build -o dist/restore src/restore.ts && ncc build -o dist/save src/save.ts && ncc build -o dist/restore-only src/restoreOnly.ts && ncc build -o dist/save-only src/saveOnly.ts",
"test": "tsc --noEmit && jest --coverage", "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
"lint": "eslint **/*.ts --cache", "lint": "eslint **/*.ts --cache",
"format": "prettier --write **/*.ts", "format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts" "format-check": "prettier --check **/*.ts"
@@ -23,13 +24,13 @@
"author": "GitHub", "author": "GitHub",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/cache": "^5.0.5", "@actions/cache": "^6.0.1",
"@actions/core": "^2.0.3", "@actions/core": "^3.0.1",
"@actions/exec": "^2.0.0", "@actions/exec": "^3.0.0",
"@actions/io": "^2.0.0" "@actions/io": "^3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.14", "@types/jest": "^30.0.0",
"@types/nock": "^11.1.0", "@types/nock": "^11.1.0",
"@types/node": "^24.1.0", "@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/eslint-plugin": "^7.2.0",
@@ -41,11 +42,11 @@
"eslint-plugin-jest": "^27.9.0", "eslint-plugin-jest": "^27.9.0",
"eslint-plugin-prettier": "^5.5.3", "eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"jest": "^29.7.0", "jest": "^30.2.0",
"jest-circus": "^29.7.0",
"nock": "^13.2.9", "nock": "^13.2.9",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"ts-jest": "^29.4.0", "ts-jest": "^29.4.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"engines": { "engines": {

View File

@@ -1,63 +1,13 @@
{ {
"compilerOptions": { "compilerOptions": {
/* Basic Options */ "target": "ES2022",
// "incremental": true, /* Enable incremental compilation */ "module": "ESNext",
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "outDir": "./lib",
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "rootDir": "./src",
// "allowJs": true, /* Allow javascript files to be compiled. */ "strict": true,
// "checkJs": true, /* Report errors in .js files. */ "noImplicitAny": false,
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "moduleResolution": "bundler",
// "declaration": true, /* Generates corresponding '.d.ts' file. */ "esModuleInterop": true
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./lib", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}, },
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts", "jest.config.ts", "__tests__"]
} }