- Added a proposal for depth surface strip cleanup, detailing the rationale and changes to the depth plane and cutout nodes. - Created specifications for the new cleanup requirements, including scenarios for strip removal and boundary edge detection. - Developed tasks for implementing shared strip detection logic and integrating new interfaces for depth nodes. - Established a new CLI structure for organizing node groups and model commands, replacing old entry points with a more intuitive command structure. - Introduced a prototype for panorama surface generation, including requirements for distance reconstruction and geometry node surfaces. - Added comprehensive tests for depth strip cleanup functionality, ensuring correct behavior for various mesh topologies and edge cases.
139 lines
4.8 KiB
Python
139 lines
4.8 KiB
Python
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()
|