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 * as core from "@actions/core";
import { jest, test, expect, beforeEach, afterAll } from "@jest/globals";
import { Events, RefKey } from "../src/constants";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
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");
jest.mock("@actions/cache");
// 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;
beforeAll(() => {
pristineEnv = process.env;
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
return jest.requireActual("@actions/core").getInput(name, options);
});
});
beforeEach(() => {
jest.resetModules();
process.env = pristineEnv;
pristineEnv = { ...process.env };
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[RefKey];
});
@@ -47,74 +91,44 @@ test("isGhes returns false when server url is github.com", () => {
});
test("isExactKeyMatch with undefined cache key returns false", () => {
const key = "linux-rust";
const cacheKey = undefined;
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
expect(actionUtils.isExactKeyMatch("linux-rust", undefined)).toBe(false);
});
test("isExactKeyMatch with empty cache key returns false", () => {
const key = "linux-rust";
const cacheKey = "";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
expect(actionUtils.isExactKeyMatch("linux-rust", "")).toBe(false);
});
test("isExactKeyMatch with different keys returns false", () => {
const key = "linux-rust";
const cacheKey = "linux-";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
expect(actionUtils.isExactKeyMatch("linux-rust", "linux-")).toBe(false);
});
test("isExactKeyMatch with different key accents returns false", () => {
const key = "linux-áccent";
const cacheKey = "linux-accent";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
expect(actionUtils.isExactKeyMatch("linux-áccent", "linux-accent")).toBe(false);
});
test("isExactKeyMatch with same key returns true", () => {
const key = "linux-rust";
const cacheKey = "linux-rust";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(true);
expect(actionUtils.isExactKeyMatch("linux-rust", "linux-rust")).toBe(true);
});
test("isExactKeyMatch with same key and different casing returns true", () => {
const key = "linux-rust";
const cacheKey = "LINUX-RUST";
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(true);
expect(actionUtils.isExactKeyMatch("linux-rust", "LINUX-RUST")).toBe(true);
});
test("logWarning logs a message with a warning prefix", () => {
const message = "A warning occurred.";
const infoMock = jest.spyOn(core, "info");
actionUtils.logWarning(message);
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`);
expect(core.info).toHaveBeenCalledWith(`[warning]${message}`);
});
test("isValidEvent returns false for event that does not have a branch or tag", () => {
const event = "foo";
process.env[Events.Key] = event;
const isValidEvent = actionUtils.isValidEvent();
expect(isValidEvent).toBe(false);
process.env[Events.Key] = "foo";
expect(actionUtils.isValidEvent()).toBe(false);
});
test("isValidEvent returns true for event that has a ref", () => {
const event = Events.Push;
process.env[Events.Key] = event;
process.env[Events.Key] = Events.Push;
process.env[RefKey] = "ref/heads/feature";
const isValidEvent = actionUtils.isValidEvent();
expect(isValidEvent).toBe(true);
expect(actionUtils.isValidEvent()).toBe(true);
});
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", () => {
expect(() =>
actionUtils.getInputAsArray("foo", { required: true })
).toThrowError();
).toThrow();
});
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", () => {
expect(() =>
actionUtils.getInputAsInt("undefined", { required: true })
).toThrowError();
).toThrow();
});
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", () => {
expect(() =>
actionUtils.getInputAsBool("undefined2", { required: true })
).toThrowError();
).toThrow();
});
test("isCacheFeatureAvailable for ac enabled", () => {
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => true);
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(true);
expect(actionUtils.isCacheFeatureAvailable()).toBe(true);
});
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.
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 {
process.env["GITHUB_SERVER_URL"] = "http://example.com";
expect(actionUtils.isCacheFeatureAvailable()).toBe(false);
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`);
expect(core.info).toHaveBeenCalledWith(`[warning]${message}`);
} finally {
delete process.env["GITHUB_SERVER_URL"];
}
});
test("isCacheFeatureAvailable for ac disabled on dotcom", () => {
jest.spyOn(cache, "isFeatureAvailable").mockImplementation(() => false);
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
const message =
"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 {
process.env["GITHUB_SERVER_URL"] = "http://github.com";
expect(actionUtils.isCacheFeatureAvailable()).toBe(false);
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`);
expect(core.info).toHaveBeenCalledWith(`[warning]${message}`);
} finally {
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"];
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";
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";
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";
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";
expect(actionUtils.isGhes()).toBeTruthy();
});

View File

@@ -1,50 +1,69 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { jest, test, expect, beforeEach, afterEach } from "@jest/globals";
import { Events, RefKey } from "../src/constants";
import { restoreRun } from "../src/restoreImpl";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isExactKeyMatch(key, cacheResult);
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
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.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
(name, options) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.getInputAsArray(name, options);
// 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";
}
);
}
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
(name, options) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.getInputAsBool(name, options);
}
);
});
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, RefKey } = await import("../src/constants");
const { restoreRun } = await import("../src/restoreImpl");
const testUtils = await import("../src/utils/testUtils");
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[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
});
afterEach(() => {
@@ -62,19 +81,12 @@ test("restore with no cache found", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(cache.restoreCache).toHaveBeenCalledWith(
[path],
key,
[],
@@ -84,12 +96,10 @@ test("restore with no cache found", async () => {
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}`
);
});
@@ -105,19 +115,12 @@ test("restore with restore keys and no cache found", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(cache.restoreCache).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
@@ -127,12 +130,10 @@ test("restore with restore keys and no cache found", async () => {
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}, ${restoreKey}`
);
});
@@ -146,20 +147,12 @@ test("restore with cache found for key", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
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);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(key);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(cache.restoreCache).toHaveBeenCalledWith(
[path],
key,
[],
@@ -169,15 +162,13 @@ test("restore with cache found for key", async () => {
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", key);
expect(stateMock).toHaveBeenCalledTimes(2);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledWith("CACHE_RESULT", key);
expect(core.saveState).toHaveBeenCalledTimes(2);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
expect(core.info).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("restore with cache found for restore key", async () => {
@@ -191,39 +182,20 @@ test("restore with cache found for restore key", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
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);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(restoreKey);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
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(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledWith("CACHE_RESULT", restoreKey);
expect(core.saveState).toHaveBeenCalledTimes(2);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
expect(core.info).toHaveBeenCalledWith(
`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 () => {
@@ -237,35 +209,17 @@ test("Fail restore when fail on cache miss is enabled and primary + restore keys
failOnCacheMiss: true
});
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(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.setOutput).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledWith(
`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 () => {
@@ -279,40 +233,20 @@ test("restore when fail on cache miss is enabled and primary key doesn't match r
failOnCacheMiss: true
});
const infoMock = jest.spyOn(core, "info");
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);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(restoreKey);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
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(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledWith("CACHE_RESULT", restoreKey);
expect(core.saveState).toHaveBeenCalledTimes(2);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
expect(core.info).toHaveBeenCalledWith(
`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 () => {
@@ -326,33 +260,15 @@ test("restore with fail on cache miss disabled and no cache found", async () =>
failOnCacheMiss: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledTimes(1);
expect(infoMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledTimes(1);
expect(core.info).toHaveBeenCalledWith(
`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 * as core from "@actions/core";
import { jest, test, expect, beforeEach, afterEach } from "@jest/globals";
import { Events, Inputs, RefKey } from "../src/constants";
import { restoreImpl } from "../src/restoreImpl";
import { StateProvider } from "../src/stateProvider";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isExactKeyMatch(key, cacheResult);
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
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.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
(name, options) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.getInputAsArray(name, options);
// 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";
}
);
}
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
(name, options) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.getInputAsBool(name, options);
}
);
});
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, Inputs, RefKey } = await import("../src/constants");
const { restoreImpl } = await import("../src/restoreImpl");
const { StateProvider } = await import("../src/stateProvider");
const testUtils = await import("../src/utils/testUtils");
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[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
});
afterEach(() => {
@@ -55,52 +75,37 @@ afterEach(() => {
});
test("restore with invalid event outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const invalidEvent = "commit_comment";
process.env[Events.Key] = invalidEvent;
delete process.env[RefKey];
await restoreImpl(new StateProvider());
expect(logWarningMock).toHaveBeenCalledWith(
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
expect(core.info).toHaveBeenCalledWith(
`[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 () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => false
);
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
});
test("restore on GHES without AC available should no-op", async () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => false
);
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
});
test("restore on GHES with AC available ", async () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
const path = "node_modules";
const key = "node-test";
testUtils.setInputs({
@@ -109,20 +114,12 @@ test("restore on GHES with AC available ", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
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);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(key);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(cache.restoreCache).toHaveBeenCalledWith(
[path],
key,
[],
@@ -132,32 +129,26 @@ test("restore on GHES with AC available ", async () => {
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);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
expect(core.info).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("restore with no path should fail", async () => {
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
// this input isn't necessary for restore b/c tarball contains entries relative to workspace
expect(failedMock).not.toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(core.setFailed).not.toHaveBeenCalledWith(
"Input required and not supplied: path"
);
});
test("restore with no key", async () => {
testUtils.setInput(Inputs.Path, "node_modules");
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledWith(
"Input required and not supplied: key"
);
});
@@ -172,46 +163,30 @@ test("restore with too many keys should fail", async () => {
restoreKeys,
enableCrossOsArchive: false
});
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
restoreKeys,
{
lookupOnly: false
},
false
(cache.restoreCache as jest.Mock).mockRejectedValue(
new Error("Key Validation Error: Keys are limited to a maximum of 10.")
);
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.`
);
});
test("restore with large key should fail", async () => {
const path = "node_modules";
const key = "foo".repeat(512); // Over the 512 character limit
const key = "foo".repeat(512);
testUtils.setInputs({
path: path,
key,
enableCrossOsArchive: false
});
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[],
{
lookupOnly: false
},
false
(cache.restoreCache as jest.Mock).mockRejectedValue(
new Error(`Key Validation Error: ${key} cannot be larger than 512 characters.`)
);
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.`
);
});
@@ -224,20 +199,12 @@ test("restore with invalid key should fail", async () => {
key,
enableCrossOsArchive: false
});
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[],
{
lookupOnly: false
},
false
(cache.restoreCache as jest.Mock).mockRejectedValue(
new Error(`Key Validation Error: ${key} cannot contain commas.`)
);
expect(failedMock).toHaveBeenCalledWith(
await restoreImpl(new StateProvider());
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledWith(
`Key Validation Error: ${key} cannot contain commas.`
);
});
@@ -251,32 +218,14 @@ test("restore with no cache found", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}`
);
});
@@ -292,32 +241,14 @@ test("restore with restore keys and no cache found", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}, ${restoreKey}`
);
});
@@ -331,35 +262,16 @@ test("restore with cache found for key", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
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);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(key);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[],
{
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);
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
expect(core.info).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("restore with cache found for restore key", async () => {
@@ -373,36 +285,18 @@ test("restore with cache found for restore key", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
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);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(restoreKey);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
expect(infoMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
expect(core.info).toHaveBeenCalledWith(
`Cache restored from key: ${restoreKey}`
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("restore with lookup-only set", async () => {
@@ -414,20 +308,12 @@ test("restore with lookup-only set", async () => {
lookupOnly: true
});
const infoMock = jest.spyOn(core, "info");
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);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(key);
await restoreImpl(new StateProvider());
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(cache.restoreCache).toHaveBeenCalledWith(
[path],
key,
[],
@@ -437,31 +323,27 @@ test("restore with lookup-only set", async () => {
false
);
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", key);
expect(stateMock).toHaveBeenCalledTimes(2);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
expect(infoMock).toHaveBeenCalledWith(
expect(core.saveState).toHaveBeenCalledWith("CACHE_KEY", key);
expect(core.saveState).toHaveBeenCalledWith("CACHE_RESULT", key);
expect(core.saveState).toHaveBeenCalledTimes(2);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
expect(core.info).toHaveBeenCalledWith(
`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 () => {
testUtils.setInput(Inputs.Path, "node_modules");
const failedMock = jest.spyOn(core, "setFailed");
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
const processExitMock = jest.spyOn(process, "exit").mockImplementation();
const processExitMock = jest.spyOn(process, "exit").mockImplementation((() => {}) as any);
// call restoreImpl with `earlyExit` set to true
await restoreImpl(new StateProvider(), true);
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledWith(
"Input required and not supplied: key"
);
expect(processExitMock).toHaveBeenCalledWith(1);
processExitMock.mockRestore();
});

View File

@@ -1,51 +1,69 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { jest, test, expect, beforeEach, afterEach } from "@jest/globals";
import { Events, RefKey } from "../src/constants";
import { restoreOnlyRun } from "../src/restoreImpl";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isExactKeyMatch(key, cacheResult);
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
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.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
(name, options) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.getInputAsArray(name, options);
// 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";
}
);
}
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsBool(name, options);
}
);
});
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, RefKey } = await import("../src/constants");
const { restoreOnlyRun } = await import("../src/restoreImpl");
const testUtils = await import("../src/utils/testUtils");
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[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
});
afterEach(() => {
@@ -63,19 +81,12 @@ test("restore with no cache found", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreOnlyRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(cache.restoreCache).toHaveBeenCalledWith(
[path],
key,
[],
@@ -85,11 +96,10 @@ test("restore with no cache found", async () => {
false
);
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
expect(outputMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(core.setOutput).toHaveBeenCalledWith("cache-primary-key", key);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}`
);
});
@@ -105,32 +115,14 @@ test("restore with restore keys and no cache found", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(undefined);
await restoreOnlyRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
lookupOnly: false
},
false
);
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-primary-key", key);
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`Cache not found for input keys: ${key}, ${restoreKey}`
);
});
@@ -144,36 +136,17 @@ test("restore with cache found for key", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(key);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(key);
await restoreOnlyRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[],
{
lookupOnly: false
},
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);
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-primary-key", key);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
expect(core.setOutput).toHaveBeenCalledWith("cache-matched-key", key);
expect(core.setOutput).toHaveBeenCalledTimes(3);
expect(core.info).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("restore with cache found for restore key", async () => {
@@ -187,36 +160,17 @@ test("restore with cache found for restore key", async () => {
enableCrossOsArchive: false
});
const infoMock = jest.spyOn(core, "info");
const failedMock = jest.spyOn(core, "setFailed");
const outputMock = jest.spyOn(core, "setOutput");
const restoreCacheMock = jest
.spyOn(cache, "restoreCache")
.mockImplementationOnce(() => {
return Promise.resolve(restoreKey);
});
(cache.restoreCache as jest.Mock).mockResolvedValue(restoreKey);
await restoreOnlyRun();
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
expect(restoreCacheMock).toHaveBeenCalledWith(
[path],
key,
[restoreKey],
{
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(
expect(cache.restoreCache).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenCalledWith("cache-primary-key", key);
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
expect(core.setOutput).toHaveBeenCalledWith("cache-matched-key", restoreKey);
expect(core.setOutput).toHaveBeenCalledTimes(3);
expect(core.info).toHaveBeenCalledWith(
`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 * as core from "@actions/core";
import { jest, test, expect, beforeEach, afterEach } from "@jest/globals";
import { Events, Inputs, RefKey } from "../src/constants";
import { saveRun } from "../src/saveImpl";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
jest.mock("@actions/core");
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(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);
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
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.spyOn(actionUtils, "getInputAsInt").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsInt(name, options);
// 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";
}
);
}
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsBool(name, options);
}
);
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();
});
});
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, Inputs, RefKey } = await import("../src/constants");
const { saveRun } = await import("../src/saveImpl");
const testUtils = await import("../src/utils/testUtils");
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[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
});
afterEach(() => {
@@ -74,36 +73,24 @@ afterEach(() => {
});
test("save with valid inputs uploads a cache", async () => {
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return primaryKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return savedCacheKey;
});
(core.getState as jest.Mock)
.mockReturnValueOnce(primaryKey)
.mockReturnValueOnce(savedCacheKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = 4;
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
(cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
await saveRun();
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath],
primaryKey,
{
@@ -112,5 +99,5 @@ test("save with valid inputs uploads a cache", async () => {
false
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});

View File

@@ -1,68 +1,71 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { jest, test, expect, beforeEach, afterEach } from "@jest/globals";
import { Events, Inputs, RefKey } from "../src/constants";
import { saveImpl } from "../src/saveImpl";
import { StateProvider } from "../src/stateProvider";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
jest.mock("@actions/core");
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);
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
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.spyOn(actionUtils, "getInputAsInt").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsInt(name, options);
// 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";
}
);
}
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsBool(name, options);
}
);
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();
});
});
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, Inputs, RefKey } = await import("../src/constants");
const { saveImpl } = await import("../src/saveImpl");
const { StateProvider } = await import("../src/stateProvider");
const testUtils = await import("../src/utils/testUtils");
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[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
});
afterEach(() => {
@@ -72,99 +75,61 @@ afterEach(() => {
});
test("save with invalid event outputs warning", async () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const invalidEvent = "commit_comment";
process.env[Events.Key] = invalidEvent;
delete process.env[RefKey];
await saveImpl(new StateProvider());
expect(logWarningMock).toHaveBeenCalledWith(
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
expect(core.info).toHaveBeenCalledWith(
`[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 () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
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");
(core.getState as jest.Mock).mockReturnValue("");
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0);
expect(logWarningMock).toHaveBeenCalledWith(`Key is not specified.`);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(cache.saveCache).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(`[warning]Key is not specified.`);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("save without AC available should no-op", async () => {
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => false
);
const saveCacheMock = jest.spyOn(cache, "saveCache");
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0);
expect(cache.saveCache).toHaveBeenCalledTimes(0);
});
test("save on ghes without AC available should no-op", async () => {
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => false
);
const saveCacheMock = jest.spyOn(cache, "saveCache");
(cache.isFeatureAvailable as jest.Mock).mockReturnValue(false);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0);
expect(cache.saveCache).toHaveBeenCalledTimes(0);
});
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 savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
(core.getState as jest.Mock)
.mockReturnValueOnce(primaryKey)
.mockReturnValueOnce(savedCacheKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = 4;
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
(cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath],
primaryKey,
{
@@ -173,229 +138,135 @@ test("save on GHES with AC available", async () => {
false
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
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 savedCacheKey = primaryKey;
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
const saveCacheMock = jest.spyOn(cache, "saveCache");
(core.getState as jest.Mock)
.mockReturnValueOnce(primaryKey)
.mockReturnValueOnce(primaryKey);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0);
expect(infoMock).toHaveBeenCalledWith(
expect(cache.saveCache).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
`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 () => {
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
const saveCacheMock = jest.spyOn(cache, "saveCache");
(core.getState as jest.Mock)
.mockReturnValueOnce(savedCacheKey)
.mockReturnValueOnce(primaryKey);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(0);
expect(logWarningMock).toHaveBeenCalledWith(
"Input required and not supplied: path"
expect(cache.saveCache).toHaveBeenCalledTimes(0);
expect(core.info).toHaveBeenCalledWith(
"[warning]Input required and not supplied: path"
);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
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 savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
(core.getState as jest.Mock)
.mockReturnValueOnce(savedCacheKey)
.mockReturnValueOnce(primaryKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
throw new Error(
"Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
);
});
(cache.saveCache as jest.Mock).mockRejectedValue(
new Error(
"Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
)
);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
[inputPath],
primaryKey,
expect.anything(),
false
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(core.info).toHaveBeenCalledWith(
"[warning]Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
);
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);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
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 savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
(core.getState as jest.Mock)
.mockReturnValueOnce(savedCacheKey)
.mockReturnValueOnce(primaryKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
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;
});
(cache.saveCache as jest.Mock).mockRejectedValue(
new Error(
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
)
);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
[inputPath],
primaryKey,
expect.anything(),
false
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(core.info).toHaveBeenCalledWith(
`[warning]Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
);
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);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
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 savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
(core.getState as jest.Mock)
.mockReturnValueOnce(savedCacheKey)
.mockReturnValueOnce(primaryKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
throw new Error("HTTP Error Occurred");
});
(cache.saveCache as jest.Mock).mockRejectedValue(
new Error("HTTP Error Occurred")
);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
[inputPath],
primaryKey,
expect.anything(),
false
);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
expect(failedMock).toHaveBeenCalledTimes(0);
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(core.info).toHaveBeenCalledWith("[warning]HTTP Error Occurred");
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("save with valid inputs uploads a cache", async () => {
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const savedCacheKey = "Linux-node-";
jest.spyOn(core, "getState")
// Cache Entry State
.mockImplementationOnce(() => {
return savedCacheKey;
})
// Cache Key State
.mockImplementationOnce(() => {
return primaryKey;
});
(core.getState as jest.Mock)
.mockReturnValueOnce(primaryKey)
.mockReturnValueOnce(savedCacheKey);
const inputPath = "node_modules";
testUtils.setInput(Inputs.Path, inputPath);
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = 4;
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
(cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
await saveImpl(new StateProvider());
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath],
primaryKey,
{
@@ -404,5 +275,5 @@ test("save with valid inputs uploads a cache", async () => {
false
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});

View File

@@ -1,70 +1,69 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { jest, test, expect, beforeEach, afterEach } from "@jest/globals";
import { Events, Inputs, RefKey } from "../src/constants";
import { saveOnlyRun } from "../src/saveImpl";
import * as actionUtils from "../src/utils/actionUtils";
import * as testUtils from "../src/utils/testUtils";
jest.mock("@actions/core");
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(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);
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
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.spyOn(actionUtils, "getInputAsInt").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsInt(name, options);
// 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";
}
);
}
}));
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
(name, options) => {
return jest
.requireActual("../src/utils/actionUtils")
.getInputAsBool(name, options);
}
);
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();
});
});
const core = await import("@actions/core");
const cache = await import("@actions/cache");
const { Events, Inputs, RefKey } = await import("../src/constants");
const { saveOnlyRun } = await import("../src/saveImpl");
const testUtils = await import("../src/utils/testUtils");
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[RefKey] = "refs/heads/feature-branch";
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
() => true
);
});
afterEach(() => {
@@ -74,8 +73,6 @@ afterEach(() => {
});
test("save with valid inputs uploads a cache", async () => {
const failedMock = jest.spyOn(core, "setFailed");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const inputPath = "node_modules";
@@ -84,16 +81,12 @@ test("save with valid inputs uploads a cache", async () => {
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = 4;
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
(cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
await saveOnlyRun();
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath],
primaryKey,
{
@@ -102,12 +95,10 @@ test("save with valid inputs uploads a cache", async () => {
false
);
expect(failedMock).toHaveBeenCalledTimes(0);
expect(core.setFailed).toHaveBeenCalledTimes(0);
});
test("save failing logs the warning message", async () => {
const warningMock = jest.spyOn(core, "warning");
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
const inputPath = "node_modules";
@@ -116,16 +107,12 @@ test("save failing logs the warning message", async () => {
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
const cacheId = -1;
const saveCacheMock = jest
.spyOn(cache, "saveCache")
.mockImplementationOnce(() => {
return Promise.resolve(cacheId);
});
(cache.saveCache as jest.Mock).mockResolvedValue(cacheId);
await saveOnlyRun();
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(
expect(cache.saveCache).toHaveBeenCalledTimes(1);
expect(cache.saveCache).toHaveBeenCalledWith(
[inputPath],
primaryKey,
{
@@ -134,6 +121,6 @@ test("save failing logs the warning message", async () => {
false
);
expect(warningMock).toHaveBeenCalledTimes(1);
expect(warningMock).toHaveBeenCalledWith("Cache save failed.");
expect(core.warning).toHaveBeenCalledTimes(1);
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";
import {
IStateProvider,
NullStateProvider,
StateProvider
} from "../src/stateProvider";
// Mock @actions/core
jest.unstable_mockModule("@actions/core", () => ({
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");
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(() => {
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").setOutput(key, value);
});
beforeEach(() => {
jest.clearAllMocks();
(core.getState as jest.Mock).mockReturnValue("");
});
afterEach(() => {
@@ -26,21 +43,10 @@ afterEach(() => {
test("StateProvider saves states", async () => {
const states = new Map<string, string>();
const getStateMock = jest
.spyOn(core, "getState")
.mockImplementation(key => states.get(key) || "");
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);
});
(core.getState as jest.Mock).mockImplementation((key: string) => states.get(key) || "");
(core.saveState as jest.Mock).mockImplementation((key: string, value: string) => {
states.set(key, value);
});
const cacheMatchedKey = "node-cache";
@@ -52,38 +58,19 @@ test("StateProvider saves states", async () => {
expect(stateValue).toBe("stateValue");
expect(cacheStateValue).toBe(cacheMatchedKey);
expect(getStateMock).toHaveBeenCalledTimes(2);
expect(saveStateMock).toHaveBeenCalledTimes(2);
expect(setOutputMock).toHaveBeenCalledTimes(0);
expect(core.getState).toHaveBeenCalledTimes(2);
expect(core.saveState).toHaveBeenCalledTimes(2);
expect(core.setOutput).toHaveBeenCalledTimes(0);
});
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();
nullStateProvider.setState(State.CacheMatchedKey, "outputValue");
nullStateProvider.setState(State.CachePrimaryKey, cacheMatchedKey);
nullStateProvider.setState(State.CachePrimaryKey, "node-cache");
nullStateProvider.getState("outputKey");
nullStateProvider.getCacheState();
expect(getStateMock).toHaveBeenCalledTimes(0);
expect(setOutputMock).toHaveBeenCalledTimes(2);
expect(saveStateMock).toHaveBeenCalledTimes(0);
expect(core.getState).toHaveBeenCalledTimes(0);
expect(core.setOutput).toHaveBeenCalledTimes(2);
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",
"version": "5.0.4",
"version": "6.0.0",
"private": true,
"description": "Cache dependencies and build outputs",
"type": "module",
"main": "dist/restore/index.js",
"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",
"test": "tsc --noEmit && jest --coverage",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
"lint": "eslint **/*.ts --cache",
"format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts"
@@ -23,13 +24,13 @@
"author": "GitHub",
"license": "MIT",
"dependencies": {
"@actions/cache": "^5.0.5",
"@actions/core": "^2.0.3",
"@actions/exec": "^2.0.0",
"@actions/io": "^2.0.0"
"@actions/cache": "^6.0.1",
"@actions/core": "^3.0.1",
"@actions/exec": "^3.0.0",
"@actions/io": "^3.0.2"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/jest": "^30.0.0",
"@types/nock": "^11.1.0",
"@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^7.2.0",
@@ -41,11 +42,11 @@
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-simple-import-sort": "^12.1.1",
"jest": "^29.7.0",
"jest-circus": "^29.7.0",
"jest": "^30.2.0",
"nock": "^13.2.9",
"prettier": "^3.6.2",
"ts-jest": "^29.4.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
},
"engines": {

View File

@@ -1,63 +1,13 @@
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "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. */
"target": "ES2022",
"module": "ESNext",
"outDir": "./lib",
"rootDir": "./src",
"strict": true,
"noImplicitAny": false,
"moduleResolution": "bundler",
"esModuleInterop": true
},
"exclude": ["node_modules", "**/*.test.ts"]
"exclude": ["node_modules", "**/*.test.ts", "jest.config.ts", "__tests__"]
}