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
« 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
28import json
29import os
30import tempfile
31from typing import *
32import unittest
34from klayout_pex.pex25d.diagnostics import ExitCode
35from klayout_pex.pex25d.pex25d_cli import Pex25DCLI
37from .pex25d_fixtures import MINIMAL, replacing
40class Workspace:
41 """A directory with the fixture in it, and a way to run the tool on it."""
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)
49 def path(self, name: str) -> str:
50 return os.path.join(self.directory, name)
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
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)
68 # ------------------------------------------------------------ exit codes
70 def test_a_valid_file_exits_zero(self):
71 workspace = self.workspace()
72 assert workspace.run('validate', workspace.input_path) == ExitCode.OK
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
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
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
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
99 # ---------------------------------------------------------------- verbs
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')
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
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
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()
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)
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
140 # ---------------------------------------------------------- diagnostics
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
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_')