from tests.support.paths import PROJECT_ROOT import importlib.util from unittest.mock import patch import pytest SCRIPT_PATH = PROJECT_ROOT / "tools" / "nodes" / "cli.py" def load_launcher(): specification = importlib.util.spec_from_file_location( "node_asset_launcher", SCRIPT_PATH, ) module = importlib.util.module_from_spec(specification) specification.loader.exec_module(module) return module def test_blender_path_prefers_explicit_path(tmp_path): launcher = load_launcher() executable = tmp_path / "blender" executable.touch() with patch.dict("os.environ", {"BLENDER_BIN": "ignored"}): assert launcher.blender_path(executable) == executable def test_blender_path_reads_environment(tmp_path): launcher = load_launcher() executable = tmp_path / "blender" executable.touch() with patch.dict("os.environ", {"BLENDER_BIN": str(executable)}): assert launcher.blender_path() == executable def test_blender_path_reads_project_dotenv(tmp_path): launcher = load_launcher() executable = tmp_path / "Blender 4.5" / "blender.exe" executable.parent.mkdir() executable.touch() env_path = tmp_path / ".env" env_path.write_text( f'BLENDER_BIN="{executable}"\n', encoding="utf-8", ) with patch.dict("os.environ", {}, clear=True), patch.object( launcher, "PROJECT_ROOT", tmp_path, ): assert launcher.blender_path() == executable def test_blender_path_reports_missing_configuration(tmp_path): launcher = load_launcher() with patch.dict("os.environ", {}, clear=True), patch.object( launcher.shutil, "which", return_value=None, ), patch.object( launcher, "PROJECT_ROOT", tmp_path, ): with pytest.raises(RuntimeError, match="Blender not found"): launcher.blender_path() def test_launcher_runs_asset_build_and_validations_before_tests(monkeypatch, tmp_path): launcher = load_launcher() executable = tmp_path / "blender" executable.touch() calls = [] monkeypatch.setattr(launcher, "blender_path", lambda _: executable) monkeypatch.setattr(launcher, "run", calls.append) monkeypatch.setattr("sys.argv", ["node-group", "build"]) launcher.main() assert len(calls) == 3 assert "runpy.run_module('tools.nodes.build', run_name='__main__')" in calls[0][-1] assert "runpy.run_module('tools.nodes.check', run_name='__main__')" in calls[1][-1] assert all(command[1:6] == ("--factory-startup", "--background", "--python-exit-code", "1", "--python-expr") for command in calls[:-1]) assert calls[-1][1:] == ("-m", "pytest") @pytest.mark.parametrize("skip_tests", [False, True]) def test_check_validates_without_building(monkeypatch, skip_tests): launcher = load_launcher() calls = [] monkeypatch.setattr(launcher, "blender_path", lambda _: "blender") monkeypatch.setattr(launcher, "run", calls.append) monkeypatch.setattr("sys.argv", ["node-group", "check", *(["--skip-tests"] if skip_tests else [])]) launcher.main() assert len(calls) == (1 if skip_tests else 2) assert "tools.nodes.check" in calls[0][-1] assert "tools.nodes.build" not in calls[0][-1] def test_build_stops_after_blender_failure(monkeypatch): launcher = load_launcher() monkeypatch.setattr(launcher, "blender_path", lambda _: "blender") monkeypatch.setattr("sys.argv", ["node-group", "build"]) with patch.object(launcher, "run", side_effect=RuntimeError("failed")) as run: with pytest.raises(RuntimeError, match="failed"): launcher.main() assert run.call_count == 1 @pytest.mark.parametrize("arguments,opens", [([], True), (["--no-open"], False), (["--fragment"], False)]) def test_preview_passes_isolated_arguments_and_opens_result(monkeypatch, tmp_path, arguments, opens): launcher = load_launcher() output = tmp_path / "node groups.html" calls = [] monkeypatch.setattr(launcher, "blender_path", lambda _: "blender") monkeypatch.setattr(launcher, "run", calls.append) monkeypatch.setattr("sys.argv", ["node-group", "preview", "--output", str(output), *arguments]) with patch.object(launcher.webbrowser, "open") as open_browser: launcher.main() assert len(calls) == 1 expression = calls[0][-1] import ast assignment = next( node for node in ast.parse(expression).body if isinstance(node, ast.Assign) and ast.unparse(node.targets[0]) == "sys.argv" ) assert ast.literal_eval(assignment.value) == [ "tools.nodes.preview.export", "--output", str(output), *(["--fragment"] if "--fragment" in arguments else []), ] if opens: open_browser.assert_called_once_with(output.as_uri()) else: open_browser.assert_not_called()