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,81 +1,29 @@
import argparse
import os
import shutil
import importlib.util
import subprocess
import sys
import webbrowser
from pathlib import Path
from tempfile import TemporaryDirectory, mkdtemp
from zipfile import ZipFile
from tempfile import mkdtemp
PROJECT_ROOT = Path(__file__).resolve().parents[2]
def project_env_value(name, path=None):
env_path = path or PROJECT_ROOT / ".env"
if not env_path.is_file():
return None
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("export "):
line = line.removeprefix("export ").lstrip()
key, separator, value = line.partition("=")
if separator and key.strip() == name:
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
value = value[1:-1]
return value or None
return None
def blender_path(explicit=None):
candidate = (
explicit
or os.environ.get("BLENDER_BIN")
or project_env_value("BLENDER_BIN")
or shutil.which("blender")
)
if not candidate:
raise RuntimeError(
"Blender not found: pass --blender, set BLENDER_BIN, add it to .env, "
"or add blender to PATH"
)
path = Path(candidate).expanduser()
if not path.is_file():
raise RuntimeError(f"Blender executable does not exist: {path}")
return path
def run(command):
print("+", subprocess.list2cmdline([str(part) for part in command]), flush=True)
subprocess.run(command, cwd=PROJECT_ROOT, check=True)
def run_blender_modules(executable, modules, arguments=()):
with TemporaryDirectory(prefix="anyimage-node-dependencies-") as directory:
for wheel in (PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("scipy-*.whl"):
with ZipFile(wheel) as archive:
archive.extractall(Path(directory) / wheel.name.split("-")[2])
paths = [str(PROJECT_ROOT), str(PROJECT_ROOT / "src"),
str(next((PROJECT_ROOT / "src/anyimage/wheels").glob("blendjob-*.whl")))]
bootstrap = (
f"import sys, runpy; sys.path[:0] = {paths!r}; "
f"sys.path.insert(0, {directory!r} + '/cp' + str(sys.version_info.major) + str(sys.version_info.minor)); "
def require_bpy():
if importlib.util.find_spec("bpy") is None:
raise RuntimeError(
"bpy is required; run with: uv run --group blender node-group ..."
)
for module_name in modules:
run(
(
executable,
"--factory-startup",
"--background",
"--python-exit-code", "1",
"--python-expr", bootstrap + f"sys.argv = {[module_name, *arguments]!r}; "
+ f"runpy.run_module({module_name!r}, run_name='__main__')",
)
)
def run_module(module_name, arguments=()):
run((sys.executable, "-m", module_name, *arguments))
def main():
@@ -87,7 +35,6 @@ def main():
("preview", "Preview node groups from current source"),
):
command = commands.add_parser(name, help=help_text)
command.add_argument("--blender", help="Blender executable; overrides BLENDER_BIN, .env, and PATH")
if name == "preview":
command.add_argument("--output", type=Path, help="Output HTML path; defaults to a temporary directory")
command.add_argument("--no-open", action="store_true", help="Save without opening a browser")
@@ -98,21 +45,22 @@ def main():
if options.command is None:
parser.print_help()
return
executable = blender_path(options.blender)
require_bpy()
if options.command == "preview":
output = (options.output or Path(mkdtemp(prefix="anyimage-node-preview-")) / "nodes.html").resolve()
arguments = ["--output", str(output)]
if options.fragment:
arguments.append("--fragment")
run_blender_modules(executable, ("tools.nodes.preview.export",), arguments)
run_module("tools.nodes.preview.export", arguments)
print(output)
if not options.no_open and not options.fragment:
webbrowser.open(output.as_uri())
return
modules = ("tools.nodes.build", "tools.nodes.check") if options.command == "build" else ("tools.nodes.check",)
run_blender_modules(executable, modules)
for module_name in modules:
run_module(module_name)
if not options.skip_tests:
run((sys.executable, "-m", "pytest"))
run((sys.executable, "-m", "pytest", "tests/nodes", "tests/tools/nodes"))
if __name__ == "__main__":