Add comprehensive tests for image processing nodes and refactor node imports

- Introduced new test files for image layer, image plane, normal scale, object normal rotation, shadeless, and temporary attributes.
- Implemented tests to validate the behavior of alpha curves, depth layers, and normal scales in various rendering engines.
- Refactored import paths for node groups to align with the new directory structure.
- Added checks for temporary attribute lifetimes and ensured proper cleanup without warnings.
- Enhanced CLI commands to require the Blender group and validate the presence of generated node assets before packaging.
- Updated packaging scripts to enforce the existence of necessary assets, preventing build failures.
This commit is contained in:
2026-09-19 21:23:19 +08:00
parent 2b3c0194a6
commit 9debab5c0c
73 changed files with 500 additions and 217 deletions

View File

@@ -1,5 +1,6 @@
from tests.support.paths import PROJECT_ROOT
import importlib.util
import subprocess
from unittest.mock import patch
import pytest
@@ -17,84 +18,75 @@ def load_launcher():
return module
def test_blender_path_prefers_explicit_path(tmp_path):
def test_commands_require_the_blender_group(monkeypatch):
launcher = load_launcher()
executable = tmp_path / "blender"
executable.touch()
with patch.dict("os.environ", {"BLENDER_BIN": "ignored"}):
assert launcher.blender_path(executable) == executable
monkeypatch.setattr("sys.argv", ["node-group", "check"])
with patch.object(launcher.importlib.util, "find_spec", return_value=None):
with pytest.raises(RuntimeError, match="--group blender"):
launcher.main()
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()
@pytest.mark.parametrize("arguments,modules,run_tests", [
(["build"], ("tools.nodes.build", "tools.nodes.check"), True),
(["check", "--skip-tests"], ("tools.nodes.check",), False),
(["preview", "--fragment"], ("tools.nodes.preview.export",), False),
@pytest.mark.parametrize("arguments,commands", [
(["build"], [
("tools.nodes.build", ()),
("tools.nodes.check", ()),
("pytest", ("tests/nodes", "tests/tools/nodes")),
]),
(["check", "--skip-tests"], [("tools.nodes.check", ())]),
(["preview", "--fragment"], [
("tools.nodes.preview.export", "preview"),
]),
])
def test_commands_dispatch_requested_work(monkeypatch, tmp_path, arguments, modules, run_tests):
def test_commands_dispatch_requested_work(monkeypatch, tmp_path, arguments, commands):
launcher = load_launcher()
output = tmp_path / "node groups.html"
command = ["node-group", *arguments]
if arguments[0] == "preview":
command += ["--output", str(output)]
monkeypatch.setattr("sys.argv", command)
monkeypatch.setattr(launcher, "blender_path", lambda _: "blender")
with (
patch.object(launcher, "run_blender_modules") as blender,
patch.object(launcher, "require_bpy"),
patch.object(launcher, "run_module") as run_module,
patch.object(launcher, "run") as run,
patch.object(launcher.webbrowser, "open") as open_browser,
):
launcher.main()
if arguments[0] == "preview":
blender.assert_called_once_with("blender", modules, ["--output", str(output), "--fragment"])
run_module.assert_called_once_with(
"tools.nodes.preview.export",
["--output", str(output), "--fragment"],
)
open_browser.assert_not_called()
else:
blender.assert_called_once_with("blender", modules)
if run_tests:
run.assert_called_once_with((launcher.sys.executable, "-m", "pytest"))
expected_modules = [item[0] for item in commands if item[0] != "pytest"]
assert [call.args[0] for call in run_module.call_args_list] == expected_modules
if any(item[0] == "pytest" for item in commands):
run.assert_called_once_with(
(
launcher.sys.executable,
"-m",
"pytest",
"tests/nodes",
"tests/tools/nodes",
)
)
else:
run.assert_not_called()
def test_module_failure_stops_later_steps(monkeypatch):
launcher = load_launcher()
monkeypatch.setattr("sys.argv", ["node-group", "build"])
with (
patch.object(launcher, "require_bpy"),
patch.object(
launcher,
"run_module",
side_effect=subprocess.CalledProcessError(1, "build"),
) as run_module,
patch.object(launcher, "run") as run,
):
with pytest.raises(subprocess.CalledProcessError):
launcher.main()
run_module.assert_called_once_with("tools.nodes.build")
run.assert_not_called()