hitl_tester.test_cases.bms.conftest
Pytest hooks for modifying tests as they run.
1""" 2Pytest hooks for modifying tests as they run. 3""" 4 5import html 6import platform 7import re 8import sys 9import time 10from datetime import datetime, timedelta, timezone 11from pathlib import Path 12 13import pytest 14from pytest_metadata.plugin import metadata_key 15 16from hitl_tester.modules.bms.bms_hw import BMSHardware 17from hitl_tester.modules.bms.event_watcher import SerialWatcher 18from hitl_tester.modules.bms.plateset import Plateset 19from hitl_tester.modules.html_merger import Duration 20from hitl_tester.modules.logger import logger 21 22_bms = BMSHardware(pytest.flags) # type: ignore[arg-type] 23_plateset = Plateset() 24_serial_watcher = SerialWatcher() 25 26 27def pytest_collection_finish(session): 28 """Calculate total estimated runtime from docstrings.""" 29 estimated_runtime = timedelta() 30 for test in session.items: 31 time_match = ( 32 re.search( 33 r""" 34 \|\s*Estimated\sDuration\s*\|\s* # Only look at the duration row 35 (\d+) # Capture digits 36 \s+ # Space 37 (seconds* | minutes* | hours* | days* | weeks*) # Capture unit of time (optional "s") 38 """, 39 test.function.__doc__, 40 re.IGNORECASE | re.VERBOSE, 41 ) 42 if test.function.__doc__ 43 else None 44 ) 45 if time_match is not None: 46 unit_name = f"{time_match.group(2).rstrip('s')}s" 47 estimated_runtime += timedelta(**{unit_name: int(time_match.group(1))}) 48 session.config.stash[metadata_key]["Estimated Runtime (hh:mm:ss)"] = str(Duration(estimated_runtime)) 49 50 51def pytest_html_report_title(report): 52 """Change the html report title.""" 53 report.title = "HITL Report" 54 55 56def pytest_html_results_table_header(cells): 57 """Generate html report headers.""" 58 del cells[:] 59 cells.extend( 60 [ 61 '<th class="sortable" data-column-type="result">Pass / Fail</th>', 62 "<th>Result</th>", 63 "<th>Description</th>", 64 '<th class="sortable time" data-column-type="time">Start Time</th>', 65 '<th class="sortable" data-column-type="duration">Duration (h:mm:ss.u)</th>', 66 '<th class="sortable" data-column-type="testId">Test / Docs</th>', 67 ] 68 ) 69 70 71def pytest_html_results_table_row(report, cells): 72 """Generate the row for a test in the html report.""" 73 del cells[:] 74 75 # Generate URL 76 test_case, line_number, test_name = report.location 77 test_name = test_name.partition("[")[0] # Remove fixture information 78 test_case_html_path = str(Path(test_case).with_suffix(".html")) 79 base_link = html.escape(f"https://dev.bms-docs.taf.work/{test_case_html_path}#{test_name}") 80 81 # Generate result 82 pass_fail = report.outcome.title() 83 result = "<br>".join(report.result_text) 84 duration = timedelta(seconds=report.duration) 85 start_time = (datetime.now(timezone.utc) - duration).strftime("%Y/%m/%d %I:%M:%S %p") 86 safe_test_name = html.escape(test_name) 87 safe_test_case = html.escape(test_case) 88 cells.extend( 89 [ 90 f'<td class="col-result">{pass_fail}</td>', 91 f"<td>{result}</td>", 92 f"<td>{report.description}</td>", 93 f'<td class="col-time">{start_time}</td>', 94 f'<td class="col-duration">{duration}</td>', 95 f'<td><a target="_blank" style="color:#337788;" href="{base_link}">{safe_test_name}</a> ' 96 f"(File {safe_test_case}, line {line_number})</td>", 97 ] 98 ) 99 100 101@pytest.hookimpl(hookwrapper=True) 102def pytest_runtest_makereport(item, call): # pylint: disable=unused-argument 103 """Generate the report for one test.""" 104 outcome = yield 105 report = outcome.get_result() 106 report.result_text = item.module.logger.html_result_text if hasattr(item.module, "logger") else "" 107 description_match = ( 108 re.search( 109 r""" 110 \|\s*Description\s*\|\s* # Description row 111 (.*?) # Capture description text 112 \s*\| # End of row 113 """, 114 item.function.__doc__, 115 re.IGNORECASE | re.VERBOSE, 116 ) 117 if item.function.__doc__ 118 else None 119 ) 120 report.description = description_match.group(1) if description_match is not None else "" 121 122 123def pytest_configure(config): 124 """Add custom markers and information to the beginning of the test html report.""" 125 config.addinivalue_line("markers", "live_cells: this test uses live cells 🔥") 126 config.addinivalue_line("markers", "sim_cells: this test uses cell simulators") 127 128 terminal_command = html.escape(" ".join(sys.argv)) 129 hitl = html.escape(platform.node()) 130 test_plan = html.escape(pytest.flags.test_plan.stem) 131 log_folder = html.escape(str(pytest.flags.report_filename.parent)) 132 hardware_config = html.escape(pytest.flags.plateset_id) 133 properties = {html.escape(name): html.escape(str(value)) for name, value in pytest.flags.properties.items()} 134 135 config.stash[metadata_key]["Terminal Command"] = f"<pre><code>{terminal_command}</code></pre>" 136 config.stash[metadata_key]["HITL"] = hitl 137 config.stash[metadata_key]["Test Plan"] = test_plan 138 config.stash[metadata_key]["Hardware Config"] = hardware_config 139 config.stash[metadata_key]["Properties"] = properties 140 config.stash[metadata_key]["Logs"] = log_folder 141 config.stash[metadata_key]["Estimated Runtime (hh:mm:ss)"] = "Calculating..." 142 143 # Remove some rows 144 config.stash[metadata_key].pop("Plugins", None) 145 config.stash[metadata_key].pop("Packages", None) 146 # config.stash[metadata_key]["Packages"] = {d.project_name: d.version for d in pkg_resources.working_set} 147 148 149@pytest.fixture(scope="function") 150def cycle_smbus(): 151 """Log Serial and SMBus.""" 152 # Save old CSV recorder 153 old_cycle_function = _bms.csv.cycle 154 _bms.csv.cycle = _bms.csv.cycle_smbus 155 156 # Run test 157 yield 158 159 # Use old CSV recorder 160 _bms.csv.cycle = old_cycle_function 161 162 163@pytest.fixture(scope="function") 164def serial_watcher(): 165 """Log Serial and SMBus.""" 166 # Save old CSV recorder 167 old_cycle_function = _bms.csv.cycle 168 _bms.csv.cycle = _bms.csv.cycle_smbus 169 170 # Start serial watcher 171 _serial_watcher.start() 172 173 # Run test 174 yield _serial_watcher 175 176 # Stop serial watcher 177 _serial_watcher.stop() 178 179 # Use old CSV recorder 180 _bms.csv.cycle = old_cycle_function 181 182 183@pytest.fixture(scope="function") 184def reset_cell_sims(request): 185 """ 186 Before each test, reset cell sims / BMS and set appropriate temperatures. 187 188 Fixture arguments are provided in an abnormal way, see below tests for details on how to provide these arguments. 189 A default value is used if neither soc nor volts is provided. 190 191 :param float temperature: the initial temperature in C 192 :param float soc: the initial state of charge 193 :param float volts: the initial voltage 194 """ 195 default_temperature_c = 23 196 default_soc = 0.50 197 args = getattr(request, "param", {}) 198 199 # Reset cell sims 200 starting_temperature = args.get("temperature", default_temperature_c) 201 if len(_bms.cells) > 0: 202 logger.write_info_to_report(f"Setting temperature to {starting_temperature}°C") 203 _plateset.thermistor1 = _plateset.thermistor2 = starting_temperature 204 logger.write_info_to_report("Powering down cell sims") 205 for cell in _bms.cells.values(): 206 cell.disengage_safety_protocols = True 207 cell.volts = 0.0001 208 time.sleep(5) 209 for cell in _bms.cells.values(): 210 new_soc = args.get("soc") or default_soc or cell.volts_to_soc(args.get("volts")) 211 logger.write_info_to_report(f"Powering up cell sim {cell.id} to {new_soc:%}") 212 cell.state_of_charge = new_soc 213 cell.disengage_safety_protocols = False 214 215 yield # Run test 216 217 218@pytest.hookimpl(trylast=True) 219def pytest_collection_modifyitems(items): 220 """Check if any tests are marked to use the cell sims.""" 221 for item in items: 222 if item.get_closest_marker("sim_cells"): 223 item.fixturenames.append("reset_cell_sims")
def
pytest_collection_finish(session):
28def pytest_collection_finish(session): 29 """Calculate total estimated runtime from docstrings.""" 30 estimated_runtime = timedelta() 31 for test in session.items: 32 time_match = ( 33 re.search( 34 r""" 35 \|\s*Estimated\sDuration\s*\|\s* # Only look at the duration row 36 (\d+) # Capture digits 37 \s+ # Space 38 (seconds* | minutes* | hours* | days* | weeks*) # Capture unit of time (optional "s") 39 """, 40 test.function.__doc__, 41 re.IGNORECASE | re.VERBOSE, 42 ) 43 if test.function.__doc__ 44 else None 45 ) 46 if time_match is not None: 47 unit_name = f"{time_match.group(2).rstrip('s')}s" 48 estimated_runtime += timedelta(**{unit_name: int(time_match.group(1))}) 49 session.config.stash[metadata_key]["Estimated Runtime (hh:mm:ss)"] = str(Duration(estimated_runtime))
Calculate total estimated runtime from docstrings.
def
pytest_html_report_title(report):
52def pytest_html_report_title(report): 53 """Change the html report title.""" 54 report.title = "HITL Report"
Change the html report title.
def
pytest_html_results_table_header(cells):
57def pytest_html_results_table_header(cells): 58 """Generate html report headers.""" 59 del cells[:] 60 cells.extend( 61 [ 62 '<th class="sortable" data-column-type="result">Pass / Fail</th>', 63 "<th>Result</th>", 64 "<th>Description</th>", 65 '<th class="sortable time" data-column-type="time">Start Time</th>', 66 '<th class="sortable" data-column-type="duration">Duration (h:mm:ss.u)</th>', 67 '<th class="sortable" data-column-type="testId">Test / Docs</th>', 68 ] 69 )
Generate html report headers.
def
pytest_html_results_table_row(report, cells):
72def pytest_html_results_table_row(report, cells): 73 """Generate the row for a test in the html report.""" 74 del cells[:] 75 76 # Generate URL 77 test_case, line_number, test_name = report.location 78 test_name = test_name.partition("[")[0] # Remove fixture information 79 test_case_html_path = str(Path(test_case).with_suffix(".html")) 80 base_link = html.escape(f"https://dev.bms-docs.taf.work/{test_case_html_path}#{test_name}") 81 82 # Generate result 83 pass_fail = report.outcome.title() 84 result = "<br>".join(report.result_text) 85 duration = timedelta(seconds=report.duration) 86 start_time = (datetime.now(timezone.utc) - duration).strftime("%Y/%m/%d %I:%M:%S %p") 87 safe_test_name = html.escape(test_name) 88 safe_test_case = html.escape(test_case) 89 cells.extend( 90 [ 91 f'<td class="col-result">{pass_fail}</td>', 92 f"<td>{result}</td>", 93 f"<td>{report.description}</td>", 94 f'<td class="col-time">{start_time}</td>', 95 f'<td class="col-duration">{duration}</td>', 96 f'<td><a target="_blank" style="color:#337788;" href="{base_link}">{safe_test_name}</a> ' 97 f"(File {safe_test_case}, line {line_number})</td>", 98 ] 99 )
Generate the row for a test in the html report.
@pytest.hookimpl(hookwrapper=True)
def
pytest_runtest_makereport(item, call):
102@pytest.hookimpl(hookwrapper=True) 103def pytest_runtest_makereport(item, call): # pylint: disable=unused-argument 104 """Generate the report for one test.""" 105 outcome = yield 106 report = outcome.get_result() 107 report.result_text = item.module.logger.html_result_text if hasattr(item.module, "logger") else "" 108 description_match = ( 109 re.search( 110 r""" 111 \|\s*Description\s*\|\s* # Description row 112 (.*?) # Capture description text 113 \s*\| # End of row 114 """, 115 item.function.__doc__, 116 re.IGNORECASE | re.VERBOSE, 117 ) 118 if item.function.__doc__ 119 else None 120 ) 121 report.description = description_match.group(1) if description_match is not None else ""
Generate the report for one test.
def
pytest_configure(config):
124def pytest_configure(config): 125 """Add custom markers and information to the beginning of the test html report.""" 126 config.addinivalue_line("markers", "live_cells: this test uses live cells 🔥") 127 config.addinivalue_line("markers", "sim_cells: this test uses cell simulators") 128 129 terminal_command = html.escape(" ".join(sys.argv)) 130 hitl = html.escape(platform.node()) 131 test_plan = html.escape(pytest.flags.test_plan.stem) 132 log_folder = html.escape(str(pytest.flags.report_filename.parent)) 133 hardware_config = html.escape(pytest.flags.plateset_id) 134 properties = {html.escape(name): html.escape(str(value)) for name, value in pytest.flags.properties.items()} 135 136 config.stash[metadata_key]["Terminal Command"] = f"<pre><code>{terminal_command}</code></pre>" 137 config.stash[metadata_key]["HITL"] = hitl 138 config.stash[metadata_key]["Test Plan"] = test_plan 139 config.stash[metadata_key]["Hardware Config"] = hardware_config 140 config.stash[metadata_key]["Properties"] = properties 141 config.stash[metadata_key]["Logs"] = log_folder 142 config.stash[metadata_key]["Estimated Runtime (hh:mm:ss)"] = "Calculating..." 143 144 # Remove some rows 145 config.stash[metadata_key].pop("Plugins", None) 146 config.stash[metadata_key].pop("Packages", None) 147 # config.stash[metadata_key]["Packages"] = {d.project_name: d.version for d in pkg_resources.working_set}
Add custom markers and information to the beginning of the test html report.
@pytest.fixture(scope='function')
def
cycle_smbus():
150@pytest.fixture(scope="function") 151def cycle_smbus(): 152 """Log Serial and SMBus.""" 153 # Save old CSV recorder 154 old_cycle_function = _bms.csv.cycle 155 _bms.csv.cycle = _bms.csv.cycle_smbus 156 157 # Run test 158 yield 159 160 # Use old CSV recorder 161 _bms.csv.cycle = old_cycle_function
Log Serial and SMBus.
@pytest.fixture(scope='function')
def
serial_watcher():
164@pytest.fixture(scope="function") 165def serial_watcher(): 166 """Log Serial and SMBus.""" 167 # Save old CSV recorder 168 old_cycle_function = _bms.csv.cycle 169 _bms.csv.cycle = _bms.csv.cycle_smbus 170 171 # Start serial watcher 172 _serial_watcher.start() 173 174 # Run test 175 yield _serial_watcher 176 177 # Stop serial watcher 178 _serial_watcher.stop() 179 180 # Use old CSV recorder 181 _bms.csv.cycle = old_cycle_function
Log Serial and SMBus.
@pytest.fixture(scope='function')
def
reset_cell_sims(request):
184@pytest.fixture(scope="function") 185def reset_cell_sims(request): 186 """ 187 Before each test, reset cell sims / BMS and set appropriate temperatures. 188 189 Fixture arguments are provided in an abnormal way, see below tests for details on how to provide these arguments. 190 A default value is used if neither soc nor volts is provided. 191 192 :param float temperature: the initial temperature in C 193 :param float soc: the initial state of charge 194 :param float volts: the initial voltage 195 """ 196 default_temperature_c = 23 197 default_soc = 0.50 198 args = getattr(request, "param", {}) 199 200 # Reset cell sims 201 starting_temperature = args.get("temperature", default_temperature_c) 202 if len(_bms.cells) > 0: 203 logger.write_info_to_report(f"Setting temperature to {starting_temperature}°C") 204 _plateset.thermistor1 = _plateset.thermistor2 = starting_temperature 205 logger.write_info_to_report("Powering down cell sims") 206 for cell in _bms.cells.values(): 207 cell.disengage_safety_protocols = True 208 cell.volts = 0.0001 209 time.sleep(5) 210 for cell in _bms.cells.values(): 211 new_soc = args.get("soc") or default_soc or cell.volts_to_soc(args.get("volts")) 212 logger.write_info_to_report(f"Powering up cell sim {cell.id} to {new_soc:%}") 213 cell.state_of_charge = new_soc 214 cell.disengage_safety_protocols = False 215 216 yield # Run test
Before each test, reset cell sims / BMS and set appropriate temperatures.
Fixture arguments are provided in an abnormal way, see below tests for details on how to provide these arguments. A default value is used if neither soc nor volts is provided.
Parameters
- float temperature: the initial temperature in C
- float soc: the initial state of charge
- float volts: the initial voltage
@pytest.hookimpl(trylast=True)
def
pytest_collection_modifyitems(items):
219@pytest.hookimpl(trylast=True) 220def pytest_collection_modifyitems(items): 221 """Check if any tests are marked to use the cell sims.""" 222 for item in items: 223 if item.get_closest_marker("sim_cells"): 224 item.fixturenames.append("reset_cell_sims")
Check if any tests are marked to use the cell sims.