diff --git a/ntxbuild/cli.py b/ntxbuild/cli.py index d43dfef..00f3210 100644 --- a/ntxbuild/cli.py +++ b/ntxbuild/cli.py @@ -330,23 +330,28 @@ def kconfig(read, set_value, set_str, apply, value, merge): """ env = prepare_env() try: - config_manager = ConfigManager(env.get("nuttxspace_path"), env.get("nuttx_dir")) + config_manager = ConfigManager( + env.get("nuttxspace_path"), env.get("nuttx_dir"), env.get("apps_dir") + ) if read: config_manager.kconfig_read(read) elif set_value: if not value: click.echo("โŒ Set value is required") + sys.exit(1) config_manager.kconfig_set_value(set_value, value) elif set_str: if not value: click.echo("โŒ Set string is required") + sys.exit(1) config_manager.kconfig_set_str(set_str, value) elif apply: config_manager.kconfig_apply_changes() elif merge: if not value: click.echo("โŒ Merge file is required") - config_manager.kconfig_merge_config_file(value, None) + sys.exit(1) + config_manager.kconfig_merge_config_file(value) else: click.echo("โŒ No action specified") except click.ClickException as e: diff --git a/ntxbuild/config.py b/ntxbuild/config.py index 3ed7158..962bf11 100644 --- a/ntxbuild/config.py +++ b/ntxbuild/config.py @@ -1,175 +1,441 @@ """ Configuration management for NuttX builds. + +This module provides classes and utilities for managing Kconfig-based +configuration for NuttX builds, including reading, modifying, and merging +configuration options. """ import logging -from enum import Enum +import os +from dataclasses import dataclass +from functools import wraps from pathlib import Path +import kconfiglib + from . import utils # Get logger for this module logger = logging.getLogger("ntxbuild.config") -KCONFIG_TWEAK = "kconfig-tweak" -KCONFIG_MERGE = "kconfig-merge" +@dataclass(frozen=True) +class KconfigEnvironmentContext: + """Context manager for Kconfig environment variables. + + Manages the environment variables required by Kconfiglib to properly + resolve paths in the NuttX build system. These variables are set + before Kconfig operations and restored afterward to avoid interfering + with other build system operations. + + Attributes: + bindir: Path to the NuttX binary directory. + appsbindir: Path to the NuttX apps binary directory. + appsdir: Path to the NuttX apps directory. + externaldir: Path to the external directory. + """ + + bindir: Path + appsbindir: Path + appsdir: Path + externaldir: Path + + def set_environment(self): + """Set environment variables for Kconfig operations.""" + os.environ["BINDIR"] = str(self.bindir) + os.environ["APPSBINDIR"] = str(self.appsbindir) + os.environ["APPSDIR"] = str(self.appsdir) + os.environ["EXTERNALDIR"] = str(self.externaldir) + + def restore_environment(self): + """Restore environment by removing Kconfig-related variables.""" + os.unsetenv("BINDIR") + os.unsetenv("APPSBINDIR") + os.unsetenv("APPSDIR") + os.unsetenv("EXTERNALDIR") + + +def kconfig_chdir(f): + """Decorator to manage environment variables for Kconfig operations. + + This decorator ensures that environment variables are properly set before + Kconfig operations and restored afterward. This is necessary because: + - NuttX build system uses environment variables to determine the source + tree and apps directory + - We should not interfere with other modules using the build system + - Leaving these variables set causes path-related issues + + The decorator sets the environment, executes the wrapped method, and + restores the environment regardless of success or failure. + """ + + @wraps(f) + def wrap(self, *args, **kwargs): + self.environment_context.set_environment() + ret = f(self, *args, **kwargs) + self.environment_context.restore_environment() + return ret + + return wrap -class KconfigTweakAction(str, Enum): - """Enumeration of kconfig-tweak actions. - This enum defines the available actions that can be performed - using the kconfig-tweak tool. +class KconfigParser(kconfiglib.Kconfig): + """Parser for NuttX Kconfig files. + + Extends kconfiglib.Kconfig to provide NuttX-specific initialization + and environment management. Handles setting up environment variables, + loading existing configuration files, and managing the working directory. """ - def __str__(self): - return str(self.value) + KCONFIG_FILE = "Kconfig" + KCONFIG_CONFIG = ".config" - def __repr__(self): - return str(self) + def __init__(self, nuttx_path: Path, apps_path: Path = None): + """Initialize the Kconfig parser. - ENABLE = "--enable" - DISABLE = "--disable" - MODULE = "--module" - SET_STR = "--set-str" - SET_VAL = "--set-val" - UNDEFINE = "--undefine" - STATE = "--state" - ENABLE_AFTER = "--enable-after" - DISABLE_AFTER = "--disable-after" - MODULE_AFTER = "--module-after" - FILE = "--file" - KEEP_CASE = "--keep-case" + Args: + nuttx_path: Path to the NuttX source directory. + apps_path: Path to the NuttX apps directory. If None, defaults + to nuttx_path.parent / "nuttx-apps". + Raises: + FileNotFoundError: If the apps_path does not exist. + RuntimeError: If the .config file is not found (NuttX must be + initialized first). + """ + self.nuttx_path = Path(nuttx_path) + self.original_dir = os.getcwd() + self.external_path = self.nuttx_path / "external" + self.use_custom_ext_path = False + + if not apps_path: + self.apps_path = self.nuttx_path.parent / "nuttx-apps" + if not self.apps_path.exists(): + raise FileNotFoundError(f"Apps path found at {self.apps_path}") + + logger.debug( + "Initializing Kconfig parser with Kconfig at " + f"{self.nuttx_path / self.KCONFIG_FILE}" + ) + if not self.external_path.exists(): + self.external_path = self.nuttx_path / "dummy" + + self.environment_context = KconfigEnvironmentContext( + bindir=self.nuttx_path, + appsbindir=self.apps_path, + appsdir=self.apps_path, + externaldir=self.external_path, + ) + self.environment_context.set_environment() + + os.chdir(self.nuttx_path) + try: + super().__init__(self.KCONFIG_FILE, suppress_traceback=False) + except Exception as e: + logger.error(f"Error initializing Kconfig parser: {e}") + self.environment_context.restore_environment() + raise e + + config_file = self.nuttx_path / self.KCONFIG_CONFIG + if config_file.exists(): + logger.debug(f"Loading existing .config from {config_file}") + super().load_config(str(config_file)) + else: + self.environment_context.restore_environment() + raise RuntimeError( + f".config file not found at {config_file}. " + "NuttX must be initialized first." + ) -class ConfigManager: + self.environment_context.restore_environment() + + +class ConfigManager(KconfigParser): """Manages NuttX build configurations. This class provides methods to read, modify, and manage Kconfig options for NuttX builds. """ - def __init__(self, nuttxspace_path: Path, nuttx_dir: str = "nuttx"): + def __init__( + self, nuttxspace_path: Path, nuttx_dir: str = "nuttx", apps_dir: str = None + ): """Initialize the ConfigManager. Args: nuttxspace_path: Path to the NuttX repository workspace. nuttx_dir: Name of the NuttX OS directory within the workspace. Defaults to "nuttx". + apps_dir: Name of the NuttX apps directory within the workspace. + If None, defaults to "nuttx-apps". + Defaults to None. + + Raises: + FileNotFoundError: If the apps directory does not exist. + RuntimeError: If the .config file is not found (NuttX must be + initialized first). """ self.nuttxspace_path = Path(nuttxspace_path) self.nuttx_path = self.nuttxspace_path / nuttx_dir + # Determine apps directory + if apps_dir: + self.apps_path = self.nuttxspace_path / apps_dir + else: + self.apps_path = self.nuttxspace_path / "nuttx-apps" + + super().__init__(self.nuttx_path, self.apps_path) + + @kconfig_chdir def kconfig_read(self, config: str) -> str: """Read the current state of a Kconfig option. - Reads and prints the current value of a Kconfig option to stdout. - Args: - config: The name of the Kconfig option to read. + config: The name of the Kconfig option to read. The "CONFIG_" + prefix is optional and will be removed if present. Returns: - str: The current value of the Kconfig option. - """ - result = utils.run_kconfig_command( - [KCONFIG_TWEAK, KconfigTweakAction.STATE, config], cwd=self.nuttx_path - ) - value = result.stdout.strip() - print(f"{config}={value}") - return value + str: The current value of the Kconfig option. Returns "y", "n", "m" + for bool/tristate options, or the string/int/hex value for other + types. Returns empty string if not set. + Raises: + KeyError: If the config option does not exist. + """ + try: + config = config.removeprefix("CONFIG_") + if config not in self.syms: + raise KeyError(f"Kconfig option '{config}' not found") + + symbol = self.syms[config] + value = symbol.str_value + + logger.debug( + f"Read symbol :value {config}: returns {value} " + f"of type: {kconfiglib.TYPE_TO_STR[symbol.type]} " + f"assignable: {symbol.assignable}" + ) + + logger.info(f"Kconfig read: {config}={value}") + return value + except Exception as e: + self.environment_context.restore_environment() + raise e + + @kconfig_chdir def kconfig_enable(self, config: str) -> int: """Enable a Kconfig option. Args: - config: The name of the Kconfig option to enable. + config: The name of the Kconfig option to enable. The "CONFIG_" + prefix is optional and will be removed if present. Returns: - int: Exit code of the kconfig-tweak command. Returns 0 on success, - non-zero on failure. - """ - result = utils.run_kconfig_command( - [KCONFIG_TWEAK, KconfigTweakAction.ENABLE, config], cwd=self.nuttx_path - ) - logging.info(f"Kconfig enable: {config}") - return result.returncode + int: Returns 0 on success, non-zero on failure. + Raises: + KeyError: If the config option does not exist. + ValueError: If the config option cannot be enabled (e.g., not + assignable or wrong symbol type). + """ + try: + config = config.removeprefix("CONFIG_") + if config not in self.syms: + raise KeyError(f"Kconfig option '{config}' not found") + + symbol = self.syms[config] + symbol_type = kconfiglib.TYPE_TO_STR[symbol.type] + + if not symbol.assignable: + raise ValueError( + f"Kconfig option '{config}' can't be enabled: " + f"symbol type is {symbol_type}" + ) + + ret = symbol.set_value("y") + if ret: + logger.info(f"Kconfig option '{config}' enabled") + else: + logger.error(f"Kconfig option '{config}' enable failed") + + return ret + except Exception as e: + self.environment_context.restore_environment() + raise e + + @kconfig_chdir def kconfig_disable(self, config: str) -> int: """Disable a Kconfig option. Args: - config: The name of the Kconfig option to disable. + config: The name of the Kconfig option to disable. The "CONFIG_" + prefix is optional and will be removed if present. Returns: - int: Exit code of the kconfig-tweak command. Returns 0 on success, - non-zero on failure. - """ - result = utils.run_kconfig_command( - [KCONFIG_TWEAK, KconfigTweakAction.DISABLE, config], cwd=self.nuttx_path - ) - logging.info(f"Kconfig disable: {config}") - return result.returncode + int: Returns 0 on success, non-zero on failure. + Raises: + KeyError: If the config option does not exist. + ValueError: If the config option cannot be disabled (e.g., not + assignable or wrong symbol type). + """ + try: + config = config.removeprefix("CONFIG_") + if config not in self.syms: + raise KeyError(f"Kconfig option '{config}' not found") + + symbol = self.syms[config] + symbol_type = kconfiglib.TYPE_TO_STR[symbol.type] + + if not symbol.assignable: + raise ValueError( + f"Kconfig option '{config}' can't be disabled: " + f"symbol type is {symbol_type}" + ) + + ret = symbol.set_value("n") + if ret: + logger.info(f"Kconfig option '{config}' disabled") + else: + logger.error(f"Kconfig option '{config}' disable failed") + + return ret + except Exception as e: + self.environment_context.restore_environment() + raise e + + @kconfig_chdir def kconfig_apply_changes(self) -> int: - """Apply Kconfig changes by running olddefconfig. + """Apply Kconfig changes by writing the configuration. - This method runs 'make olddefconfig' to apply any pending - Kconfig changes and update the configuration. + This method writes the current Kconfig state to .config file. + kconfiglib automatically handles dependency resolution. Returns: - int: Exit code of the make command. Returns 0 on success, - non-zero on failure. + int: Returns 0 on success, non-zero on failure. """ - result = utils.run_make_command(["make", "olddefconfig"], cwd=self.nuttx_path) - if result.returncode != 0: - logging.error("Kconfig change apply may have failed") - else: - logging.info("Kconfig changes applied") - return result.returncode - + try: + ret = self.write_config() + logger.info(f"Kconfig apply changes: {ret}") + return ret + except Exception as e: + self.environment_context.restore_environment() + raise e + + @kconfig_chdir def kconfig_set_value(self, config: str, value: str) -> int: """Set a numerical Kconfig option value. + Sets the value for INT or HEX type Kconfig options. For HEX options, + the value must be prefixed with "0x". For INT options, hexadecimal + values are not allowed. + Args: - config: The name of the Kconfig option. - value: The numerical value to set. Must be convertible to int. + config: The name of the Kconfig option. The "CONFIG_" prefix is + optional and will be removed if present. + value: The numerical value to set as a string. Must be convertible + to int. For HEX options, must start with "0x". Returns: - int: Exit code of the kconfig-tweak command. Returns 0 on success, - non-zero on failure. + int: Returns 0 on success, non-zero on failure. Raises: - ValueError: If the value cannot be converted to an integer. + AssertionError: If value is not a string. + ValueError: If the value cannot be converted to an integer, if + the config option is not INT or HEX type, if the symbol is + assignable (should use enable/disable instead), if INT type + receives hexadecimal value, or if HEX type doesn't have "0x" + prefix. + KeyError: If the config option does not exist. """ try: - value = int(value) - except ValueError: - raise ValueError("Value must be numerical") - - result = utils.run_kconfig_command( - [KCONFIG_TWEAK, KconfigTweakAction.SET_VAL, config, str(value)], - cwd=self.nuttx_path, - ) - logging.info(f"Kconfig set value: {config}={value}") - return result.returncode - + assert isinstance(value, str), ( + "Set value must be string representation of a numerical or " + "hexadecimal value." + ) + try: + int(value, 0) + except ValueError: + raise ValueError( + "Set value must be string representation of a numerical or " + "hexadecimal value" + ) + + config = config.removeprefix("CONFIG_") + if config not in self.syms: + raise KeyError(f"Kconfig option '{config}' not found") + + symbol = self.syms[config] + symbol_type = kconfiglib.TYPE_TO_STR[symbol.type] + + if symbol.type not in (kconfiglib.INT, kconfiglib.HEX): + raise ValueError( + f"{config} ({symbol_type}) requires a numerical or " + "hexadecimal input" + ) + + if symbol.assignable: + raise ValueError( + f"Kconfig value for '{config}' can't be set: " + f"symbol type is {symbol_type}" + ) + + if symbol.type == kconfiglib.INT and value.startswith("0x"): + raise ValueError( + f"{config} ({symbol_type}) requires a int input, not hexadecimal" + ) + + if symbol.type == kconfiglib.HEX and not value.startswith("0x"): + raise ValueError( + f"{config} ({symbol_type}) requires a hexadecimal input (0x), " + "not int." + ) + + ret = symbol.set_value(value) + if not ret: + logger.error(f"Kconfig set value: {config}={value} failed") + logger.info(f"Kconfig set value: {config}={value}") + return ret + except Exception as e: + self.environment_context.restore_environment() + raise e + + @kconfig_chdir def kconfig_set_str(self, config: str, value: str) -> int: """Set a string Kconfig option value. Args: - config: The name of the Kconfig option. + config: The name of the Kconfig option. The "CONFIG_" prefix is + optional and will be removed if present. value: The string value to set. Returns: - int: Exit code of the kconfig-tweak command. Returns 0 on success, - non-zero on failure. + int: Returns 0 on success, non-zero on failure. + + Raises: + KeyError: If the config option does not exist. + ValueError: If the config option is not a STRING type. """ - result = utils.run_kconfig_command( - [KCONFIG_TWEAK, KconfigTweakAction.SET_STR, config, value], - cwd=self.nuttx_path, - ) - logging.info(f"Kconfig set string: {config}={value}") - return result.returncode + try: + config = config.removeprefix("CONFIG_") + if config not in self.syms: + raise KeyError(f"Kconfig option '{config}' not found") + + symbol = self.syms[config] + symbol_type = kconfiglib.TYPE_TO_STR[symbol.type] + + if symbol.type != kconfiglib.STRING: + raise ValueError(f"{config} ({symbol_type}) requires a string input") + + ret = symbol.set_value(value) + if not ret: + logger.error(f"Kconfig set string: {config}={value} failed") + logger.info(f"Kconfig set string: {config}={value}") + return ret + except Exception as e: + self.environment_context.restore_environment() + raise e def kconfig_menuconfig(self) -> int: """Run the interactive menuconfig interface. @@ -178,46 +444,51 @@ def kconfig_menuconfig(self) -> int: This is a curses-based interface that allows interactive configuration of Kconfig options. + After menuconfig completes, the Kconfig object is reloaded to reflect + any changes made interactively. + Returns: int: Exit code of the menuconfig command. Returns 0 on success, non-zero on failure. """ - logging.debug("Opening menuconfig") - result = utils.run_kconfig_command( - [KCONFIG_TWEAK, KconfigTweakAction.MENUCONFIG], cwd=self.nuttx_path - ) - return result.returncode - - def kconfig_merge_config_file( - self, source_file: str, config_file: str = None - ) -> int: + logger.debug("Opening menuconfig") + # Use make menuconfig as it's the standard way to run menuconfig + # Use run_curses_command for interactive curses-based tools + result = utils.run_curses_command(["make", "menuconfig"], cwd=self.nuttx_path) + # Reload Kconfig object to reflect changes made in menuconfig + # run_curses_command returns CompletedProcess or int(1) on exception + returncode = result.returncode if hasattr(result, "returncode") else result + if returncode == 0: + self._kconf = None + return returncode + + def kconfig_merge_config_file(self, source_file: str) -> int: """Merge a Kconfig file into the current configuration. - Merges configuration options from a source file into the target - configuration file using kconfig-merge. + Merges configuration options from a source file into the current + Kconfig state using kconfiglib's load_config method. The merged + configuration is not automatically written to disk; call + kconfig_apply_changes() to persist changes. Args: source_file: Path to the source configuration file to merge. - config_file: Path to the target configuration file. If None, - defaults to .config in the NuttX directory. Returns: - int: Exit code of the kconfig-merge command. Returns 0 on success, - non-zero on failure. + int: Always returns 0. The actual merge result is available + from load_config but is not currently exposed. Raises: ValueError: If source_file is not provided or is empty. """ - if not source_file: - raise ValueError("Source file is required") - - if not config_file: - config_file = (Path(self.nuttx_path) / ".config").as_posix() - - logging.info(f"Kconfig merge config file: {source_file} into {config_file}") - - source_file = Path(source_file).resolve().as_posix() - result = utils.run_kconfig_command( - [KCONFIG_MERGE, "-m", config_file, source_file], cwd=self.nuttx_path - ) - return result.returncode + try: + if not source_file: + raise ValueError("Source file is required") + + logger.info(f"Kconfig merge config file: {source_file}") + source_file = Path(source_file).resolve().as_posix() + result = self.load_config(str(source_file), replace=False) + logger.info(f"Kconfig merge config file result: {result}") + except Exception as e: + self.environment_context.restore_environment() + raise e + return 0 diff --git a/ntxbuild/utils.py b/ntxbuild/utils.py index e304550..03b5278 100644 --- a/ntxbuild/utils.py +++ b/ntxbuild/utils.py @@ -64,38 +64,6 @@ def run_bash_script( return result -def run_kconfig_command( - cmd: List[str], cwd: Optional[str] = None -) -> subprocess.CompletedProcess: - """Run a kconfig-tweak command and return CompletedProcess object. - - Executes a kconfig-tweak command with captured output. Raises an - exception if the command fails. - - Args: - cmd: Command to run as a list of strings (e.g., - ["kconfig-tweak", "--enable", "CONFIG_FOO"]). - cwd: Working directory for the command. Defaults to None (current directory). - - Returns: - subprocess.CompletedProcess: The result of the command execution - with captured stdout and stderr. - - Raises: - subprocess.CalledProcessError: If the command returns a non-zero exit code. - """ - logger.debug(f"Running kconfig command: {' '.join(cmd)} in cwd={cwd}") - try: - result = subprocess.run( - cmd, cwd=cwd, check=True, capture_output=True, text=True - ) - logger.debug(f"Kconfig command succeeded with return code: {result.returncode}") - return result - except subprocess.CalledProcessError as e: - logger.error(f"Kconfig command failed: {' '.join(cmd)}, error: {e}") - raise - - def run_make_command( cmd: List[str], cwd: Optional[str] = None, diff --git a/tests/conftest.py b/tests/conftest.py index 9aa956e..7ae5881 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ """ import logging +import shutil from pathlib import Path import pytest @@ -53,7 +54,7 @@ def nuttxspace(): # Cleanup: remove the entire workspace if workspace.exists(): logging.info(f"๐Ÿงน Cleaning up NuttX workspace at {workspace}") - # shutil.rmtree(workspace) + shutil.rmtree(workspace, ignore_errors=True) logging.info("โœ… Workspace cleanup completed") diff --git a/tests/test_config.py b/tests/test_config.py index 2046afc..6b5034d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -90,9 +90,9 @@ def test_merge_config(nuttxspace_path): config_manager = ConfigManager(nuttxspace_path, "nuttx") this_file = Path(__file__).resolve() - config_manager.kconfig_merge_config_file( - this_file.parent / "configs" / "test_config", None - ) + config_file = this_file.parent / "configs" / "test_config" + assert config_file.exists() + config_manager.kconfig_merge_config_file(config_file) config_manager.kconfig_apply_changes() value = config_manager.kconfig_read("CONFIG_NSH_SYSINITSCRIPT") @@ -101,3 +101,146 @@ def test_merge_config(nuttxspace_path): assert value == "n" value = config_manager.kconfig_read("CONFIG_DEV_GPIO_NSIGNALS") assert value == "2" + + +# Exception tests +def test_config_manager_file_not_found_error(nuttxspace_path): + """Test FileNotFoundError when apps_path doesn't exist.""" + with pytest.raises(FileNotFoundError, match="Apps path found at"): + ConfigManager(nuttxspace_path, "nuttx", apps_dir="nonexistent_apps") + + +def test_config_manager_runtime_error_no_config(nuttxspace_path, tmp_path): + """Test RuntimeError when .config file doesn't exist.""" + # Create a temporary nuttx directory without .config + temp_nuttx = tmp_path / "nuttx" + temp_nuttx.mkdir() + (temp_nuttx / "Kconfig").touch() + + # Create a dummy apps directory + temp_apps = tmp_path / "nuttx-apps" + temp_apps.mkdir() + + with pytest.raises(RuntimeError, match="\\.config file not found"): + ConfigManager(tmp_path, "nuttx", apps_dir="nuttx-apps") + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_read_key_error(nuttxspace_path): + """Test KeyError when reading non-existent config option.""" + config_manager = ConfigManager(nuttxspace_path, "nuttx") + with pytest.raises(KeyError, match="Kconfig option 'NONEXISTENT_CONFIG' not found"): + config_manager.kconfig_read("CONFIG_NONEXISTENT_CONFIG") + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_enable_key_error(nuttxspace_path): + """Test KeyError when enabling non-existent config option.""" + config_manager = ConfigManager(nuttxspace_path, "nuttx") + with pytest.raises(KeyError, match="Kconfig option 'NONEXISTENT_CONFIG' not found"): + config_manager.kconfig_enable("CONFIG_NONEXISTENT_CONFIG") + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_disable_key_error(nuttxspace_path): + """Test KeyError when disabling non-existent config option.""" + config_manager = ConfigManager(nuttxspace_path, "nuttx") + with pytest.raises(KeyError, match="Kconfig option 'NONEXISTENT_CONFIG' not found"): + config_manager.kconfig_disable("CONFIG_NONEXISTENT_CONFIG") + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_set_value_key_error(nuttxspace_path): + """Test KeyError when setting value for non-existent config option.""" + config_manager = ConfigManager(nuttxspace_path, "nuttx") + with pytest.raises(KeyError, match="Kconfig option 'NONEXISTENT_CONFIG' not found"): + config_manager.kconfig_set_value("CONFIG_NONEXISTENT_CONFIG", "123") + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_set_str_key_error(nuttxspace_path): + """Test KeyError when setting string for non-existent config option.""" + config_manager = ConfigManager(nuttxspace_path, "nuttx") + with pytest.raises(KeyError, match="Kconfig option 'NONEXISTENT_CONFIG' not found"): + config_manager.kconfig_set_str("CONFIG_NONEXISTENT_CONFIG", "test_value") + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_set_value_non_string(nuttxspace_path): + """Test ValueError when setting value with non-string input.""" + config_manager = ConfigManager(nuttxspace_path, "nuttx") + with pytest.raises(AssertionError): + config_manager.kconfig_set_value(NUM_CONFIGS[0], 123) # int instead of str + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_set_value_invalid_int(nuttxspace_path): + """Test ValueError when setting value with invalid integer.""" + config_manager = ConfigManager(nuttxspace_path, "nuttx") + with pytest.raises(ValueError, match="Set value must be string representation"): + config_manager.kconfig_set_value(NUM_CONFIGS[0], "not_a_number") + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_set_value_wrong_type(nuttxspace_path): + """Test ValueError when setting value on non-INT/HEX config option.""" + config_manager = ConfigManager(nuttxspace_path, "nuttx") + # Try to set a value on a STRING config + with pytest.raises(ValueError, match="requires a numerical or hexadecimal input"): + config_manager.kconfig_set_value(STR_CONFIGS[0], "123") + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_set_value_assignable_symbol(nuttxspace_path): + """Test ValueError when setting value on assignable symbol. + + Note: This test uses a BOOL config which will fail at type check, + not at assignable check. Testing assignable check for INT/HEX would + require finding a specific assignable INT/HEX symbol which is fragile. + """ + config_manager = ConfigManager(nuttxspace_path, "nuttx") + # Try to set a value on a BOOL config (which is assignable) + # This will fail at type check since BOOL is not INT/HEX + with pytest.raises(ValueError, match="requires a numerical or hexadecimal input"): + config_manager.kconfig_set_value(BOOL_CONFIGS[0], "1") + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_set_value_int_with_hex(nuttxspace_path): + """Test ValueError when setting INT config with hexadecimal value.""" + config_manager = ConfigManager(nuttxspace_path, "nuttx") + with pytest.raises(ValueError, match="requires a int input, not hexadecimal"): + config_manager.kconfig_set_value(NUM_CONFIGS[0], "0x10") + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_set_value_hex_without_prefix(nuttxspace_path): + """Test ValueError when setting HEX config without 0x prefix.""" + config_manager = ConfigManager(nuttxspace_path, "nuttx") + with pytest.raises(ValueError, match="requires a hexadecimal input"): + config_manager.kconfig_set_value(HEX_CONFIGS[0], "10") + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_set_str_wrong_type(nuttxspace_path): + """Test ValueError when setting string on non-STRING config option.""" + config_manager = ConfigManager(nuttxspace_path, "nuttx") + # Try to set a string on a BOOL config + with pytest.raises(ValueError, match="requires a string input"): + config_manager.kconfig_set_str(BOOL_CONFIGS[0], "test_value") + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_merge_config_file_empty_source(nuttxspace_path): + """Test ValueError when merging config with empty source file.""" + config_manager = ConfigManager(nuttxspace_path, "nuttx") + with pytest.raises(ValueError, match="Source file is required"): + config_manager.kconfig_merge_config_file("") + + +@pytest.mark.usefixtures("setup_board_sim_environment") +def test_kconfig_merge_config_file_none_source(nuttxspace_path): + """Test ValueError when merging config with None source file.""" + config_manager = ConfigManager(nuttxspace_path, "nuttx") + with pytest.raises(ValueError, match="Source file is required"): + config_manager.kconfig_merge_config_file(None)