|
| 1 | +#!/usr/bin/env python3 |
| 2 | +import subprocess |
| 3 | +import signal |
| 4 | +import os |
| 5 | +import logging |
| 6 | + |
| 7 | +# Set up logging |
| 8 | +logging.basicConfig( |
| 9 | + level=logging.INFO, |
| 10 | + format="%(asctime)s - %(levelname)s - %(message)s", |
| 11 | + datefmt="%Y-%m-%d %H:%M:%S", |
| 12 | +) |
| 13 | + |
| 14 | +# --- Configuration --- |
| 15 | +THERMALD_CMD = ["thermald", "--no-daemon", "--loglevel=debug", "--adaptive"] |
| 16 | + |
| 17 | +# Patterns to search for (as bytes). |
| 18 | +FAILURE_PATTERN = b"Also unable to evaluate any conditions" |
| 19 | +SUCCESS_PATTERN = b"Start main loop" |
| 20 | + |
| 21 | +# The grep command will search for either pattern, stop after the first match (-m 1), |
| 22 | +# and flush its output immediately (--line-buffered). |
| 23 | +GREP_CMD = [ |
| 24 | + "grep", |
| 25 | + "--line-buffered", |
| 26 | + "-m", |
| 27 | + "1", |
| 28 | + "-e", |
| 29 | + FAILURE_PATTERN.decode("utf-8"), |
| 30 | + "-e", |
| 31 | + SUCCESS_PATTERN.decode("utf-8"), |
| 32 | +] |
| 33 | + |
| 34 | +TIMEOUT_SECONDS = 15 |
| 35 | + |
| 36 | + |
| 37 | +def stop_thermald_service(): |
| 38 | + """ |
| 39 | + Stop the thermald systemd service if it's running. |
| 40 | + Returns True if service was running and stopped, False if it wasn't running. |
| 41 | + """ |
| 42 | + logging.info("--- Checking thermald systemd service ---") |
| 43 | + |
| 44 | + try: |
| 45 | + # Check if thermald service is active |
| 46 | + result = subprocess.run( |
| 47 | + ["systemctl", "is-active", "thermald"], |
| 48 | + capture_output=True, |
| 49 | + text=True, |
| 50 | + ) |
| 51 | + |
| 52 | + if result.returncode == 0 and result.stdout.strip() == "active": |
| 53 | + logging.info("Thermald service is running. Stopping it...") |
| 54 | + stop_result = subprocess.run( |
| 55 | + ["systemctl", "stop", "thermald"], |
| 56 | + capture_output=True, |
| 57 | + text=True, |
| 58 | + ) |
| 59 | + |
| 60 | + if stop_result.returncode == 0: |
| 61 | + logging.info("Thermald service stopped successfully.") |
| 62 | + return True |
| 63 | + else: |
| 64 | + logging.error( |
| 65 | + f"Failed to stop thermald service: {stop_result.stderr}" |
| 66 | + ) |
| 67 | + return False |
| 68 | + else: |
| 69 | + logging.info("Thermald service is not running.") |
| 70 | + return False |
| 71 | + |
| 72 | + except Exception as e: |
| 73 | + logging.error(f"Error checking/stopping thermald service: {e}") |
| 74 | + return False |
| 75 | + |
| 76 | + |
| 77 | +def start_thermald_service(): |
| 78 | + """ |
| 79 | + Start the thermald systemd service. |
| 80 | + """ |
| 81 | + logging.info("--- Restarting thermald systemd service ---") |
| 82 | + |
| 83 | + try: |
| 84 | + result = subprocess.run( |
| 85 | + ["systemctl", "start", "thermald"], capture_output=True, text=True |
| 86 | + ) |
| 87 | + |
| 88 | + if result.returncode == 0: |
| 89 | + logging.info("Thermald service restarted successfully.") |
| 90 | + else: |
| 91 | + logging.error( |
| 92 | + f"Failed to restart thermald service: {result.stderr}" |
| 93 | + ) |
| 94 | + |
| 95 | + except Exception as e: |
| 96 | + logging.error(f"Error restarting thermald service: {e}") |
| 97 | + |
| 98 | + |
| 99 | +def run_thermald_grep_test(): |
| 100 | + """ |
| 101 | + Test thermald's adaptive engine startup by monitoring its output. |
| 102 | +
|
| 103 | + This test launches thermald with adaptive engine and uses grep to detect |
| 104 | + which pattern appears first: |
| 105 | + - SUCCESS: "Start main loop" - adaptive engine started correctly |
| 106 | + - FAILURE: "Also unable to evaluate any conditions" - adaptive engine failed, |
| 107 | + thermald falls back to highest power profile |
| 108 | +
|
| 109 | + The test captures the first occurrence of either pattern to determine if |
| 110 | + the adaptive engine initialization was successful. |
| 111 | + """ |
| 112 | + logging.info("=== THERMALD ADAPTIVE ENGINE TEST ===") |
| 113 | + logging.info( |
| 114 | + "This test verifies that thermald's adaptive engine starts correctly." |
| 115 | + ) |
| 116 | + logging.info( |
| 117 | + "Monitoring thermald output for adaptive engine initialization patterns..." |
| 118 | + ) |
| 119 | + logging.info(f"Success pattern: '{SUCCESS_PATTERN.decode('utf-8')}'") |
| 120 | + logging.info(f"Failure pattern: '{FAILURE_PATTERN.decode('utf-8')}'") |
| 121 | + logging.info(f"Command: {' '.join(THERMALD_CMD)}") |
| 122 | + logging.info(f"Timeout: {TIMEOUT_SECONDS} seconds") |
| 123 | + logging.info("=" * 70) |
| 124 | + |
| 125 | + if os.geteuid() != 0: |
| 126 | + logging.error("\nERROR: This script requires sudo privileges.") |
| 127 | + logging.error("Please run it with: sudo python3 test_thermald.py") |
| 128 | + raise SystemExit(1) |
| 129 | + |
| 130 | + # Stop thermald service if it's running |
| 131 | + service_was_running = stop_thermald_service() |
| 132 | + |
| 133 | + thermald_proc = None |
| 134 | + grep_proc = None |
| 135 | + result_message = "" |
| 136 | + exit_code = 1 # Default to failure |
| 137 | + |
| 138 | + try: |
| 139 | + # 1. Start the thermald process. |
| 140 | + # - Redirect stderr to stdout so grep sees all output. |
| 141 | + # - preexec_fn=os.setsid creates a new process group for robust cleanup. |
| 142 | + thermald_proc = subprocess.Popen( |
| 143 | + THERMALD_CMD, |
| 144 | + stdout=subprocess.PIPE, |
| 145 | + stderr=subprocess.STDOUT, |
| 146 | + preexec_fn=os.setsid, |
| 147 | + ) |
| 148 | + logging.info( |
| 149 | + f"Started thermald with adaptive engine (PGID: {thermald_proc.pid})" |
| 150 | + ) |
| 151 | + |
| 152 | + # 2. Start the grep process, piping thermald's output to its input. |
| 153 | + grep_proc = subprocess.Popen( |
| 154 | + GREP_CMD, stdin=thermald_proc.stdout, stdout=subprocess.PIPE |
| 155 | + ) |
| 156 | + logging.info(f"Started pattern monitoring (grep PID: {grep_proc.pid})") |
| 157 | + logging.info("Waiting for adaptive engine initialization patterns...") |
| 158 | + |
| 159 | + # 3. Allow thermald's stdout pipe to be closed. This is VERY important. |
| 160 | + # It prevents thermald from hanging if grep exits before thermald does. |
| 161 | + thermald_proc.stdout.close() |
| 162 | + |
| 163 | + # 4. Wait for grep to finish or for the timeout to expire. |
| 164 | + # communicate() reads the output and waits for the process. |
| 165 | + try: |
| 166 | + grep_output, _ = grep_proc.communicate(timeout=TIMEOUT_SECONDS) |
| 167 | + |
| 168 | + # 5. Analyze grep's output to determine the result. |
| 169 | + if FAILURE_PATTERN in grep_output: |
| 170 | + result_message = ( |
| 171 | + "FAILURE: Thermald adaptive engine failed to initialize.\n" |
| 172 | + "Found failure pattern first - thermald fell back to highest power profile." |
| 173 | + ) |
| 174 | + exit_code = 1 |
| 175 | + elif SUCCESS_PATTERN in grep_output: |
| 176 | + result_message = ( |
| 177 | + "SUCCESS: Thermald adaptive engine started correctly.\n" |
| 178 | + "Found success pattern - adaptive engine is active." |
| 179 | + ) |
| 180 | + exit_code = 0 |
| 181 | + else: |
| 182 | + # This case means thermald exited before grep found anything. |
| 183 | + result_message = ( |
| 184 | + "FAILURE: Thermald process exited unexpectedly.\n" |
| 185 | + "No adaptive engine patterns detected in output." |
| 186 | + ) |
| 187 | + exit_code = 1 |
| 188 | + |
| 189 | + except subprocess.TimeoutExpired: |
| 190 | + result_message = ( |
| 191 | + f"FAILURE: Test timed out after {TIMEOUT_SECONDS} seconds.\n" |
| 192 | + "Thermald may be stuck or taking too long to initialize adaptive engine." |
| 193 | + ) |
| 194 | + exit_code = 1 |
| 195 | + |
| 196 | + except FileNotFoundError as e: |
| 197 | + logging.error( |
| 198 | + f"\nERROR: Command not found: '{e.filename}'. Is it installed?" |
| 199 | + ) |
| 200 | + raise SystemExit(1) |
| 201 | + except Exception as e: |
| 202 | + result_message = ( |
| 203 | + f"FAILURE: An unexpected error occurred during the test: {e}\n" |
| 204 | + "This may indicate system configuration issues." |
| 205 | + ) |
| 206 | + exit_code = 1 |
| 207 | + finally: |
| 208 | + # --- Graceful Cleanup --- |
| 209 | + logging.info("--- Cleaning up test processes ---") |
| 210 | + # First, ensure the grep process is gone |
| 211 | + if grep_proc and grep_proc.poll() is None: |
| 212 | + logging.info( |
| 213 | + f"Terminating leftover grep process {grep_proc.pid}..." |
| 214 | + ) |
| 215 | + grep_proc.terminate() |
| 216 | + grep_proc.wait(timeout=2) |
| 217 | + if grep_proc.poll() is None: |
| 218 | + grep_proc.kill() |
| 219 | + |
| 220 | + # Then, clean up the main thermald process and its children |
| 221 | + if thermald_proc and thermald_proc.poll() is None: |
| 222 | + logging.info( |
| 223 | + f"Sending SIGTERM to thermald process group {thermald_proc.pid}..." |
| 224 | + ) |
| 225 | + try: |
| 226 | + os.killpg(thermald_proc.pid, signal.SIGTERM) |
| 227 | + thermald_proc.wait(timeout=5) |
| 228 | + logging.info("Thermald process group terminated gracefully.") |
| 229 | + except ProcessLookupError: |
| 230 | + logging.info("Thermald process group already gone.") |
| 231 | + except subprocess.TimeoutExpired: |
| 232 | + logging.warning( |
| 233 | + "Thermald did not respond to SIGTERM. Sending SIGKILL..." |
| 234 | + ) |
| 235 | + os.killpg(thermald_proc.pid, signal.SIGKILL) |
| 236 | + logging.info("Thermald process group killed.") |
| 237 | + else: |
| 238 | + logging.info("Thermald process already terminated.") |
| 239 | + |
| 240 | + # Restart thermald service if it was originally running |
| 241 | + if service_was_running: |
| 242 | + start_thermald_service() |
| 243 | + |
| 244 | + logging.info("=" * 70) |
| 245 | + logging.info("=== THERMALD ADAPTIVE ENGINE TEST RESULT ===") |
| 246 | + logging.info(result_message) |
| 247 | + if exit_code == 0: |
| 248 | + logging.info("PASSED: Thermald adaptive engine is working correctly.") |
| 249 | + else: |
| 250 | + logging.info( |
| 251 | + "FAILED: Thermald adaptive engine did not start properly." |
| 252 | + ) |
| 253 | + logging.info("=" * 70) |
| 254 | + return exit_code |
| 255 | + |
| 256 | + |
| 257 | +def main(): |
| 258 | + test_result = run_thermald_grep_test() |
| 259 | + raise SystemExit(test_result) |
| 260 | + |
| 261 | + |
| 262 | +if __name__ == "__main__": |
| 263 | + main() |
0 commit comments