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
« 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
26import allure
27import argparse
28import io
29import os
30import tempfile
31import unittest
32from typing import List
33from unittest import mock
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
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"""
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
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
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")
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)
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())
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))
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))
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()
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)
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)
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])
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'))
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')
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")
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')
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}")