# conftest.py
#
# SPDX-FileCopyrightText: <text>Copyright 2025-2026 Arm Limited and/or its
# affiliates <open-source-office@arm.com></text>
#
# SPDX-License-Identifier: MIT
import logging
import os
import re
from pathlib import Path
import pytest
from datetime import datetime
from importlib import import_module, resources
from test_automation.utils.auto_platform_base import AutoTestPlatformBase
from test_automation.configs.config import Config
from test_automation.targets.fpga.fpga_controller import (
load_fpga_controller_from_yaml,
)
from test_automation.targets.registry import get_factory, DriverBundle
[docs]
logger = logging.getLogger(__name__)
# -------------
# Small helpers
# -------------
[docs]
def _compute_run_paths(config: pytest.Config) -> dict:
"""
Create a unique run directory and return run metadata dict.
:param pytestconfig: Pytest configuration object.
:returns: Dictionary.
"""
platform_opt = str(config.getoption("--platform") or "unknown").lower()
logs_root = Path("logs")
run_id = datetime.now().strftime("%Y%m%d-%H%M%S")
run_dir = logs_root / f"{platform_opt}_{run_id}"
run_dir.mkdir(parents=True, exist_ok=True)
return {
"run_id": run_id,
"platform": platform_opt,
"log_dir": run_dir,
"prefix": platform_opt or "fvp_boot",
}
[docs]
def _load_target_plugin(config: pytest.Config) -> None:
"""
Dynamically import target plugin (e.g., for FVP) if selected.
:param pytestconfig: Pytest configuration object.
:returns: None.
"""
try:
cfg = Config(
path=str(Path(config.getoption("--config")).expanduser()),
build_dir=config.getoption("--build-dir"),
fvp_binary=config.getoption("--fvp-binary"),
)
plat_key = str(config.getoption("--platform")).lower()
plat_dict = cfg.get_platform(plat_key)
ptype = str(plat_dict.get("type", "")).lower()
if ptype == "fvp":
import_module("test_automation.targets.fvp.plugin")
except Exception as e:
logger.debug("Skipping dynamic target plugin load: %s", e)
[docs]
def _apply_debug_logging(config: pytest.Config) -> None:
"""
Enable CLI logging if --debug-logs was passed.
:param pytestconfig: Pytest configuration object.
:returns: None.
"""
if not config.getoption("--debug-logs"):
return
config.option.log_cli = True
config.option.log_cli_level = "DEBUG"
config.option.log_cli_format = (
"%(asctime)s %(levelname)s %(name)s:%(lineno)d - %(message)s"
)
config.option.log_cli_date_format = "%H:%M:%S"
[docs]
def _prepare_bundle_paths(
pytestconfig: pytest.Config, bundle: DriverBundle
) -> dict:
"""
Ensure bundle paths (manager + driver) are set up; return run paths dict.
If pytest_configure already created run paths, reuse them; otherwise,
compute a fallback here.
"""
rp = getattr(pytestconfig, "_arm_run_paths", None)
if not rp:
# Fallback when pytest_configure didn't set it (e.g., external usage)
platform_opt = str(
getattr(pytestconfig, "getoption", lambda *_: "unknown")(
"--platform", "unknown"
)
).lower()
run_id = datetime.now().strftime("%Y%m%d-%H%M%S")
log_dir = Path("logs") / f"{platform_opt}-{run_id}"
log_dir.mkdir(parents=True, exist_ok=True)
rp = {
"run_id": run_id,
"platform": platform_opt,
"log_dir": log_dir,
"prefix": platform_opt or "fvp_boot",
}
# set attribute directly instead of setattr
pytestconfig._arm_run_paths = rp
# Telnet manager path
mgr = getattr(bundle, "manager", None)
if mgr and hasattr(mgr, "set_log_dir"):
mgr.set_log_dir(str(Path(rp["log_dir"]).expanduser().resolve()))
# Driver paths/prefix
drv = bundle.driver
if hasattr(drv, "log_dir"):
drv.log_dir = Path(rp["log_dir"]).expanduser().resolve()
drv.log_dir.mkdir(parents=True, exist_ok=True)
if hasattr(drv, "log_prefix"):
drv.log_prefix = rp["prefix"]
return rp
[docs]
def _power_on(bundle: DriverBundle, ptype: str, plat: dict) -> None:
"""Bring the platform up and wait until ready."""
logger.info(
"Powering on platform type=%s name=%s", ptype, plat.get("name")
)
getattr(bundle.driver, "init", lambda: None)()
bundle.driver.start()
logger.info("Waiting for platform to become ready…")
bundle.driver.wait_ready()
logger.info("Platform ready")
[docs]
def _cleanup_manager(mgr) -> None:
"""Best-effort session cleanup on the manager."""
for method in ("close_sessions", "stop_all"):
try:
getattr(mgr, method)()
# Once one cleanup method succeeds, we're done.
return
except Exception:
# Try the next method if this one fails.
continue
[docs]
def _power_off(bundle: DriverBundle) -> None:
"""Attempt graceful power-off and session cleanup."""
logger.info("Powering off platform")
try:
bundle.driver.stop()
except Exception:
# Don't hide the error; log it and still attempt cleanup.
logger.exception("Error while stopping device; going with cleanup")
finally:
mgr = getattr(bundle, "manager", None)
if mgr:
_cleanup_manager(mgr)
# ------------------------------
# Initial configuration + conditional plugin load
# ------------------------------
# ------------------------------
# Shared config/platform loaders
# ------------------------------
@pytest.fixture(scope="session")
[docs]
def cfg(pytestconfig: pytest.Config) -> Config:
"""
Load the YAML configuration once per test session.
:param pytestconfig: Pytest configuration object.
:returns: Parsed :class:`Config` instance.
"""
config_path = Path(pytestconfig.getoption("--config")).expanduser()
build_dir = pytestconfig.getoption("--build-dir")
fvp_binary = pytestconfig.getoption("--fvp-binary")
logger.debug(
"Loading YAML config from %s (build-dir=%r, fvp-binary=%r)",
config_path,
build_dir,
fvp_binary,
)
try:
return Config(
path=str(config_path), build_dir=build_dir, fvp_binary=fvp_binary
)
except FileNotFoundError:
pytest.skip(f"Config file not found: {config_path}")
# Helper reduce fixture complexity
[docs]
def _apply_cfg2_overrides(platform_config: dict) -> None:
"""Temporarily inject cfg2 specific configs before boot."""
if os.getenv("RD_ASPEN_VARIANT", "").lower() != "cfg2":
return
si_cl1_prompt = _get_cfg2_si_cluster1_prompt()
if not si_cl1_prompt:
return
extra_arg = "-C css.smb.si.terminal_uart_si_cluster1.start_telnet=0"
args = platform_config.setdefault("arguments", [])
if extra_arg not in args:
args.append(extra_arg)
required_terminals = platform_config.setdefault("required_terminals", [])
if "terminal_uart_si_cluster1" not in required_terminals:
required_terminals.append("terminal_uart_si_cluster1")
prompts = platform_config.setdefault("prompts", {})
shell_map = prompts.setdefault("shell_map", {})
shell_map["terminal_uart_si_cluster1"] = si_cl1_prompt
port_map = platform_config.setdefault("port_map", {})
port_map["si_cl1"] = "terminal_uart_si_cluster1"
@pytest.fixture(scope="session")
# ------------------------------
# Core bundle: build/start/login/teardown once
# ------------------------------
@pytest.fixture(scope="session")
# ------------------------------
# Thin fixtures used by tests
# ------------------------------
@pytest.fixture(scope="session")
@pytest.fixture(scope="session")
[docs]
def session_manager(platform_bundle):
"""
Provide the session/console manager for tests.
:param platform_bundle: Initialized driver bundle.
:returns: Session manager object.
"""
if platform_bundle.manager is None:
pytest.skip("This platform does not expose a session manager")
return platform_bundle.manager
# ------------------------------
# Utility / metadata fixtures
# ------------------------------
@pytest.fixture(scope="session")
@pytest.fixture(scope="session")
@pytest.fixture(scope="session")
[docs]
def fpga_device(request):
"""
Session-scoped FPGA device fixture.
- Initializes and starts the FPGA platform
- Waits for ready state
- Starts UART keepalive
- Ensures graceful shutdown and log collection
"""
config_path = Path(request.config.getoption("--config"))
platform = request.config.getoption("--platform")
host = request.config.getoption("--host")
logger.info(
"pytest --config used: %s; looking for platform: %s",
config_path,
platform,
)
dev = load_fpga_controller_from_yaml(config_path, platform, host=host)
logger.info("Initializing FPGA device...")
dev.init()
logger.info("Starting FPGA device...")
dev.start()
logger.info("Waiting for ready state...")
dev.wait_ready()
uart_log_path, vuart, pts = dev.get_login_uart()
dev.net.start_uart_keepalive(vuart=vuart, pts=pts, interval_s=25.0)
yield dev
logger.info("Test teardown: stopping UART keepalive and stopping device")
dev.net.stop_uart_keepalive()
dev.stop()
logger.info("FPGA controller stopped and logs collected")
# ------------------------------
# CLI options
# ------------------------------
[docs]
def pytest_addoption(parser) -> None:
"""
Register custom CLI options used by the test framework.
:param parser: Pytest argument parser.
:returns: None.
"""
default_cfg = (
resources.files("test_automation.configs") / "standalone_config.yaml"
)
parser.addoption(
"--platform",
action="store",
required=True,
help="Platform key in the config (e.g., fvp_rd_aspen, fpga_rd_aspen)",
)
parser.addoption(
"--config",
action="store",
default=str(default_cfg),
help="Specify the configuration file to load)",
)
parser.addoption(
"--build-dir",
action="store",
default=None,
help="Optional directory to substitute for BUILD_IMAGE_DIR.",
)
parser.addoption(
"--fvp-binary",
action="store",
default=None,
help="Optional path to substitute for FVP_BINARY.",
)
parser.addoption(
"--debug-logs",
action="store_true",
help="Enable live DEBUG logging for this run",
)
parser.addoption(
"--host",
action="store",
default=None,
help=(
"Hostname/IP for platforms that require a remote host (e.g. fpga)"
),
)
[docs]
def _get_cfg2_si_cluster1_prompt() -> str | None:
"""
Return the expected prompt for si_cluster1 based on build type.
"""
image_name = os.getenv("OPTIONAL_EXISTING_IMAGE_FILENAME", "").lower()
if "baremetal" in image_name:
return r"Secondary CPU core 1 \(MPID:0x10100\) is up"
if "bsp" in image_name:
return "Hello World"
return None