hitl_tester.modules.bms.test_handler
Provides helper code for testing serial / SMBus.
1"""Provides helper code for testing serial / SMBus.""" 2 3from __future__ import annotations 4 5from typing import Any, ClassVar, Type, Literal 6 7import pytest 8 9from hitl_tester.modules.bms.bms_hw import BMSHardware 10from hitl_tester.modules.bms.csv_tables import CSVBase 11from hitl_tester.modules.logger import logger 12 13 14_bms = BMSHardware(pytest.flags) # type: ignore[arg-type] 15 16 17class CSVRecordEvent: 18 """Handler for serial tests.""" 19 20 _failed: ClassVar[bool | None] = None # Final failure status of test suite 21 _registry: dict[str, list[Type[CSVRecordEvent]]] = {} # Keep track of nested subclasses 22 _registered_tests: list[Type[CSVRecordEvent]] = [] 23 _old_cycle_function: CSVBase = _bms.csv.cycle 24 _failing_average: float = 0 # How successful a test is between 0 (pass) and 1 (fail) 25 _samples: int = 0 # The amount of samples taken so far 26 failure_status: bool = False # Last failure status of a test 27 28 def __init_subclass__(cls): 29 """Map nested subclasses to their parent class.""" 30 super().__init_subclass__() 31 main_class, was_nested, _ = cls.__qualname__.partition(".") 32 if was_nested: 33 cls._registry[main_class] = cls._registry.get(main_class, []) + [cls] 34 35 @classmethod 36 def current_test(cls, test_class: type | None): 37 """Prepars to run a serial test.""" 38 CSVRecordEvent._old_cycle_function = _bms.csv.cycle # Save logging function 39 _bms.csv.cycle = _bms.csv.cycle_smbus # Record serial and SMBus 40 _bms.csv.cycle.postfix_fn = lambda: CSVRecordEvent.verify( # Verify when a row is recorded in the CSV 41 _bms.csv.cycle.last_row, # type: ignore[attr-defined] 42 _bms.csv.cycle.last_serial_data, # type: ignore[attr-defined] 43 _bms.csv.cycle.last_cell_data, # type: ignore[attr-defined] 44 ) 45 if test_class: 46 for test_type in cls._registry.get(test_class.__name__, []): 47 cls.register(test_type) 48 49 @classmethod 50 def register(cls, test: Type[CSVRecordEvent]): 51 """Register a test to run.""" 52 cls._registered_tests.append(test) 53 54 @classmethod 55 def update_failing_average(cls, failed: bool): 56 """Monitor how successful a test is from 0 (pass) to 1 (fail).""" 57 cls._failing_average = (failed + cls._samples * cls._failing_average) / (cls._samples + 1) 58 cls._samples += 1 59 60 @classmethod 61 def fail_rate(cls): 62 """Generate a failure rate string.""" 63 return f" [{cls._failing_average:.1%} failed]" if cls._failing_average > 0 else "" 64 65 @classmethod 66 def clear_tests(cls): 67 """Remove current serial tests.""" 68 69 cls._failed = None # Clear failure cache 70 cls._registered_tests = [] 71 _bms.csv.cycle = cls._old_cycle_function # Restore logging function (in case of modification) 72 73 @classmethod 74 def failed(cls) -> bool: 75 """Log results and whether the test failed.""" 76 if cls._failed is not None: 77 return cls._failed 78 79 cls._failed = False 80 for test in cls._registered_tests: 81 cls._failed |= test.failed() 82 result = test.result() + test.fail_rate() 83 logger.write_result_to_html_report((f'<font color="#990000">{result}</font>' if test.failed() else result)) 84 return cls._failed 85 86 @classmethod 87 def result(cls) -> str: 88 """Overall failure or success message.""" 89 text = "anomalies detected." 90 return text.capitalize() if cls.failed() else f"No {text}" 91 92 @classmethod 93 def verify(cls, row: dict[str, Any], serial_data: dict[str, int | str], cell_data: dict[int, dict[str, int]]): 94 """Verify all serial data is as expected.""" 95 for test in cls._registered_tests: 96 try: 97 test.verify(row, serial_data, cell_data) 98 failed = test.failed() 99 test.update_failing_average(failed) 100 if failed != test.failure_status: 101 if failed: 102 logger.write_warning_to_report(f"Now failing: {test.result()}") 103 else: 104 logger.write_info_to_report(f"Now passing: {test.result()}") 105 test.failure_status = failed 106 except ZeroDivisionError: # FIXME(JA): fix in child class 107 pass 108 109 @classmethod 110 def cmp( 111 cls, a: float, sign: Literal["<", "<=", ">", ">=", "=="], b: float, unit: str = "", form: str = ".1%" 112 ) -> str: 113 """Generate a formatted string based on a comparison.""" 114 sign_str = { 115 "<": ("≮", "<")[a < b], 116 "<=": ("≰", "≤")[a <= b], 117 ">": ("≯", ">")[a > b], 118 ">=": ("≱", "≥")[a >= b], 119 "==": ("≠", "=")[a == b], 120 } 121 return f"{a:{form}}{unit} {sign_str[sign]} {b:{form}}{unit}"
class
CSVRecordEvent:
18class CSVRecordEvent: 19 """Handler for serial tests.""" 20 21 _failed: ClassVar[bool | None] = None # Final failure status of test suite 22 _registry: dict[str, list[Type[CSVRecordEvent]]] = {} # Keep track of nested subclasses 23 _registered_tests: list[Type[CSVRecordEvent]] = [] 24 _old_cycle_function: CSVBase = _bms.csv.cycle 25 _failing_average: float = 0 # How successful a test is between 0 (pass) and 1 (fail) 26 _samples: int = 0 # The amount of samples taken so far 27 failure_status: bool = False # Last failure status of a test 28 29 def __init_subclass__(cls): 30 """Map nested subclasses to their parent class.""" 31 super().__init_subclass__() 32 main_class, was_nested, _ = cls.__qualname__.partition(".") 33 if was_nested: 34 cls._registry[main_class] = cls._registry.get(main_class, []) + [cls] 35 36 @classmethod 37 def current_test(cls, test_class: type | None): 38 """Prepars to run a serial test.""" 39 CSVRecordEvent._old_cycle_function = _bms.csv.cycle # Save logging function 40 _bms.csv.cycle = _bms.csv.cycle_smbus # Record serial and SMBus 41 _bms.csv.cycle.postfix_fn = lambda: CSVRecordEvent.verify( # Verify when a row is recorded in the CSV 42 _bms.csv.cycle.last_row, # type: ignore[attr-defined] 43 _bms.csv.cycle.last_serial_data, # type: ignore[attr-defined] 44 _bms.csv.cycle.last_cell_data, # type: ignore[attr-defined] 45 ) 46 if test_class: 47 for test_type in cls._registry.get(test_class.__name__, []): 48 cls.register(test_type) 49 50 @classmethod 51 def register(cls, test: Type[CSVRecordEvent]): 52 """Register a test to run.""" 53 cls._registered_tests.append(test) 54 55 @classmethod 56 def update_failing_average(cls, failed: bool): 57 """Monitor how successful a test is from 0 (pass) to 1 (fail).""" 58 cls._failing_average = (failed + cls._samples * cls._failing_average) / (cls._samples + 1) 59 cls._samples += 1 60 61 @classmethod 62 def fail_rate(cls): 63 """Generate a failure rate string.""" 64 return f" [{cls._failing_average:.1%} failed]" if cls._failing_average > 0 else "" 65 66 @classmethod 67 def clear_tests(cls): 68 """Remove current serial tests.""" 69 70 cls._failed = None # Clear failure cache 71 cls._registered_tests = [] 72 _bms.csv.cycle = cls._old_cycle_function # Restore logging function (in case of modification) 73 74 @classmethod 75 def failed(cls) -> bool: 76 """Log results and whether the test failed.""" 77 if cls._failed is not None: 78 return cls._failed 79 80 cls._failed = False 81 for test in cls._registered_tests: 82 cls._failed |= test.failed() 83 result = test.result() + test.fail_rate() 84 logger.write_result_to_html_report((f'<font color="#990000">{result}</font>' if test.failed() else result)) 85 return cls._failed 86 87 @classmethod 88 def result(cls) -> str: 89 """Overall failure or success message.""" 90 text = "anomalies detected." 91 return text.capitalize() if cls.failed() else f"No {text}" 92 93 @classmethod 94 def verify(cls, row: dict[str, Any], serial_data: dict[str, int | str], cell_data: dict[int, dict[str, int]]): 95 """Verify all serial data is as expected.""" 96 for test in cls._registered_tests: 97 try: 98 test.verify(row, serial_data, cell_data) 99 failed = test.failed() 100 test.update_failing_average(failed) 101 if failed != test.failure_status: 102 if failed: 103 logger.write_warning_to_report(f"Now failing: {test.result()}") 104 else: 105 logger.write_info_to_report(f"Now passing: {test.result()}") 106 test.failure_status = failed 107 except ZeroDivisionError: # FIXME(JA): fix in child class 108 pass 109 110 @classmethod 111 def cmp( 112 cls, a: float, sign: Literal["<", "<=", ">", ">=", "=="], b: float, unit: str = "", form: str = ".1%" 113 ) -> str: 114 """Generate a formatted string based on a comparison.""" 115 sign_str = { 116 "<": ("≮", "<")[a < b], 117 "<=": ("≰", "≤")[a <= b], 118 ">": ("≯", ">")[a > b], 119 ">=": ("≱", "≥")[a >= b], 120 "==": ("≠", "=")[a == b], 121 } 122 return f"{a:{form}}{unit} {sign_str[sign]} {b:{form}}{unit}"
Handler for serial tests.
@classmethod
def
current_test(cls, test_class: type | None):
36 @classmethod 37 def current_test(cls, test_class: type | None): 38 """Prepars to run a serial test.""" 39 CSVRecordEvent._old_cycle_function = _bms.csv.cycle # Save logging function 40 _bms.csv.cycle = _bms.csv.cycle_smbus # Record serial and SMBus 41 _bms.csv.cycle.postfix_fn = lambda: CSVRecordEvent.verify( # Verify when a row is recorded in the CSV 42 _bms.csv.cycle.last_row, # type: ignore[attr-defined] 43 _bms.csv.cycle.last_serial_data, # type: ignore[attr-defined] 44 _bms.csv.cycle.last_cell_data, # type: ignore[attr-defined] 45 ) 46 if test_class: 47 for test_type in cls._registry.get(test_class.__name__, []): 48 cls.register(test_type)
Prepars to run a serial test.
50 @classmethod 51 def register(cls, test: Type[CSVRecordEvent]): 52 """Register a test to run.""" 53 cls._registered_tests.append(test)
Register a test to run.
@classmethod
def
update_failing_average(cls, failed: bool):
55 @classmethod 56 def update_failing_average(cls, failed: bool): 57 """Monitor how successful a test is from 0 (pass) to 1 (fail).""" 58 cls._failing_average = (failed + cls._samples * cls._failing_average) / (cls._samples + 1) 59 cls._samples += 1
Monitor how successful a test is from 0 (pass) to 1 (fail).
@classmethod
def
fail_rate(cls):
61 @classmethod 62 def fail_rate(cls): 63 """Generate a failure rate string.""" 64 return f" [{cls._failing_average:.1%} failed]" if cls._failing_average > 0 else ""
Generate a failure rate string.
@classmethod
def
clear_tests(cls):
66 @classmethod 67 def clear_tests(cls): 68 """Remove current serial tests.""" 69 70 cls._failed = None # Clear failure cache 71 cls._registered_tests = [] 72 _bms.csv.cycle = cls._old_cycle_function # Restore logging function (in case of modification)
Remove current serial tests.
@classmethod
def
failed(cls) -> bool:
74 @classmethod 75 def failed(cls) -> bool: 76 """Log results and whether the test failed.""" 77 if cls._failed is not None: 78 return cls._failed 79 80 cls._failed = False 81 for test in cls._registered_tests: 82 cls._failed |= test.failed() 83 result = test.result() + test.fail_rate() 84 logger.write_result_to_html_report((f'<font color="#990000">{result}</font>' if test.failed() else result)) 85 return cls._failed
Log results and whether the test failed.
@classmethod
def
result(cls) -> str:
87 @classmethod 88 def result(cls) -> str: 89 """Overall failure or success message.""" 90 text = "anomalies detected." 91 return text.capitalize() if cls.failed() else f"No {text}"
Overall failure or success message.
@classmethod
def
verify( cls, row: dict[str, typing.Any], serial_data: dict[str, int | str], cell_data: dict[int, dict[str, int]]):
93 @classmethod 94 def verify(cls, row: dict[str, Any], serial_data: dict[str, int | str], cell_data: dict[int, dict[str, int]]): 95 """Verify all serial data is as expected.""" 96 for test in cls._registered_tests: 97 try: 98 test.verify(row, serial_data, cell_data) 99 failed = test.failed() 100 test.update_failing_average(failed) 101 if failed != test.failure_status: 102 if failed: 103 logger.write_warning_to_report(f"Now failing: {test.result()}") 104 else: 105 logger.write_info_to_report(f"Now passing: {test.result()}") 106 test.failure_status = failed 107 except ZeroDivisionError: # FIXME(JA): fix in child class 108 pass
Verify all serial data is as expected.
@classmethod
def
cmp( cls, a: float, sign: Literal['<', '<=', '>', '>=', '=='], b: float, unit: str = '', form: str = '.1%') -> str:
110 @classmethod 111 def cmp( 112 cls, a: float, sign: Literal["<", "<=", ">", ">=", "=="], b: float, unit: str = "", form: str = ".1%" 113 ) -> str: 114 """Generate a formatted string based on a comparison.""" 115 sign_str = { 116 "<": ("≮", "<")[a < b], 117 "<=": ("≰", "≤")[a <= b], 118 ">": ("≯", ">")[a > b], 119 ">=": ("≱", "≥")[a >= b], 120 "==": ("≠", "=")[a == b], 121 } 122 return f"{a:{form}}{unit} {sign_str[sign]} {b:{form}}{unit}"
Generate a formatted string based on a comparison.