-
Notifications
You must be signed in to change notification settings - Fork 3
Making big modules 'optional' for Docker #209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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: | ||||||||||||||||||||||||||||||
| from . webpageController import webpageController | ||||||||||||||||||||||||||||||
| except ModuleNotFoundError: | ||||||||||||||||||||||||||||||
| webpageController = None # selenium not installed (e.g. Docker) | ||||||||||||||||||||||||||||||
|
FitzerIRL marked this conversation as resolved.
FitzerIRL marked this conversation as resolved.
FitzerIRL marked this conversation as resolved.
FitzerIRL marked this conversation as resolved.
Comment on lines
+58
to
+59
|
||||||||||||||||||||||||||||||
| 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 |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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() | ||||||
|
||||||
| self.serialCon.flushInput() | |
| self.serialCon.reset_input_buffer() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
| import socket | ||
|
|
||
| from .consoleInterface import consoleInterface | ||
|
|
@@ -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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
| 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): | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||||||
|
FitzerIRL marked this conversation as resolved.
FitzerIRL marked this conversation as resolved.
FitzerIRL marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||
| import shlex, subprocess | ||||||||||||||||||||||||||||||||||||||||||||||
| import traceback | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
| 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
AI
Mar 24, 2026
There was a problem hiding this comment.
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].
Uh oh!
There was an error while loading. Please reload this page.