"""WizardKit: CPU test functions""" # vim: sts=2 sw=2 ts=2 import logging import re import subprocess from typing import TextIO from wk import exe from wk.cfg.hw import CPU_TEMPS from wk.os.mac import set_fans as macos_set_fans from wk.std import PLATFORM from wk.ui import ansi # STATIC VARIABLES LOG = logging.getLogger(__name__) SysbenchType = tuple[subprocess.Popen, TextIO] # Functions def check_cooling_results(sensors, test_object) -> None: """Check cooling result via sensor data.""" idle_temp = sensors.get_cpu_temp('Idle') cooldown_temp = sensors.get_cpu_temp('Cooldown') max_temp = sensors.get_cpu_temp('Max') test_object.report.append(ansi.color_string('Temps', 'BLUE')) # Check temps if max_temp > CPU_TEMPS['Critical']: test_object.failed = True test_object.set_status('Failed') test_object.report.extend([ ansi.color_string( f' WARNING: Critical CPU temp of {CPU_TEMPS["Critical"]} exceeded.', 'RED', ), '', ]) elif idle_temp >= CPU_TEMPS['Idle High']: test_object.failed = True test_object.set_status('Failed') test_object.report.extend([ ansi.color_string( f' WARNING: Max idle temp of {CPU_TEMPS["Idle High"]} exceeded.', 'YELLOW', ), '', ]) elif ( cooldown_temp <= CPU_TEMPS['Cooling Low Cutoff'] or max_temp - cooldown_temp >= CPU_TEMPS['Cooling Delta'] ): test_object.passed = True test_object.set_status('Passed') else: test_object.passed = False test_object.set_status('Unknown') if cooldown_temp - idle_temp >= CPU_TEMPS['Idle Delta']: test_object.report.extend([ ansi.color_string( f' WARNING: Cooldown temp at least {CPU_TEMPS["Idle Delta"]}° over idle.', 'YELLOW', ), '', ]) # Build report report_labels = ['Idle'] if 'Sysbench' in sensors.temp_labels: report_labels.extend(['Sysbench', 'Cooldown']) if 'Prime95' in sensors.temp_labels: report_labels.append('Prime95') if 'Cooldown' not in report_labels: report_labels.append('Cooldown') if len(sensors.temp_labels.intersection(['Prime95', 'Sysbench'])) < 1: # Include overall max temp if needed report_labels.append('Max') for line in sensors.generate_report(*report_labels, only_cpu=True): test_object.report.append(f' {line}') def check_mprime_results(test_obj, working_dir) -> None: """Check mprime log files and update test_obj.""" passing_lines = set() warning_lines = set() def _read_file(log_name) -> list[str]: """Read file and split into lines, returns list.""" lines = [] try: with open(f'{working_dir}/{log_name}', 'r', encoding='utf-8') as _f: lines = _f.readlines() except FileNotFoundError: # File may be missing on older systems lines = [] return lines # results.txt (check if failed) for line in _read_file('results.txt'): line = line.strip() if re.search(r'(error|fail)', line, re.IGNORECASE): warning_lines.add(line) # prime.log (check if passed) for line in _read_file('prime.log'): line = line.strip() match = re.search( r'(completed.*(\d+) errors, (\d+) warnings)', line, re.IGNORECASE) if match: if int(match.group(2)) + int(match.group(3)) > 0: # Errors and/or warnings encountered warning_lines.add(match.group(1).capitalize()) else: # No errors/warnings passing_lines.add(match.group(1).capitalize()) # Update status if warning_lines: test_obj.failed = True test_obj.set_status('Failed') elif passing_lines and 'Aborted' not in test_obj.status: test_obj.passed = True test_obj.set_status('Passed') else: test_obj.set_status('Unknown') # Update report for line in passing_lines: test_obj.report.append(f' {line}') for line in warning_lines: test_obj.report.append(ansi.color_string(f' {line}', 'YELLOW')) if not (passing_lines or warning_lines): test_obj.report.append(ansi.color_string(' Unknown result', 'YELLOW')) def start_mprime(working_dir, log_path) -> subprocess.Popen: """Start mprime and save filtered output to log, returns Popen object.""" set_apple_fan_speed('max') proc_mprime = subprocess.Popen( ['mprime', '-t'], cwd=working_dir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) proc_grep = subprocess.Popen( 'grep --ignore-case --invert-match --line-buffered stress.txt'.split(), stdin=proc_mprime.stdout, stdout=subprocess.PIPE, ) proc_mprime.stdout.close() # type: ignore[reportOptionalMemberAccess] save_nbsr = exe.NonBlockingStreamReader( proc_grep.stdout, # type: ignore[reportGeneralTypeIssues] ) exe.start_thread( save_nbsr.save_to_file, args=(proc_grep, log_path), ) # Return objects return proc_mprime def set_apple_fan_speed(speed) -> None: """Set Apple fan speed.""" cmd = None # Check if speed not in ('auto', 'max'): raise RuntimeError(f'Invalid speed {speed}') # Set cmd if PLATFORM == 'Darwin': try: macos_set_fans(speed) except (RuntimeError, ValueError, subprocess.CalledProcessError) as err: LOG.error('Failed to set fans to %s', speed) LOG.error('Error: %s', err) #ui.print_error(f'Failed to set fans to {speed}') #for line in str(err).splitlines(): # ui.print_warning(f' {line.strip()}') elif PLATFORM == 'Linux': cmd = ['apple-fans', speed] exe.run_program(cmd, check=False) def start_sysbench(log_path) -> SysbenchType: """Start sysbench, returns tuple with Popen object and file handle.""" set_apple_fan_speed('max') cmd = [ 'sysbench', f'--threads={exe.psutil.cpu_count()}', '--cpu-max-prime=1000000000', 'cpu', 'run', ] # Start sysbench filehandle = open( log_path, 'a', encoding='utf-8', ) proc = exe.popen_program(cmd, stdout=filehandle) # Done return (proc, filehandle) def stop_mprime(proc_mprime) -> None: """Stop mprime gracefully, then forcefully as needed.""" proc_mprime.terminate() try: proc_mprime.wait(timeout=5) except subprocess.TimeoutExpired: proc_mprime.kill() set_apple_fan_speed('auto') def stop_sysbench(proc_sysbench, filehandle_sysbench) -> None: """Stop sysbench.""" proc_sysbench.terminate() try: proc_sysbench.wait(timeout=5) except subprocess.TimeoutExpired: proc_sysbench.kill() filehandle_sysbench.flush() filehandle_sysbench.close() set_apple_fan_speed('auto') if __name__ == '__main__': print("This file is not meant to be called directly.")