Files
IfcOpenShell/src/bonsai/test/tool/test_blender.py
T
Gorgious56 251157f8d4 Fix np_frombuffer_legacy length-vs-dtype check
The check `len(bytedata) == n * 2` was wrong: float64 is 8 bytes per
element, not 2. Legacy float64 checksums fell through to the float32
reader and produced a (2n,)-shaped array, breaking is_moved() and
is_camera_moved() with `ValueError: operands could not be broadcast`
on .blend files saved by Blender <5.0.

Adds a parametrized regression test covering both n=3 (translation)
and n=9 (rotation) for both dtypes.

Generated with the assistance of an AI coding tool.
2026-06-10 17:37:41 +02:00

186 lines
6.6 KiB
Python

# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
#
# This file was modified with the assistance of an AI coding tool.
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING
import bpy
import ifcopenshell
import numpy as np
import pytest
import bonsai
import bonsai.core.tool
import bonsai.tool as tool
from bonsai.tool.blender import Blender as subject
from test.bim.bootstrap import NewFile
if TYPE_CHECKING:
import bpy.stub_internal.rna_enums as rna_enums
class TestImplementsTool(NewFile):
def test_run(self):
assert isinstance(subject(), bonsai.core.tool.Blender)
class TestCopyNodeGraph(NewFile):
def test_run(self):
material_to = bpy.data.materials.new("material_to")
tool.Style.set_use_nodes(material_to, True)
assert material_to.node_tree
material_to_nodes = material_to.node_tree.nodes
assert len(material_to_nodes) == 2
for node in material_to_nodes:
material_to_nodes.remove(node)
assert len(material_to_nodes) == 0
material_from = bpy.data.materials.new("material_from")
tool.Style.set_use_nodes(material_from, True)
subject.copy_node_graph(material_to, material_from)
assert len(material_to_nodes) == 2
class TestSortPanelsForRegister(NewFile):
def test_run(self):
items = ["A", "B", "C", "D"]
items_to_parents = {"A": "D", "D": "C", "C": "B"}
sorted_items = subject.sort_panels_for_register(items, items_to_parents)
assert tuple(sorted_items) == ("B", "C", "D", "A")
with pytest.raises(AssertionError):
subject.sort_panels_for_register(items, {"A": "K"})
with pytest.raises(AssertionError):
subject.sort_panels_for_register(items, {"J": "A"})
class TestBlenderErrorMessageExtraction(NewFile):
def test_extract_operator_reports(self) -> None:
ERROR_REPORTS = ["ERROR!!!\nERROR", "ERROR"]
class OBJECT_OT_test_fail_operator(bpy.types.Operator):
bl_idname = "object.test_fail_operator"
bl_label = "Test Fail Operator"
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
self.report({"INFO"}, "Info message.")
subject.report_operator_errors(self, ERROR_REPORTS)
return {"FINISHED"}
bpy.utils.register_class(OBJECT_OT_test_fail_operator)
try:
bpy.ops.object.test_fail_operator()
except RuntimeError as e:
error_reports = subject.extract_error_reports(e)
assert error_reports == ERROR_REPORTS
bpy.utils.unregister_class(OBJECT_OT_test_fail_operator)
def test_ignore_actual_runtime_errors_from_operators(self) -> None:
class OBJECT_OT_test_fail_operator(bpy.types.Operator):
bl_idname = "object.test_fail_operator"
bl_label = "Test Fail Operator"
def execute(self, context):
raise RuntimeError("Intentional runtime error.")
bpy.utils.register_class(OBJECT_OT_test_fail_operator)
try:
bpy.ops.object.test_fail_operator()
except RuntimeError as e:
error_reports = subject.extract_error_reports(e)
assert error_reports == []
bpy.utils.unregister_class(OBJECT_OT_test_fail_operator)
class TestGetSelectedFiles(NewFile):
def test_get_a_single_file(self) -> None:
with tempfile.NamedTemporaryFile() as f:
file = type("", (object,), {"name": f.name})()
assert subject.get_selected_files(Path(f.name).parent, [file]) == [f.name]
def test_get_multiple_files(self) -> None:
with tempfile.NamedTemporaryFile() as f:
with tempfile.NamedTemporaryFile() as g:
file = type("", (object,), {"name": f.name})()
file2 = type("", (object,), {"name": g.name})()
assert subject.get_selected_files(Path(f.name).parent, [file, file2]) == [f.name, g.name]
def test_exclude_directories(self) -> None:
with tempfile.NamedTemporaryFile() as f:
with tempfile.TemporaryDirectory() as d:
file = type("", (object,), {"name": f.name})()
directory = type("", (object,), {"name": d})()
assert subject.get_selected_files(Path(f.name).parent, [file, directory]) == [f.name]
def test_get_relative_paths(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
base_path = Path(tmp_dir)
with tempfile.NamedTemporaryFile(dir=tmp_dir, suffix=".ifc") as f:
tool.Ifc.set_path(str(f.name))
with tempfile.NamedTemporaryFile(dir=tmp_dir) as g:
file = type("", (object,), {"name": g.name})
assert subject.get_selected_files(Path(g.name).parent, [file], use_relative_path=True) == [
Path(g.name).name
]
class TestGetDebugInfo(NewFile):
# Only keys that are safe to set if Bonsai fails to load.
EXPECTED_KEYS = {
"os",
"os_version",
"python_version",
"architecture",
"machine",
"processor",
"blender_version",
"bonsai_version",
"bonsai_commit_hash",
"bonsai_commit_date",
"last_actions",
"last_error",
}
def test_failed_to_load_returns_only_base_keys(self):
info = bonsai.get_debug_info(bonsai_failed_to_load=True)
assert set(info.keys()) == self.EXPECTED_KEYS
class TestNpFrombufferLegacy(NewFile):
"""Decoding ``n`` floats from a buffer must yield a length-``n`` array
regardless of whether the buffer was written as ``float32`` or ``float64``."""
@pytest.mark.parametrize("n", [3, 9])
@pytest.mark.parametrize("dtype", [np.float32, np.float64])
def test_decodes_to_n_elements(self, n, dtype):
data = np.arange(n, dtype=dtype).tobytes()
result = subject.np_frombuffer_legacy(data, n)
assert result.shape == (n,)
np.testing.assert_allclose(result, np.arange(n))