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

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# 

24 

25from __future__ import annotations 

26 

27import allure 

28from fractions import Fraction 

29import unittest 

30 

31from klayout_pex.pex25d.writer import ( 

32 CONTINUATION, WRAP_COLUMN, WriteError, format_exact, write_pex25d_text, 

33) 

34 

35from .pex25d_fixtures import MINIMAL, read, read_codes, replacing 

36 

37 

38@allure.parent_suite("Unit Tests") 

39@allure.tag("PEX25D", "Writer") 

40class Pex25DWriterTest(unittest.TestCase): 

41 # ------------------------------------------------------- exact rendering 

42 

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 

58 

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) 

66 

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 

71 

72 # ------------------------------------------------------------------ wrap 

73 

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() 

80 

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 

86 

87 assert write_pex25d_text(read(written)) == written.encode() 

88 

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() 

96 

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" 

105 

106 # ------------------------------------------------------------ round trip 

107 

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 

112 

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) 

118 

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) == [] 

124 

125 def records(text: str) -> list: 

126 return [line for line in text.splitlines() 

127 if line and not line.lstrip().startswith('#')] 

128 

129 assert records(annotated) == records(plain)