Skip to content

Commit cd2032e

Browse files
committed
#148 and #149
1 parent 520637d commit cd2032e

11 files changed

Lines changed: 467 additions & 10 deletions

batch_render.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,5 @@ def unregister():
222222
from bpy.utils import unregister_class
223223

224224
for cls in reversed(classes):
225-
unregister_class(cls)
225+
if hasattr(cls, 'bl_rna'):
226+
unregister_class(cls)

camera_controlls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,8 @@ def unregister():
675675
from bpy.utils import unregister_class
676676

677677
for cls in reversed(classes):
678-
unregister_class(cls)
678+
if hasattr(cls, 'bl_rna'):
679+
unregister_class(cls)
679680

680681
if load_post_handler in bpy.app.handlers.load_post:
681682
bpy.app.handlers.load_post.remove(load_post_handler)

camera_gizmos.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,4 +281,5 @@ def unregister():
281281
_draw_handler = None
282282

283283
for cls in reversed(classes):
284-
unregister_class(cls)
284+
if hasattr(cls, 'bl_rna'):
285+
unregister_class(cls)

dolly_zoom_modal.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,4 +421,5 @@ def unregister():
421421
from bpy.utils import unregister_class
422422

423423
for cls in reversed(classes):
424-
unregister_class(cls)
424+
if hasattr(cls, 'bl_rna'):
425+
unregister_class(cls)

keymap.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,5 @@ def unregister():
209209

210210
from bpy.utils import unregister_class
211211
for cls in reversed(classes):
212-
unregister_class(cls)
212+
if hasattr(cls, 'bl_rna'):
213+
unregister_class(cls)

pie_menu.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,4 +410,5 @@ def unregister():
410410
from bpy.utils import unregister_class
411411

412412
for cls in reversed(classes):
413-
unregister_class(cls)
413+
if hasattr(cls, 'bl_rna'):
414+
unregister_class(cls)

preferences.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ def _apply():
4747

4848
def update_panel_category(self, context):
4949
"""Update panel tab for simple export"""
50+
from .ui import VIEW3D_PT_SimpleCameraManager as _panel_cls
5051
panels = [
51-
VIEW3D_PT_SimpleCameraManager,
52+
_panel_cls,
5253
]
5354

5455
for panel in panels:
@@ -415,4 +416,5 @@ def unregister():
415416
from bpy.utils import unregister_class
416417

417418
for cls in reversed(classes):
418-
unregister_class(cls)
419+
if hasattr(cls, 'bl_rna'):
420+
unregister_class(cls)

tests/run_tests.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"""
2+
Test runner for simple_camera_manager with a formatted output overview.
3+
4+
Usage:
5+
python3.12 tests/run_tests.py
6+
"""
7+
8+
import sys
9+
import time
10+
import unittest
11+
from pathlib import Path
12+
13+
# ── ANSI colours ─────────────────────────────────────────────────────────────
14+
GREEN = "\033[32m"
15+
RED = "\033[31m"
16+
YELLOW = "\033[33m"
17+
CYAN = "\033[36m"
18+
BOLD = "\033[1m"
19+
DIM = "\033[2m"
20+
RESET = "\033[0m"
21+
22+
PASS_MARK = f"{GREEN}{RESET}"
23+
FAIL_MARK = f"{RED}{RESET}"
24+
ERROR_MARK = f"{YELLOW}!{RESET}"
25+
26+
27+
class GroupedResult(unittest.TestResult):
28+
"""Collect results grouped by TestCase class for a structured overview."""
29+
30+
def __init__(self):
31+
super().__init__()
32+
self.results: dict[str, list[tuple[str, str, str | None]]] = {}
33+
# (method_name, status, detail)
34+
self._start_times: dict[str, float] = {}
35+
36+
def _group(self, test) -> str:
37+
return type(test).__name__
38+
39+
def startTest(self, test):
40+
super().startTest(test)
41+
self._start_times[test.id()] = time.perf_counter()
42+
self.results.setdefault(self._group(test), [])
43+
44+
def _elapsed(self, test) -> str:
45+
ms = (time.perf_counter() - self._start_times.get(test.id(), 0)) * 1000
46+
return f"{ms:.1f} ms"
47+
48+
def addSuccess(self, test):
49+
super().addSuccess(test)
50+
self.results[self._group(test)].append(
51+
(test._testMethodName, "pass", self._elapsed(test))
52+
)
53+
54+
def addFailure(self, test, err):
55+
super().addFailure(test, err)
56+
self.results[self._group(test)].append(
57+
(test._testMethodName, "fail", self._formatErr(err))
58+
)
59+
60+
def addError(self, test, err):
61+
super().addError(test, err)
62+
self.results[self._group(test)].append(
63+
(test._testMethodName, "error", self._formatErr(err))
64+
)
65+
66+
def addSkip(self, test, reason):
67+
super().addSkip(test, reason)
68+
self.results[self._group(test)].append(
69+
(test._testMethodName, "skip", reason)
70+
)
71+
72+
@staticmethod
73+
def _formatErr(err) -> str:
74+
import traceback
75+
return "".join(traceback.format_exception(*err)).strip()
76+
77+
78+
# ── Friendly method-name formatting ──────────────────────────────────────────
79+
80+
def _friendly(method_name: str) -> str:
81+
"""'test_register_order' → 'register order'"""
82+
return method_name.removeprefix("test_").replace("_", " ")
83+
84+
85+
# ── Friendly class-name → section header ─────────────────────────────────────
86+
87+
_SECTION_LABELS = {
88+
"TestRegister": "register()",
89+
"TestUnregister": "unregister()",
90+
"TestRegisterUnregisterCycle": "register() → unregister() cycle",
91+
"TestReload": "Addon reload (bpy already in namespace)",
92+
}
93+
94+
95+
def _section_label(class_name: str) -> str:
96+
return _SECTION_LABELS.get(class_name, class_name)
97+
98+
99+
# ── Printer ───────────────────────────────────────────────────────────────────
100+
101+
def _print_report(result: GroupedResult, elapsed_total: float):
102+
width = 70
103+
print()
104+
print(f"{BOLD}{'━' * width}{RESET}")
105+
print(f"{BOLD} simple_camera_manager – Registration Tests{RESET}")
106+
print(f"{BOLD}{'━' * width}{RESET}")
107+
108+
totals = {"pass": 0, "fail": 0, "error": 0, "skip": 0}
109+
110+
for class_name, entries in result.results.items():
111+
label = _section_label(class_name)
112+
group_pass = sum(1 for _, s, _ in entries if s == "pass")
113+
group_total = len(entries)
114+
status_badge = (
115+
f"{GREEN}{group_pass}/{group_total}{RESET}"
116+
if group_pass == group_total
117+
else f"{RED}{group_pass}/{group_total}{RESET}"
118+
)
119+
print(f"\n {BOLD}{CYAN}{label}{RESET} {DIM}({status_badge}{DIM}){RESET}")
120+
print(f" {'─' * (width - 2)}")
121+
122+
for method_name, status, detail in entries:
123+
if status == "pass":
124+
mark = PASS_MARK
125+
totals["pass"] += 1
126+
elif status == "fail":
127+
mark = FAIL_MARK
128+
totals["fail"] += 1
129+
elif status == "error":
130+
mark = ERROR_MARK
131+
totals["error"] += 1
132+
else:
133+
mark = f"{YELLOW}{RESET}"
134+
totals["skip"] += 1
135+
136+
label_text = _friendly(method_name)
137+
# Right-align timing if it's a pass (detail holds elapsed string)
138+
if status == "pass" and detail:
139+
timing = f"{DIM}{detail}{RESET}"
140+
pad = width - 6 - len(label_text) - len(detail)
141+
print(f" {mark} {label_text}{' ' * max(pad, 1)}{timing}")
142+
else:
143+
print(f" {mark} {label_text}")
144+
if detail and status in ("fail", "error"):
145+
for line in detail.splitlines():
146+
print(f" {RED}{line}{RESET}")
147+
148+
# ── Summary bar ──────────────────────────────────────────────────────────
149+
print()
150+
print(f" {'─' * (width - 2)}")
151+
total = sum(totals.values())
152+
passed = totals["pass"]
153+
failed = totals["fail"] + totals["error"]
154+
155+
if failed == 0:
156+
outcome = f"{GREEN}{BOLD}ALL {passed} TESTS PASSED{RESET}"
157+
else:
158+
outcome = f"{RED}{BOLD}{failed} FAILED | {passed} passed{RESET}"
159+
160+
skipped = f" {YELLOW}{totals['skip']} skipped{RESET}" if totals["skip"] else ""
161+
elapsed_str = f"{elapsed_total * 1000:.0f} ms"
162+
print(f" {outcome}{skipped} {DIM}({elapsed_str}){RESET}")
163+
print(f"{BOLD}{'━' * width}{RESET}")
164+
print()
165+
166+
167+
# ── Entry point ───────────────────────────────────────────────────────────────
168+
169+
def main():
170+
loader = unittest.TestLoader()
171+
suite = loader.discover(
172+
start_dir=str(Path(__file__).parent),
173+
pattern="test_*.py",
174+
)
175+
176+
result = GroupedResult()
177+
t0 = time.perf_counter()
178+
suite.run(result)
179+
elapsed = time.perf_counter() - t0
180+
181+
_print_report(result, elapsed)
182+
183+
sys.exit(0 if result.wasSuccessful() else 1)
184+
185+
186+
if __name__ == "__main__":
187+
main()

0 commit comments

Comments
 (0)