Coverage for tests/pex25d/writer_test.py: 100%
61 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 fractions import Fraction
29import unittest
31from klayout_pex.pex25d.writer import (
32 CONTINUATION, WRAP_COLUMN, WriteError, format_exact, write_pex25d_text,
33)
35from .pex25d_fixtures import MINIMAL, read, read_codes, replacing
38@allure.parent_suite("Unit Tests")
39@allure.tag("PEX25D", "Writer")
40class Pex25DWriterTest(unittest.TestCase):
41 # ------------------------------------------------------- exact rendering
43 def test_format_exact(self):
44 cases = {
45 Fraction(0): '0.0',
46 Fraction(1): '1.0',
47 Fraction(-1): '-1.0',
48 Fraction(1, 2): '0.5',
49 Fraction(1, 8): '0.125',
50 Fraction(1, 10000): '0.0001',
51 Fraction(3262, 10000): '0.3262',
52 Fraction(-43, 100): '-0.43',
53 Fraction(5, 4): '1.25',
54 }
55 for value, expected in cases.items():
56 with self.subTest(value=value):
57 assert format_exact(value) == expected
59 def test_format_exact_refuses_what_it_cannot_render(self):
60 # A denominator with a factor other than 2 or 5 has no finite decimal
61 # form, and rounding one silently would move geometry off the grid.
62 for value in (Fraction(1, 3), Fraction(2, 7)):
63 with self.subTest(value=value):
64 with self.assertRaises(WriteError):
65 format_exact(value)
67 def test_lengths_are_rendered_exactly(self):
68 text = write_pex25d_text(read(MINIMAL)).decode()
69 assert 'METAL met1 Z_OFFSETS 1.0 1.4' in text
70 assert 'GROUND_PLANE subs Z_OFFSETS -0.4 -0.1' in text
72 # ------------------------------------------------------------------ wrap
74 def test_long_records_wrap_and_read_back_identically(self):
75 ring = ' '.join(f"{x / 10} {x % 7}" for x in range(40))
76 text = replacing(
77 'POLYGON CONDUCTOR B LAYER met1 OUTER 2.0 0.0 3.0 0.0 3.0 1.0 2.0 1.0',
78 f'POLYGON CONDUCTOR B LAYER met1 OUTER {ring}')
79 written = write_pex25d_text(read(text)).decode()
81 wrapped = [line for line in written.splitlines()
82 if line.endswith(CONTINUATION)]
83 assert wrapped, "the long polygon should have wrapped"
84 for line in written.splitlines():
85 assert len(line) <= WRAP_COLUMN
87 assert write_pex25d_text(read(written)) == written.encode()
89 def test_a_wrap_never_splits_a_coordinate_pair(self):
90 ring = ' '.join(f"{x / 10} {x % 7}" for x in range(40))
91 text = replacing(
92 'POLYGON CONDUCTOR B LAYER met1 OUTER 2.0 0.0 3.0 0.0 3.0 1.0 2.0 1.0',
93 f'POLYGON CONDUCTOR B LAYER met1 OUTER {ring}')
94 def is_number(token: str) -> bool:
95 return token.lstrip('-').replace('.', '', 1).isdigit()
97 checked = 0
98 for line in write_pex25d_text(read(text)).decode().splitlines():
99 tokens = line.rstrip(CONTINUATION).split()
100 if not tokens or not all(is_number(token) for token in tokens):
101 continue
102 checked += 1
103 assert len(tokens) % 2 == 0, f"odd number of coordinates in {line!r}"
104 assert checked >= 3, "expected several pure-coordinate continuation lines"
106 # ------------------------------------------------------------ round trip
108 def test_round_trip_is_byte_identical(self):
109 once = write_pex25d_text(read(MINIMAL))
110 twice = write_pex25d_text(read(once.decode()))
111 assert once == twice
113 def test_round_trip_preserves_the_message(self):
114 original = read(MINIMAL)
115 again = read(write_pex25d_text(original).decode())
116 assert original.SerializeToString(deterministic=True) == \
117 again.SerializeToString(deterministic=True)
119 def test_comments_change_nothing_but_the_comments(self):
120 plain = write_pex25d_text(read(MINIMAL), comments=False).decode()
121 annotated = write_pex25d_text(read(MINIMAL), comments=True).decode()
122 assert len(annotated) > len(plain)
123 assert read_codes(annotated) == []
125 def records(text: str) -> list:
126 return [line for line in text.splitlines()
127 if line and not line.lstrip().startswith('#')]
129 assert records(annotated) == records(plain)