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

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 

25 

26import os 

27import tempfile 

28import unittest 

29 

30import allure 

31 

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 

35 

36from .pex25d_fixtures import MINIMAL 

37 

38 

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" 

46 

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 

54 

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 

61 

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))}" 

70 

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()) 

76 

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) 

81 

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 

85 

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) 

89 

90 with tempfile.TemporaryDirectory() as tmp_dir: 

91 def path(name: str) -> str: 

92 return os.path.join(tmp_dir, name) 

93 

94 pex25d.write(file, path('tiny.pex25d')) 

95 pex25d.write(file, path('tiny.pex25d.pb')) 

96 pex25d.write(scene, path('tiny.pex25d.scene.pb')) 

97 

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 

101 

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')) 

109 

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'))