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

1"""Functions that can be used to test component implementations.""" 

2from typing import Final 

3 

4from pycommons.types import type_error, type_name_of 

5 

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 

15 

16 

17def validate_component(component: Component) -> None: 

18 """ 

19 Check whether an object is a valid moptipy component. 

20 

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. 

24 

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

36 

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

47 

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

58 

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

63 

64 if not (hasattr(component, "initialize") 

65 and callable(getattr(component, "initialize"))): 

66 raise ValueError("component must have method initialize.") 

67 component.initialize() 

68 

69 if name != str(component): 

70 raise ValueError(f"name changed to {str(component)!r} " 

71 f"from {name!r} after initialize!") 

72 

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

77 

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

83 

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) + "'.") 

89 

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

95 

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

112 

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

128 

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)