Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
852 changes: 852 additions & 0 deletions AGENTS.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,23 @@ All notable changes to this project will be documented in this file. Dates are d

Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).

#### [1.9.0](https://github.com/rdkcentral/python_raft/compare/1.8.2...1.9.0)

- Fix virtual cec [`#220`](https://github.com/rdkcentral/python_raft/pull/220)
- Add AI-ingestible framework brief [`#207`](https://github.com/rdkcentral/python_raft/pull/207)
- - Add Python 3.13+ compatibility shim for removed telnetlib module [`#210`](https://github.com/rdkcentral/python_raft/pull/210)
- Add AI-ingestible framework brief for python_raft [`#206`](https://github.com/rdkcentral/python_raft/issues/206)
Comment thread
kanjoe24 marked this conversation as resolved.
- Making big modules 'optional' for Docker [`c466bd6`](https://github.com/rdkcentral/python_raft/commit/c466bd6ab22e0e5f9aec43b8765612f24af2ae2d)
- Add : gh #206 : Refresh ai-brief for current develop + spec-to-suite recipe [`e5c5c50`](https://github.com/rdkcentral/python_raft/commit/e5c5c50b9a6d25e5c0b7016efcc603840ef28d6d)
- Update framework/core/commandModules/telnetClass.py [`0a7aa6f`](https://github.com/rdkcentral/python_raft/commit/0a7aa6f0d59df746034034ea67c64b48ea4313cb)

#### [1.8.2](https://github.com/rdkcentral/python_raft/compare/1.8.1...1.8.2)

> 10 March 2026

- Enhance keySimulator sendKey to prevent missed keys [`#205`](https://github.com/rdkcentral/python_raft/pull/205)
- Comms fixes [`#203`](https://github.com/rdkcentral/python_raft/pull/203)
- Bumped CHANGELOG [`0103b1b`](https://github.com/rdkcentral/python_raft/commit/0103b1bfceec6d724531bec12e6ea645060727d7)
- Removed new line feed & updated the return result [`871cdde`](https://github.com/rdkcentral/python_raft/commit/871cdde0a490d669260e4beaa626bed4f4845ae6)
- Merge tag '1.8.1' into develop [`13bf879`](https://github.com/rdkcentral/python_raft/commit/13bf87916437160f500b573c2166d68811ad7d85)

Expand Down
27 changes: 25 additions & 2 deletions framework/core/__init__.py
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()
Comment thread
kanjoe24 marked this conversation as resolved.
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

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)

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

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
Comment thread
kanjoe24 marked this conversation as resolved.
# 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
Comment thread
kanjoe24 marked this conversation as resolved.
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
1 change: 1 addition & 0 deletions framework/core/hdmicecModules/virtualCECController.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def __init__(self, adaptor: str, logger: logModule, streamLogger: StreamToFile,
except Exception as e:
self._log.critical(f"Failed to load device configuration: {e}")
raise
self.start()
Comment thread
kanjoe24 marked this conversation as resolved.

def loadCecDeviceNetworkConfiguration(self, configString: str):
"""
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
Loading