diff --git a/framework/core/__init__.py b/framework/core/__init__.py index 2e44f9d..1a2fd90 100644 --- a/framework/core/__init__.py +++ b/framework/core/__init__.py @@ -32,13 +32,36 @@ #__all__ = ["testControl", "logModule", "rcCodes", "rackController", "slotInfo" ] #expose only the components required for the upper classes to operate -from . capture import capture +try: + from . capture import capture +except ModuleNotFoundError as e: + # Only treat known optional image-processing dependencies as optional. + _optional_capture_deps = {"cv2", "pytesseract", "PIL"} + if e.name in _optional_capture_deps: + class _CaptureMissingDependency: + def __call__(self, *args, **kwargs): + raise ImportError( + "Image capture functionality is unavailable because optional " + "dependencies (cv2, pytesseract, PIL) are not installed." + ) from e + capture = _CaptureMissingDependency() + else: + # Unexpected missing module (e.g., bug inside capture.py) - do not hide it. + raise from . commonRemote import commonRemoteClass from . deviceManager import deviceManager from . logModule import logModule from . logModule import DEBUG, INFO, WARNING, ERROR, CRITICAL from . testControl import testController -from . webpageController import webpageController +try: + from . webpageController import webpageController +except ModuleNotFoundError as e: + if e.name == "selenium": + # selenium not installed (e.g. Docker) + webpageController = None + else: + # Unexpected missing module (e.g., bug inside webpageController.py) - do not hide it. + raise from . rcCodes import rcCode as rc from . utilities import utilities diff --git a/framework/core/commandModules/serialClass.py b/framework/core/commandModules/serialClass.py index 7c617fd..f094fbd 100644 --- a/framework/core/commandModules/serialClass.py +++ b/framework/core/commandModules/serialClass.py @@ -200,5 +200,8 @@ def flush(self) -> bool: True if can successfully clear the serial console. """ self.log.info("Clearing Serial console log") - self.serialCon.reset_input_buffer() + if hasattr(self.serialCon, "reset_input_buffer"): + self.serialCon.reset_input_buffer() + else: + self.serialCon.flushInput() return True diff --git a/framework/core/commandModules/telnetClass.py b/framework/core/commandModules/telnetClass.py index f163386..0f52135 100644 --- a/framework/core/commandModules/telnetClass.py +++ b/framework/core/commandModules/telnetClass.py @@ -30,7 +30,126 @@ #* #* ****************************************************************************** -import telnetlib +try: + import telnetlib +except ModuleNotFoundError: + # telnetlib was removed in Python 3.13, provide a minimal compatibility layer + import socket + import time + + class Telnet: + def __init__(self, host=None, port=23, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): + self.host = host + self.port = port + self.timeout = timeout + self.sock = None + if host: + self.open(host, port, timeout) + + def open(self, host, port=23, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): + self.host = host + self.port = port + self.sock = socket.create_connection((host, port), timeout) + + def close(self): + if self.sock: + self.sock.close() + self.sock = None + + def write(self, buffer): + if self.sock: + self.sock.sendall(buffer) + + def read_until(self, match, timeout=None): + if not self.sock: + return b'' + + buffer = b'' + start_time = time.time() + while True: + if timeout and (time.time() - start_time) > timeout: + break + try: + data = self.sock.recv(1024) + if not data: + break + buffer += data + if match in buffer: + break + except socket.timeout: + break + except Exception: + break + return buffer + + def read_all(self): + if not self.sock: + return b'' + buffer = b'' + try: + while True: + data = self.sock.recv(1024) + if not data: + break + buffer += data + except Exception: + pass + return buffer + + def read_very_eager(self): + """ + Read all data available on the socket without blocking. + This is a minimal approximation of telnetlib.Telnet.read_very_eager(). + """ + if not self.sock: + return b'' + + buffer = b'' + # Use non-blocking recv to drain any immediately available data. + self.sock.setblocking(False) + try: + while True: + try: + data = self.sock.recv(1024) + except BlockingIOError: + # No more data available without blocking. + break + if not data: + break + buffer += data + finally: + # Restore blocking mode for subsequent operations. + self.sock.setblocking(True) + return buffer + + def read_eager(self): + """ + Read any data available on the socket without significant blocking. + For this compatibility shim, delegate to read_very_eager(). + """ + return self.read_very_eager() + + def read_some(self): + """ + Read at least some data if available, without blocking if none is ready. + Returns an empty bytes object if no data is immediately available. + """ + if not self.sock: + return b'' + + self.sock.setblocking(False) + try: + try: + data = self.sock.recv(1024) + except BlockingIOError: + data = b'' + finally: + self.sock.setblocking(True) + return data + # Create a mock telnetlib module + class telnetlib: + Telnet = Telnet + import socket from .consoleInterface import consoleInterface @@ -153,7 +272,7 @@ def read_until(self,value: str, timeout: int = 10) -> str: message = value.encode() result = self.tn.read_until(message,self.timeout) return result.decode() - + def read_all(self) -> str: """Read all readily available information displayed in the console. @@ -161,7 +280,7 @@ def read_all(self) -> str: str: Information currently displayed in the console. """ return self.read_eager() - + def write(self,message:list|str, lineFeed:str="\r\n", wait_for_prompt:bool=False) -> bool: """Write a message into the session console. Optional: waits for prompt. diff --git a/framework/core/deviceManager.py b/framework/core/deviceManager.py index 5412183..cadf2a1 100644 --- a/framework/core/deviceManager.py +++ b/framework/core/deviceManager.py @@ -67,6 +67,11 @@ def __init__(self, log:logModule, logPath:str, configElements:dict): """ for element in configElements: config = configElements.get(element) + # Respect 'enabled: false' in console config — skip disabled consoles + if config.get("enabled") is False: + self.type = None + self.session = None + return self.type = config.get("type") self.prompt = config.get("prompt") # Create a new console since it hasn't been created @@ -172,7 +177,9 @@ def __init__(self, log:logModule, logPath:str, devices:dict): consoles = device.get("consoles") for element in consoles: for name in element: - self.consoles[name] = consoleClass(log, logPath, element ) + console = consoleClass(log, logPath, element) + if console.type is not None: # skip disabled consoles + self.consoles[name] = console config = device.get("outbound") if config != None: self.outBoundClient = outboundClientClass(log, **config) @@ -218,9 +225,15 @@ def getConsoleSession(self, consoleName:str="default" ): Returns: consoleClass: Console class, or None on failure """ - console = self.consoles[consoleName] - if console == None: - self.log.error("Invalid consoleName [{}]".format(consoleName)) + console = self.consoles.get(consoleName) + # If requested console was disabled/missing, fall back to first enabled console + if console is None and self.consoles: + fallback = next(iter(self.consoles)) + self.log.info("Console '{}' not available, falling back to '{}'".format(consoleName, fallback)) + console = self.consoles[fallback] + if console is None: + self.log.error("No consoles available (all disabled?)") + return None return console.session def pingTest(self, logPingTime=False): diff --git a/framework/core/logModule.py b/framework/core/logModule.py index d1e76ca..48d5953 100755 --- a/framework/core/logModule.py +++ b/framework/core/logModule.py @@ -118,9 +118,42 @@ def __init__(self, moduleName, level=INFO): def __del__(self): """Deletes the logger instance. """ - while self.log.hasHandlers(): - self.log.removeHandler(self.log.handlers[0]) - + # Ensure all handlers are flushed and closed to avoid leaking resources + for handler in list(getattr(self, "log", {}).handlers if getattr(self, "log", None) else []): + try: + # Not all handlers implement flush; guard just in case + if hasattr(handler, "flush"): + handler.flush() + except Exception: + # Best-effort cleanup; ignore flush errors during destruction + pass + try: + handler.close() + except Exception: + # Ignore close errors during destruction + pass + try: + self.log.removeHandler(handler) + except Exception: + # If removal fails, there's nothing more we can safely do here + pass + + # Also clean up CSV logger handlers, if this instance created one + if hasattr(self, "csvLogger") and getattr(self, "csvLogger") is not None: + for handler in list(self.csvLogger.handlers): + try: + if hasattr(handler, "flush"): + handler.flush() + except Exception: + pass + try: + handler.close() + except Exception: + pass + try: + self.csvLogger.removeHandler(handler) + except Exception: + pass def setFilename( self, logPath, logFileName ): """ Sets the filename for logging. @@ -134,11 +167,11 @@ def setFilename( self, logPath, logFileName ): return self.logPath = logPath logFileName = os.path.join(logPath + logFileName) - self.logFile = logging.FileHandler(logFileName) + self.logFile = logging.FileHandler(logFileName, encoding='utf-8') self.logFile.setFormatter( self.format ) self.log.addHandler( self.logFile ) #Create the CSV Logger module - self.csvLogFile = logging.FileHandler( logFileName+".csv" ) + self.csvLogFile = logging.FileHandler( logFileName+".csv", encoding='utf-8' ) self.csvLogger.addHandler( self.csvLogFile ) self.csvLogger.info("QcId, TestName, Result, Failed Step, Failure, Duration [hh:mm:ss]") self.log.info( "Log File: [{}]".format(logFileName) ) diff --git a/framework/core/testControl.py b/framework/core/testControl.py index cbe230b..30cba8b 100644 --- a/framework/core/testControl.py +++ b/framework/core/testControl.py @@ -33,7 +33,11 @@ import time import signal import random -import telnetlib +try: + import telnetlib +except ModuleNotFoundError: + # telnetlib was removed in Python 3.13, this is handled in telnetClass.py + telnetlib = None import os import shlex, subprocess import traceback @@ -52,8 +56,34 @@ from framework.core.configParser import configParser from framework.core.utilities import utilities from framework.core.decodeParams import decodeParams -from framework.core.capture import capture -from framework.core.webpageController import webpageController +try: + from framework.core.capture import capture +except ModuleNotFoundError as e: + # cv2/pytesseract/PIL are optional dependencies (e.g. in Docker images) + if e.name in ("cv2", "pytesseract", "PIL", "PIL.Image"): + def _capture_missing_dependency(*args, **kwargs): + raise ImportError( + "The 'capture' functionality requires optional dependencies " + "(cv2, pytesseract, and Pillow). One or more of these are not " + f"installed (missing module: {e.name!r}). Please install the " + "required packages to use capture-related features." + ) + + capture = _capture_missing_dependency + else: + raise +try: + from framework.core.webpageController import webpageController +except ModuleNotFoundError as e: + # selenium is an optional dependency (e.g. in Docker images) + if e.name == "selenium": + def _missing_webpage_controller(*args, **kwargs): + raise ImportError( + "webpageController requires the 'selenium' package, which is not installed" + ) + webpageController = _missing_webpage_controller + else: + raise from framework.core.deviceManager import deviceManager class testController(): @@ -149,9 +179,30 @@ def __init__(self, testName="", qcId="", maxRunTime=TEST_MAX_RUN_TIME, level=log #Start the rest of the testControl requirements self.devices = deviceManager(self.slotInfo.config.get("devices"), self.log, self.testLogPath) - # Set up the session from the default console + # Set up the session from the enabled console (not just "default") self.dut = self.devices.getDevice( "dut" ) - self.session = self.dut.getConsoleSession() + + # CRITICAL: Select console based on which is enabled, not hardcoded "default" + # Check console configuration to find the enabled one + console_name = "default" # Fallback to default + try: + # Get the raw config to check enabled status + dut_config = self.slotInfo.config.get("devices", [{}])[0].get("dut", {}) + consoles_config = dut_config.get("consoles", []) + + # Find the enabled console + for console_item in consoles_config: + for name, config in console_item.items(): + if config.get("enabled", False): + console_name = name + self.log.info(f"Using enabled console: {console_name} (type: {config.get('type')})") + break + if console_name != "default": + break + except Exception as e: + self.log.warn(f"Failed to detect enabled console, using default: {e}") + + self.session = self.dut.getConsoleSession(console_name) self.outboundClient = self.dut.outBoundClient self.powerControl = self.dut.powerControl self.commonRemote = self.dut.remoteController