"""Validation normalization tests; fixtures do not claim Blender coverage.""" from pathlib import Path import tempfile import unittest from wbv2_validation import prepare_validation_command class ValidationCommandTests(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) self.root = Path(temporary.name) for relative in [".emscripten-web", ".emsdk/node/24.19.0_64bit/bin/node", "build_blender_native/bin/blender", "web/node_modules/@playwright/test/cli.js", "check.py", "check.mjs"]: path = self.root / relative path.parent.mkdir(parents=True, exist_ok=True) path.touch() (self.root / ".emsdk/upstream/bin").mkdir(parents=True) def prepare(self, argv, cwd=".", **kwargs): return prepare_validation_command(self.root, argv, cwd, **kwargs) def test_actual_source_build_wrapper_normalizes(self): entries = { "EM_CONFIG": str(self.root / ".emscripten-web"), "EM_LLVM_ROOT": str(self.root / ".emsdk/upstream/bin"), "EM_BINARYEN_ROOT": str(self.root / ".emsdk/upstream"), "EM_NODE_JS": str(self.root / ".emsdk/node/24.19.0_64bit/bin/node"), "EM_LLVM_ADD_VERSION": "", "EM_CLANG_ADD_VERSION": "", } argv = ["env", *[f"{name}={value}" for name, value in entries.items()], "cmake", "--build", "build_web_blender6", "--target", "web_engine", "--parallel", "4"] command, directory, env, unset = self.prepare(argv) self.assertEqual(["cmake", "--build", "build_web_blender6", "--target", "web_engine", "-j", "4"], command) self.assertEqual(entries, env) self.assertEqual(self.root, directory) self.assertEqual([], unset) def test_actual_chromium_wrapper_uses_supervisor_receipt_directory(self): argv = ["env", "-u", "SERVER_OPERATOR_NATIVE_REPORT", f"BLENDER_BIN={self.root}/build_blender_native/bin/blender", "WEB_TEST_PORT=5197", f"WBV2_RECEIPT_DIR={self.root}/tests/golden/old-attempt/cases", "node", "node_modules/@playwright/test/cli.js", "test", "--config", "playwright.config.ts", "tests/e2e/server-operator-native.spec.ts", "--workers=1"] command, directory, env, unset = self.prepare(argv, "web") self.assertEqual("node", command[0]) self.assertEqual(self.root / "web", directory) self.assertEqual({"BLENDER_BIN": str(self.root / "build_blender_native/bin/blender"), "WEB_TEST_PORT": "5197"}, env) self.assertEqual(["SERVER_OPERATOR_NATIVE_REPORT"], unset) self.assertNotIn("WBV2_RECEIPT_DIR", env) def test_worker_cannot_override_supervisor_environment(self): _, _, env, unset = self.prepare([ "env", "WBV2_RECEIPT_DIR=/tmp/stale", "WBV2_DEPLOYED_ORIGIN=http://wrong", "WEB_BUILD_JOBS=1000", "CMAKE_BUILD_PARALLEL_LEVEL=1000", "node", "check.mjs", ]) self.assertEqual({}, env) self.assertEqual([], unset) def test_parallel_flags_respect_resource_budget(self): for program in ["cmake", "ninja", "ctest"]: for flags in [["--parallel", "999"], ["--parallel=999"], ["-j999"], ["-j=999"], ["-j", "0"], ["-j"]]: with self.subTest(program=program, flags=flags): command, _, _, _ = self.prepare([program, *flags], max_build_jobs=3) self.assertEqual([program, "-j", "3"], command) self.assertEqual(["ninja", "-j", "8"], self.prepare(["ninja", "-j100"], max_build_jobs=20)[0]) self.assertEqual(["ninja", "-j", "2"], self.prepare(["ninja", "-j2"])[0]) def test_default_build_jobs_are_explicitly_bounded(self): self.assertEqual(["ninja", "-j", "3"], self.prepare(["ninja"], max_build_jobs=3)[0]) self.assertEqual(["ctest", "-j", "3"], self.prepare(["ctest"], max_build_jobs=3)[0]) self.assertEqual(["cmake", "--build", "build", "-j", "3", "--", "target"], self.prepare(["cmake", "--build", "build", "--", "target"], max_build_jobs=3)[0]) self.assertEqual(["cmake", "--version"], self.prepare(["cmake", "--version"])[0]) def test_rejects_env_injection_options_and_unsets(self): wrappers = [ ["PATH=/tmp"], ["NODE_OPTIONS=--require=/tmp/evil.js"], ["PYTHONPATH=/tmp"], ["LD_PRELOAD=/tmp/evil.so"], ["-i"], ["-S", "node check.mjs"], ["--chdir=/tmp"], ["-u", "WBV2_RECEIPT_DIR"], ["-u", "PATH"], ["WEB_TEST_PORT=5197", "WEB_TEST_PORT=5198"], ["EM_CLANG_ADD_VERSION=evil"], ] for wrapper in wrappers: with self.subTest(wrapper=wrapper), self.assertRaises(ValueError): self.prepare(["env", *wrapper, "node", "check.mjs"]) def test_invalid_ports_and_missing_paths(self): for assignment in ["WEB_TEST_PORT=0", "WEB_TEST_PORT=65536", "WEB_TEST_PORT=abc", "WEB_TEST_PORT=80", "EM_CONFIG=", "EM_CONFIG=check.py", "BLENDER_BIN=/tmp/blender", "EM_NODE_JS=check.py"]: with self.subTest(assignment=assignment), self.assertRaises(ValueError): self.prepare(["env", assignment, "node", "check.mjs"]) def test_toolchain_symlink_escape_is_rejected(self): (self.root / ".emscripten-web").unlink() (self.root / ".emscripten-web").symlink_to("/etc/passwd") with self.assertRaisesRegex(ValueError, "escapes"): self.prepare(["env", f"EM_CONFIG={self.root}/.emscripten-web", "node", "check.mjs"]) def test_preserves_mandatory_validation_guards(self): commands = [ ["bash", "check.py"], ["env", "node", "-e", "check.mjs"], ["node", "--eval=1", "check.mjs"], ["node", "-eprocess.exit(0)", "check.mjs"], ["python3", "-cprint(0)", "check.py"], ["python3", "missing.py"], ["node", "check.mjs", "--project=firefox"], ["npm", "run", "test:webkit"], ["npm", "install"], ["git", "reset", "--hard"], ["env", "env", "node", "check.mjs"], ["env"], ] for command in commands: with self.subTest(command=command), self.assertRaises(ValueError): self.prepare(command) def test_accepts_plain_validation_and_env_option_terminator(self): for command in [["git", "diff", "--check"], ["npm", "run", "test"], ["node", "check.mjs"], ["python3", "check.py"]]: self.assertEqual((command, self.root, {}, []), self.prepare(command)) self.assertEqual((["node", "check.mjs"], self.root, {}, []), self.prepare(["env", "--", "node", "check.mjs"])) def test_rejects_non_argv_input_and_repository_escape(self): for command in ["node check.mjs", [], ["node", ""], ["node", "bad\0arg"]]: with self.subTest(command=command), self.assertRaises(ValueError): self.prepare(command) for cwd in ["../outside", "/tmp", "missing"]: with self.subTest(cwd=cwd), self.assertRaises(ValueError): self.prepare(["npm", "run", "test"], cwd) if __name__ == "__main__": unittest.main()