154 lines
6.2 KiB
Python
154 lines
6.2 KiB
Python
from __future__ import annotations # Postponed annotations for forward references and speed
|
|
|
|
import os # For environment variables and filesystem checks
|
|
import pathlib # Path handling across platforms
|
|
from dataclasses import dataclass, field # Lightweight typed containers
|
|
from typing import Any, Dict, Optional # Type hints for clarity
|
|
|
|
import yaml # Safe YAML parsing for configuration files
|
|
|
|
|
|
@dataclass
|
|
class FlashConfig:
|
|
"""Flashing-related configuration.
|
|
|
|
enabled: Whether to trigger flashing at session start.
|
|
hex_path: Path to the firmware HEX file (if any).
|
|
"""
|
|
|
|
enabled: bool = False # Off by default
|
|
hex_path: Optional[str] = None # No default file path
|
|
|
|
|
|
@dataclass
|
|
class InterfaceConfig:
|
|
"""LIN interface configuration.
|
|
|
|
type: Adapter type name: "mock" for the simulated adapter, "babylin" for real hardware via SDK.
|
|
channel: Channel index to use (0-based in most SDKs); default chosen by project convention.
|
|
bitrate: Informational; typically SDF/schedule defines effective bitrate for BabyLIN.
|
|
dll_path: Legacy/optional pointer to vendor DLLs when using ctypes (not used by SDK wrapper).
|
|
node_name: Optional friendly name for display/logging.
|
|
func_names: Legacy mapping for ctypes function names; ignored by SDK wrapper.
|
|
sdf_path: Path to the SDF to load on connect (BabyLIN only).
|
|
schedule_nr: Schedule index to start after connect (BabyLIN only).
|
|
"""
|
|
|
|
type: str = "mock" # "mock" or "babylin"
|
|
channel: int = 1 # Default channel index (project-specific default)
|
|
bitrate: int = 19200 # Typical LIN bitrate; SDF may override
|
|
dll_path: Optional[str] = None # Legacy ctypes option; not used with SDK wrapper
|
|
node_name: Optional[str] = None # Optional label for node/adapter
|
|
func_names: Dict[str, str] = field(default_factory=dict) # Legacy ctypes mapping; safe to leave empty
|
|
# SDK wrapper options
|
|
sdf_path: Optional[str] = None # Path to SDF file to load (BabyLIN)
|
|
schedule_nr: int = 0 # Schedule number to start after connect (BabyLIN)
|
|
|
|
|
|
@dataclass
|
|
class EcuTestConfig:
|
|
"""Top-level, fully-typed configuration for the framework.
|
|
|
|
interface: Settings for LIN communication (mock or BabyLIN).
|
|
flash: Optional flashing behavior configuration.
|
|
"""
|
|
|
|
interface: InterfaceConfig = field(default_factory=InterfaceConfig)
|
|
flash: FlashConfig = field(default_factory=FlashConfig)
|
|
|
|
|
|
DEFAULT_CONFIG_RELATIVE = pathlib.Path("config") / "test_config.yaml" # Default config path relative to repo root
|
|
ENV_CONFIG_PATH = "ECU_TESTS_CONFIG" # Env var to override config file location
|
|
|
|
|
|
def _deep_update(base: Dict[str, Any], updates: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Recursively merge dict 'updates' into dict 'base'.
|
|
|
|
- Nested dicts are merged by key
|
|
- Scalars/collections at any level are replaced entirely
|
|
- Mutation occurs in-place on 'base' and the same object is returned
|
|
"""
|
|
for k, v in updates.items(): # Iterate all update keys
|
|
if isinstance(v, dict) and isinstance(base.get(k), dict): # Both sides dict → recurse
|
|
base[k] = _deep_update(base[k], v)
|
|
else: # Otherwise replace
|
|
base[k] = v
|
|
return base # Return the mutated base for chaining
|
|
|
|
|
|
def _to_dataclass(cfg: Dict[str, Any]) -> EcuTestConfig:
|
|
"""Convert a merged plain dict config into strongly-typed dataclasses.
|
|
|
|
Defensive casting is used to ensure correct types even if YAML contains strings.
|
|
"""
|
|
iface = cfg.get("interface", {}) # Sub-config for interface
|
|
flash = cfg.get("flash", {}) # Sub-config for flashing
|
|
return EcuTestConfig(
|
|
interface=InterfaceConfig(
|
|
type=str(iface.get("type", "mock")).lower(), # Normalize to lowercase
|
|
channel=int(iface.get("channel", 1)), # Coerce to int
|
|
bitrate=int(iface.get("bitrate", 19200)), # Coerce to int
|
|
dll_path=iface.get("dll_path"), # Optional legacy field
|
|
node_name=iface.get("node_name"), # Optional friendly name
|
|
func_names=dict(iface.get("func_names", {}) or {}), # Ensure a dict
|
|
sdf_path=iface.get("sdf_path"), # Optional SDF path
|
|
schedule_nr=int(iface.get("schedule_nr", 0)), # Coerce to int
|
|
),
|
|
flash=FlashConfig(
|
|
enabled=bool(flash.get("enabled", False)), # Coerce to bool
|
|
hex_path=flash.get("hex_path"), # Optional hex path
|
|
),
|
|
)
|
|
|
|
|
|
def load_config(workspace_root: Optional[str] = None, overrides: Optional[Dict[str, Any]] = None) -> EcuTestConfig:
|
|
"""Load configuration from YAML file, environment, overrides, or defaults.
|
|
|
|
Precedence (highest to lowest):
|
|
1. in-memory 'overrides' dict
|
|
2. YAML file specified by env var ECU_TESTS_CONFIG
|
|
3. YAML at ./config/test_config.yaml (relative to workspace_root)
|
|
4. built-in defaults in this function
|
|
"""
|
|
# Start with built-in defaults; minimal, safe baseline
|
|
base: Dict[str, Any] = {
|
|
"interface": {
|
|
"type": "mock", # mock by default for developer friendliness
|
|
"channel": 1,
|
|
"bitrate": 19200,
|
|
},
|
|
"flash": {
|
|
"enabled": False,
|
|
"hex_path": None,
|
|
},
|
|
}
|
|
|
|
cfg_path: Optional[pathlib.Path] = None # Resolved configuration file path
|
|
|
|
# 2) Environment variable can point to any YAML file
|
|
env_path = os.getenv(ENV_CONFIG_PATH)
|
|
if env_path:
|
|
candidate = pathlib.Path(env_path)
|
|
if candidate.is_file(): # Only accept existing files
|
|
cfg_path = candidate
|
|
|
|
# 3) Fallback to default path under the provided workspace root
|
|
if cfg_path is None and workspace_root:
|
|
candidate = pathlib.Path(workspace_root) / DEFAULT_CONFIG_RELATIVE
|
|
if candidate.is_file():
|
|
cfg_path = candidate
|
|
|
|
# Load YAML file if we have one
|
|
if cfg_path and cfg_path.is_file():
|
|
with open(cfg_path, "r", encoding="utf-8") as f:
|
|
file_cfg = yaml.safe_load(f) or {} # Parse YAML safely; empty → {}
|
|
if isinstance(file_cfg, dict): # Only merge dicts
|
|
_deep_update(base, file_cfg)
|
|
|
|
# 1) In-memory overrides always win
|
|
if overrides:
|
|
_deep_update(base, overrides)
|
|
|
|
# Convert to typed dataclasses for ergonomic downstream usage
|
|
return _to_dataclass(base)
|