Coverage for moptipy / tests / component.py: 76%
78 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
1"""Functions that can be used to test component implementations."""
2from typing import Final
4from pycommons.types import type_error, type_name_of
6from moptipy.api import logging
7from moptipy.api.component import Component
8from moptipy.utils.logger import (
9 KEY_VALUE_SEPARATOR,
10 SECTION_END,
11 SECTION_START,
12 InMemoryLogger,
13)
14from moptipy.utils.strings import sanitize_name
17def validate_component(component: Component) -> None:
18 """
19 Check whether an object is a valid moptipy component.
21 This test checks the conversion to string and the logging of parameters.
22 This method must be called before the other `validate_*` methods provided
23 in this package.
25 :param component: the component to test
26 :raises ValueError: if `component` is not a valid
27 :class:`~moptipy.api.component.Component` instance
28 :raises TypeError: if a type is wrong or `component` is not even an
29 instance of :class:`~moptipy.api.component.Component`
30 """
31 if not isinstance(component, Component):
32 raise type_error(component, "component", Component)
33 if component.__class__ == Component:
34 raise ValueError(
35 "component cannot be an instance of Component directly.")
37 name = str(component)
38 if not isinstance(name, str):
39 raise type_error(name, "str(component)", str)
40 if len(name) <= 0:
41 raise ValueError("str(component) must return a non-empty string, "
42 f"but returns a {name!r}.")
43 if name.strip() != name:
44 raise ValueError("str(component) must return a string without "
45 "leading or trailing white space, "
46 f"but returns a {name!r}.")
48 clean_name = sanitize_name(name)
49 if clean_name != name:
50 raise ValueError(
51 "str(component) must return a string which does not "
52 f"change when being sanitized, but returned {name!r},"
53 f" which becomes {clean_name!r}.")
54 name = str(component)
55 if clean_name != name:
56 raise ValueError("str(component) must always return the same value, "
57 f"but returns a {name!r} and {clean_name!r}.")
59 name = repr(component)
60 if name != clean_name:
61 raise ValueError("repr(component) must equal str(component), but "
62 f"got {clean_name!r} vs. {name!r}.")
64 if not (hasattr(component, "initialize")
65 and callable(getattr(component, "initialize"))):
66 raise ValueError("component must have method initialize.")
67 component.initialize()
69 if name != str(component):
70 raise ValueError(f"name changed to {str(component)!r} "
71 f"from {name!r} after initialize!")
73 # test the logging of parameter values
74 if not (hasattr(component, "log_parameters_to")
75 and callable(getattr(component, "log_parameters_to"))):
76 raise ValueError("component must have method log_parameters_to.")
78 secname: Final[str] = "KV"
79 with InMemoryLogger() as log:
80 with log.key_values(secname) as kv:
81 component.log_parameters_to(kv)
82 lines = log.get_log()
84 ll = len(lines) - 1
85 if (lines[0] != SECTION_START + secname) or \
86 (lines[ll] != SECTION_END + secname):
87 raise ValueError("Invalid log data produced '"
88 + "\n".join(lines) + "'.")
90 kvs: Final[str] = KEY_VALUE_SEPARATOR
91 lines = lines[1:ll]
92 if len(lines) < 2:
93 raise ValueError("A component must produce at least two lines of "
94 f"key-value data, but produced {len(lines)}.")
96 done_keys: set[str] = set()
97 idx: int = 0
98 key: str = logging.KEY_NAME
99 done_keys.add(key)
100 keystr: str = f"{key}{kvs}"
101 line: str = lines[idx]
102 idx += 1
103 if not line.startswith(keystr):
104 raise ValueError(
105 f"First log line must begin with {keystr!r}, but starts"
106 f" with {line!r}.")
107 rest = line[len(keystr):]
108 if rest != name:
109 raise ValueError(
110 f"value of key {keystr!r} should equal "
111 f"{name!r} but is {rest!r}.")
113 key = logging.KEY_CLASS
114 keystr = f"{key}{kvs}"
115 done_keys.add(key)
116 line = lines[idx]
117 idx += 1
118 if not line.startswith(keystr):
119 raise ValueError(
120 f"Second log line must begin with {keystr!r}, but "
121 f"starts with {line!r}.")
122 rest = line[len(keystr):]
123 want = type_name_of(component)
124 if rest != want:
125 raise ValueError(
126 f"value of key {keystr!r} should equal "
127 f"{want!r} but is {rest!r}.")
129 for line in lines[idx:]:
130 i = line.index(kvs)
131 key = line[0:i].strip()
132 b = line[i + len(kvs)].strip()
133 if (len(key) <= 0) or (len(b) <= 0):
134 raise ValueError(
135 f"Invalid key-value pair {line!r} - "
136 f"splits to {(key + kvs + b)!r}!")
137 if key in done_keys:
138 raise ValueError(f"key {key!r} appears twice!")
139 done_keys.add(key)