diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 00000000..69bf095e --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,61 @@ +on: + schedule: + - cron: "0 2 * * *" + workflow_dispatch: + +env: + DENO_VERSION: "1.39.0" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GHJK_LOG_PANIC_LEVEL: error + +jobs: + test-e2e: + runs-on: "${{ matrix.os }}" + strategy: + matrix: + include: + - os: ubuntu-latest + e2eType: "docker" + - os: macos-latest + e2eType: "local" + # - os: windows-latest + # e2eType: "local" + env: + GHJK_TEST_E2E_TYPE: ${{ matrix.e2eType }} + steps: + - uses: actions/checkout@v4 + - uses: denoland/setup-deno@v1 + with: + deno-version: ${{ env.DENO_VERSION }} + + - if: "${{ matrix.os == 'macos-latest' }}" + run: brew install fish zsh + - name: Cache deno dir + if: "${{ matrix.os == 'macos-latest' }}" + uses: actions/cache@v3 + with: + path: ${{ env.DENO_DIR }} + key: deno-mac-${{ hashFiles('**/deno.lock') }} + + - if: "${{ matrix.e2eType == 'docker' }}" + uses: docker/setup-buildx-action@v3 + - if: "${{ matrix.e2eType == 'docker' }}" + uses: actions-hub/docker/cli@master + env: + SKIP_LOGIN: true + + - run: deno task test + + test-action: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: metatypedev/setup-ghjk@v1 + with: + installer-url: ./install.ts + env: + GHJK_CONFIG: ./examples/protoc/ghjk.ts + - run: | + cd examples/protoc + . $(dirname $BASH_ENV)/env.bash + protoc --version diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c896a511..e42911fb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,8 +9,9 @@ on: - ready_for_review env: - DENO_VERSION: "1.38.5" + DENO_VERSION: "1.39.0" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GHJK_LOG_PANIC_LEVEL: error jobs: changes: diff --git a/deno.lock b/deno.lock index 371c326d..cef3a475 100644 --- a/deno.lock +++ b/deno.lock @@ -319,6 +319,38 @@ "https://deno.land/std@0.209.0/assert/unreachable.ts": "1af8c99421cc5fb7332454b2b9eca074a4e394895a180bc837750dedcca75338", "https://deno.land/std@0.209.0/fmt/colors.ts": "34b3f77432925eb72cf0bfb351616949746768620b8e5ead66da532f93d10ba2", "https://deno.land/std@0.209.0/testing/asserts.ts": "605bbd2ef0695e2a4324d810c4ad22e56041d51afb9584fc0b4e81084b14b1d6", + "https://deno.land/std@0.210.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", + "https://deno.land/std@0.210.0/assert/_diff.ts": "2c9371f17cf08cbb843c924bc31ca77af422ec4fe162f73d42c651d547573fa8", + "https://deno.land/std@0.210.0/assert/_format.ts": "335ce8e15c65b679ad142dbc9e5e97e5d58602c39dd3c9175cef6c85fe22d6d5", + "https://deno.land/std@0.210.0/assert/assert.ts": "e265ad50a9341f3b40e51dd4cb41ab253d976943ba78a977106950e52e0302ab", + "https://deno.land/std@0.210.0/assert/assert_almost_equals.ts": "a70d637856e1c6128dc733346d32aa73c67058489495116ca85091c39a60c767", + "https://deno.land/std@0.210.0/assert/assert_array_includes.ts": "59d005d8897c1fbcbd5792170833f13a867f6a5ecd5a6b34a3d86b4b430de63c", + "https://deno.land/std@0.210.0/assert/assert_equals.ts": "991b0c2b437a015d623654f758e48bfd931068211a52e8131b397cdf005c595f", + "https://deno.land/std@0.210.0/assert/assert_exists.ts": "f24ecb0d3febad358a6cee235f012551077e07692517ebfe0630a561ba40a703", + "https://deno.land/std@0.210.0/assert/assert_false.ts": "99cf237fe374cabf57072d2fb41b3eaff389029f850fbb96f643c875792f10ce", + "https://deno.land/std@0.210.0/assert/assert_greater.ts": "e0bba9ac76a780573a864ab6eeb8b9fd71435b750bdd36d56a270e22ab9a79d9", + "https://deno.land/std@0.210.0/assert/assert_greater_or_equal.ts": "aea1c7dc868926ba55f1e59f8c3560bb44706b5e3b6b009453ee4064eecf6746", + "https://deno.land/std@0.210.0/assert/assert_instance_of.ts": "7c093d36b1a86666d5a60a8c290c91a51a627153b821a5a4dc40b24cab69f1e7", + "https://deno.land/std@0.210.0/assert/assert_is_error.ts": "a8a758581661edec514c453910bee2f9c91b1346a515c58404963b130d81cd80", + "https://deno.land/std@0.210.0/assert/assert_less.ts": "855aa58e49afa6a9e825f1abcd5947dc789c5878fc1b6f48b8a08115d48da32b", + "https://deno.land/std@0.210.0/assert/assert_less_or_equal.ts": "2ae5246bd0e83da26e5c8e2815d1493252f71f7dc02afb83dc2fc0e0fb0bd894", + "https://deno.land/std@0.210.0/assert/assert_match.ts": "e541a9769cf5726312ff9e15031e2faa2df3c59fbdc5573c8758b1f4668ccc62", + "https://deno.land/std@0.210.0/assert/assert_not_equals.ts": "6bce4b28f3316029c0aef107f8390796798835c382d31c1004160baef0b80db0", + "https://deno.land/std@0.210.0/assert/assert_not_instance_of.ts": "866243fd28bc6665e2ffcc027a9df1d2a69cb644aef1e9b8d1ce34377c6b8a84", + "https://deno.land/std@0.210.0/assert/assert_not_match.ts": "59707eceb0d2b16d6892fbf92ec86f92fd76fcfc55f8b61508299db7d2972cab", + "https://deno.land/std@0.210.0/assert/assert_not_strict_equals.ts": "c84b8e229450e8dfc44b9910d602788313ff7333d67d5bd8528371567b6a3632", + "https://deno.land/std@0.210.0/assert/assert_object_match.ts": "ebeff248d48e5810f787e8742ae4f6b39904f4640edc2f69796596ceb6dbcdf8", + "https://deno.land/std@0.210.0/assert/assert_rejects.ts": "f7e83272d816e1b39710012a0597ed950db2de6b193adcc5e50ddbcd9e177767", + "https://deno.land/std@0.210.0/assert/assert_strict_equals.ts": "4007dabef1c2e9d6f1bb0e948ba7ba99ec9b1bee97ba34d67f7c10e7e5d794f7", + "https://deno.land/std@0.210.0/assert/assert_string_includes.ts": "108a30d9348e5ff7a8b0b7cc836cf0a8cff27d5b33e861b8c56b52cc60b8219a", + "https://deno.land/std@0.210.0/assert/assert_throws.ts": "a8767e6a06e94bac42ca9eebdad5d4e2decbc0c48bc892da7e06aa1fe0b388ba", + "https://deno.land/std@0.210.0/assert/assertion_error.ts": "26ed1863d905005f00785c89750c001c3522c5417e4f58f95044b8143cfc1593", + "https://deno.land/std@0.210.0/assert/equal.ts": "6f81c8a3b12c08bdc3510c8a1293b4db1c083692219d7e3828d2234b448d3d3d", + "https://deno.land/std@0.210.0/assert/fail.ts": "f56fc64f9a141f98c1be5ff1005ddf158db888b7b206510e955bb3fedc69021c", + "https://deno.land/std@0.210.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", + "https://deno.land/std@0.210.0/assert/unimplemented.ts": "4e3e504792c87c485dbc5f4020489d8806ef697741403af2008dfa7b5a4711e8", + "https://deno.land/std@0.210.0/assert/unreachable.ts": "1af8c99421cc5fb7332454b2b9eca074a4e394895a180bc837750dedcca75338", + "https://deno.land/std@0.210.0/fmt/colors.ts": "2685c524bef9b16b3059a417daf6860c754eb755e19e812762ef5dff62f24481", "https://deno.land/x/cliffy@v1.0.0-rc.3/_utils/distance.ts": "02af166952c7c358ac83beae397aa2fbca4ad630aecfcd38d92edb1ea429f004", "https://deno.land/x/cliffy@v1.0.0-rc.3/command/_argument_types.ts": "ab269dacea2030f865a07c2a1e953ec437a64419a05bad1f1ddaab3f99752ead", "https://deno.land/x/cliffy@v1.0.0-rc.3/command/_errors.ts": "12d513ff401020287a344e0830e1297ce1c80c077ecb91e0ac5db44d04a6019c", diff --git a/deps/common.ts b/deps/common.ts index a3278be0..6c21e9fa 100644 --- a/deps/common.ts +++ b/deps/common.ts @@ -2,7 +2,8 @@ export { z as zod } from "https://deno.land/x/zod@v3.22.4/mod.ts"; export * as semver from "https://deno.land/std@0.205.0/semver/mod.ts"; -export * as log from "https://deno.land/std@0.205.0/log/mod.ts"; +export * as std_log from "https://deno.land/std@0.205.0/log/mod.ts"; +export * as std_log_levels from "https://deno.land/std@0.205.0/log/levels.ts"; export * as std_fmt_colors from "https://deno.land/std@0.205.0/fmt/colors.ts"; export * as std_url from "https://deno.land/std@0.205.0/url/mod.ts"; export * as std_path from "https://deno.land/std@0.205.0/path/mod.ts"; diff --git a/deps/dev.ts b/deps/dev.ts index 59af4329..11a4597c 100644 --- a/deps/dev.ts +++ b/deps/dev.ts @@ -1,4 +1,4 @@ //! dependencies used by tests export * from "./common.ts"; -export * as std_assert from "https://deno.land/std@0.205.0/assert/mod.ts"; +export * as std_assert from "https://deno.land/std@0.210.0/assert/mod.ts"; diff --git a/ghjk.ts b/ghjk.ts index 83ef8cfc..7f3de5b7 100644 --- a/ghjk.ts +++ b/ghjk.ts @@ -38,14 +38,14 @@ ghjk.install( // protoc(), // ruff(), // whiz(), - // jco()[0], + ...jco(), // cpython(), ); // these are used for developing ghjk ghjk.install( - act(), - ...pipi({ packageName: "pre-commit" }), + // act(), + // ...pipi({ packageName: "pre-commit" }), ); export const secureConfig = ghjk.secureConfig({ diff --git a/install/hook.fish b/install/hook.fish index 538d90dc..b44eb79f 100644 --- a/install/hook.fish +++ b/install/hook.fish @@ -14,6 +14,8 @@ function ghjk_reload --on-variable PWD # load the shim . $envDir/loader.fish + # FIXME: older versions of fish don't recognize -ot + # those in debian if test $envDir/loader.fish -ot $cur_dir/ghjk.ts set_color FF4500 echo "[ghjk] Detected changes, please sync..." diff --git a/modules/ports/sync.ts b/modules/ports/sync.ts index 14b49f60..2d0cf513 100644 --- a/modules/ports/sync.ts +++ b/modules/ports/sync.ts @@ -34,7 +34,7 @@ export ${k}='${v}';` ...Object.entries(pathVars).map(([k, v]) => // NOTE: double quote the path vars for expansion // single quote GHJK_CLEANUP additions to avoid expansion/exec before eval - `GHJK_CLEANUP_POSIX=$GHJK_CLEANUP_POSIX'${k}=$(echo "$${k}" | tr ":" "\\n" | grep -vE "^${envDir}" | tr "\\n" ":");${k}="\${${k}%:}"'; + `GHJK_CLEANUP_POSIX=$GHJK_CLEANUP_POSIX'${k}=$(echo "$${k}" | tr ":" "\\n" | grep -vE "^${envDir}" | tr "\\n" ":");${k}="\${${k}%:}";'; export ${k}="${v}:$${k}"; ` ), @@ -59,23 +59,35 @@ set --global --prepend ${k} ${v}; ]); } -/// this returns a tmp path that's guaranteed to be -/// on the same file system as targetDir by -/// checking if $TMPDIR satisfies that constraint -/// or just pointing to targetDir/tmp -/// This is handy for making moves atomics from -/// tmp dirs to to locations within targetDir -async function movebleTmpPath(targetDir: string, targetTmpDirName = "dir") { +/* * + * This returns a tmp path that's guaranteed to be + * on the same file system as targetDir by + * checking if $TMPDIR satisfies that constraint + * or just pointing to targetDir/tmp + * This is handy for making moves atomics from + * tmp dirs to to locations within targetDir + * + * Make sure to remove the dir after use + */ +async function movebleTmpRoot(targetDir: string, targetTmpDirName = "dir") { const defaultTmp = Deno.env.get("TMPDIR"); const targetPath = $.path(targetDir); if (!defaultTmp) { - return targetPath.join(targetTmpDirName); + // this doens't return a unique tmp dir on every sync + // this allows subsequent syncs to clean up after + // some previously failing sync as this is not a system managed + // tmp dir but this means two concurrent syncing will clash + // TODO: mutex file to prevent block concurrent syncinc + return await targetPath.join(targetTmpDirName).ensureDir(); } const defaultTmpPath = $.path(defaultTmp); if ((await targetPath.stat())?.dev != (await defaultTmpPath.stat())?.dev) { - return targetPath.join(targetTmpDirName); + return await targetPath.join(targetTmpDirName).ensureDir(); } - return defaultTmpPath.join("ghjkTmp"); + // when using the system managed tmp dir, we create a new tmp dir in it + // we don't care if the sync fails before it cleans as the system will + // take care of it + return $.path(await Deno.makeTempDir({ prefix: "ghjk_sync" })); } export async function sync( @@ -89,7 +101,7 @@ export async function sync( await Promise.all([ ghjkPathR.join("ports", "installs").ensureDir(), ghjkPathR.join("ports", "downloads").ensureDir(), - (await movebleTmpPath(ghjkDir)).ensureDir(), + movebleTmpRoot(ghjkDir), ]) ).map($.pathToString); @@ -483,13 +495,7 @@ async function doInstall( manifest: PortManifestX, depArts: DepArts, ) { - logger().debug("installing", { - installsDir, - downloadsDir, - instUnclean, - port: manifest, - }); - + logger().debug("installing", instUnclean); // instantiate the right Port impl according to manifest.ty let port; let inst: InstallConfigLiteX; @@ -530,6 +536,8 @@ async function doInstall( config: inst, manifest, }; + logger().debug("baseArgs", installId, baseArgs); + { logger().info(`downloading ${installId}:${installVersion}`); const tmpDirPath = await Deno.makeTempDir({ diff --git a/ports/dummy.ts b/ports/dummy.ts new file mode 100644 index 00000000..10de2284 --- /dev/null +++ b/ports/dummy.ts @@ -0,0 +1,53 @@ +//! this is a dumb port to be used for testing + +import type { + DownloadArgs, + InstallArgs, + InstallConfigSimple, +} from "../port.ts"; +import { $, ALL_ARCH, ALL_OS, osXarch, PortBase, std_fs } from "../port.ts"; + +const manifest = { + ty: "denoWorker@v1" as const, + name: "dummy", + version: "0.1.0", + moduleSpecifier: import.meta.url, + deps: [], + platforms: osXarch([...ALL_OS], [...ALL_ARCH]), +}; + +export default function conf(config: InstallConfigSimple = {}) { + return { + ...config, + port: manifest, + }; +} + +export class Port extends PortBase { + execEnv() { + return { + DUMMY_ENV: "dummy", + }; + } + + listAll() { + return ["dummy"]; + } + + async download(args: DownloadArgs) { + // TODO: windows suport + await $.path(args.downloadPath).join("bin", "dummy").writeText( + `#!/bin/sh +echo 'dummy hey'`, + { + mode: 0o700, + }, + ); + } + + async install(args: InstallArgs) { + const installPath = $.path(args.installPath); + await $.removeIfExists(installPath); + await std_fs.copy(args.downloadPath, args.installPath); + } +} diff --git a/ports/jco.ts b/ports/jco.ts index ba446bc3..6248ba85 100644 --- a/ports/jco.ts +++ b/ports/jco.ts @@ -8,14 +8,10 @@ import { ALL_ARCH, ALL_OS, depExecShimPath, - downloadFile, - dwnUrlOut, osXarch, pathsWithDepArts, PortBase, std_fs, - std_path, - unarchive, } from "../port.ts"; import node from "./node.ts"; import * as std_ports from "../modules/ports/std.ts"; @@ -58,47 +54,38 @@ export class Port extends PortBase { return versions; } - downloadUrls(args: DownloadArgs) { - const { installVersion } = args; - return [ - `https://registry.npmjs.org/@bytecodealliance/jco/-/jco-${installVersion}.tgz`, - ].map(dwnUrlOut); - } - async download(args: DownloadArgs) { - const urls = this.downloadUrls(args); - await Promise.all( - urls.map((obj) => downloadFile({ ...args, ...obj })), - ); + if (await $.path(args.downloadPath).exists()) { + return; + } + await $.raw`${ + depExecShimPath(std_ports.node_org, "npm", args.depArts) + } install --no-fund @bytecodealliance/jco@${args.installVersion}` + .cwd(args.tmpDirPath) + .env(pathsWithDepArts(args.depArts, args.platform.os)); + await std_fs.move(args.tmpDirPath, args.downloadPath); } + // FIXME: replace shebangs with the runtime dep node path + // default shebangs just use #!/bin/env node async install(args: InstallArgs) { - const [{ name: fileName }] = this.downloadUrls(args); - const fileDwnPath = std_path.resolve(args.downloadPath, fileName); - - await unarchive(fileDwnPath, args.tmpDirPath); + await std_fs.copy( + args.downloadPath, + args.tmpDirPath, + { overwrite: true }, + ); const installPath = $.path(args.installPath); - if (await installPath.exists()) { - await installPath.remove({ recursive: true }); - } - await std_fs.copy( - std_path.resolve( - args.tmpDirPath, - "package", - ), - args.installPath, - ); - await $`${ - depExecShimPath(std_ports.node_org, "npm", args.depArts) - } install --no-fund` - .cwd(args.installPath) - .env(pathsWithDepArts(args.depArts, args.platform.os)); - await installPath.join("bin").ensureDir(); - await installPath.join("bin", "jco") - .createSymlinkTo(installPath.join("src", "jco.js").toString(), { - // kind: "relative", - }); + const tmpDirPath = $.path(args.tmpDirPath); + await tmpDirPath.join("bin").ensureDir(); + await tmpDirPath.join("bin", "jco") + .createSymlinkTo( + installPath + .join("node_modules", ".bin", "jco") + .toString(), + ); + await $.removeIfExists(installPath); + await std_fs.move(tmpDirPath.toString(), installPath.toString()); } } diff --git a/tests/hooks.ts b/tests/hooks.ts new file mode 100644 index 00000000..9c78b241 --- /dev/null +++ b/tests/hooks.ts @@ -0,0 +1,143 @@ +import "../setup_logger.ts"; +import { dockerE2eTest, E2eTestCase, localE2eTest } from "./utils.ts"; +import dummy from "../ports/dummy.ts"; + +// avoid using single quotes in this script +const posixInteractiveScript = ` +set -eux +[ "$DUMMY_ENV" = "dummy" ] || exit 101 +dummy +pushd ../ +# it shouldn't be avail here +[ $(set +e; dummy) ] && exit 102 +# cd back in +popd +# now it should be avail +dummy +`; + +// avoid using single quotes in this script +const posixNonInteractiveScript = ` +set -eux +# test that ghjk_reload is avail because BASH_ENV exposed by the suite +ghjk_reload +[ "$DUMMY_ENV" = "dummy" ] || exit 101 +dummy +pushd ../ +# no reload so it's stil avail +dummy +ghjk_reload +# it shouldn't be avail now +[ $(set +e; dummy) ] && exit 102 +[ "$DUMMY_ENV" = "dummy" ] && exit 103 +# cd back in +popd +# not avail yet +[ $(set +e; dummy) ] && exit 104 +[ "$DUMMY_ENV" = "dummy" ] && exit 105 +ghjk_reload +# now it should be avail +dummy +[ "$DUMMY_ENV" = "dummy" ] || exit 106 +`; + +const fishScript = ` +dummy; or exit 101 +test $DUMMY_ENV = "dummy"; or exit 102 +pushd ../ +# it shouldn't be avail here +dummy; and exit 103 +test $DUMMY_ENV = "dummy"; and exit 104 +# cd back in +popd +# now it should be avail +dummy; or exit 123 +test $DUMMY_ENV = "dummy"; or exit 105 +`; + +type CustomE2eTestCase = Omit & { + ePoint: string; + stdin: string; +}; +const cases: CustomE2eTestCase[] = [ + { + installConf: dummy(), + name: "hook_test_bash_interactive", + // -s: read from stdin + // -l: login/interactive mode + ePoint: `bash -sl`, + stdin: posixInteractiveScript, + }, + { + installConf: dummy(), + name: "hook_test_bash_scripting", + ePoint: `bash -s`, + stdin: posixNonInteractiveScript, + }, + { + installConf: dummy(), + name: "hook_test_zsh_interactive", + ePoint: `zsh -sl`, + stdin: posixInteractiveScript, + }, + { + installConf: dummy(), + name: "hook_test_zsh_scripting", + ePoint: `zsh -s`, + stdin: posixNonInteractiveScript, + }, + { + installConf: dummy(), + name: "hook_test_fish_interactive", + ePoint: `fish -l`, + stdin: fishScript, + }, + { + installConf: dummy(), + name: "hook_test_fish_scripting", + ePoint: `fish`, + // the fish implementation triggers changes + // on any pwd changes so it's identical to + // interactive usage + stdin: fishScript, + }, +]; + +function testMany( + testGroup: string, + cases: CustomE2eTestCase[], + testFn: (inp: E2eTestCase) => Promise, + defaultEnvs: Record = {}, +) { + for (const testCase of cases) { + Deno.test( + `${testGroup} - ${testCase.name}`, + () => + testFn({ + ...testCase, + ePoints: [{ cmd: testCase.ePoint, stdin: testCase.stdin }], + envs: { + ...defaultEnvs, + ...testCase.envs, + }, + }), + ); + } +} + +const e2eType = Deno.env.get("GHJK_TEST_E2E_TYPE"); +if (e2eType == "both") { + testMany("hooksDockerE2eTest", cases, dockerE2eTest); + testMany(`hooksLocalE2eTest`, cases, localE2eTest); +} else if (e2eType == "local") { + testMany("hooksLocalE2eTest", cases, localE2eTest); +} else if ( + e2eType == "docker" || + !e2eType +) { + testMany("hooksDockerE2eTest", cases, dockerE2eTest); +} else { + throw new Error( + `unexpected GHJK_TEST_E2E_TYPE: ${e2eType}`, + ); +} diff --git a/tests/ports.ts b/tests/ports.ts index 1fe4a4c5..8ee3d711 100644 --- a/tests/ports.ts +++ b/tests/ports.ts @@ -19,20 +19,12 @@ import whiz from "../ports/whiz.ts"; import cpython from "../ports/cpy_bs.ts"; import pipi from "../ports/pipi.ts"; -type CustomE2eTestCase = Omit & { ePoint: string }; +type CustomE2eTestCase = Omit & { + ePoint: string; + ignore?: boolean; +}; // order tests by download size to make failed runs less expensive const cases: CustomE2eTestCase[] = [ - ...(Deno.build.os == "linux" - ? [ - // 8 megs - { - name: "mold", - installConf: mold(), - ePoint: `mold -V`, - }, - ] - : []), - // 3 megs { name: "protoc", @@ -63,6 +55,13 @@ const cases: CustomE2eTestCase[] = [ installConf: cargo_binstall(), ePoint: `cargo-binstall -V`, }, + // 8 megs + { + name: "mold", + installConf: mold(), + ePoint: `mold -V`, + ignore: Deno.build.os != "linux", + }, // 16 megs { name: "wasmedge", @@ -148,18 +147,21 @@ function testMany( ) { for (const testCase of cases) { Deno.test( - `${testGroup} - ${testCase.name}`, - () => - testFn({ - ...testCase, - ePoints: ["bash -c", "fish -c", "zsh -c"].map((sh) => - `env ${sh} '${testCase.ePoint}'` - ), - envs: { - ...defaultEnvs, - ...testCase.envs, - }, - }), + { + name: `${testGroup} - ${testCase.name}`, + ignore: testCase.ignore, + fn: () => + testFn({ + ...testCase, + ePoints: ["bash -c", "fish -c", "zsh -c"].map((sh) => ({ + cmd: `env ${sh} '${testCase.ePoint}'`, + })), + envs: { + ...defaultEnvs, + ...testCase.envs, + }, + }), + }, ); } } diff --git a/tests/utils.ts b/tests/utils.ts index ffd0ee1f..8c4f517c 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -11,7 +11,7 @@ export type E2eTestCase = { installConf: InstallConfigFat | InstallConfigFat[]; secureConf?: PortsModuleSecureConfig; envs?: Record; - ePoints: string[]; + ePoints: { cmd: string; stdin?: string }[]; }; const dockerCmd = (Deno.env.get("DOCKER_CMD") ?? "docker").split(/\s/); @@ -29,21 +29,23 @@ export async function dockerE2eTest(testCase: E2eTestCase) { const installConfArray = Array.isArray(installConf) ? installConf : [installConf]; - // replace all file urls that point to the ghjk - // repo in the host fs to point to the copy of the - // repo in the image const devGhjkPath = import.meta.resolve("../"); const serializedPortsInsts = JSON.stringify( installConfArray, (_, val) => typeof val == "string" - ? val.replace(devGhjkPath, "file://$ghjk/").replaceAll( - /\\/g, - // we need to escape from a json string embedded js string - // embedded embeded in a js file embedded in a Dockerfile - // 4x - "\\\\\\\\", - ) + ? val + // replace all file urls that point to the ghjk + // repo in the host fs to point to the copy of the + // repo in the image + .replace(devGhjkPath, "file://$ghjk/") + .replaceAll( + /\\/g, + // we need to escape from a json string embedded js string + // embedded embeded in a js file embedded in a Dockerfile + // 4x + "\\\\\\\\", + ) : val, ); const serializedSecConf = JSON.stringify( @@ -84,13 +86,21 @@ export const secureConfig = JSON.parse(secConfStr); } --tag '${tag}' --network=host --output type=docker -f- .` .env(env) .stdinText(dFile); + for (const ePoint of ePoints) { - await $ - .raw`${dockerCmd} run --rm ${ - Object.entries(env).map(([key, val]) => ["-e", `${key}=${val}`]) - .flat() - } ${tag} ${ePoint}` + let cmd = $.raw`${dockerCmd} run --rm ${[ + /* we want to enable interactivity when piping in */ + ePoint.stdin ? "-i " : "", + ...Object.entries(env).map(([key, val]) => ["-e", `${key}=${val}`]) + .flat(), + tag, + ePoint.cmd, + ]}` .env(env); + if (ePoint.stdin) { + cmd = cmd.stdinText(ePoint.stdin); + } + await cmd; } await $ .raw`${dockerCmd} rmi '${tag}'` @@ -104,15 +114,8 @@ export async function localE2eTest(testCase: E2eTestCase) { prefix: "ghjk_le2e_", }), ); - const ghjkDir = await tmpDir.join("ghjk").ensureDir(); - await install({ - ...defaultInstallArgs, - skipExecInstall: false, - ghjkExecInstallDir: ghjkDir.toString(), - ghjkDir: ghjkDir.toString(), - shellsToHook: [], - }); + const installConfArray = Array.isArray(installConf) ? installConf : [installConf]; @@ -159,14 +162,17 @@ export const secureConfig = JSON.parse(secConfStr); ZDOTDIR: ghjkDir.toString(), GHJK_DIR: ghjkDir.toString(), }; - { - const confHome = await ghjkDir.join(".config").ensureDir(); - const fishConfDir = await confHome.join("fish").ensureDir(); - await fishConfDir.join("config.fish").createSymlinkTo( - ghjkDir.join("env.fish").toString(), - ); - env["XDG_CONFIG_HOME"] = confHome.toString(); - } + // install ghjk + await install({ + ...defaultInstallArgs, + skipExecInstall: false, + ghjkExecInstallDir: ghjkDir.toString(), + // share the system's deno cache + ghjkDenoCacheDir: Deno.env.get("DENO_DIR"), + ghjkDir: ghjkDir.toString(), + // don't modify system shell configs + shellsToHook: [], + }); await $`${ghjkDir.join("ghjk").toString()} print config` .cwd(tmpDir.toString()) .env(env); @@ -181,10 +187,22 @@ export const secureConfig = JSON.parse(secConfStr); entry.path.toString().slice(ghjkDirLen), ])); */ + { + const confHome = await ghjkDir.join(".config").ensureDir(); + const fishConfDir = await confHome.join("fish").ensureDir(); + await fishConfDir.join("config.fish").createSymlinkTo( + ghjkDir.join("env.fish").toString(), + ); + env["XDG_CONFIG_HOME"] = confHome.toString(); + } for (const ePoint of ePoints) { - await $.raw`${ePoint}` + let cmd = $.raw`${ePoint.cmd}` .cwd(tmpDir.toString()) .env(env); + if (ePoint.stdin) { + cmd = cmd.stdinText(ePoint.stdin); + } + await cmd; } await tmpDir.remove({ recursive: true }); } diff --git a/utils/logger.ts b/utils/logger.ts index 795abdc2..f63cc09e 100644 --- a/utils/logger.ts +++ b/utils/logger.ts @@ -1,4 +1,10 @@ -import { log, std_fmt_colors, std_path, std_url } from "../deps/common.ts"; +import { + std_fmt_colors, + std_log, + std_log_levels, + std_path, + std_url, +} from "../deps/common.ts"; // TODO: consult GHJK_LOG variable export default function logger( @@ -8,10 +14,10 @@ export default function logger( name = std_url.basename(name.url); name = name.replace(std_path.extname(name), ""); } - return log.getLogger(name); + return std_log.getLogger(name); } -function formatter(lr: log.LogRecord) { +function formatter(lr: std_log.LogRecord) { const loggerName = lr.loggerName !== "default" ? " " + lr.loggerName : ""; let msg = `[${lr.levelName}${loggerName}] ${lr.msg}`; @@ -29,10 +35,19 @@ function formatter(lr: log.LogRecord) { return msg; } -export function setup() { - log.setup({ +export function setup(handler = new ConsoleErrHandler("NOTSET")) { + const panicLevelName = Deno.env.get("GHJK_LOG_PANIC_LEVEL"); + if (panicLevelName) { + handler = new TestConsoleErrHandler( + std_log_levels.getLevelByName( + panicLevelName.toUpperCase() as std_log_levels.LevelName, + ), + "NOTSET", + ); + } + std_log.setup({ handlers: { - console: new ConsoleErrHandler("NOTSET"), + console: handler, }, loggers: { @@ -48,33 +63,33 @@ export function setup() { }); } -export class ConsoleErrHandler extends log.handlers.BaseHandler { +export class ConsoleErrHandler extends std_log.handlers.BaseHandler { constructor( - levelName: log.LevelName, - options: log.HandlerOptions = { formatter }, + levelName: std_log.LevelName, + options: std_log.HandlerOptions = { formatter }, ) { super(levelName, options); } override log(msg: string): void { console.error(msg); } - override format(logRecord: log.LogRecord): string { + override format(logRecord: std_log.LogRecord): string { let msg = super.format(logRecord); switch (logRecord.level) { - case log.LogLevels.INFO: + case std_log.LogLevels.INFO: msg = std_fmt_colors.green(msg); break; - case log.LogLevels.WARNING: + case std_log.LogLevels.WARNING: msg = std_fmt_colors.yellow(msg); break; - case log.LogLevels.ERROR: + case std_log.LogLevels.ERROR: msg = std_fmt_colors.red(msg); break; - case log.LogLevels.CRITICAL: + case std_log.LogLevels.CRITICAL: msg = std_fmt_colors.bold(std_fmt_colors.red(msg)); break; - case log.LogLevels.DEBUG: + case std_log.LogLevels.DEBUG: msg = std_fmt_colors.dim(msg); break; default: @@ -85,6 +100,23 @@ export class ConsoleErrHandler extends log.handlers.BaseHandler { } } +export class TestConsoleErrHandler extends ConsoleErrHandler { + constructor( + public throwLevel: number, + levelName: std_log.LevelName, + options: std_log.HandlerOptions = { formatter }, + ) { + super(levelName, options); + } + + handle(lr: std_log.LogRecord): void { + if (lr.level >= this.throwLevel) { + throw new Error(`detected ${lr.levelName} log record:`, { cause: lr }); + } + super.handle(lr); + } +} + let colorEnvFlagSet = false; Deno.permissions.query({ name: "env",