mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-19 19:54:07 +00:00
251157f8d4
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.
186 lines
6.6 KiB
Python
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))
|