Coverage for tests/pex25d/api_test.py: 100%
63 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#
24from __future__ import annotations
26import os
27import tempfile
28import unittest
30import allure
32# The point of this test: everything the format's consumers need comes from
33# one import. Nothing below may reach into a submodule.
34from klayout_pex import pex25d
36from .pex25d_fixtures import MINIMAL
39@allure.parent_suite('Unit Tests')
40@allure.tag('PEX25D', 'API')
41class Pex25DApiTest(unittest.TestCase):
42 def test_every_exported_name_resolves(self):
43 for name in pex25d.__all__:
44 with self.subTest(name=name):
45 assert hasattr(pex25d, name), f"{name} is in __all__ but not importable"
47 def test_the_generated_messages_are_reachable_by_name(self):
48 assert pex25d.proto.PEX25DFile().DESCRIPTOR.name == 'PEX25DFile'
49 assert pex25d.proto.PEX25DScene().DESCRIPTOR.name == 'PEX25DScene'
50 # a module, for the enums a consumer needs
51 assert pex25d.proto.dielectric.DIELECTRIC_KIND_SIMPLE is not None
52 with self.assertRaises(AttributeError):
53 pex25d.proto.NoSuchMessage
55 def test_every_generated_schema_is_reachable(self):
56 # The namespace is discovered, not listed, so this also states that
57 # nothing in it has gone stale after a protobuf re-generation.
58 for name in dir(pex25d.proto):
59 with self.subTest(name=name):
60 assert getattr(pex25d.proto, name) is not None
62 def test_the_schemas_the_format_is_made_of_are_present(self):
63 # Fails when a .proto is renamed or removed — which the API and the
64 # documentation have to follow. An added one needs no change here.
65 required = {'diagnostics', 'dielectric', 'domain', 'file', 'geometry',
66 'meta', 'resistance', 'scene', 'source_ref', 'terminal',
67 'units', 'PEX25DFile', 'PEX25DScene'}
68 assert required <= set(dir(pex25d.proto)), \
69 f"missing from the PEX25D protobuf namespace: {required - set(dir(pex25d.proto))}"
71 def test_the_kind_of_a_message_follows_its_descriptor(self):
72 assert pex25d.kind_of(pex25d.proto.PEX25DFile()) == pex25d.ArtifactKind.FILE
73 assert pex25d.kind_of(pex25d.proto.PEX25DScene()) == pex25d.ArtifactKind.SCENE
74 with self.assertRaises(ValueError):
75 pex25d.kind_of(pex25d.proto.units.Units())
77 def test_the_pipeline_needs_no_other_import(self):
78 report = pex25d.DiagnosticsReport()
79 file = pex25d.read_text(MINIMAL.encode('utf-8'), report=report)
80 scene = pex25d.resolve(file, report=report)
82 assert pex25d.kind_of(file) == pex25d.ArtifactKind.FILE
83 assert pex25d.kind_of(scene) == pex25d.ArtifactKind.SCENE
84 assert pex25d.validate(file, scene=scene).exit_code == pex25d.ExitCode.OK
86 def test_a_path_carries_the_kind_and_the_encoding(self):
87 file = pex25d.read_text(MINIMAL.encode('utf-8'))
88 scene = pex25d.resolve(file)
90 with tempfile.TemporaryDirectory() as tmp_dir:
91 def path(name: str) -> str:
92 return os.path.join(tmp_dir, name)
94 pex25d.write(file, path('tiny.pex25d'))
95 pex25d.write(file, path('tiny.pex25d.pb'))
96 pex25d.write(scene, path('tiny.pex25d.scene.pb'))
98 assert pex25d.read(path('tiny.pex25d')) == file
99 assert pex25d.read(path('tiny.pex25d.pb')) == file
100 assert pex25d.read(path('tiny.pex25d.scene.pb')) == scene
102 def test_a_message_the_path_does_not_describe_is_refused(self):
103 # '.pex25d' is a file in text format, and the text format spells a
104 # PEX25DFile only — writing a scene there has to say so.
105 scene = pex25d.resolve(pex25d.read_text(MINIMAL.encode('utf-8')))
106 with tempfile.TemporaryDirectory() as tmp_dir:
107 with self.assertRaises(ValueError):
108 pex25d.write(scene, os.path.join(tmp_dir, 'scene.pex25d'))
110 def test_a_path_without_a_conventional_suffix_is_reported_as_such(self):
111 file = pex25d.read_text(MINIMAL.encode('utf-8'))
112 with self.assertRaises(pex25d.ArtifactNamingError):
113 pex25d.read('cell.unknown')
114 with tempfile.TemporaryDirectory() as tmp_dir:
115 with self.assertRaises(pex25d.ArtifactNamingError):
116 pex25d.write(file, os.path.join(tmp_dir, 'cell.unknown'))