feat(linter): Develop custom AST linter for ImGui scopes
This commit is contained in:
+1
-1
@@ -58,7 +58,7 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
*Link: [./tracks/gui_architecture_refinement_20260512/](./tracks/gui_architecture_refinement_20260512/)*
|
*Link: [./tracks/gui_architecture_refinement_20260512/](./tracks/gui_architecture_refinement_20260512/)*
|
||||||
*Goal: Reduce nesting and compactness of ImGui code in `gui_2.py`, and formalize ImGui Defer patterns.*
|
*Goal: Reduce nesting and compactness of ImGui code in `gui_2.py`, and formalize ImGui Defer patterns.*
|
||||||
|
|
||||||
13. [ ] **Track: GUI Refactor & Stabilization**
|
13. [~] **Track: GUI Refactor & Stabilization**
|
||||||
*Link: [./tracks/gui_refactor_stabilization_20260512/](./tracks/gui_refactor_stabilization_20260512/)*
|
*Link: [./tracks/gui_refactor_stabilization_20260512/](./tracks/gui_refactor_stabilization_20260512/)*
|
||||||
*Goal: Refactor gui_2.py to fix regressions and enforce better imgui scoping patterns.*
|
*Goal: Refactor gui_2.py to fix regressions and enforce better imgui scoping patterns.*
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Implementation Plan: GUI Refactor & Stabilization
|
# Implementation Plan: GUI Refactor & Stabilization
|
||||||
|
|
||||||
## Phase 1: Linting & Verification Foundations
|
## Phase 1: Linting & Verification Foundations
|
||||||
- [ ] Task: Develop custom AST linter for ImGui scope/indentation in `scripts/check_imgui_scopes.py`.
|
- [~] Task: Develop custom AST linter for ImGui scope/indentation in `scripts/check_imgui_scopes.py`.
|
||||||
- [ ] Task: Write tests for the new AST linter to ensure it catches unclosed scopes and indentation mismatches.
|
- [ ] Task: Write tests for the new AST linter to ensure it catches unclosed scopes and indentation mismatches.
|
||||||
- [ ] Task: Expand API hooks in `src/api_hooks.py` to better simulate complex UI interactions (e.g., specific widget clicks, drag operations).
|
- [ ] Task: Expand API hooks in `src/api_hooks.py` to better simulate complex UI interactions (e.g., specific widget clicks, drag operations).
|
||||||
- [ ] Task: Write tests for the new API hooks.
|
- [ ] Task: Write tests for the new API hooks.
|
||||||
|
|||||||
+3
-3
@@ -35,7 +35,7 @@ separate_external_tools = false
|
|||||||
"Project Settings" = true
|
"Project Settings" = true
|
||||||
"Files & Media" = true
|
"Files & Media" = true
|
||||||
"AI Settings" = true
|
"AI Settings" = true
|
||||||
"MMA Dashboard" = false
|
"MMA Dashboard" = true
|
||||||
"Task DAG" = false
|
"Task DAG" = false
|
||||||
"Usage Analytics" = true
|
"Usage Analytics" = true
|
||||||
"Tier 1" = false
|
"Tier 1" = false
|
||||||
@@ -50,8 +50,8 @@ separate_external_tools = false
|
|||||||
"Operations Hub" = true
|
"Operations Hub" = true
|
||||||
Message = false
|
Message = false
|
||||||
Response = false
|
Response = false
|
||||||
"Tool Calls" = false
|
"Tool Calls" = true
|
||||||
Theme = true
|
Theme = false
|
||||||
"Log Management" = false
|
"Log Management" = false
|
||||||
Diagnostics = false
|
Diagnostics = false
|
||||||
"External Tools" = false
|
"External Tools" = false
|
||||||
|
|||||||
+75
-51
@@ -12,7 +12,7 @@ ViewportPos=43,95
|
|||||||
ViewportId=0x78C57832
|
ViewportId=0x78C57832
|
||||||
Size=897,649
|
Size=897,649
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,0
|
DockId=0x00000010,0
|
||||||
|
|
||||||
[Window][Files]
|
[Window][Files]
|
||||||
ViewportPos=3125,170
|
ViewportPos=3125,170
|
||||||
@@ -33,7 +33,7 @@ DockId=0x0000000A,0
|
|||||||
Pos=0,17
|
Pos=0,17
|
||||||
Size=1680,730
|
Size=1680,730
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,0
|
DockId=0x00000010,0
|
||||||
|
|
||||||
[Window][Provider]
|
[Window][Provider]
|
||||||
ViewportPos=43,95
|
ViewportPos=43,95
|
||||||
@@ -41,7 +41,7 @@ ViewportId=0x78C57832
|
|||||||
Pos=0,651
|
Pos=0,651
|
||||||
Size=897,468
|
Size=897,468
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,0
|
DockId=0x00000010,0
|
||||||
|
|
||||||
[Window][Message]
|
[Window][Message]
|
||||||
Pos=475,163
|
Pos=475,163
|
||||||
@@ -54,8 +54,8 @@ Size=1442,1129
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Tool Calls]
|
[Window][Tool Calls]
|
||||||
Pos=488,28
|
Pos=2063,28
|
||||||
Size=1777,1750
|
Size=1777,2132
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000000E,0
|
DockId=0x0000000E,0
|
||||||
|
|
||||||
@@ -75,9 +75,9 @@ DockId=0xAFC85805,2
|
|||||||
|
|
||||||
[Window][Theme]
|
[Window][Theme]
|
||||||
Pos=0,28
|
Pos=0,28
|
||||||
Size=32,1172
|
Size=1652,2132
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,3
|
DockId=0x00000010,3
|
||||||
|
|
||||||
[Window][Text Viewer - Entry #7]
|
[Window][Text Viewer - Entry #7]
|
||||||
Pos=379,324
|
Pos=379,324
|
||||||
@@ -85,9 +85,10 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Diagnostics]
|
[Window][Diagnostics]
|
||||||
Pos=739,582
|
Pos=332,28
|
||||||
Size=1211,713
|
Size=886,1172
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
DockId=0x00000006,3
|
||||||
|
|
||||||
[Window][Context Hub]
|
[Window][Context Hub]
|
||||||
Pos=0,975
|
Pos=0,975
|
||||||
@@ -102,28 +103,28 @@ Collapsed=0
|
|||||||
DockId=0x0000000D,0
|
DockId=0x0000000D,0
|
||||||
|
|
||||||
[Window][Discussion Hub]
|
[Window][Discussion Hub]
|
||||||
Pos=34,28
|
Pos=794,28
|
||||||
Size=1184,1172
|
Size=886,1172
|
||||||
Collapsed=0
|
|
||||||
DockId=0x00000006,0
|
|
||||||
|
|
||||||
[Window][Operations Hub]
|
|
||||||
Pos=0,28
|
|
||||||
Size=32,1172
|
|
||||||
Collapsed=0
|
|
||||||
DockId=0x00000005,2
|
|
||||||
|
|
||||||
[Window][Files & Media]
|
|
||||||
Pos=34,28
|
|
||||||
Size=1184,1172
|
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000006,1
|
DockId=0x00000006,1
|
||||||
|
|
||||||
|
[Window][Operations Hub]
|
||||||
|
Pos=0,28
|
||||||
|
Size=792,1172
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000010,2
|
||||||
|
|
||||||
|
[Window][Files & Media]
|
||||||
|
Pos=794,28
|
||||||
|
Size=886,1172
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000006,0
|
||||||
|
|
||||||
[Window][AI Settings]
|
[Window][AI Settings]
|
||||||
Pos=0,28
|
Pos=0,28
|
||||||
Size=32,1172
|
Size=792,1172
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,0
|
DockId=0x00000010,1
|
||||||
|
|
||||||
[Window][Approve Tool Execution]
|
[Window][Approve Tool Execution]
|
||||||
Pos=3,524
|
Pos=3,524
|
||||||
@@ -131,14 +132,14 @@ Size=416,325
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][MMA Dashboard]
|
[Window][MMA Dashboard]
|
||||||
Pos=1012,28
|
Pos=794,28
|
||||||
Size=668,1172
|
Size=886,1172
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000006,2
|
DockId=0x00000006,2
|
||||||
|
|
||||||
[Window][Log Management]
|
[Window][Log Management]
|
||||||
Pos=949,32
|
Pos=1203,28
|
||||||
Size=1368,1622
|
Size=1040,1710
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000006,2
|
DockId=0x00000006,2
|
||||||
|
|
||||||
@@ -329,8 +330,8 @@ Size=967,499
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Usage Analytics]
|
[Window][Usage Analytics]
|
||||||
Pos=2267,28
|
Pos=3380,28
|
||||||
Size=460,1750
|
Size=460,2132
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000001,0
|
DockId=0x00000001,0
|
||||||
|
|
||||||
@@ -380,9 +381,10 @@ Size=1366,1032
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Shader Editor]
|
[Window][Shader Editor]
|
||||||
Pos=457,710
|
Pos=0,1976
|
||||||
Size=573,280
|
Size=1652,184
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
DockId=0x00000011,0
|
||||||
|
|
||||||
[Window][Text Viewer - list_directory]
|
[Window][Text Viewer - list_directory]
|
||||||
Pos=1376,796
|
Pos=1376,796
|
||||||
@@ -407,9 +409,9 @@ DockId=0x00000006,1
|
|||||||
|
|
||||||
[Window][Project Settings]
|
[Window][Project Settings]
|
||||||
Pos=0,28
|
Pos=0,28
|
||||||
Size=32,1172
|
Size=792,1172
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,1
|
DockId=0x00000010,0
|
||||||
|
|
||||||
[Window][Undo/Redo History]
|
[Window][Undo/Redo History]
|
||||||
Pos=1220,28
|
Pos=1220,28
|
||||||
@@ -627,23 +629,45 @@ Column 2 Width=69
|
|||||||
Column 3 Width=91
|
Column 3 Width=91
|
||||||
Column 4 Width=70
|
Column 4 Width=70
|
||||||
|
|
||||||
|
[Table][0xB17BCA58,3]
|
||||||
|
RefScale=20
|
||||||
|
Column 0 Weight=1.0000
|
||||||
|
Column 1 Width=80
|
||||||
|
Column 2 Width=150
|
||||||
|
|
||||||
|
[Table][0x7804123E,5]
|
||||||
|
RefScale=20
|
||||||
|
Column 0 Width=20
|
||||||
|
Column 1 Weight=1.0000
|
||||||
|
Column 2 Width=27
|
||||||
|
Column 3 Width=36
|
||||||
|
Column 4 Width=45
|
||||||
|
|
||||||
|
[Table][0x09B0112E,3]
|
||||||
|
RefScale=20
|
||||||
|
Column 0 Weight=1.0000
|
||||||
|
Column 1 Width=80
|
||||||
|
Column 2 Width=150
|
||||||
|
|
||||||
[Docking][Data]
|
[Docking][Data]
|
||||||
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
||||||
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
||||||
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
||||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=1680,1172 Split=X
|
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=1680,1172 Split=X
|
||||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2254,1183 Split=X
|
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2254,1183 Split=X
|
||||||
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
||||||
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=1512,858 Split=X Selected=0x8CA2375C
|
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=1512,858 Split=X Selected=0x8CA2375C
|
||||||
DockNode ID=0x00000005 Parent=0x00000007 SizeRef=1199,1681 CentralNode=1 Selected=0x8CA2375C
|
DockNode ID=0x00000005 Parent=0x00000007 SizeRef=792,1681 Split=Y Selected=0x3F1379AF
|
||||||
DockNode ID=0x00000006 Parent=0x00000007 SizeRef=1379,1681 Selected=0x6F2B5B04
|
DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x3F1379AF
|
||||||
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1777,858 Selected=0x1D56B311
|
DockNode ID=0x00000011 Parent=0x00000005 SizeRef=983,184 Selected=0x432BAE4E
|
||||||
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
DockNode ID=0x00000006 Parent=0x00000007 SizeRef=886,1681 Selected=0x6F2B5B04
|
||||||
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=460,1183 Split=X Selected=0x3AEC3498
|
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1777,858 Selected=0x1D56B311
|
||||||
DockNode ID=0x0000000C Parent=0x00000004 SizeRef=916,380 Selected=0x655BC6E9
|
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
||||||
DockNode ID=0x0000000F Parent=0x00000004 SizeRef=281,380 Split=Y Selected=0xDEB547B6
|
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=460,1183 Split=X Selected=0x3AEC3498
|
||||||
DockNode ID=0x00000001 Parent=0x0000000F SizeRef=460,383 Selected=0xDEB547B6
|
DockNode ID=0x0000000C Parent=0x00000004 SizeRef=916,380 Selected=0x655BC6E9
|
||||||
DockNode ID=0x00000002 Parent=0x0000000F SizeRef=460,1312 Selected=0xEFE478AD
|
DockNode ID=0x0000000F Parent=0x00000004 SizeRef=281,380 Split=Y Selected=0xDEB547B6
|
||||||
|
DockNode ID=0x00000001 Parent=0x0000000F SizeRef=460,383 Selected=0xDEB547B6
|
||||||
|
DockNode ID=0x00000002 Parent=0x0000000F SizeRef=460,1312 Selected=0xEFE478AD
|
||||||
|
|
||||||
;;;<<<Layout_655921752_Default>>>;;;
|
;;;<<<Layout_655921752_Default>>>;;;
|
||||||
;;;<<<HelloImGui_Misc>>>;;;
|
;;;<<<HelloImGui_Misc>>>;;;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ dependencies = [
|
|||||||
"pyopengl>=3.1.10",
|
"pyopengl>=3.1.10",
|
||||||
"chromadb>=1.5.8",
|
"chromadb>=1.5.8",
|
||||||
"sentence-transformers>=5.4.1",
|
"sentence-transformers>=5.4.1",
|
||||||
|
"python-defer"
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import ast
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class ImGuiScopeLinter:
|
||||||
|
def __init__(self):
|
||||||
|
self.pairs = {
|
||||||
|
'begin': 'end',
|
||||||
|
'begin_child': 'end_child',
|
||||||
|
'begin_group': 'end_group',
|
||||||
|
'begin_combo': 'end_combo',
|
||||||
|
'begin_main_menu_bar': 'end_main_menu_bar',
|
||||||
|
'begin_menu_bar': 'end_menu_bar',
|
||||||
|
'begin_menu': 'end_menu',
|
||||||
|
'begin_tooltip': 'end_tooltip',
|
||||||
|
'begin_popup': 'end_popup',
|
||||||
|
'begin_popup_modal': 'end_popup',
|
||||||
|
'begin_popup_context_item': 'end_popup',
|
||||||
|
'begin_popup_context_window': 'end_popup',
|
||||||
|
'begin_popup_context_void': 'end_popup',
|
||||||
|
'begin_table': 'end_table',
|
||||||
|
'begin_tab_bar': 'end_tab_bar',
|
||||||
|
'begin_tab_item': 'end_tab_item',
|
||||||
|
'push_style_var': 'pop_style_var',
|
||||||
|
'push_style_color': 'pop_style_color',
|
||||||
|
'push_font': 'pop_font',
|
||||||
|
'push_id': 'pop_id',
|
||||||
|
'push_item_width': 'pop_item_width',
|
||||||
|
'push_text_wrap_pos': 'pop_text_wrap_pos',
|
||||||
|
'push_clip_rect': 'pop_clip_rect',
|
||||||
|
'tree_node': 'tree_pop',
|
||||||
|
'tree_push': 'tree_pop',
|
||||||
|
'indent': 'unindent',
|
||||||
|
}
|
||||||
|
self.starts = set(self.pairs.keys())
|
||||||
|
self.ends = set(self.pairs.values())
|
||||||
|
|
||||||
|
def check_source(self, source_code: str) -> list[str]:
|
||||||
|
try:
|
||||||
|
tree = ast.parse(source_code)
|
||||||
|
except SyntaxError as e:
|
||||||
|
return [f"Syntax error: {e}"]
|
||||||
|
visitor = ImGuiVisitor(self.pairs)
|
||||||
|
visitor.visit(tree)
|
||||||
|
return visitor.errors
|
||||||
|
|
||||||
|
class ImGuiVisitor(ast.NodeVisitor):
|
||||||
|
def __init__(self, pairs):
|
||||||
|
self.pairs = pairs
|
||||||
|
self.stack = []
|
||||||
|
self.errors = []
|
||||||
|
self.starts = set(pairs.keys())
|
||||||
|
self.ends = set(pairs.values())
|
||||||
|
|
||||||
|
def visit_FunctionDef(self, node):
|
||||||
|
saved_stack = self.stack
|
||||||
|
self.stack = []
|
||||||
|
self.generic_visit(node)
|
||||||
|
while self.stack:
|
||||||
|
start_type, lineno = self.stack.pop()
|
||||||
|
self.errors.append(f"Function '{node.name}': Unclosed scope '{start_type}' started at line {lineno}")
|
||||||
|
self.stack = saved_stack
|
||||||
|
|
||||||
|
def visit_AsyncFunctionDef(self, node):
|
||||||
|
self.visit_FunctionDef(node)
|
||||||
|
|
||||||
|
def visit_Call(self, node):
|
||||||
|
func_name = self._get_func_name(node.func)
|
||||||
|
if func_name:
|
||||||
|
parts = func_name.split('.')
|
||||||
|
if len(parts) >= 2 and parts[0] in ('imgui', 'ed', 'imgui_node_editor'):
|
||||||
|
method = parts[-1]
|
||||||
|
if method in self.starts:
|
||||||
|
self.stack.append((method, node.lineno))
|
||||||
|
elif method in self.ends:
|
||||||
|
if not self.stack:
|
||||||
|
self.errors.append(f"Extra '{method}' at line {node.lineno} (no matching start)")
|
||||||
|
else:
|
||||||
|
start_type, start_lineno = self.stack[-1]
|
||||||
|
expected_end = self.pairs.get(start_type)
|
||||||
|
if expected_end == method:
|
||||||
|
self.stack.pop()
|
||||||
|
else:
|
||||||
|
self.stack.pop()
|
||||||
|
self.errors.append(f"Mismatched scope: '{method}' at line {node.lineno} does not match '{start_type}' at line {start_lineno}")
|
||||||
|
self.generic_visit(node)
|
||||||
|
|
||||||
|
def _get_func_name(self, node):
|
||||||
|
if isinstance(node, ast.Name):
|
||||||
|
return node.id
|
||||||
|
elif isinstance(node, ast.Attribute):
|
||||||
|
value = self._get_func_name(node.value)
|
||||||
|
if value:
|
||||||
|
return f"{value}.{node.attr}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python check_imgui_scopes.py <file1> <file2> ...")
|
||||||
|
return
|
||||||
|
linter = ImGuiScopeLinter()
|
||||||
|
for path in sys.argv[1:]:
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
source = f.read()
|
||||||
|
errors = linter.check_source(source)
|
||||||
|
if errors:
|
||||||
|
print(f"Errors in {path}:")
|
||||||
|
for err in errors:
|
||||||
|
print(f" {err}")
|
||||||
|
else:
|
||||||
|
print(f"{path}: OK")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading {path}: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Binary file not shown.
+84
-194
@@ -1,202 +1,92 @@
|
|||||||
from __future__ import annotations
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from scripts.check_imgui_scopes import ImGuiScopeLinter
|
||||||
|
|
||||||
|
def test_valid_scopes():
|
||||||
|
linter = ImGuiScopeLinter()
|
||||||
|
source = """
|
||||||
|
def valid_func():
|
||||||
|
imgui.begin("Window")
|
||||||
|
imgui.push_id("sub")
|
||||||
|
imgui.text("Hello")
|
||||||
|
imgui.pop_id()
|
||||||
|
imgui.end()
|
||||||
|
"""
|
||||||
|
errors = linter.check_source(source)
|
||||||
|
assert not errors
|
||||||
|
|
||||||
class TestImGuiScope:
|
def test_unclosed_scope():
|
||||||
def test_enter_calls_begin_with_args(self) -> None:
|
linter = ImGuiScopeLinter()
|
||||||
from src.imgui_scopes import ImGuiScope
|
source = """
|
||||||
mock_begin = MagicMock(return_value=True)
|
def unclosed_func():
|
||||||
mock_end = MagicMock()
|
imgui.begin("Window")
|
||||||
scope = ImGuiScope(mock_begin, mock_end, "arg1", kwarg="val")
|
imgui.text("Hello")
|
||||||
with scope:
|
# Missing imgui.end()
|
||||||
pass
|
"""
|
||||||
mock_begin.assert_called_once_with("arg1", kwarg="val")
|
errors = linter.check_source(source)
|
||||||
|
assert len(errors) == 1
|
||||||
|
assert "Unclosed scope 'begin'" in errors[0]
|
||||||
|
|
||||||
def test_exit_calls_end_when_entered(self) -> None:
|
def test_extra_pop():
|
||||||
from src.imgui_scopes import ImGuiScope
|
linter = ImGuiScopeLinter()
|
||||||
mock_begin = MagicMock(return_value=True)
|
source = """
|
||||||
mock_end = MagicMock()
|
def extra_pop_func():
|
||||||
scope = ImGuiScope(mock_begin, mock_end)
|
imgui.begin("Window")
|
||||||
with scope:
|
imgui.end()
|
||||||
pass
|
imgui.end() # Extra
|
||||||
mock_end.assert_called_once()
|
"""
|
||||||
|
errors = linter.check_source(source)
|
||||||
|
assert len(errors) == 1
|
||||||
|
assert "Extra 'end'" in errors[0]
|
||||||
|
|
||||||
def test_exit_does_not_call_end_when_not_entered(self) -> None:
|
def test_mismatched_scopes():
|
||||||
from src.imgui_scopes import ImGuiScope
|
linter = ImGuiScopeLinter()
|
||||||
mock_begin = MagicMock(return_value=False)
|
source = """
|
||||||
mock_end = MagicMock()
|
def mismatched_func():
|
||||||
scope = ImGuiScope(mock_begin, mock_end)
|
imgui.begin("Window")
|
||||||
with scope:
|
imgui.push_id("id")
|
||||||
pass
|
imgui.end() # Should be pop_id
|
||||||
mock_end.assert_not_called()
|
imgui.pop_id() # Should be end
|
||||||
|
"""
|
||||||
|
errors = linter.check_source(source)
|
||||||
|
# mismatch pops 'push_id', then pop_id() mismatches with 'begin'
|
||||||
|
assert len(errors) == 2
|
||||||
|
assert "Mismatched scope" in errors[0]
|
||||||
|
assert "end" in errors[0]
|
||||||
|
assert "push_id" in errors[0]
|
||||||
|
assert "Mismatched scope" in errors[1]
|
||||||
|
assert "pop_id" in errors[1]
|
||||||
|
assert "begin" in errors[1]
|
||||||
|
|
||||||
def test_enter_returns_opened_value(self) -> None:
|
def test_nested_functions():
|
||||||
from src.imgui_scopes import ImGuiScope
|
linter = ImGuiScopeLinter()
|
||||||
mock_begin = MagicMock(return_value=True)
|
source = """
|
||||||
mock_end = MagicMock()
|
def outer():
|
||||||
scope = ImGuiScope(mock_begin, mock_end)
|
imgui.begin("Outer")
|
||||||
result = scope.__enter__()
|
def inner():
|
||||||
assert result is True
|
imgui.push_id("Inner")
|
||||||
|
# Missing pop_id
|
||||||
|
imgui.end()
|
||||||
|
"""
|
||||||
|
errors = linter.check_source(source)
|
||||||
|
assert len(errors) == 1
|
||||||
|
assert "Function 'inner': Unclosed scope 'push_id'" in errors[0]
|
||||||
|
|
||||||
def test_enter_handles_tuple_return(self) -> None:
|
def test_node_editor_scopes():
|
||||||
from src.imgui_scopes import ImGuiScope
|
linter = ImGuiScopeLinter()
|
||||||
mock_begin = MagicMock(return_value=(True, "extra"))
|
source = """
|
||||||
mock_end = MagicMock()
|
def ed_func():
|
||||||
scope = ImGuiScope(mock_begin, mock_end)
|
ed.begin("NodeEditor")
|
||||||
with scope:
|
ed.end()
|
||||||
pass
|
"""
|
||||||
mock_end.assert_called_once()
|
errors = linter.check_source(source)
|
||||||
|
assert not errors
|
||||||
|
|
||||||
|
def test_popup_modal_end():
|
||||||
class TestImguiWindow:
|
linter = ImGuiScopeLinter()
|
||||||
def test_imgui_window_returns_scope(self) -> None:
|
source = """
|
||||||
from src.imgui_scopes import imgui_window, ImGuiScope
|
def popup_func():
|
||||||
scope = imgui_window("Test")
|
imgui.begin_popup_modal("Modal")
|
||||||
assert isinstance(scope, ImGuiScope)
|
imgui.end_popup()
|
||||||
|
"""
|
||||||
def test_imgui_window_calls_begin_end(self) -> None:
|
errors = linter.check_source(source)
|
||||||
from src.imgui_scopes import imgui_window
|
assert not errors
|
||||||
with patch("src.imgui_scopes.imgui.begin", return_value=True) as mock_begin:
|
|
||||||
with patch("src.imgui_scopes.imgui.end") as mock_end:
|
|
||||||
scope = imgui_window("Test")
|
|
||||||
with scope:
|
|
||||||
pass
|
|
||||||
mock_begin.assert_called_once_with("Test", True, 0)
|
|
||||||
mock_end.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
class TestImguiTable:
|
|
||||||
def test_imgui_table_returns_scope(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_table, ImGuiScope
|
|
||||||
scope = imgui_table("TestTable", 3)
|
|
||||||
assert isinstance(scope, ImGuiScope)
|
|
||||||
|
|
||||||
def test_imgui_table_calls_begin_end(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_table
|
|
||||||
with patch("src.imgui_scopes.imgui.begin_table", return_value=True) as mock_begin:
|
|
||||||
with patch("src.imgui_scopes.imgui.end_table") as mock_end:
|
|
||||||
scope = imgui_table("TestTable", 3)
|
|
||||||
with scope:
|
|
||||||
pass
|
|
||||||
mock_begin.assert_called_once_with("TestTable", 3, 0)
|
|
||||||
mock_end.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
class TestImguiMenuBar:
|
|
||||||
def test_imgui_menu_bar_returns_scope(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_menu_bar, ImGuiScope
|
|
||||||
scope = imgui_menu_bar()
|
|
||||||
assert isinstance(scope, ImGuiScope)
|
|
||||||
|
|
||||||
def test_imgui_menu_bar_calls_begin_end(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_menu_bar
|
|
||||||
with patch("src.imgui_scopes.imgui.begin_menu_bar", return_value=True) as mock_begin:
|
|
||||||
with patch("src.imgui_scopes.imgui.end_menu_bar") as mock_end:
|
|
||||||
scope = imgui_menu_bar()
|
|
||||||
with scope:
|
|
||||||
pass
|
|
||||||
mock_begin.assert_called_once_with()
|
|
||||||
mock_end.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
class TestImguiMenu:
|
|
||||||
def test_imgui_menu_returns_scope(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_menu, ImGuiScope
|
|
||||||
scope = imgui_menu("File")
|
|
||||||
assert isinstance(scope, ImGuiScope)
|
|
||||||
|
|
||||||
def test_imgui_menu_calls_begin_end(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_menu
|
|
||||||
with patch("src.imgui_scopes.imgui.begin_menu", return_value=True) as mock_begin:
|
|
||||||
with patch("src.imgui_scopes.imgui.end_menu") as mock_end:
|
|
||||||
scope = imgui_menu("File")
|
|
||||||
with scope:
|
|
||||||
pass
|
|
||||||
mock_begin.assert_called_once_with("File")
|
|
||||||
mock_end.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
class TestImguiChild:
|
|
||||||
def test_imgui_child_returns_scope(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_child, ImGuiScope
|
|
||||||
scope = imgui_child("child_id", 100, 200)
|
|
||||||
assert isinstance(scope, ImGuiScope)
|
|
||||||
|
|
||||||
def test_imgui_child_calls_begin_end(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_child
|
|
||||||
with patch("src.imgui_scopes.imgui.begin_child", return_value=True) as mock_begin:
|
|
||||||
with patch("src.imgui_scopes.imgui.end_child") as mock_end:
|
|
||||||
scope = imgui_child("child_id", 100, 200)
|
|
||||||
with scope:
|
|
||||||
pass
|
|
||||||
mock_begin.assert_called_once_with("child_id", 100, 200, 0)
|
|
||||||
mock_end.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
class TestImguiGroup:
|
|
||||||
def test_imgui_group_returns_scope(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_group, ImGuiScope
|
|
||||||
scope = imgui_group()
|
|
||||||
assert isinstance(scope, ImGuiScope)
|
|
||||||
|
|
||||||
def test_imgui_group_calls_begin_end(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_group
|
|
||||||
with patch("src.imgui_scopes.imgui.begin_group", return_value=True) as mock_begin:
|
|
||||||
with patch("src.imgui_scopes.imgui.end_group") as mock_end:
|
|
||||||
scope = imgui_group()
|
|
||||||
with scope:
|
|
||||||
pass
|
|
||||||
mock_begin.assert_called_once_with()
|
|
||||||
mock_end.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
class TestImguiPopup:
|
|
||||||
def test_imgui_popup_returns_scope(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_popup, ImGuiScope
|
|
||||||
scope = imgui_popup("popup_id")
|
|
||||||
assert isinstance(scope, ImGuiScope)
|
|
||||||
|
|
||||||
def test_imgui_popup_calls_begin_end(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_popup
|
|
||||||
with patch("src.imgui_scopes.imgui.begin_popup", return_value=True) as mock_begin:
|
|
||||||
with patch("src.imgui_scopes.imgui.end_popup") as mock_end:
|
|
||||||
scope = imgui_popup("popup_id")
|
|
||||||
with scope:
|
|
||||||
pass
|
|
||||||
mock_begin.assert_called_once_with("popup_id")
|
|
||||||
mock_end.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
class TestImguiTooltip:
|
|
||||||
def test_imgui_tooltip_returns_scope(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_tooltip, ImGuiScope
|
|
||||||
scope = imgui_tooltip()
|
|
||||||
assert isinstance(scope, ImGuiScope)
|
|
||||||
|
|
||||||
def test_imgui_tooltip_calls_begin_end(self) -> None:
|
|
||||||
from src.imgui_scopes import imgui_tooltip
|
|
||||||
with patch("src.imgui_scopes.imgui.begin_tooltip", return_value=True) as mock_begin:
|
|
||||||
with patch("src.imgui_scopes.imgui.end_tooltip") as mock_end:
|
|
||||||
scope = imgui_tooltip()
|
|
||||||
with scope:
|
|
||||||
pass
|
|
||||||
mock_begin.assert_called_once_with()
|
|
||||||
mock_end.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
class TestNodeEditorScope:
|
|
||||||
def test_node_editor_scope_returns_scope(self) -> None:
|
|
||||||
from src.imgui_scopes import node_editor_scope, ImGuiScope
|
|
||||||
scope = node_editor_scope("TestNode")
|
|
||||||
assert isinstance(scope, ImGuiScope)
|
|
||||||
|
|
||||||
def test_node_editor_scope_calls_begin_end(self) -> None:
|
|
||||||
from src.imgui_scopes import node_editor_scope
|
|
||||||
with patch("src.imgui_scopes.imgui_node_editor.begin", return_value=True) as mock_begin:
|
|
||||||
with patch("src.imgui_scopes.imgui_node_editor.end") as mock_end:
|
|
||||||
scope = node_editor_scope("TestNode")
|
|
||||||
with scope:
|
|
||||||
pass
|
|
||||||
mock_begin.assert_called_once_with("TestNode")
|
|
||||||
mock_end.assert_called_once()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user