Files
AnyImage/tests/tools/nodes/test_cli.py
nan 27ca25224a feat: Implement depth surface strip cleanup and organize tool CLI
- 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.
2026-09-10 00:53:43 +08:00

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()