Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions framework/core/__init__.py
Comment thread
FitzerIRL marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
TB-1993 marked this conversation as resolved.
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

Expand Down
5 changes: 4 additions & 1 deletion framework/core/commandModules/serialClass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
125 changes: 122 additions & 3 deletions framework/core/commandModules/telnetClass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Comment thread
FitzerIRL marked this conversation as resolved.
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

Comment thread
FitzerIRL marked this conversation as resolved.
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().
Comment thread
FitzerIRL marked this conversation as resolved.
"""
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
Expand Down Expand Up @@ -153,15 +272,15 @@ 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.

Returns:
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.
Expand Down
21 changes: 17 additions & 4 deletions framework/core/deviceManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
43 changes: 38 additions & 5 deletions framework/core/logModule.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) )
Expand Down
61 changes: 56 additions & 5 deletions framework/core/testControl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
TB-1993 marked this conversation as resolved.
import os
import shlex, subprocess
import traceback
Expand All @@ -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
Comment thread
TB-1993 marked this conversation as resolved.
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():
Expand Down Expand Up @@ -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
Comment thread
TB-1993 marked this conversation as resolved.
for console_item in consoles_config:
for name, config in console_item.items():
if config.get("enabled", False):
console_name = name
Comment thread
TB-1993 marked this conversation as resolved.
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
Expand Down
Loading