Skip to content
Closed
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
22 changes: 20 additions & 2 deletions framework/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,31 @@

#__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:
Comment thread
FitzerIRL marked this conversation as resolved.
from . webpageController import webpageController
except ModuleNotFoundError:
webpageController = None # selenium not installed (e.g. Docker)
Comment thread
FitzerIRL marked this conversation as resolved.
Comment thread
FitzerIRL marked this conversation as resolved.
Comment thread
FitzerIRL marked this conversation as resolved.
Comment thread
FitzerIRL marked this conversation as resolved.
Comment on lines +58 to +59
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

except ModuleNotFoundError: here will swallow any missing-module error while importing webpageController, potentially hiding real packaging bugs unrelated to Selenium. To avoid masking unexpected failures, catch ModuleNotFoundError as e and only treat it as optional when e.name == 'selenium' (and otherwise re-raise). Also consider providing a callable stub that raises a clear ImportError when used, rather than setting webpageController = None, to fail with a more actionable message.

Suggested change
except ModuleNotFoundError:
webpageController = None # selenium not installed (e.g. Docker)
except ModuleNotFoundError as e:
if e.name == "selenium":
class _WebpageControllerMissingDependency:
def __init__(self, *args, **kwargs):
raise ImportError(
"webpageController functionality is unavailable because the "
"'selenium' package is not installed."
) from e
webpageController = _WebpageControllerMissingDependency
else:
# Unexpected missing module (e.g., bug inside webpageController.py) - do not hide it.
raise

Copilot uses AI. Check for mistakes.
from . rcCodes import rcCode as rc
from . utilities import utilities

Expand Down
2 changes: 1 addition & 1 deletion framework/core/commandModules/serialClass.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,5 +200,5 @@ def flush(self) -> bool:
True if can successfully clear the serial console.
"""
self.log.info("Clearing Serial console log")
self.serialCon.reset_input_buffer()
self.serialCon.flushInput()
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flushInput() is deprecated in pyserial in favor of reset_input_buffer(). Since this repo pins pyserial==3.5, reset_input_buffer() should be available and is the preferred API; using the deprecated alias can introduce warnings and makes the intent less clear.

Suggested change
self.serialCon.flushInput()
self.serialCon.reset_input_buffer()

Copilot uses AI. Check for mistakes.
return True
75 changes: 72 additions & 3 deletions framework/core/commandModules/telnetClass.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,76 @@
#*
#* ******************************************************************************

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.send(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

# Create a mock telnetlib module
class telnetlib:
Telnet = Telnet

Comment on lines +33 to +102
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Python 3.13 compatibility Telnet shim does not implement several methods that the rest of this file calls (read_eager, read_very_eager, read_some). In environments without stdlib telnetlib, telnet.read_eager() will raise AttributeError at runtime. Either implement the missing methods in the shim (matching the subset of the telnetlib API used here) or refactor the wrapper to avoid calling methods that aren't provided by the fallback.

Copilot uses AI. Check for mistakes.
import socket

from .consoleInterface import consoleInterface
Expand Down Expand Up @@ -153,15 +222,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
Comment on lines +70 to +74
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior around enabled: false and console fallback would benefit from unit coverage (e.g., verify disabled consoles are excluded from self.consoles, and that getConsoleSession() falls back to the first enabled console when the requested one is missing/disabled). There are existing deviceManager unit tests under tests/singleton_tests/; extending them would help prevent regressions.

Copilot uses AI. Check for mistakes.
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
6 changes: 3 additions & 3 deletions framework/core/logModule.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def __init__(self, moduleName, level=INFO):
def __del__(self):
"""Deletes the logger instance.
"""
while self.log.hasHandlers():
while self.log.handlers:
self.log.removeHandler(self.log.handlers[0])

Comment on lines +121 to 123
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In __del__, handlers are removed but never closed. For file handlers this can leak file descriptors until process exit (and __del__ is not guaranteed to run promptly). Consider calling handler.close() after removing each handler (and similarly for csvLogger handlers if applicable).

Suggested change
while self.log.handlers:
self.log.removeHandler(self.log.handlers[0])
# Properly remove and close all handlers associated with this logger
try:
# Close handlers attached to the main logger
for handler in list(getattr(self.log, "handlers", [])):
try:
self.log.removeHandler(handler)
finally:
try:
handler.close()
except Exception:
# Suppress all exceptions in __del__
pass
# Close handlers attached to the CSV logger, if present
csv_logger = getattr(self, "csvLogger", None)
if csv_logger is not None:
for handler in list(getattr(csv_logger, "handlers", [])):
try:
csv_logger.removeHandler(handler)
finally:
try:
handler.close()
except Exception:
# Suppress all exceptions in __del__
pass
except Exception:
# Ensure __del__ never raises
pass

Copilot uses AI. Check for mistakes.
def setFilename( self, logPath, logFileName ):
Expand All @@ -134,11 +134,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
49 changes: 44 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
FitzerIRL marked this conversation as resolved.
Comment thread
FitzerIRL marked this conversation as resolved.
Comment thread
FitzerIRL marked this conversation as resolved.
import os
import shlex, subprocess
import traceback
Expand All @@ -52,8 +56,22 @@
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"):
capture = None
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":
webpageController = None
else:
raise
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

capture and webpageController are set to None on missing optional deps, but later in __init__ they are called as constructors when captureConfig / webDriverConfig are present. This will raise a TypeError: 'NoneType' object is not callable instead of gracefully disabling the feature. Consider using a callable stub that raises a clear ImportError (similar to framework/core/__init__.py) and/or guarding instantiation with if capture is None: / if webpageController is None: to log and skip setup.

Suggested change
raise
raise
# Provide callable stubs when optional dependencies are missing so that
# attempts to instantiate these classes fail with a clear ImportError
if capture is None:
def _capture_missing(*args, **kwargs):
raise ImportError(
"Optional dependencies for 'capture' (cv2/pytesseract/PIL) are not "
"installed; capture functionality is disabled."
)
capture = _capture_missing
if 'webpageController' in globals() and webpageController is None:
def _webpage_controller_missing(*args, **kwargs):
raise ImportError(
"Optional dependency 'selenium' is not installed; webpageController "
"functionality is disabled."
)
webpageController = _webpage_controller_missing

Copilot uses AI. Check for mistakes.
from framework.core.deviceManager import deviceManager

class testController():
Expand Down Expand Up @@ -149,9 +167,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
Comment on lines +175 to +189
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enabled-console detection only looks at self.slotInfo.config.get('devices', [{}])[0], so it will miss the DUT config if dut isn't the first entry in the devices list. In that case console_name may remain default even when another console is explicitly marked enabled, which undermines the purpose of the change. Consider deriving the enabled console from the DUT device config found by name (or directly from self.dut.consoles / self.dut.rawConfig) instead of indexing [0].

Copilot uses AI. Check for mistakes.
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