Coverage for tests/pex25d/validator_test.py: 99%
155 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-17 19:08 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-17 19:08 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2026 Martin Jan Köhler and Harald Pretl
4# Johannes Kepler University, Institute for Integrated Circuits.
5#
6# This file is part of KPEX
7# (see https://github.com/iic-jku/klayout-pex).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21# SPDX-License-Identifier: GPL-3.0-or-later
22# --------------------------------------------------------------------------------
23#
25from __future__ import annotations
27import allure
28from typing import *
29import unittest
31from klayout_pex.pex25d.diagnostics import DiagnosticsReport, Severity
32from klayout_pex.pex25d.reader import ReadError
33from klayout_pex.pex25d.resolver import resolve
34from klayout_pex.pex25d.validator import validate
36from .pex25d_fixtures import MINIMAL, codes, read, replacing
39def check(text: str, strict: bool = False) -> List[str]:
40 """The codes validating ``text`` produces, reading errors included."""
41 report = DiagnosticsReport()
42 try:
43 message = read(text, report=report)
44 except ReadError:
45 return codes(report)
46 validate(message, report=report, strict=strict)
47 return codes(report)
50def check_scene(scene: Any, strict: bool = False) -> List[str]:
51 report = DiagnosticsReport()
52 validate(scene, report=report, strict=strict)
53 return codes(report)
56def geometry(*records: str) -> str:
57 """The fixture with its conductors and shapes replaced by ``records``."""
58 body = '\n'.join(records)
59 return replacing(
60 "CONDUCTOR A neta\n"
61 "CONDUCTOR B netb\n"
62 "BOX CONDUCTOR A LAYER met1 LL 0.0 0.0 UR 1.0 0.5\n"
63 "BOX CONDUCTOR A LAYER via1 LL 0.2 0.1 UR 0.4 0.3\n"
64 "BOX CONDUCTOR A LAYER met2 LL 0.0 0.0 UR 1.0 0.5\n"
65 "POLYGON CONDUCTOR B LAYER met1 OUTER 2.0 0.0 3.0 0.0 3.0 1.0 2.0 1.0\n"
66 "TERMINAL t1 CONDUCTOR A LAYER met1 KIND PIN LL 0.0 0.0 UR 0.2 0.5\n",
67 body + '\n')
70@allure.parent_suite("Unit Tests")
71@allure.tag("PEX25D", "Validator")
72class Pex25DValidatorTest(unittest.TestCase):
73 def test_the_fixture_is_clean(self):
74 assert check(MINIMAL) == []
75 assert check(MINIMAL, strict=True) == []
77 def test_a_resolved_scene_is_clean(self):
78 assert check_scene(resolve(read(MINIMAL)), strict=True) == []
80 def test_validating_never_raises(self):
81 # Whatever is wrong with a message, the diagnostics are the answer.
82 broken = read(MINIMAL)
83 broken.ClearField('ground_plane')
84 broken.ClearField('units')
85 del broken.metals[:]
86 assert check_scene(broken) or True # the point is that it returned
88 # ------------------------------------------------------- semantic checks
90 def test_e0260_a_simple_dielectric_wrapping_an_inner_link(self):
91 # Wrapping an inner link gives the fill the same depth as a film it
92 # contains, and at equal depth there is no rule to break the tie.
93 assert 'PEX25D-E0260' in check(
94 replacing('DIELECTRIC_SIMPLE ild WRAPS lint', 'DIELECTRIC_SIMPLE ild WRAPS met1'))
96 def test_e0261_two_films_at_the_same_depth_on_one_root(self):
97 assert 'PEX25D-E0261' in check(
98 replacing('DIELECTRIC_SIMPLE ild WRAPS lint PERMITTIVITY 4.1 BETWEEN met1 met2',
99 'DIELECTRIC_CONFORMAL other WRAPS met1 PERMITTIVITY 4.1 '
100 'THICKNESS_OVER_WRAPPED 0.1 THICKNESS_BESIDE_WRAPPED 0.08 '
101 'THICKNESS_ON_FIELD 0.0'))
103 def test_e0262_a_grid_that_does_not_divide_the_source_dbu(self):
104 assert 'PEX25D-E0262' in check(
105 replacing('META source_cell tiny',
106 'META source_cell tiny\nMETA source_dbu 0.00015'))
108 def test_a_grid_that_does_divide_the_source_dbu(self):
109 assert check(replacing('META source_cell tiny',
110 'META source_cell tiny\nMETA source_dbu 0.001')) == []
112 def test_e0263_a_terminal_on_a_floating_conductor(self):
113 assert 'PEX25D-E0263' in check(replacing('CONDUCTOR A neta', 'CONDUCTOR A FLOATING'))
115 def test_w0264_a_conductor_with_no_geometry(self):
116 report = DiagnosticsReport()
117 message = read(replacing('CONDUCTOR B netb', 'CONDUCTOR B netb\nCONDUCTOR C netc'),
118 report=report)
119 validate(message, report=report)
120 assert codes(report) == ['PEX25D-W0264']
121 assert report.diagnostics[0].severity == Severity.WARNING
123 def test_w0265_geometry_without_a_resistance_when_others_have_one(self):
124 assert 'PEX25D-W0265' in check(replacing('RESISTANCE METAL met1 SHEET 0.125\n', ''))
126 def test_no_w0265_when_the_file_states_no_resistance_at_all(self):
127 # A pure capacitance file is exactly what it was before RESISTANCE
128 # existed, and must not start warning.
129 text = MINIMAL
130 for line in ('RESISTANCE TEMPERATURE 25.0\n',
131 'RESISTANCE METAL met1 SHEET 0.125\n',
132 'RESISTANCE METAL met2 SHEET 0.125\n',
133 'RESISTANCE VIA via1 PER_CUT 4.5\n'):
134 text = text.replace(line, '')
135 assert check(text) == []
137 def test_e0266_a_conductor_declared_twice(self):
138 assert 'PEX25D-E0266' in check(
139 replacing('CONDUCTOR B netb', 'CONDUCTOR B netb\nCONDUCTOR A netc'))
141 def test_e0267_a_terminal_declared_twice(self):
142 assert 'PEX25D-E0267' in check(
143 replacing('TERMINAL t1 CONDUCTOR A LAYER met1 KIND PIN LL 0.0 0.0 UR 0.2 0.5',
144 'TERMINAL t1 CONDUCTOR A LAYER met1 KIND PIN LL 0.0 0.0 UR 0.2 0.5\n'
145 'TERMINAL t1 CONDUCTOR A LAYER met2 KIND PIN LL 0.0 0.0 UR 0.2 0.5'))
147 def test_e0268_two_resistance_records_for_one_profile(self):
148 assert 'PEX25D-E0268' in check(
149 replacing('RESISTANCE METAL met2 SHEET 0.125',
150 'RESISTANCE METAL met2 SHEET 0.125\nRESISTANCE METAL met2 SHEET 0.2'))
152 def test_w0269_coefficients_with_no_reference_temperature(self):
153 assert 'PEX25D-W0269' in check(
154 replacing('RESISTANCE TEMPERATURE 25.0\n', '').replace(
155 'RESISTANCE METAL met1 SHEET 0.125',
156 'RESISTANCE METAL met1 SHEET 0.125 TC1 0.003 TC2 0.0'))
158 # ---------------------------------------------------------- scene checks
160 def test_e0270_a_layer_with_no_kind(self):
161 scene = resolve(read(MINIMAL))
162 scene.layers[0].ClearField('kind')
163 assert 'PEX25D-E0270' in check_scene(scene)
165 def test_e0271_a_metal_with_no_height(self):
166 scene = resolve(read(MINIMAL))
167 scene.layers[0].zhigh = scene.layers[0].zlow
168 assert 'PEX25D-E0271' in check_scene(scene)
170 def test_a_via_between_abutting_layers_is_not_an_error(self):
171 scene = resolve(read(MINIMAL))
172 via = next(l for l in scene.layers if l.name == 'via1')
173 via.zhigh = via.zlow
174 assert 'PEX25D-E0271' not in check_scene(scene)
176 def test_e0272_a_via_without_its_resolved_endpoints(self):
177 scene = resolve(read(MINIMAL))
178 next(l for l in scene.layers if l.name == 'via1').ClearField('connects_above')
179 assert 'PEX25D-E0272' in check_scene(scene)
181 def test_e0273_a_metal_carrying_a_via_resistance(self):
182 scene = resolve(read(MINIMAL))
183 met1 = next(l for l in scene.layers if l.name == 'met1')
184 met1.ClearField('metal_resistance')
185 met1.via_resistance.per_cut = 1.0
186 assert 'PEX25D-E0273' in check_scene(scene)
188 def test_e0274_dielectrics_out_of_depth_order(self):
189 scene = resolve(read(MINIMAL))
190 reordered = list(scene.dielectrics)[::-1]
191 del scene.dielectrics[:]
192 for dielectric in reordered:
193 scene.dielectrics.add().CopyFrom(dielectric)
194 assert 'PEX25D-E0274' in check_scene(scene)
196 def test_e0275_a_dielectric_naming_a_profile_the_scene_has_not_got(self):
197 scene = resolve(read(MINIMAL))
198 scene.dielectrics[0].wraps = 'nowhere'
199 assert 'PEX25D-E0275' in check_scene(scene)
201 def test_e0276_a_terminal_on_a_layer_the_conductor_does_not_use(self):
202 scene = resolve(read(MINIMAL))
203 scene.conductors[0].terminals[0].layer = 'met2'
204 assert 'PEX25D-E0276' not in check_scene(scene) # A does have met2
205 scene.conductors[0].terminals[0].layer = 'nowhere'
206 assert 'PEX25D-E0276' in check_scene(scene)
208 def test_e0277_an_empty_terminal_node(self):
209 scene = resolve(read(MINIMAL))
210 del scene.conductors[0].terminals[0].boxes[:]
211 assert 'PEX25D-E0277' in check_scene(scene)
213 def test_e0279_geometry_on_a_layer_the_scene_has_not_got(self):
214 scene = resolve(read(MINIMAL))
215 scene.conductors[0].regions[0].layer = 'nowhere'
216 assert 'PEX25D-E0279' in check_scene(scene)
218 # ------------------------------------------------------- geometric tier
220 def test_the_geometric_tier_only_runs_under_strict(self):
221 overlapping = geometry(
222 'CONDUCTOR A neta',
223 'CONDUCTOR B netb',
224 'BOX CONDUCTOR A LAYER met1 LL 0.0 0.0 UR 1.0 1.0',
225 'BOX CONDUCTOR B LAYER met1 LL 0.5 0.5 UR 1.5 1.5')
226 assert 'PEX25D-E0309' not in check(overlapping)
227 assert 'PEX25D-E0309' in check(overlapping, strict=True)
229 def test_e0302_a_ring_closed_explicitly(self):
230 assert 'PEX25D-E0302' in check(geometry(
231 'CONDUCTOR A neta',
232 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 1.0 0.0 1.0 1.0 0.0 0.0'),
233 strict=True)
235 def test_e0303_a_repeated_vertex(self):
236 assert 'PEX25D-E0303' in check(geometry(
237 'CONDUCTOR A neta',
238 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 1.0 0.0 1.0 0.0 1.0 1.0'),
239 strict=True)
241 def test_e0304_a_collinear_ring(self):
242 assert 'PEX25D-E0304' in check(geometry(
243 'CONDUCTOR A neta',
244 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 1.0 0.0 3.0 0.0 2.0 0.0'),
245 strict=True)
247 def test_e0305_a_self_intersecting_ring(self):
248 assert 'PEX25D-E0305' in check(geometry(
249 'CONDUCTOR A neta',
250 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 4.0 4.0 4.0 0.0 0.0 3.0'),
251 strict=True)
253 def test_e0306_a_hole_touching_its_outer_ring(self):
254 assert 'PEX25D-E0306' in check(geometry(
255 'CONDUCTOR A neta',
256 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 4.0 0.0 4.0 4.0 0.0 4.0 \\',
257 ' HOLE 0.0 1.0 2.0 1.0 2.0 2.0 0.0 2.0'),
258 strict=True)
260 def test_e0307_holes_that_touch(self):
261 assert 'PEX25D-E0307' in check(geometry(
262 'CONDUCTOR A neta',
263 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 8.0 0.0 8.0 8.0 0.0 8.0 \\',
264 ' HOLE 1.0 1.0 3.0 1.0 3.0 3.0 1.0 3.0 \\',
265 ' HOLE 3.0 1.0 5.0 1.0 5.0 3.0 3.0 3.0'),
266 strict=True)
268 def test_a_polygon_with_two_separate_holes_is_fine(self):
269 assert check(geometry(
270 'CONDUCTOR A neta',
271 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 8.0 0.0 8.0 8.0 0.0 8.0 \\',
272 ' HOLE 1.0 1.0 3.0 1.0 3.0 3.0 1.0 3.0 \\',
273 ' HOLE 4.0 1.0 6.0 1.0 6.0 3.0 4.0 3.0'),
274 strict=True) == []
276 def test_e0308_an_empty_box(self):
277 assert 'PEX25D-E0308' in check(geometry(
278 'CONDUCTOR A neta',
279 'BOX CONDUCTOR A LAYER met1 LL 1.0 1.0 UR 1.0 2.0'),
280 strict=True)
282 # ---------------------------------------------------- conductor overlap
284 def overlap(self, *records: str) -> List[str]:
285 return check(geometry(*records), strict=True)
287 def test_two_conductors_may_abut(self):
288 assert self.overlap(
289 'CONDUCTOR A neta',
290 'CONDUCTOR B netb',
291 'BOX CONDUCTOR A LAYER met1 LL 0.0 0.0 UR 2.0 2.0',
292 'BOX CONDUCTOR B LAYER met1 LL 2.0 0.0 UR 4.0 2.0') == []
294 def test_one_conductor_may_overlap_itself(self):
295 assert self.overlap(
296 'CONDUCTOR A neta',
297 'BOX CONDUCTOR A LAYER met1 LL 0.0 0.0 UR 2.0 2.0',
298 'BOX CONDUCTOR A LAYER met1 LL 1.0 1.0 UR 3.0 3.0') == []
300 def test_layers_whose_z_extents_only_touch_do_not_overlap(self):
301 # met1 and the via above it share a face by construction.
302 assert self.overlap(
303 'CONDUCTOR A neta',
304 'CONDUCTOR B netb',
305 'BOX CONDUCTOR A LAYER met1 LL 0.0 0.0 UR 2.0 2.0',
306 'BOX CONDUCTOR B LAYER via1 LL 0.0 0.0 UR 2.0 2.0') == []
308 def test_e0309_boxes(self):
309 assert 'PEX25D-E0309' in self.overlap(
310 'CONDUCTOR A neta',
311 'CONDUCTOR B netb',
312 'BOX CONDUCTOR A LAYER met1 LL 0.0 0.0 UR 2.0 2.0',
313 'BOX CONDUCTOR B LAYER met1 LL 1.0 1.0 UR 3.0 3.0')
315 def test_e0309_identical_polygons(self):
316 # Nothing on either boundary is strictly inside the other, which is why
317 # sampling vertices and midpoints cannot answer this one.
318 assert 'PEX25D-E0309' in self.overlap(
319 'CONDUCTOR A neta',
320 'CONDUCTOR B netb',
321 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 2.0 0.0 2.0 2.0 0.0 2.0',
322 'POLYGON CONDUCTOR B LAYER met1 OUTER 0.0 0.0 2.0 0.0 2.0 2.0 0.0 2.0')
324 def test_e0309_a_shape_reaching_out_of_a_hole(self):
325 assert 'PEX25D-E0309' in self.overlap(
326 'CONDUCTOR A neta',
327 'CONDUCTOR B netb',
328 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 8.0 0.0 8.0 8.0 0.0 8.0 \\',
329 ' HOLE 2.0 2.0 6.0 2.0 6.0 6.0 2.0 6.0',
330 'BOX CONDUCTOR B LAYER met1 LL 1.0 3.0 UR 5.0 5.0')
332 def test_a_conductor_may_sit_inside_another_ones_hole(self):
333 assert self.overlap(
334 'CONDUCTOR A neta',
335 'CONDUCTOR B netb',
336 'POLYGON CONDUCTOR A LAYER met1 OUTER 0.0 0.0 8.0 0.0 8.0 8.0 0.0 8.0 \\',
337 ' HOLE 2.0 2.0 6.0 2.0 6.0 6.0 2.0 6.0',
338 'BOX CONDUCTOR B LAYER met1 LL 3.0 3.0 UR 5.0 5.0') == []
340 def test_a_box_in_the_notch_of_an_l_shape(self):
341 assert self.overlap(
342 'CONDUCTOR A neta',
343 'CONDUCTOR B netb',
344 'POLYGON CONDUCTOR A LAYER met1 '
345 'OUTER 0.0 0.0 4.0 0.0 4.0 1.0 1.0 1.0 1.0 4.0 0.0 4.0',
346 'BOX CONDUCTOR B LAYER met1 LL 1.0 1.0 UR 4.0 4.0') == []