diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..9a75e12 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,23 @@ +import { createDefaultPreset } from "ts-jest"; + +const presetConfig = createDefaultPreset(); + +const jestConfig = { + ...presetConfig, + resetMocks: true, + testEnvironment: "node", + extensionsToTreatAsEsm: [".ts"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + transform: { + "^.+\\.ts$": [ + "ts-jest", + { + useESM: true, + }, + ], + }, +}; + +export default jestConfig; diff --git a/jest.config.ts b/jest.config.ts deleted file mode 100644 index b0ac37e..0000000 --- a/jest.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createDefaultPreset, type JestConfigWithTsJest } from "ts-jest"; - -const presetConfig = createDefaultPreset({ - isolatedModules: true, -}); - -const jestConfig: JestConfigWithTsJest = { - ...presetConfig, - resetMocks: true, - testEnvironment: "node", -}; - -export default jestConfig; diff --git a/package.json b/package.json index 25d905d..d380268 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "stack-action", "version": "0.0.0", + "type": "module", "description": "Build and test stack-based Haskell projects", "main": "lib/main.js", "scripts": { "build": "tsc && ncc build lib/main.js && sed -i 's/\\x0D$//' ./dist/index.js", "format": "prettier --write \"**/*.ts\"", "format-check": "prettier --check \"**/*.ts\"", - "test": "jest", + "test": "NODE_OPTIONS='--experimental-vm-modules' jest", "readme": "npx action-docs -u && prettier --write README.md" }, "repository": { diff --git a/src/dirty-files.test.ts b/src/dirty-files.test.ts index fad91fb..32a67fb 100644 --- a/src/dirty-files.test.ts +++ b/src/dirty-files.test.ts @@ -1,4 +1,6 @@ -import { parseGitStatus, isInterestingFile } from "./dirty-files"; +import { jest } from "@jest/globals"; + +import { parseGitStatus, isInterestingFile } from "./dirty-files.js"; describe("parseGitStatus", () => { test("parse file name, and filters untracked", () => { diff --git a/src/envsubst.test.ts b/src/envsubst.test.ts index 3f842d4..472ebf1 100644 --- a/src/envsubst.test.ts +++ b/src/envsubst.test.ts @@ -1,4 +1,6 @@ -import { envsubst } from "./envsubst"; +import { jest } from "@jest/globals"; + +import { envsubst } from "./envsubst.js"; const HOME = process.env.HOME; diff --git a/src/get-cache-keys.test.ts b/src/get-cache-keys.test.ts index 24731c6..cb6d2b1 100644 --- a/src/get-cache-keys.test.ts +++ b/src/get-cache-keys.test.ts @@ -1,4 +1,6 @@ -import { getCacheKeys } from "./get-cache-keys"; +import { jest } from "@jest/globals"; + +import { getCacheKeys } from "./get-cache-keys.js"; test("getCacheKeys", () => { const keys = getCacheKeys(["prefix-os-compiler", "package", "source"]); diff --git a/src/hie.ts b/src/hie.ts index 170b8ea..ad45d74 100644 --- a/src/hie.ts +++ b/src/hie.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as core from "@actions/core"; -import { StackCLI } from "./stack-cli"; +import { StackCLI } from "./stack-cli.js" export const HIE_YAML: string = "hie.yaml"; diff --git a/src/inputs.ts b/src/inputs.ts index c48fcea..a2ed41a 100644 --- a/src/inputs.ts +++ b/src/inputs.ts @@ -1,7 +1,7 @@ import * as core from "@actions/core"; import * as Shellwords from "shellwords-ts"; -import { envsubst } from "./envsubst"; -import { type OnDirtyFiles, parseOnDirtyFiles } from "./dirty-files"; +import { envsubst } from "./envsubst.js" +import { type OnDirtyFiles, parseOnDirtyFiles } from "./dirty-files.js" export type Inputs = { workingDirectory: string | null; diff --git a/src/main.ts b/src/main.ts index 4c5df21..3a6f792 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,13 @@ import * as core from "@actions/core"; -import { checkDirtyFiles } from "./dirty-files"; -import { StackCLI } from "./stack-cli"; -import { getCacheKeys } from "./get-cache-keys"; -import { hashProject } from "./hash-project"; -import { getInputs } from "./inputs"; -import { readStackYamlSync, getStackDirectories } from "./stack-yaml"; -import { DEFAULT_CACHE_OPTIONS, withCache } from "./with-cache"; -import { GenHIE } from "./hie"; +import { checkDirtyFiles } from "./dirty-files.js" +import { StackCLI } from "./stack-cli.js" +import { getCacheKeys } from "./get-cache-keys.js" +import { hashProject } from "./hash-project.js" +import { getInputs } from "./inputs.js" +import { readStackYamlSync, getStackDirectories } from "./stack-yaml.js" +import { DEFAULT_CACHE_OPTIONS, withCache } from "./with-cache.js" +import { GenHIE } from "./hie.js" async function run() { try { diff --git a/src/parse-stack-path.test.ts b/src/parse-stack-path.test.ts index 557ff0c..b9d5c35 100644 --- a/src/parse-stack-path.test.ts +++ b/src/parse-stack-path.test.ts @@ -1,4 +1,6 @@ -import { parseStackPath } from "./parse-stack-path"; +import { jest } from "@jest/globals"; + +import { parseStackPath } from "./parse-stack-path.js"; const EXAMPLE = [ "snapshot-doc-root: /home/patrick/.stack/snapshots/x86_64-linux-tinfo6/0dd02c1d8a380045321779c4567c2aa8873910743eac342f72d56f3d26881028/9.2.7/doc", diff --git a/src/parse-stack-query.test.ts b/src/parse-stack-query.test.ts index 5dde394..24fdf64 100644 --- a/src/parse-stack-query.test.ts +++ b/src/parse-stack-query.test.ts @@ -1,4 +1,6 @@ -import { parseStackQuery } from "./parse-stack-query"; +import { jest } from "@jest/globals"; + +import { parseStackQuery } from "./parse-stack-query.js"; const EXAMPLE = [ "compiler:", diff --git a/src/stack-cli.test.ts b/src/stack-cli.test.ts index f142206..b382c26 100644 --- a/src/stack-cli.test.ts +++ b/src/stack-cli.test.ts @@ -1,12 +1,17 @@ -import * as exec from "@actions/exec"; +import { ExecOptions } from "@actions/exec"; +import { jest } from "@jest/globals"; -import { StackCLI } from "./stack-cli"; +import { ExecDelegate, StackCLI } from "./stack-cli.js"; -jest.spyOn(exec, "exec"); +const exec: ExecDelegate = { + exec: jest.fn((command: string, args: string[], options?: ExecOptions) => + Promise.resolve(0), + ), +}; describe("StackCLI", () => { test("Respects --resolver given", async () => { - const stackCLI = new StackCLI(["--resolver", "lts"], false); + const stackCLI = new StackCLI(["--resolver", "lts"], false, exec); await stackCLI.setup([]); @@ -21,6 +26,7 @@ describe("StackCLI", () => { const stackCLI = new StackCLI( ["--stack-yaml", "sub/stack-nightly.yaml"], false, + exec, ); await stackCLI.setup([]); @@ -47,6 +53,7 @@ describe("StackCLI", () => { "nightly-20240201", ], false, + exec, ); await stackCLI.setup([]); @@ -65,7 +72,7 @@ describe("StackCLI", () => { }); test("installCompilerTools", async () => { - const stackCLI = new StackCLI([], false); + const stackCLI = new StackCLI([], false, exec); await stackCLI.installCompilerTools(["hlint", "weeder"]); expect(exec.exec).toHaveBeenCalledWith( @@ -76,14 +83,14 @@ describe("StackCLI", () => { }); test("installCompilerTools with empty arguments", async () => { - const stackCLI = new StackCLI([], false); + const stackCLI = new StackCLI([], false, exec); await stackCLI.installCompilerTools([]); expect(exec.exec).not.toHaveBeenCalled(); }); test("buildDependencies", async () => { - const stackCLI = new StackCLI([], false); + const stackCLI = new StackCLI([], false, exec); await stackCLI.buildDependencies(["--coverage"]); @@ -101,7 +108,7 @@ describe("StackCLI", () => { }); test("buildNoTest", async () => { - const stackCLI = new StackCLI([], false); + const stackCLI = new StackCLI([], false, exec); await stackCLI.buildNoTest(["--coverage"]); @@ -113,7 +120,7 @@ describe("StackCLI", () => { }); test("buildTest", async () => { - const stackCLI = new StackCLI([], false); + const stackCLI = new StackCLI([], false, exec); await stackCLI.buildTest(["--coverage"]); @@ -125,7 +132,7 @@ describe("StackCLI", () => { }); test("build", async () => { - const stackCLI = new StackCLI([], false); + const stackCLI = new StackCLI([], false, exec); await stackCLI.build(["--coverage"]); diff --git a/src/stack-cli.ts b/src/stack-cli.ts index 7d7b0c6..cce736f 100644 --- a/src/stack-cli.ts +++ b/src/stack-cli.ts @@ -2,12 +2,12 @@ import * as fs from "fs"; import * as path from "path"; import { devNull } from "os"; import type { ExecOptions } from "@actions/exec"; -import * as exec from "@actions/exec"; +import * as realExec from "@actions/exec"; -import type { StackPath } from "./parse-stack-path"; -import { parseStackPath } from "./parse-stack-path"; -import type { StackQuery } from "./parse-stack-query"; -import { parseStackQuery } from "./parse-stack-query"; +import type { StackPath } from "./parse-stack-path.js"; +import { parseStackPath } from "./parse-stack-path.js"; +import type { StackQuery } from "./parse-stack-query.js"; +import { parseStackQuery } from "./parse-stack-query.js"; export interface ExecDelegate { exec: ( @@ -23,10 +23,12 @@ export class StackCLI { private debug: boolean; private globalArgs: string[]; + private execImpl: ExecDelegate; - constructor(args: string[], debug?: boolean) { + constructor(args: string[], debug?: boolean, exec?: ExecDelegate) { this.debug = debug ?? false; this.globalArgs = args; + this.execImpl = exec ?? realExec; // Capture --stack-yaml if given const stackYamlIdx = args.indexOf("--stack-yaml"); @@ -49,7 +51,7 @@ export class StackCLI { } async installed(): Promise { - const ec = await exec.exec("which", ["stack"], { + const ec = await this.execImpl.exec("which", ["stack"], { silent: true, ignoreReturnCode: true, }); @@ -59,14 +61,14 @@ export class StackCLI { async install(): Promise { const url = "https://get.haskellstack.org"; const tmp = "install-stack.sh"; - await exec.exec("curl", ["-sSL", "-o", tmp, url]); - await exec.exec("sh", [tmp]); + await this.execImpl.exec("curl", ["-sSL", "-o", tmp, url]); + await this.execImpl.exec("sh", [tmp]); fs.rmSync(tmp); } async upgrade(): Promise { // Avoid this.exec because we don't need/want globalArgs - return await exec.exec("stack", ["upgrade"]); + return await this.execImpl.exec("stack", ["upgrade"]); } async setup(args: string[]): Promise { @@ -139,6 +141,10 @@ export class StackCLI { } private async exec(args: string[], options?: ExecOptions): Promise { - return await exec.exec("stack", this.globalArgs.concat(args), options); + return await this.execImpl.exec( + "stack", + this.globalArgs.concat(args), + options, + ); } } diff --git a/src/stack-yaml.test.ts b/src/stack-yaml.test.ts index 56779c6..600e61c 100644 --- a/src/stack-yaml.test.ts +++ b/src/stack-yaml.test.ts @@ -1,5 +1,7 @@ -import { parseStackYaml, getStackDirectories } from "./stack-yaml"; -import { StackCLI } from "./stack-cli"; +import { jest } from "@jest/globals"; + +import { parseStackYaml, getStackDirectories } from "./stack-yaml.js"; +import { StackCLI } from "./stack-cli.js"; const testStackRoot = "/home/me/.stack"; const testPrograms = `${testStackRoot}/programs/x86_64-linux`; diff --git a/src/stack-yaml.ts b/src/stack-yaml.ts index 708c68c..b1e8f8a 100644 --- a/src/stack-yaml.ts +++ b/src/stack-yaml.ts @@ -2,7 +2,7 @@ import * as fs from "fs"; import { join as pathJoin } from "path"; import * as yaml from "js-yaml"; -import { StackCLI } from "./stack-cli"; +import { StackCLI } from "./stack-cli.js" export type StackYaml = { resolver: string; diff --git a/src/with-cache.test.ts b/src/with-cache.test.ts index ac9fa1a..f19438e 100644 --- a/src/with-cache.test.ts +++ b/src/with-cache.test.ts @@ -1,12 +1,19 @@ import * as core from "@actions/core"; -import * as cache from "@actions/cache"; +import { jest } from "@jest/globals"; -import { getCacheKeys } from "./get-cache-keys"; -import { DEFAULT_CACHE_OPTIONS, withCache } from "./with-cache"; +import { getCacheKeys } from "./get-cache-keys.js"; +import { + CacheDelegate, + DEFAULT_CACHE_OPTIONS, + withCache, +} from "./with-cache.js"; + +const cache: CacheDelegate = { + restoreCache: jest.fn(() => Promise.resolve("")), + saveCache: jest.fn(() => Promise.resolve(0)), +}; const restoreCacheMock = jest.spyOn(cache, "restoreCache"); -jest.spyOn(cache, "saveCache"); -jest.spyOn(core, "info"); async function testFunction(): Promise { return 42; @@ -41,7 +48,8 @@ test("withCache skips on primary-key hit", async () => { cachePaths, cacheKeys, testFunction, - DEFAULT_CACHE_OPTIONS, + { ...DEFAULT_CACHE_OPTIONS, silent: true }, + cache, ); expect(result).toBeUndefined(); @@ -62,7 +70,8 @@ test("withCache acts and saves if no primary-key hit", async () => { cachePaths, cacheKeys, testFunction, - DEFAULT_CACHE_OPTIONS, + { ...DEFAULT_CACHE_OPTIONS, silent: true }, + cache, ); expect(result).toEqual(42); @@ -82,10 +91,17 @@ test("withCache can be configured to act and save anyway", async () => { const cacheKeys = getCacheKeys(["a-b", "c", "d"]); restoreCacheMock.mockImplementation(simulateCacheHit); - const result = await withCache(cachePaths, cacheKeys, testFunction, { - ...DEFAULT_CACHE_OPTIONS, - skipOnHit: false, - }); + const result = await withCache( + cachePaths, + cacheKeys, + testFunction, + { + ...DEFAULT_CACHE_OPTIONS, + skipOnHit: false, + silent: true, + }, + cache, + ); expect(result).toEqual(42); expect(cache.restoreCache).toHaveBeenCalledWith( @@ -108,7 +124,8 @@ test("withCache does not save on error", async () => { cachePaths, cacheKeys, testFunctionThrows, - DEFAULT_CACHE_OPTIONS, + { ...DEFAULT_CACHE_OPTIONS, silent: true }, + cache, ); }).rejects.toThrow(); @@ -128,10 +145,17 @@ test("withCache can be configured to save on error", async () => { restoreCacheMock.mockImplementation(simulateCacheMiss); await expect(async () => { - await withCache(cachePaths, cacheKeys, testFunctionThrows, { - ...DEFAULT_CACHE_OPTIONS, - saveOnError: true, - }); + await withCache( + cachePaths, + cacheKeys, + testFunctionThrows, + { + ...DEFAULT_CACHE_OPTIONS, + saveOnError: true, + silent: true, + }, + cache, + ); }).rejects.toThrow(); expect(cache.restoreCache).toHaveBeenCalledWith( diff --git a/src/with-cache.ts b/src/with-cache.ts index 590225e..92e3a6b 100644 --- a/src/with-cache.ts +++ b/src/with-cache.ts @@ -1,30 +1,45 @@ import * as core from "@actions/core"; -import * as cache from "@actions/cache"; -import type { CacheKeys } from "./get-cache-keys"; +import * as realCache from "@actions/cache"; +import type { CacheKeys } from "./get-cache-keys.js"; export type CacheOptions = { skipOnHit: boolean; saveOnError: boolean; + silent: boolean; }; export const DEFAULT_CACHE_OPTIONS = { skipOnHit: true, saveOnError: false, + silent: false, }; +export interface CacheDelegate { + restoreCache: ( + paths: string[], + primaryKey: string, + restoreKeys?: string[], + ) => Promise; + saveCache: (paths: string[], key: string) => Promise; +} + export async function withCache( paths: string[], keys: CacheKeys, fn: () => Promise, options: CacheOptions = DEFAULT_CACHE_OPTIONS, + cache?: CacheDelegate, ): Promise { - const { skipOnHit, saveOnError } = options; + const cacheImpl = cache ?? realCache; + const { skipOnHit, saveOnError, silent } = options; - core.info(`Cached paths:\n - ${paths.join("\n - ")}`); - core.info(`Cache key: ${keys.primaryKey}`); - core.info(`Cache restore keys:\n - ${keys.restoreKeys.join("\n - ")}`); + if (!silent) { + core.info(`Cached paths:\n - ${paths.join("\n - ")}`); + core.info(`Cache key: ${keys.primaryKey}`); + core.info(`Cache restore keys:\n - ${keys.restoreKeys.join("\n - ")}`); + } - const restoredKey = await cache.restoreCache( + const restoredKey = await cacheImpl.restoreCache( paths, keys.primaryKey, keys.restoreKeys, @@ -33,13 +48,17 @@ export async function withCache( const primaryKeyHit = restoredKey == keys.primaryKey; if (restoredKey) { - core.info(`Cache restored from key: ${restoredKey}`); + if (!silent) { + core.info(`Cache restored from key: ${restoredKey}`); + } } else { core.warning("No cache found"); } if (primaryKeyHit && skipOnHit && !saveOnError) { - core.info("Skipping due to primary key hit"); + if (!silent) { + core.info("Skipping due to primary key hit"); + } return; } @@ -49,11 +68,11 @@ export async function withCache( result = await fn(); if (!primaryKeyHit) { - await cache.saveCache(paths, keys.primaryKey); + await cacheImpl.saveCache(paths, keys.primaryKey); } } catch (ex) { if (saveOnError && !primaryKeyHit) { - await cache.saveCache(paths, keys.primaryKey); + await cacheImpl.saveCache(paths, keys.primaryKey); } throw ex; diff --git a/tsconfig.json b/tsconfig.json index 3dfd4fe..4ed1658 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,8 +14,10 @@ "noEmitOnError": true, "removeComments": true, "module": "NodeNext", + "moduleResolution": "NodeNext", "esModuleInterop": true, - "strict": true + "strict": true, + "isolatedModules": true }, "include": ["./src/**/*"], "exclude": ["./src/**/*.test.ts"]