com.wallstop-studios.unity-helpers
Version:
Treasure chest of Unity developer tools
322 lines (289 loc) • 12.1 kB
JavaScript
// MIT License — Copyright (c) wallstop studios
//
// Test runner for scripts/postinstall-hooks.js.
//
// Verifies the skipReason() env-var contract and the dry behavior in CI/
// non-repo contexts. We invoke the real script as a child process so the
// test exercises the exact code path npm install would take.
//
// Assertions:
// - CI=true prints the "CI — skipping" reason and exits 0.
// - CI=1 also triggers the CI skip.
// - SKIP_POSTINSTALL_HOOKS=1 prints the SKIP_POSTINSTALL_HOOKS reason.
// - NPM_CONFIG_IGNORE_SCRIPTS=true prints the ignore-scripts reason.
// - NPM_CONFIG_IGNORE_SCRIPTS=1 also triggers.
// - npm_config_ignore_scripts=true (lowercase; npm's canonical form) also
// triggers the ignore-scripts skip reason.
// - HUSKY=0 prints the husky reason.
// - Running in a non-git directory prints "not a git work tree — skipping"
// and exits 0 without attempting to configure hooks.
// - All skip triggers exit 0 (never fail npm install).
//
// The tests DO NOT modify the live repo's git config: each case either uses
// a skip trigger (which never calls `git config`), or exercises non-git
// behavior via a copied script inside a freshly-created tempdir.
//
// Node's built-in assert + child_process means zero external test deps.
;
const assert = require("assert");
const { spawnSync } = require("child_process");
const fs = require("fs");
const os = require("os");
const path = require("path");
const repoRoot = path.resolve(__dirname, "..", "..");
const scriptPath = path.join(repoRoot, "scripts", "postinstall-hooks.js");
let passed = 0;
let failed = 0;
const failedTests = [];
function runTest(name, fn) {
try {
fn();
console.log(` [PASS] ${name}`);
passed++;
} catch (err) {
console.log(` [FAIL] ${name}`);
console.log(` ${err.message}`);
if (err.stack) {
console.log(` ${err.stack.split("\n").slice(1, 3).join("\n ")}`);
}
failed++;
failedTests.push(name);
}
}
function runScriptAtPath(targetScriptPath, env, cwd) {
// Clear the inherited env to isolate each case — otherwise a test for
// `CI=true` could accidentally also have HUSKY=0 set from the parent
// shell and mask the actual trigger path. We start from an empty object
// and only add PATH (so `node` / `git` resolve).
const isolatedEnv = Object.assign(
{
PATH: process.env.PATH || "",
HOME: process.env.HOME || os.tmpdir(),
// Windows requires SYSTEMROOT for node to boot.
SystemRoot: process.env.SystemRoot || process.env.SYSTEMROOT || ""
},
env || {}
);
const result = spawnSync(process.execPath, [targetScriptPath], {
cwd: cwd || repoRoot,
env: isolatedEnv,
encoding: "utf8"
});
return {
status: result.status,
stdout: result.stdout || "",
stderr: result.stderr || ""
};
}
function runScript(env, cwd) {
return runScriptAtPath(scriptPath, env, cwd);
}
function createCopiedRepoHarness(prefix) {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
const scriptsDir = path.join(tempDir, "scripts");
fs.mkdirSync(scriptsDir);
fs.mkdirSync(path.join(tempDir, ".githooks"));
const copiedScript = path.join(scriptsDir, "postinstall-hooks.js");
fs.copyFileSync(scriptPath, copiedScript);
const init = spawnSync("git", ["init", "-q"], {
cwd: tempDir,
encoding: "utf8"
});
assert.strictEqual(init.status, 0, `git init failed: ${init.stderr || ""}`);
return { tempDir, copiedScript };
}
console.log("Testing scripts/postinstall-hooks.js skip triggers...");
console.log("\n Section: Documented skip triggers");
runTest("Pass_CiTrueSkips", () => {
const r = runScript({ CI: "true" });
assert.strictEqual(r.status, 0, `exit ${r.status} (stderr: ${r.stderr})`);
assert.ok(
r.stdout.includes("CI=true") && r.stdout.includes("skipping"),
`stdout did not contain CI skip reason. stdout: ${r.stdout}`
);
});
runTest("Pass_CiOneSkips", () => {
const r = runScript({ CI: "1" });
assert.strictEqual(r.status, 0, `exit ${r.status}`);
assert.ok(
r.stdout.includes("CI=true") && r.stdout.includes("skipping"),
`stdout did not contain CI skip reason. stdout: ${r.stdout}`
);
});
runTest("Pass_SkipPostinstallHooksOneSkips", () => {
const r = runScript({ SKIP_POSTINSTALL_HOOKS: "1" });
assert.strictEqual(r.status, 0, `exit ${r.status}`);
assert.ok(
r.stdout.includes("SKIP_POSTINSTALL_HOOKS=1"),
`stdout did not contain SKIP_POSTINSTALL_HOOKS skip reason. stdout: ${r.stdout}`
);
});
runTest("Pass_NpmConfigIgnoreScriptsTrueSkips", () => {
const r = runScript({ NPM_CONFIG_IGNORE_SCRIPTS: "true" });
assert.strictEqual(r.status, 0, `exit ${r.status}`);
assert.ok(
r.stdout.includes("npm_config_ignore_scripts=true"),
`stdout did not contain npm_config_ignore_scripts skip reason. stdout: ${r.stdout}`
);
});
runTest("Pass_NpmConfigIgnoreScriptsOneSkips", () => {
const r = runScript({ NPM_CONFIG_IGNORE_SCRIPTS: "1" });
assert.strictEqual(r.status, 0, `exit ${r.status}`);
assert.ok(
r.stdout.includes("npm_config_ignore_scripts=1"),
`stdout did not contain npm_config_ignore_scripts=1 skip reason. stdout: ${r.stdout}`
);
});
runTest("Pass_NpmConfigIgnoreScriptsLowercaseTrueSkips", () => {
// npm's canonical export when it processes lifecycle scripts is the
// lowercase form. We must honor this spelling just as faithfully as
// the uppercase manual-export form.
const r = runScript({ npm_config_ignore_scripts: "true" });
assert.strictEqual(r.status, 0, `exit ${r.status}`);
assert.ok(
r.stdout.includes("npm_config_ignore_scripts=true"),
`stdout did not contain npm_config_ignore_scripts (lowercase) skip reason. stdout: ${r.stdout}`
);
});
runTest("Pass_HuskyZeroSkips", () => {
const r = runScript({ HUSKY: "0" });
assert.strictEqual(r.status, 0, `exit ${r.status}`);
assert.ok(
r.stdout.includes("HUSKY=0"),
`stdout did not contain HUSKY=0 skip reason. stdout: ${r.stdout}`
);
});
runTest("Pass_NpmConfigIgnoreScriptsFalseDoesNotSkip", () => {
// Value `"false"` must NOT trigger the skip — the header explicitly
// documents accepting only `true` / `1`. We assert the CI skip still
// happens (because CI is set), so the test decouples from actual
// git state; the check is that NPM_CONFIG_IGNORE_SCRIPTS=false on its
// own is not a recognized skip value.
const r = runScript({ CI: "true", NPM_CONFIG_IGNORE_SCRIPTS: "false" });
assert.strictEqual(r.status, 0, `exit ${r.status}`);
// The reason emitted must be CI, not npm_config_ignore_scripts=false.
assert.ok(
r.stdout.includes("CI=true"),
`expected CI skip reason when NPM_CONFIG_IGNORE_SCRIPTS=false; stdout: ${r.stdout}`
);
assert.ok(
!r.stdout.includes("npm_config_ignore_scripts=false"),
`NPM_CONFIG_IGNORE_SCRIPTS=false must not be a recognized skip value; stdout: ${r.stdout}`
);
});
runTest("Pass_HuskyOneDoesNotSkip", () => {
// HUSKY=1 (or any non-"0" value) must NOT trigger the skip. Same
// pattern as above: ride on the CI skip to keep the test hermetic.
const r = runScript({ CI: "true", HUSKY: "1" });
assert.strictEqual(r.status, 0, `exit ${r.status}`);
assert.ok(
r.stdout.includes("CI=true"),
`expected CI skip reason when HUSKY=1; stdout: ${r.stdout}`
);
assert.ok(
!r.stdout.includes("HUSKY=1"),
`HUSKY=1 must not be a recognized skip value; stdout: ${r.stdout}`
);
});
console.log("\n Section: hooksPath idempotency normalization");
runTest("Pass_EquivalentHooksPathFormsAreTreatedAsInstalled", () => {
const harness = createCopiedRepoHarness("postinstall-hooks-equivalent-");
const equivalentForms = [".githooks/", "./.githooks", ".\\.githooks\\", " .githooks/ "];
try {
for (const value of equivalentForms) {
const setResult = spawnSync("git", ["config", "core.hooksPath", value], {
cwd: harness.tempDir,
encoding: "utf8"
});
assert.strictEqual(
setResult.status,
0,
`failed setting core.hooksPath=${value}: ${setResult.stderr || ""}`
);
const r = runScriptAtPath(harness.copiedScript, {}, harness.tempDir);
assert.strictEqual(r.status, 0, `exit ${r.status} for value ${value}`);
assert.ok(
!r.stdout.includes("leaving unchanged"),
`equivalent value ${value} should not be treated as custom hooksPath. stdout: ${r.stdout}`
);
assert.ok(
!r.stdout.includes("git hooks installed"),
`equivalent value ${value} should be a no-op, not a reinstall. stdout: ${r.stdout}`
);
}
} finally {
fs.rmSync(harness.tempDir, { recursive: true, force: true });
}
});
runTest("Pass_CustomHooksPathStillLeftUnchanged", () => {
const harness = createCopiedRepoHarness("postinstall-hooks-custom-");
try {
const setResult = spawnSync("git", ["config", "core.hooksPath", ".husky"], {
cwd: harness.tempDir,
encoding: "utf8"
});
assert.strictEqual(setResult.status, 0, `failed setting custom hooksPath: ${setResult.stderr}`);
const r = runScriptAtPath(harness.copiedScript, {}, harness.tempDir);
assert.strictEqual(r.status, 0, `exit ${r.status}`);
assert.ok(
r.stdout.includes('core.hooksPath is ".husky"') && r.stdout.includes("leaving unchanged"),
`custom hooksPath should be preserved with warning. stdout: ${r.stdout}`
);
} finally {
fs.rmSync(harness.tempDir, { recursive: true, force: true });
}
});
console.log("\n Section: Non-repo execution (safety net)");
runTest("Pass_NonGitDirectoryEmitsNotWorkTreeSkip", () => {
// Fully exercise the "not a git work tree — skipping" branch by
// copying the script into a fresh tempdir whose parent is NOT a git
// repo. The script's repoRoot is path.resolve(__dirname, "..") — so
// we create <temp>/scripts/postinstall-hooks.js AND a sibling
// <temp>/.githooks directory (so the "no .githooks" branch does not
// fire first), and then invoke the copied script. Because <temp> is
// not a git work tree, we should see the NOT_GIT skip log.
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "postinstall-hooks-nogit-"));
try {
const scriptsDir = path.join(tempDir, "scripts");
fs.mkdirSync(scriptsDir);
fs.mkdirSync(path.join(tempDir, ".githooks"));
const copiedScript = path.join(scriptsDir, "postinstall-hooks.js");
fs.copyFileSync(scriptPath, copiedScript);
// Invoke the COPIED script from the tempdir. We must explicitly clear
// any inherited skip env vars that might short-circuit the isGitRepo
// branch (the parent shell may have set CI=true when running under
// `npm run test`).
const isolatedEnv = {
PATH: process.env.PATH || "",
HOME: process.env.HOME || os.tmpdir(),
SystemRoot: process.env.SystemRoot || process.env.SYSTEMROOT || ""
};
const result = spawnSync(process.execPath, [copiedScript], {
cwd: tempDir,
env: isolatedEnv,
encoding: "utf8"
});
assert.strictEqual(result.status, 0, `exit ${result.status} (stderr: ${result.stderr})`);
assert.ok(
result.stdout.includes("not a git work tree"),
`expected "not a git work tree" skip; stdout: ${result.stdout}`
);
assert.ok(
!result.stdout.includes("git hooks installed"),
`must not install hooks in a non-git tempdir; stdout: ${result.stdout}`
);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
console.log("");
console.log(`Tests passed: ${passed}`);
console.log(`Tests failed: ${failed}`);
if (failedTests.length > 0) {
console.log("Failed tests:");
for (const t of failedTests) {
console.log(` - ${t}`);
}
}
process.exit(failed === 0 ? 0 : 1);