Files
IfcOpenShell/src/bonsai/test/tool/test_blender.py
T
Andrej730 9ed8f3e244 Bonsai - fix missing Bonsai Fatal Error UI
Since we added more data to debug info in fcf5614 Fatal Error itself started to fail and was never displayed due some props being inaccessible during load, should be fixed now.

Possible error that were fixed:
```
  File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 328, in <module>
    print(format_debug_info(get_debug_info()))
                            ~~~~~~~~~~~~~~^^
  File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 117, in get_debug_info
    if bpy.data.is_saved:
       ^^^^^^^^^^^^^^^^^
AttributeError: '_RestrictData' object has no attribute 'is_saved'

Traceback (most recent call last):
  File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 366, in draw
    info = get_debug_info()
  File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 152, in get_debug_info
    bim_props = tool.Blender.get_bim_props()
                ^^^^
NameError: name 'tool' is not defined. Did you mean: 'bool'?

Traceback (most recent call last):
  File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 366, in draw
    info = get_debug_info()
  File "\Blender\5.1\extensions\raw_githubusercontent_com\bonsai\__init__.py", line 141, in get_debug_info
    import bonsai.tool as tool
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\__init__.py", line 355, in <module>
    print(format_debug_info(get_debug_info()))
                            ~~~~~~~~~~~~~~^^
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\__init__.py", line 141, in get_debug_info
    import bonsai.tool as tool
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\tool\__init__.py", line 23, in <module>
    from bonsai.tool.attribute import Attribute
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\tool\attribute.py", line 31, in <module>
    import bonsai.bim.helper as helper
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\bim\__init__.py", line 28, in <module>
    from . import handler, operator, prop, ui
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\bim\handler.py", line 36, in <module>
    from bonsai.bim.module.aggregate.decorator import AggregateDecorator
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\bim\module\aggregate\__init__.py", line 21, in <module>
    from . import operator, prop, ui
  File "\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\bim\module\aggregate\operator.py", line 32, in <module>
    class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
                                                             ^^^^^^^^
AttributeError: partially initialized module 'bonsai.tool' from '\Blender\5.1\extensions\.local\lib\python3.13\site-packages\bonsai\tool\__init__.py' has no attribute 'Ifc' (most likely due to a circular import)
```
2026-03-13 20:26:23 +05:00

170 lines
6.0 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/>.
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING
import bpy
import ifcopenshell
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