blob: e57b5d8ef86dc58e12153f8c571f60e9cddc4746 [file] [edit]
import unittest
import os
import shutil
import tempfile
from pathlib import Path
import sys
from unittest.mock import patch, MagicMock
# Import the script to test
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".")))
import kconfig_gen_values
import kconfig_utils
class TestKconfigGenValues(unittest.TestCase):
def setUp(self):
self.test_dir = tempfile.mkdtemp()
self.zephyr_base = Path(self.test_dir) / "zephyr"
self.zephyr_base.mkdir()
# Create minimal Zephyr structure
(self.zephyr_base / "scripts" / "kconfig").mkdir(parents=True)
(self.zephyr_base / "arch" / "arm").mkdir(parents=True)
(self.zephyr_base / "soc" / "nordic" / "nrf52").mkdir(parents=True)
(self.zephyr_base / "boards" / "arm" / "nrf52840dk").mkdir(parents=True)
(self.zephyr_base / "cmake" / "toolchain" / "zephyr").mkdir(parents=True)
(self.zephyr_base / "cmake" / "toolchain" / "gnuarmemb").mkdir(parents=True)
# Create some Kconfig files
(self.zephyr_base / "Kconfig").write_text("mainmenu \"Test\"\n")
# Mock kconfiglib and its wrapper
self.kconfig_mock = MagicMock()
sys.modules['kconfiglib'] = self.kconfig_mock
self.kconfig_wrapper_mock = MagicMock()
sys.modules['kconfig'] = self.kconfig_wrapper_mock
# Mock pcpp
self.pcpp_mock = MagicMock()
self.pcpp_mock.__file__ = '/mock/pcpp/__init__.py'
sys.modules['pcpp'] = self.pcpp_mock
def tearDown(self):
shutil.rmtree(self.test_dir)
def test_setup_environment(self):
args = MagicMock()
args.board = "nrf52840dk/nrf52840"
args.toolchain_variant = None
output_dir = os.path.join(self.test_dir, "out")
board_dir = os.path.join(self.test_dir, "board")
kconfig_gen_values.setup_environment(args, str(self.zephyr_base), output_dir, board_dir)
self.assertEqual(os.environ["BOARD"], "nrf52840dk")
self.assertEqual(os.environ["BOARD_QUALIFIERS"], "nrf52840")
self.assertEqual(os.environ["ARCH"], "*")
self.assertEqual(os.environ["ZEPHYR_BASE"], str(self.zephyr_base))
self.assertEqual(os.environ["TOOLCHAIN_KCONFIG_DIR"], os.path.join(str(self.zephyr_base), "cmake", "toolchain", "zephyr"))
def test_setup_environment_native(self):
args = MagicMock()
args.board = "native_sim"
args.toolchain_variant = None
output_dir = os.path.join(self.test_dir, "out")
board_dir = os.path.join(self.test_dir, "boards/native/native_sim")
os.makedirs(output_dir, exist_ok=True)
kconfig_gen_values.setup_environment(args, str(self.zephyr_base), output_dir, board_dir)
self.assertEqual(os.environ["TOOLCHAIN_KCONFIG_DIR"], os.path.join(str(self.zephyr_base), "cmake", "toolchain", "host"))
def test_setup_environment_explicit_toolchain(self):
args = MagicMock()
args.board = "nrf52840dk"
args.toolchain_variant = "gnuarmemb"
output_dir = os.path.join(self.test_dir, "out")
board_dir = os.path.join(self.test_dir, "board")
os.makedirs(output_dir, exist_ok=True)
kconfig_gen_values.setup_environment(args, str(self.zephyr_base), output_dir, board_dir)
self.assertEqual(os.environ["TOOLCHAIN_KCONFIG_DIR"], os.path.join(str(self.zephyr_base), "cmake", "toolchain", "gnuarmemb"))
@patch('subprocess.run')
def test_main_success(self, mock_run):
output_dir = os.path.join(self.test_dir, "out")
board_dir = str(self.zephyr_base / "boards" / "arm" / "nrf52840dk")
# Mock subprocess.run for kconfig.py
mock_res = MagicMock()
mock_res.returncode = 0
mock_res.stderr = ""
mock_run.return_value = mock_res
# Side effect to create dummy files that main() expects to read
def create_files(*args, **kwargs):
os.makedirs(os.path.join(output_dir, "zephyr"), exist_ok=True)
with open(os.path.join(output_dir, ".config"), "w") as f:
f.write("CONFIG_FOO=y\n")
with open(os.path.join(output_dir, "zephyr", "autoconf.h"), "w") as f:
f.write("#define CONFIG_FOO 1\n")
return mock_res
mock_run.side_effect = create_files
# Mock kconfiglib
mock_kconf = MagicMock()
self.kconfig_mock.Kconfig.return_value = mock_kconf
mock_kconf.unique_defined_syms = []
with patch('sys.argv', [
'kconfig_gen_values.py',
'--zephyr-base', str(self.zephyr_base),
'--app-dir', self.test_dir,
'--board', 'nrf52840dk',
'--board-dir', board_dir,
'--parent-platform', '//platforms:test',
'--output-dir', output_dir,
'--app-name', 'my_custom_app'
]):
# We need to mock other subprocess calls in main like generate_kconfig_dts
with patch('kconfig_utils.generate_kconfig_dts'), \
patch('kconfig_gen_values.preprocess_dts') as mock_pre, \
patch('kconfig_gen_values.generate_edt_pickle'):
mock_pre.return_value = ("merged.dts", [])
kconfig_gen_values.main()
# Verify APP_NAME was added
autoconf_path = os.path.join(output_dir, "zephyr", "autoconf.h")
content = Path(autoconf_path).read_text()
self.assertIn('#define APP_NAME "my_custom_app"', content)
@patch('subprocess.run')
def test_main_failure(self, mock_run):
output_dir = os.path.join(self.test_dir, "out")
board_dir = str(self.zephyr_base / "boards" / "arm" / "nrf52840dk")
# Mock subprocess.run to fail
mock_res = MagicMock()
mock_res.returncode = 1
mock_res.stderr = "Some Kconfig error"
mock_run.return_value = mock_res
with patch('sys.argv', [
'kconfig_gen_values.py',
'--zephyr-base', str(self.zephyr_base),
'--app-dir', self.test_dir,
'--board', 'nrf52840dk',
'--board-dir', board_dir,
'--parent-platform', '//platforms:test',
'--output-dir', output_dir
]):
with patch('kconfig_utils.generate_kconfig_dts'), \
patch('kconfig_gen_values.preprocess_dts') as mock_pre, \
patch('kconfig_gen_values.generate_edt_pickle'):
mock_pre.return_value = ("merged.dts", [])
with self.assertRaises(SystemExit) as cm:
kconfig_gen_values.main()
self.assertEqual(str(cm.exception), "Kconfig generation failed with exit code 1")
def test_get_conf_files_with_qualifiers(self):
args = MagicMock()
args.app_dir = self.test_dir
board_dir = os.path.join(self.test_dir, "board")
os.makedirs(board_dir, exist_ok=True)
# Create a dummy defconfig file with underscores
defconfig_path = os.path.join(board_dir, "harriet_evb2_mimxrt595s_cm33_defconfig")
with open(defconfig_path, "w") as f:
f.write("CONFIG_FOO=y\n")
os.environ["BOARD"] = "harriet_evb2"
os.environ["BOARD_QUALIFIERS"] = "/mimxrt595s/cm33"
conf_files = kconfig_gen_values.get_conf_files(args, board_dir)
self.assertIn(defconfig_path, conf_files)
def test_discover_overlays_none(self):
app_dir = os.path.join(self.test_dir, "app")
os.makedirs(app_dir, exist_ok=True)
overlays = kconfig_gen_values.discover_overlays(app_dir, "board", "")
self.assertEqual(overlays, [])
def test_discover_overlays_app_only(self):
app_dir = os.path.join(self.test_dir, "app")
os.makedirs(app_dir, exist_ok=True)
app_overlay = os.path.join(app_dir, "app.overlay")
Path(app_overlay).touch()
overlays = kconfig_gen_values.discover_overlays(app_dir, "board", "")
self.assertEqual(overlays, [app_overlay])
def test_discover_overlays_board_only(self):
app_dir = os.path.join(self.test_dir, "app")
boards_dir = os.path.join(app_dir, "boards")
os.makedirs(boards_dir, exist_ok=True)
board_overlay = os.path.join(boards_dir, "board.overlay")
Path(board_overlay).touch()
overlays = kconfig_gen_values.discover_overlays(app_dir, "board", "")
self.assertEqual(overlays, [board_overlay])
def test_discover_overlays_qualified_board(self):
app_dir = os.path.join(self.test_dir, "app")
boards_dir = os.path.join(app_dir, "boards")
os.makedirs(boards_dir, exist_ok=True)
board_overlay = os.path.join(boards_dir, "board.overlay")
soc_overlay = os.path.join(boards_dir, "board_soc.overlay")
Path(board_overlay).touch()
Path(soc_overlay).touch()
overlays = kconfig_gen_values.discover_overlays(app_dir, "board", "/soc")
self.assertEqual(overlays, [soc_overlay, board_overlay])
def test_discover_overlays_all(self):
app_dir = os.path.join(self.test_dir, "app")
boards_dir = os.path.join(app_dir, "boards")
os.makedirs(boards_dir, exist_ok=True)
app_overlay = os.path.join(app_dir, "app.overlay")
board_overlay = os.path.join(boards_dir, "board.overlay")
soc_overlay = os.path.join(boards_dir, "board_soc.overlay")
Path(app_overlay).touch()
Path(board_overlay).touch()
Path(soc_overlay).touch()
overlays = kconfig_gen_values.discover_overlays(app_dir, "board", "/soc")
self.assertEqual(overlays, [soc_overlay, board_overlay, app_overlay])
@patch('subprocess.run')
def test_preprocess_dts_generates_input_c(self, mock_run):
app_dir = os.path.join(self.test_dir, "app")
board_dir = os.path.join(self.test_dir, "board")
output_dir = os.path.join(self.test_dir, "out")
os.makedirs(app_dir, exist_ok=True)
os.makedirs(board_dir, exist_ok=True)
os.makedirs(output_dir, exist_ok=True)
base_dts = os.path.join(board_dir, "my_board.dts")
Path(base_dts).touch()
app_overlay = os.path.join(app_dir, "app.overlay")
Path(app_overlay).touch()
os.environ["BOARD"] = "my_board"
os.environ["BOARD_QUALIFIERS"] = ""
args = MagicMock()
args.board = "my_board"
args.app_dir = app_dir
args.oot_dts_roots = []
args.modules = []
mock_run.return_value = MagicMock(returncode=0)
merged_dts_path, dts_roots = kconfig_gen_values.preprocess_dts(
args, str(self.zephyr_base), output_dir, board_dir
)
dts_input_path = os.path.join(output_dir, "dts_input.c")
self.assertTrue(os.path.exists(dts_input_path))
content = Path(dts_input_path).read_text()
self.assertIn(f'#include "{os.path.abspath(base_dts)}"', content)
self.assertIn(f'#include "{os.path.abspath(app_overlay)}"', content)
@patch('subprocess.run')
def test_generate_dts_headers(self, mock_run):
output_dir = os.path.join(self.test_dir, "out")
edt_pickle_path = os.path.join(output_dir, "edt.pickle")
kconfig_gen_values.generate_dts_headers(
str(self.zephyr_base), output_dir, edt_pickle_path
)
mock_run.assert_called_once()
args, kwargs = mock_run.call_args
cmd = args[0]
self.assertIn(os.path.join(str(self.zephyr_base), "scripts", "dts", "gen_defines.py"), cmd)
self.assertIn("--edt-pickle", cmd)
self.assertIn(edt_pickle_path, cmd)
self.assertIn("--header-out", cmd)
self.assertIn(os.path.join(output_dir, "zephyr", "devicetree_generated.h"), cmd)
def test_setup_environment_with_bzlmod_modules(self):
args = MagicMock()
args.board = "nrf52840dk"
args.toolchain_variant = None
# Bzlmod canonical name format for injected repos
module_dir = os.path.join(self.test_dir, "external", "+_repo_rules+hal_nordic")
os.makedirs(module_dir, exist_ok=True)
args.modules = [module_dir]
output_dir = os.path.join(self.test_dir, "out")
board_dir = os.path.join(self.test_dir, "board")
kconfig_gen_values.setup_environment(args, str(self.zephyr_base), output_dir, board_dir)
# Verify ZEPHYR_<MODULE>_MODULE_DIR is set correctly
# 'hal_nordic+...' -> 'HAL_NORDIC'
self.assertEqual(os.environ["ZEPHYR_HAL_NORDIC_MODULE_DIR"], os.path.abspath(module_dir))
# Check if it was written to the env file
kconfig_env_file = os.path.join(output_dir, "kconfig_module_dirs.env")
self.assertTrue(os.path.exists(kconfig_env_file))
content = Path(kconfig_env_file).read_text()
self.assertIn(f"ZEPHYR_HAL_NORDIC_MODULE_DIR={os.path.abspath(module_dir)}", content)
def test_generate_kconfig_aggregations_with_modules(self):
output_dir = os.path.join(self.test_dir, "out")
board_dir = os.path.join(self.test_dir, "board")
# Create soc.yml in Zephyr to ensure it is discovered
(self.zephyr_base / "soc" / "nordic" / "nrf52" / "soc.yml").touch()
# Create an OOT module with soc.yml
module_dir = os.path.join(self.test_dir, "my_module")
(Path(module_dir) / "soc" / "my_soc").mkdir(parents=True)
(Path(module_dir) / "soc" / "my_soc" / "soc.yml").touch()
(Path(module_dir) / "soc" / "my_soc" / "Kconfig.defconfig").write_text("config SOC_MY_SOC\n")
(Path(module_dir) / "soc" / "my_soc" / "Kconfig").write_text("config SOC_MY_SOC\n")
args = MagicMock()
args.modules = [module_dir]
os.environ["BOARD"] = "nrf52840dk"
with patch('kconfig_utils.generate_aggregation_file') as mock_gen:
kconfig_gen_values.generate_kconfig_aggregations(str(self.zephyr_base), args, output_dir, board_dir)
# Verify that generate_aggregation_file was called with the OOT soc directory
mock_gen.assert_any_call(
os.path.join(output_dir, "soc", "Kconfig.defconfig"),
[os.path.join(str(self.zephyr_base), "soc/nordic/nrf52"), os.path.join(module_dir, "soc/my_soc")],
"SoC defconfigs"
)
@patch('subprocess.run')
def test_preprocess_dts_deduplicates_roots(self, mock_run):
app_dir = os.path.join(self.test_dir, "app")
board_dir = os.path.join(self.test_dir, "board")
output_dir = os.path.join(self.test_dir, "out")
os.makedirs(app_dir, exist_ok=True)
os.makedirs(board_dir, exist_ok=True)
os.makedirs(output_dir, exist_ok=True)
base_dts = os.path.join(board_dir, "my_board.dts")
Path(base_dts).touch()
os.environ["BOARD"] = "my_board"
os.environ["BOARD_QUALIFIERS"] = ""
args = MagicMock()
args.board = "my_board"
args.app_dir = app_dir
# Duplicate roots
args.oot_dts_roots = [self.test_dir, self.test_dir]
args.modules = []
mock_run.return_value = MagicMock(returncode=0)
with patch('kconfig_gen_values.discover_overlays') as mock_overlays:
mock_overlays.return_value = []
merged_dts_path, dts_roots = kconfig_gen_values.preprocess_dts(
args, str(self.zephyr_base), output_dir, board_dir
)
# Verify dts_roots has no duplicates
self.assertEqual(dts_roots.count(Path(self.test_dir)), 1)
@patch('subprocess.run')
def test_generate_edt_pickle_resolves_and_deduplicates_bindings(self, mock_run):
output_dir = os.path.join(self.test_dir, "out")
os.makedirs(output_dir, exist_ok=True)
# Create two paths that resolve to the same absolute path (via symlink or relative)
dir1 = os.path.join(self.test_dir, "dir1")
os.makedirs(os.path.join(dir1, "dts", "bindings"))
# Relative path to dir1
dir2 = os.path.join(self.test_dir, "out", "..", "dir1")
dts_roots = [Path(dir1), Path(dir2)]
kconfig_gen_values.generate_edt_pickle(
str(self.zephyr_base), output_dir, dts_roots, "merged.dts"
)
mock_run.assert_called_once()
args = mock_run.call_args[0][0]
# Count occurrences of bindings dir in arguments
resolved_bdir = str(Path(dir1).resolve() / "dts" / "bindings")
bindings_args = [a for a in args if a == resolved_bdir]
# Should be deduplicated
self.assertEqual(len(bindings_args), 1)
if __name__ == '__main__':
unittest.main()