Skip to content

Commit a4e2395

Browse files
oharboeclaude
andcommitted
Add unit tests for KLayout-related Python utilities
34 mock-based tests that run without KLayout installed: - test_generate_klayout_tech.py: .lyt generation including validation against real platform templates (nangate45, asap7, sky130hd) - test_def2stream.py: cell clearing, VIA_ preservation, orphan detection, GDS_ALLOW_EMPTY regex, seal file merging, error counting - test_convertDrc.py: DRC report conversion (box/edge/text violations, waived items, comment assembly) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
1 parent 7a498f5 commit a4e2395

File tree

3 files changed

+954
-0
lines changed

3 files changed

+954
-0
lines changed

flow/test/test_convertDrc.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#!/usr/bin/env python3
2+
3+
import unittest
4+
from unittest.mock import MagicMock, patch
5+
import sys
6+
import os
7+
8+
# Mock pya before importing convertDrc since it imports pya at module level
9+
sys.modules["pya"] = MagicMock()
10+
11+
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "util"))
12+
13+
# convertDrc uses a global `in_drc` set by klayout -rd, so we must set it
14+
import builtins
15+
16+
builtins.in_drc = "/tmp/test.drc"
17+
builtins.out_file = "/tmp/test.json"
18+
19+
# Now we can import - but the module-level code tries to use pya.Application
20+
# We need to handle this by patching before import
21+
import importlib
22+
23+
# Import just the convert_drc function by reading the source
24+
import types
25+
26+
_util_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "util")
27+
_src_path = os.path.join(_util_dir, "convertDrc.py")
28+
29+
# Load only the convert_drc function, not the module-level klayout code
30+
with open(_src_path) as f:
31+
source = f.read()
32+
33+
# Extract just the function definition
34+
import textwrap
35+
import re as _re
36+
37+
# Parse out the convert_drc function
38+
_func_start = source.index("def convert_drc(rdb):")
39+
_func_end = source.index("\n\napp = pya.Application")
40+
_func_source = source[_func_start:_func_end]
41+
42+
# Create a module with just the function
43+
_mod = types.ModuleType("convertDrc_test")
44+
_mod.__dict__["os"] = os
45+
_mod.__dict__["in_drc"] = "/tmp/test.drc"
46+
exec(compile(_func_source, _src_path, "exec"), _mod.__dict__)
47+
convert_drc = _mod.convert_drc
48+
49+
50+
def make_mock_point(x, y):
51+
p = MagicMock()
52+
p.x = x
53+
p.y = y
54+
return p
55+
56+
57+
def make_mock_edge(p1_x, p1_y, p2_x, p2_y):
58+
edge = MagicMock()
59+
edge.p1 = make_mock_point(p1_x, p1_y)
60+
edge.p2 = make_mock_point(p2_x, p2_y)
61+
return edge
62+
63+
64+
def make_box_value(left, bottom, right, top):
65+
value = MagicMock()
66+
value.is_box.return_value = True
67+
value.is_edge.return_value = False
68+
value.is_edge_pair.return_value = False
69+
value.is_polygon.return_value = False
70+
value.is_path.return_value = False
71+
value.is_text.return_value = False
72+
value.is_string.return_value = False
73+
box = MagicMock()
74+
box.left = left
75+
box.bottom = bottom
76+
box.right = right
77+
box.top = top
78+
value.box.return_value = box
79+
return value
80+
81+
82+
def make_edge_value(p1_x, p1_y, p2_x, p2_y):
83+
value = MagicMock()
84+
value.is_box.return_value = False
85+
value.is_edge.return_value = True
86+
value.is_edge_pair.return_value = False
87+
value.is_polygon.return_value = False
88+
value.is_path.return_value = False
89+
value.is_text.return_value = False
90+
value.is_string.return_value = False
91+
value.edge.return_value = make_mock_edge(p1_x, p1_y, p2_x, p2_y)
92+
return value
93+
94+
95+
def make_text_value(text):
96+
value = MagicMock()
97+
value.is_box.return_value = False
98+
value.is_edge.return_value = False
99+
value.is_edge_pair.return_value = False
100+
value.is_polygon.return_value = False
101+
value.is_path.return_value = False
102+
value.is_text.return_value = True
103+
value.is_string.return_value = False
104+
value.text.return_value = text
105+
return value
106+
107+
108+
def make_mock_item(values, is_visited=False, tags_str="", comment=None):
109+
item = MagicMock()
110+
item.is_visited.return_value = is_visited
111+
item.tags_str = tags_str
112+
item.each_value.return_value = iter(values)
113+
if comment is not None:
114+
item.comment = comment
115+
else:
116+
# Remove hasattr for comment
117+
del item.comment
118+
return item
119+
120+
121+
def make_mock_category(name, description, rdb_id, num_items, items):
122+
cat = MagicMock()
123+
cat.name.return_value = name
124+
cat.description = description
125+
cat.rdb_id.return_value = rdb_id
126+
cat.num_items.return_value = num_items
127+
return cat, items
128+
129+
130+
class TestConvertDrc(unittest.TestCase):
131+
def test_empty_rdb(self):
132+
rdb = MagicMock()
133+
rdb.each_category.return_value = iter([])
134+
135+
result = convert_drc(rdb)
136+
137+
self.assertEqual(result["source"], os.path.abspath("/tmp/test.drc"))
138+
self.assertEqual(result["category"], {})
139+
140+
def test_empty_category_skipped(self):
141+
cat = MagicMock()
142+
cat.num_items.return_value = 0
143+
144+
rdb = MagicMock()
145+
rdb.each_category.return_value = iter([cat])
146+
147+
result = convert_drc(rdb)
148+
self.assertEqual(result["category"], {})
149+
150+
def test_box_violation(self):
151+
box_val = make_box_value(100, 200, 300, 400)
152+
item = make_mock_item([box_val])
153+
154+
cat = MagicMock()
155+
cat.name.return_value = "metal1.min_width"
156+
cat.description = "Minimum width violation"
157+
cat.rdb_id.return_value = 1
158+
cat.num_items.return_value = 1
159+
160+
rdb = MagicMock()
161+
rdb.each_category.return_value = iter([cat])
162+
rdb.each_item_per_category.return_value = iter([item])
163+
164+
result = convert_drc(rdb)
165+
166+
violations = result["category"]["metal1.min_width"]["violations"]
167+
self.assertEqual(len(violations), 1)
168+
self.assertEqual(len(violations[0]["shape"]), 1)
169+
shape = violations[0]["shape"][0]
170+
self.assertEqual(shape["type"], "box")
171+
self.assertEqual(shape["points"][0], {"x": 100, "y": 200})
172+
self.assertEqual(shape["points"][1], {"x": 300, "y": 400})
173+
174+
def test_edge_violation(self):
175+
edge_val = make_edge_value(10, 20, 30, 40)
176+
item = make_mock_item([edge_val])
177+
178+
cat = MagicMock()
179+
cat.name.return_value = "metal1.spacing"
180+
cat.description = "Spacing violation"
181+
cat.rdb_id.return_value = 2
182+
cat.num_items.return_value = 1
183+
184+
rdb = MagicMock()
185+
rdb.each_category.return_value = iter([cat])
186+
rdb.each_item_per_category.return_value = iter([item])
187+
188+
result = convert_drc(rdb)
189+
190+
violations = result["category"]["metal1.spacing"]["violations"]
191+
shape = violations[0]["shape"][0]
192+
self.assertEqual(shape["type"], "line")
193+
self.assertEqual(shape["points"][0], {"x": 10, "y": 20})
194+
self.assertEqual(shape["points"][1], {"x": 30, "y": 40})
195+
196+
def test_waived_violation(self):
197+
box_val = make_box_value(0, 0, 10, 10)
198+
item = make_mock_item([box_val], tags_str="waived")
199+
200+
cat = MagicMock()
201+
cat.name.return_value = "rule1"
202+
cat.description = "Rule 1"
203+
cat.rdb_id.return_value = 1
204+
cat.num_items.return_value = 1
205+
206+
rdb = MagicMock()
207+
rdb.each_category.return_value = iter([cat])
208+
rdb.each_item_per_category.return_value = iter([item])
209+
210+
result = convert_drc(rdb)
211+
212+
violation = result["category"]["rule1"]["violations"][0]
213+
self.assertTrue(violation["waived"])
214+
215+
def test_text_in_comment(self):
216+
text_val = make_text_value("error detail")
217+
item = make_mock_item([text_val])
218+
219+
cat = MagicMock()
220+
cat.name.return_value = "rule1"
221+
cat.description = "Rule 1"
222+
cat.rdb_id.return_value = 1
223+
cat.num_items.return_value = 1
224+
225+
rdb = MagicMock()
226+
rdb.each_category.return_value = iter([cat])
227+
rdb.each_item_per_category.return_value = iter([item])
228+
229+
result = convert_drc(rdb)
230+
231+
violation = result["category"]["rule1"]["violations"][0]
232+
self.assertEqual(violation["comment"], "error detail")
233+
234+
def test_comment_with_text(self):
235+
text_val = make_text_value("extra info")
236+
item = make_mock_item([text_val], comment="base comment")
237+
238+
cat = MagicMock()
239+
cat.name.return_value = "rule1"
240+
cat.description = "Rule 1"
241+
cat.rdb_id.return_value = 1
242+
cat.num_items.return_value = 1
243+
244+
rdb = MagicMock()
245+
rdb.each_category.return_value = iter([cat])
246+
rdb.each_item_per_category.return_value = iter([item])
247+
248+
result = convert_drc(rdb)
249+
250+
violation = result["category"]["rule1"]["violations"][0]
251+
self.assertEqual(violation["comment"], "base comment: extra info")
252+
253+
254+
if __name__ == "__main__":
255+
unittest.main()

0 commit comments

Comments
 (0)