Coverage for tests/pex25d/pex25d_cli_test.py: 99%

89 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 

28import json 

29import os 

30import tempfile 

31from typing import * 

32import unittest 

33 

34from klayout_pex.pex25d.diagnostics import ExitCode 

35from klayout_pex.pex25d.pex25d_cli import Pex25DCLI 

36 

37from .pex25d_fixtures import MINIMAL, replacing 

38 

39 

40class Workspace: 

41 """A directory with the fixture in it, and a way to run the tool on it.""" 

42 

43 def __init__(self, directory: str, text: str = MINIMAL): 

44 self.directory = directory 

45 self.input_path = self.path('tiny.pex25d') 

46 with open(self.input_path, 'w') as f: 

47 f.write(text) 

48 

49 def path(self, name: str) -> str: 

50 return os.path.join(self.directory, name) 

51 

52 def run(self, *arguments: str) -> ExitCode: 

53 try: 

54 Pex25DCLI().main(['pex25d', *arguments]) 

55 except SystemExit as exit: 

56 return ExitCode(exit.code) 

57 return ExitCode.OK 

58 

59 

60@allure.parent_suite("Unit Tests") 

61@allure.tag("PEX25D", "CLI") 

62class Pex25DCLITest(unittest.TestCase): 

63 def workspace(self, text: str = MINIMAL) -> Any: 

64 directory = tempfile.TemporaryDirectory() 

65 self.addCleanup(directory.cleanup) 

66 return Workspace(directory.name, text) 

67 

68 # ------------------------------------------------------------ exit codes 

69 

70 def test_a_valid_file_exits_zero(self): 

71 workspace = self.workspace() 

72 assert workspace.run('validate', workspace.input_path) == ExitCode.OK 

73 

74 def test_an_invalid_file_exits_one(self): 

75 workspace = self.workspace( 

76 replacing('DIELECTRIC_SIMPLE ild WRAPS lint', 'DIELECTRIC_SIMPLE ild WRAPS met1')) 

77 assert workspace.run('validate', workspace.input_path) \ 

78 == ExitCode.DIAGNOSTIC_ERRORS 

79 

80 def test_a_bad_command_line_exits_two(self): 

81 workspace = self.workspace() 

82 # 'convert' re-encodes; turning a file into a scene is 'resolve'. 

83 assert workspace.run('convert', workspace.input_path, 

84 '-o', workspace.path('out.pex25d.scene.pb')) \ 

85 == ExitCode.USAGE 

86 

87 def test_an_unconventional_output_name_exits_two(self): 

88 workspace = self.workspace() 

89 assert workspace.run('resolve', workspace.input_path, 

90 '-o', workspace.path('out.dat')) == ExitCode.USAGE 

91 

92 def test_werror_turns_a_warning_into_a_failure(self): 

93 workspace = self.workspace( 

94 replacing('CONDUCTOR B netb', 'CONDUCTOR B netb\nCONDUCTOR C netc')) 

95 assert workspace.run('validate', workspace.input_path) == ExitCode.OK 

96 assert workspace.run('validate', workspace.input_path, '--werror') \ 

97 == ExitCode.DIAGNOSTIC_ERRORS 

98 

99 # ---------------------------------------------------------------- verbs 

100 

101 def test_convert_is_idempotent_through_every_encoding(self): 

102 workspace = self.workspace() 

103 first = workspace.path('a.pex25d.pb') 

104 second = workspace.path('b.pex25d.textpb') 

105 third = workspace.path('c.pex25d') 

106 

107 assert workspace.run('convert', workspace.input_path, '-o', first) == ExitCode.OK 

108 assert workspace.run('convert', first, '-o', second) == ExitCode.OK 

109 assert workspace.run('convert', second, '-o', third) == ExitCode.OK 

110 

111 with open(first, 'rb') as f: 

112 once = f.read() 

113 fourth = workspace.path('d.pex25d.pb') 

114 assert workspace.run('convert', third, '-o', fourth) == ExitCode.OK 

115 with open(fourth, 'rb') as f: 

116 assert f.read() == once 

117 

118 def test_resolve_writes_a_scene(self): 

119 workspace = self.workspace() 

120 output = workspace.path('out.pex25d.scene.textpb') 

121 assert workspace.run('resolve', workspace.input_path, '-o', output) == ExitCode.OK 

122 with open(output) as f: 

123 assert 'wrap_depth' in f.read() 

124 

125 def test_resolve_refuses_to_write_half_a_scene(self): 

126 workspace = self.workspace( 

127 replacing('VIA via1 CONNECTS met1 met2', 'VIA via1 CONNECTS met1 met9')) 

128 output = workspace.path('out.pex25d.scene.pb') 

129 assert workspace.run('resolve', workspace.input_path, '-o', output) \ 

130 == ExitCode.DIAGNOSTIC_ERRORS 

131 assert not os.path.exists(output) 

132 

133 def test_show_reads_both_kinds(self): 

134 workspace = self.workspace() 

135 scene = workspace.path('out.pex25d.scene.pb') 

136 assert workspace.run('resolve', workspace.input_path, '-o', scene) == ExitCode.OK 

137 assert workspace.run('show', workspace.input_path) == ExitCode.OK 

138 assert workspace.run('show', scene) == ExitCode.OK 

139 

140 # ---------------------------------------------------------- diagnostics 

141 

142 def test_json_diagnostics_carry_the_codes(self): 

143 workspace = self.workspace( 

144 replacing('DIELECTRIC_SIMPLE ild WRAPS lint', 'DIELECTRIC_SIMPLE ild WRAPS met1')) 

145 report_path = workspace.path('diagnostics.json') 

146 assert workspace.run('validate', workspace.input_path, 

147 '--diagnostics', 'json', 

148 '--diagnostics_out', report_path) \ 

149 == ExitCode.DIAGNOSTIC_ERRORS 

150 

151 with open(report_path) as f: 

152 rendered = json.load(f) 

153 codes = [d['code'] for d in rendered['diagnostics']] 

154 assert 'PEX25D-E0260' in codes 

155 for diagnostic in rendered['diagnostics']: 

156 assert diagnostic['severity'].startswith('SEVERITY_') 

157 assert diagnostic['tier'].startswith('TIER_')