Coverage for tests/klayout/lvs_runner_test.py: 89%

109 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-02 23:32 +0000

1# 

2# -------------------------------------------------------------------------------- 

3# SPDX-FileCopyrightText: 2024-2025 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# 

24from __future__ import annotations 

25 

26import allure 

27import argparse 

28import io 

29import os 

30import tempfile 

31import unittest 

32from typing import List 

33from unittest import mock 

34 

35from klayout_pex.kpex_cli import InputMode, KpexCLI 

36from klayout_pex.klayout.lvs_runner import LVSError, LVSRunner 

37from klayout_pex.tool_version_constraints import Tool 

38 

39 

40# What KLayout 0.30 prints when an %include of an LVS script can't be resolved 

41# (see https://github.com/iic-jku/klayout-pex/issues/210) 

42KLAYOUT_INCLUDE_ERROR_OUTPUT = """\ 

43ERROR: In /pdk/sg13cmos5l.lvs: Unable to open file: /pdk/rule_decks//sram_integration.lvs (errno=2) in MacroInterpreter::include_expansion 

44ERROR: RuntimeError: Unable to open file: /pdk/rule_decks//sram_integration.lvs (errno=2) in MacroInterpreter::include_expansion in Executable::execute 

45 :/built-in-macros/lvs_interpreters.lym:27:in 'RBA::MacroInterpreter#include_expansion' 

46 :/built-in-macros/lvs_interpreters.lym:27:in 'LVS::LVSExecutable#execute' 

47""" 

48 

49 

50class FakeKLayoutProcess: 

51 """ 

52 Stands in for subprocess.Popen, emulating a KLayout LVS run 

53 """ 

54 def __init__(self, output: str, returncode: int, writes_report: bool): 

55 self.output = output 

56 self.returncode = returncode 

57 self.writes_report = writes_report 

58 

59 def __call__(self, args: List[str], **kwargs): 

60 if self.writes_report: 

61 report_path = next(a.removeprefix('report=') for a in args if a.startswith('report=')) 

62 with open(report_path, 'w') as f: 

63 f.write('#%lvsdb-klayout\n') 

64 proc = mock.Mock() 

65 proc.stdout = io.StringIO(self.output) 

66 proc.returncode = self.returncode 

67 return proc 

68 

69 

70@allure.parent_suite("Unit Tests") 

71@allure.tag("LVS", "KLayout") 

72class LVSRunnerFailureTest(unittest.TestCase): 

73 def setUp(self): 

74 self.tmp_dir = tempfile.mkdtemp(prefix="lvs_run_") 

75 self.log_path = os.path.join(self.tmp_dir, "cell_lvs.log") 

76 self.lvsdb_path = os.path.join(self.tmp_dir, "cell.lvsdb.gz") 

77 self.netlist_path = os.path.join(self.tmp_dir, "cell_extracted.cir") 

78 

79 def run_lvs(self, fake_klayout: FakeKLayoutProcess): 

80 with mock.patch('klayout_pex.klayout.lvs_runner.subprocess.Popen', fake_klayout): 

81 LVSRunner.run_klayout_lvs(exe_path="klayout", 

82 lvs_script="sg13cmos5l.lvs", 

83 gds_path="cell.gds", 

84 schematic_path="cell.spice", 

85 log_path=self.log_path, 

86 lvsdb_path=self.lvsdb_path, 

87 netlist_path=self.netlist_path, 

88 verbose=False) 

89 

90 def test_missing_lvsdb_raises_with_klayout_errors_and_log_path(self): 

91 with self.assertRaises(LVSError) as ctx: 

92 self.run_lvs(FakeKLayoutProcess(output=KLAYOUT_INCLUDE_ERROR_OUTPUT, 

93 returncode=1, 

94 writes_report=False)) 

95 msg = str(ctx.exception) 

96 self.assertIn('status code 1', msg) 

97 self.assertIn('ERROR: In /pdk/sg13cmos5l.lvs: Unable to open file: ' 

98 '/pdk/rule_decks//sram_integration.lvs', msg) 

99 self.assertIn(self.log_path, msg) 

100 with open(self.log_path) as f: 

101 self.assertEqual(KLAYOUT_INCLUDE_ERROR_OUTPUT, f.read()) 

102 

103 def test_missing_lvsdb_raises_even_on_status_code_zero(self): 

104 with self.assertRaises(LVSError) as ctx: 

105 self.run_lvs(FakeKLayoutProcess(output="", returncode=0, writes_report=False)) 

106 self.assertIn(self.log_path, str(ctx.exception)) 

107 

108 def test_stale_lvsdb_from_earlier_run_is_not_mistaken_for_result(self): 

109 with open(self.lvsdb_path, 'w') as f: 

110 f.write('stale') 

111 with self.assertRaises(LVSError): 

112 self.run_lvs(FakeKLayoutProcess(output=KLAYOUT_INCLUDE_ERROR_OUTPUT, 

113 returncode=1, 

114 writes_report=False)) 

115 self.assertFalse(os.path.exists(self.lvsdb_path)) 

116 

117 def test_netlist_mismatch_is_no_error(self): 

118 # e.g. the sky130 LVS script exits with 1 on a netlist mismatch 

119 with mock.patch('klayout_pex.klayout.lvs_runner.warning') as warning_mock, \ 

120 mock.patch('klayout_pex.klayout.lvs_runner.error') as error_mock: 

121 self.run_lvs(FakeKLayoutProcess(output="ERROR : Netlists don't match\n", 

122 returncode=1, 

123 writes_report=True)) 

124 self.assertTrue(os.path.isfile(self.lvsdb_path)) 

125 warning_mock.assert_not_called() 

126 error_mock.assert_not_called() 

127 

128 def test_script_parameters_are_passed(self): 

129 fake_klayout = mock.Mock(wraps=FakeKLayoutProcess(output="", returncode=0, writes_report=True)) 

130 self.run_lvs(fake_klayout) 

131 args = fake_klayout.call_args.args[0] 

132 for name, value in LVSRunner.SCRIPT_PARAMETERS.items(): 

133 self.assertIn(f"{name}={value}", args) 

134 self.assertIn('verbose=false', args) 

135 

136 def test_result_script_parameters_leave_out_the_neutral_ones(self): 

137 parameters = LVSRunner.result_script_parameters() 

138 self.assertEqual('deep', parameters['run_mode']) 

139 for name in ('thr', 'spice_net_names', 'spice_comments', 'verbose'): 

140 self.assertNotIn(name, parameters) 

141 

142 def test_cli_exits_with_error_instead_of_failing_to_cache_missing_lvsdb(self): 

143 deck_dir_path = os.path.join(self.tmp_dir, 'deck') 

144 os.makedirs(deck_dir_path) 

145 lvs_script_path = os.path.join(deck_dir_path, 'sg13cmos5l.lvs') 

146 for path in (lvs_script_path, 

147 os.path.join(self.tmp_dir, 'cell.gds'), 

148 os.path.join(self.tmp_dir, 'cell.spice')): 

149 with open(path, 'w') as f: 

150 f.write(os.path.basename(path)) 

151 args = argparse.Namespace( 

152 input_mode=InputMode.GDS, 

153 output_dir_path=self.tmp_dir, 

154 cache_dir_path=os.path.join(self.tmp_dir, '.kpex_cache'), 

155 cache_lvs=True, 

156 pdk='ihp-sg13cmos5l', 

157 gds_path=os.path.join(self.tmp_dir, 'cell.gds'), 

158 effective_gds_path=os.path.join(self.tmp_dir, 'cell.gds'), 

159 effective_schematic_path=os.path.join(self.tmp_dir, 'cell.spice'), 

160 effective_cell_name='cell', 

161 klayout_exe_path='klayout', 

162 lvs_script_path=lvs_script_path, 

163 klayout_lvs_verbose=False, 

164 ) 

165 fake_klayout = FakeKLayoutProcess(output=KLAYOUT_INCLUDE_ERROR_OUTPUT, 

166 returncode=1, 

167 writes_report=False) 

168 with mock.patch('klayout_pex.klayout.lvs_runner.subprocess.Popen', fake_klayout), \ 

169 mock.patch.object(Tool, 'detect_version', return_value=None), \ 

170 mock.patch('klayout_pex.kpex_cli.error') as error_mock: 

171 with self.assertRaises(SystemExit) as ctx: 

172 KpexCLI().create_lvsdb(args) 

173 self.assertEqual(1, ctx.exception.code) 

174 error_mock.assert_called_once() 

175 self.assertIn('cell_lvs.log', error_mock.call_args.args[0]) 

176 

177 

178@allure.parent_suite("Unit Tests") 

179@allure.tag("LVS", "KLayout") 

180@unittest.skip # NOTE: this is relatively long running! 

181class Test(unittest.TestCase): 

182 @property 

183 def testdata_dir(self) -> str: 

184 return os.path.realpath(os.path.join(__file__, '..', '..', '..', 'testdata', 'klayout', 'lvs')) 

185 

186 def test_run_klayout_lvs(self): 

187 gds_path = os.path.join(self.testdata_dir, 'nmos_diode2', 'nmos_diode2.gds.gz') 

188 schematic_path = os.path.join(self.testdata_dir, 'nmos_diode2', 'nmos_diode2.spice') 

189 

190 tmp_dir = tempfile.mkdtemp(prefix="lvs_run_") 

191 log_path = os.path.join(tmp_dir, "out.log") 

192 lvsdb_path = os.path.join(tmp_dir, "out.lvsdb.gz") 

193 netlist_path = os.path.join(tmp_dir, "out_extracted.cir") 

194 

195 # TODO! 

196 # lvs_script = os.path.join(os.environ['PDKPATH'], 'libs.tech', 'klayout', 'lvs', 'sky130.lvs') 

197 lvs_script = os.path.join(os.environ['HOME'], '.klayout', 'salt', 'sky130A_el', 

198 'lvs', 'core', 'sky130.lvs') 

199 

200 runner = LVSRunner() 

201 runner.run_klayout_lvs(exe_path="klayout", 

202 lvs_script=lvs_script, 

203 gds_path=gds_path, 

204 schematic_path=schematic_path, 

205 log_path=log_path, 

206 lvsdb_path=lvsdb_path, 

207 netlist_path=netlist_path, 

208 verbose=False) 

209 print(f"LVS log file: {log_path}") 

210 print(f"LVSDB file: {lvsdb_path}")