From: Simon Glass <simon.glass@canonical.com> Add a new test_boards.py with 26 tests to achieve 100% coverage for boards.py. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- tools/buildman/func_test.py | 1 + tools/buildman/main.py | 4 +- tools/buildman/test_boards.py | 739 ++++++++++++++++++++++++++++++++++ 3 files changed, 743 insertions(+), 1 deletion(-) create mode 100644 tools/buildman/test_boards.py diff --git a/tools/buildman/func_test.py b/tools/buildman/func_test.py index 6fc08e02fb8..5b112c81aea 100644 --- a/tools/buildman/func_test.py +++ b/tools/buildman/func_test.py @@ -1267,3 +1267,4 @@ something: me boards.ExtendedParser.parse_data('bert', 'name: katie was here') self.assertEqual('bert:1: Invalid name', str(exc.exception)) + diff --git a/tools/buildman/main.py b/tools/buildman/main.py index 77b9bebed27..9483e12e5d0 100755 --- a/tools/buildman/main.py +++ b/tools/buildman/main.py @@ -41,6 +41,7 @@ def run_tests(skip_net_tests, debug, verbose, args): # pylint: disable=C0415 from buildman import func_test from buildman import test + from buildman import test_boards test_name = args.terms and args.terms[0] or None if skip_net_tests: @@ -50,7 +51,8 @@ def run_tests(skip_net_tests, debug, verbose, args): # 'entry' module. result = test_util.run_test_suites( 'buildman', debug, verbose, False, False, args.threads, test_name, [], - [test.TestBuild, func_test.TestFunctional, 'buildman.toolchain']) + [test.TestBuild, func_test.TestFunctional, test_boards.TestBoards, + 'buildman.toolchain']) return (0 if result.wasSuccessful() else 1) diff --git a/tools/buildman/test_boards.py b/tools/buildman/test_boards.py new file mode 100644 index 00000000000..66eb82bc755 --- /dev/null +++ b/tools/buildman/test_boards.py @@ -0,0 +1,739 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright (c) 2024 Google, Inc +# + +"""Tests for boards.py""" + +import errno +import multiprocessing +import os +from pathlib import Path +import shutil +import tempfile +import time +import unittest +from unittest import mock + +from buildman import board +from buildman import boards +from buildman.boards import Extended +from u_boot_pylib import terminal +from u_boot_pylib import tools + + +BOARDS = [ + ['Active', 'arm', 'armv7', '', 'Tester', 'ARM Board 0', 'board0', ''], + ['Active', 'arm', 'armv7', '', 'Tester', 'ARM Board 1', 'board1', ''], + ['Active', 'powerpc', 'powerpc', '', 'Tester', 'PowerPC board 1', 'board2', ''], + ['Active', 'sandbox', 'sandbox', '', 'Tester', 'Sandbox board', 'board4', ''], +] + + +class TestBoards(unittest.TestCase): + """Test boards.py functionality""" + + def setUp(self): + self._base_dir = tempfile.mkdtemp() + self._output_dir = tempfile.mkdtemp() + self._git_dir = os.path.join(self._base_dir, 'src') + self._buildman_dir = os.path.dirname(os.path.realpath(__file__)) + self._test_dir = os.path.join(self._buildman_dir, 'test') + + # Set up some fake source files + shutil.copytree(self._test_dir, self._git_dir) + + # Avoid sending any output and clear all terminal output + terminal.set_print_test_mode() + terminal.get_print_test_lines() + + self._boards = boards.Boards() + for brd in BOARDS: + self._boards.add_board(board.Board(*brd)) + + def tearDown(self): + shutil.rmtree(self._base_dir) + shutil.rmtree(self._output_dir) + + def test_try_remove(self): + """Test try_remove() function""" + # Test removing a file that doesn't exist - should not raise + boards.try_remove('/nonexistent/path/to/file') + + # Test removing a file that does exist + fname = os.path.join(self._base_dir, 'test_remove') + tools.write_file(fname, b'test') + self.assertTrue(os.path.exists(fname)) + boards.try_remove(fname) + self.assertFalse(os.path.exists(fname)) + + def test_read_boards(self): + """Test Boards.read_boards() with various field counts""" + # Test normal boards.cfg file + boards_cfg = os.path.join(self._base_dir, 'boards.cfg') + content = '''# Comment line +Active arm armv7 - Tester ARM_Board_0 board0 config0 maint@test.com +Active powerpc ppc mpc85xx Tester PPC_Board_1 board2 config2 maint2@test.com + +''' + tools.write_file(boards_cfg, content.encode('utf-8')) + + brds = boards.Boards() + brds.read_boards(boards_cfg) + board_list = brds.get_list() + self.assertEqual(2, len(board_list)) + self.assertEqual('board0', board_list[0].target) + self.assertEqual('arm', board_list[0].arch) + self.assertEqual('', board_list[0].soc) # '-' converted to '' + self.assertEqual('mpc85xx', board_list[1].soc) + + # Test with fewer than 8 fields + boards_cfg = os.path.join(self._base_dir, 'boards_short.cfg') + content = '''Active arm armv7 - Tester Board target config +''' + tools.write_file(boards_cfg, content.encode('utf-8')) + brds = boards.Boards() + brds.read_boards(boards_cfg) + self.assertEqual(1, len(brds.get_list())) + + # Test with more than 8 fields (extra fields ignored) + boards_cfg = os.path.join(self._base_dir, 'boards_extra.cfg') + content = '''Active arm armv7 soc Tester Board target config maint extra +''' + tools.write_file(boards_cfg, content.encode('utf-8')) + brds = boards.Boards() + brds.read_boards(boards_cfg) + self.assertEqual('config', brds.get_list()[0].cfg_name) + + def test_boards_methods(self): + """Test Boards helper methods: get_dict, get_selected_names, find_by_target""" + brds = boards.Boards() + for brd in BOARDS: + brds.add_board(board.Board(*brd)) + + # Test get_dict() + board_dict = brds.get_dict() + self.assertEqual(4, len(board_dict)) + self.assertEqual('arm', board_dict['board0'].arch) + self.assertEqual('sandbox', board_dict['board4'].arch) + + # Test get_selected_names() + brds.select_boards(['arm']) + self.assertEqual(['board0', 'board1'], brds.get_selected_names()) + + # Test select_boards warning for missing board + brds2 = boards.Boards() + for brd in BOARDS: + brds2.add_board(board.Board(*brd)) + result, warnings = brds2.select_boards([], brds=['nonexistent', 'board0']) + self.assertEqual(1, len(warnings)) + self.assertIn('nonexistent', warnings[0]) + + # Test find_by_target() + found = brds.find_by_target('board0') + self.assertEqual('arm', found.arch) + + with terminal.capture() as (stdout, stderr): + with self.assertRaises(ValueError) as exc: + brds.find_by_target('nonexistent') + self.assertIn('nonexistent', str(exc.exception)) + + def test_kconfig_riscv(self): + """Test KconfigScanner riscv architecture detection""" + src = self._git_dir + kc_file = os.path.join(src, 'Kconfig') + orig_kc_data = tools.read_file(kc_file) + + riscv_kconfig = orig_kc_data + b''' + +config RISCV +\tbool + +config ARCH_RV32I +\tbool + +config TARGET_RISCV_BOARD +\tbool "RISC-V Board" +\tselect RISCV +\tdefault n + +if TARGET_RISCV_BOARD +config SYS_ARCH +\tdefault "riscv" + +config SYS_CPU +\tdefault "generic" + +config SYS_VENDOR +\tdefault "RiscVendor" + +config SYS_BOARD +\tdefault "RISC-V Board" + +config SYS_CONFIG_NAME +\tdefault "riscv_config" +endif +''' + tools.write_file(kc_file, riscv_kconfig) + + try: + scanner = boards.KconfigScanner(src) + defconfig = os.path.join(src, 'riscv64_defconfig') + tools.write_file(defconfig, 'CONFIG_TARGET_RISCV_BOARD=y\n', False) + + # Test riscv64 (no RV32I) + res, warnings = scanner.scan(defconfig, False) + self.assertEqual('riscv64', res['arch']) + + # Test riscv32 (with RV32I) + riscv32_kconfig = riscv_kconfig + b''' +config ARCH_RV32I +\tdefault y if TARGET_RISCV_BOARD +''' + tools.write_file(kc_file, riscv32_kconfig) + scanner = boards.KconfigScanner(src) + res, warnings = scanner.scan(defconfig, False) + self.assertEqual('riscv32', res['arch']) + finally: + tools.write_file(kc_file, orig_kc_data) + + def test_maintainers_commented(self): + """Test MaintainersDatabase with commented maintainer lines""" + src = self._git_dir + main = os.path.join(src, 'boards', 'board0', 'MAINTAINERS') + config_dir = os.path.join(src, 'configs') + orig_data = tools.read_file(main, binary=False) + + new_data = '#M: Commented Maintainer <comment@test.com>\n' + orig_data + tools.write_file(main, new_data, binary=False) + + try: + params_list, warnings = self._boards.build_board_list(config_dir, src) + self.assertEqual(2, len(params_list)) + finally: + tools.write_file(main, orig_data, binary=False) + + def test_ensure_board_list_options(self): + """Test ensure_board_list() with force and quiet flags""" + outfile = os.path.join(self._output_dir, 'test-boards-opts.cfg') + brds = boards.Boards() + + # Test force=False, quiet=False (normal generation) + with terminal.capture() as (stdout, stderr): + brds.ensure_board_list(outfile, jobs=1, force=False, quiet=False) + self.assertTrue(os.path.exists(outfile)) + + # Test force=True (regenerate even if current) + with terminal.capture() as (stdout, stderr): + brds.ensure_board_list(outfile, jobs=1, force=True, quiet=False) + self.assertTrue(os.path.exists(outfile)) + + # Test quiet=True (minimal output) + with terminal.capture() as (stdout, stderr): + brds.ensure_board_list(outfile, jobs=1, force=False, quiet=True) + self.assertNotIn('Checking', stdout.getvalue()) + + # Test quiet=True when up to date (no output) + with terminal.capture() as (stdout, stderr): + result = brds.ensure_board_list(outfile, jobs=1, force=False, + quiet=True) + self.assertTrue(result) + self.assertEqual('', stdout.getvalue()) + + def test_output_is_new_old_format(self): + """Test output_is_new() with old format containing Options""" + src = self._git_dir + config_dir = os.path.join(src, 'configs') + boards_cfg = os.path.join(self._base_dir, 'boards_old.cfg') + + content = b'''# +# List of boards +# +# Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers + +Active arm armv7 - Tester Board board0 options maint +''' + tools.write_file(boards_cfg, content) + self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src)) + + def test_maintainers_status(self): + """Test MaintainersDatabase.get_status() with various statuses""" + database = boards.MaintainersDatabase() + + # Test missing target + self.assertEqual('-', database.get_status('missing')) + self.assertIn("no status info for 'missing'", database.warnings[-1]) + + # Test 'Supported' maps to Active + database.database['test1'] = ('Supported', ['maint@test.com']) + self.assertEqual('Active', database.get_status('test1')) + + # Test 'Orphan' status + database.database['orphan'] = ('Orphan', ['maint@test.com']) + self.assertEqual('Orphan', database.get_status('orphan')) + + # Test unknown status + database.database['test2'] = ('Unknown Status', ['maint@test.com']) + self.assertEqual('-', database.get_status('test2')) + self.assertIn("unknown status for 'test2'", database.warnings[-1]) + + def test_expr_term_str(self): + """Test Expr and Term __str__() methods""" + expr = boards.Expr('arm.*') + self.assertEqual('arm.*', str(expr)) + + term = boards.Term() + term.add_expr('arm') + term.add_expr('cortex') + self.assertEqual('arm&cortex', str(term)) + + def test_kconfig_scanner_warnings(self): + """Test KconfigScanner.scan() TARGET_xxx warnings""" + src = self._git_dir + kc_file = os.path.join(src, 'Kconfig') + orig_kc_data = tools.read_file(kc_file) + + # Test missing TARGET_xxx warning + defconfig = os.path.join(src, 'configs', 'no_target_defconfig') + tools.write_file(defconfig, 'CONFIG_SYS_ARCH="arm"\n', False) + try: + scanner = boards.KconfigScanner(src) + res, warnings = scanner.scan(defconfig, warn_targets=True) + self.assertEqual(1, len(warnings)) + self.assertIn('No TARGET_NO_TARGET enabled', warnings[0]) + finally: + if os.path.exists(defconfig): + os.remove(defconfig) + + # Test duplicate TARGET_xxx warning + extra = b''' +config TARGET_BOARD0_DUP +\tbool "Duplicate target" +\tdefault y if TARGET_BOARD0 +''' + tools.write_file(kc_file, orig_kc_data + extra) + try: + scanner = boards.KconfigScanner(src) + defconfig = os.path.join(src, 'configs', 'board0_defconfig') + res, warnings = scanner.scan(defconfig, warn_targets=True) + self.assertEqual(1, len(warnings)) + self.assertIn('Duplicate TARGET_xxx', warnings[0]) + finally: + tools.write_file(kc_file, orig_kc_data) + + def test_scan_extended(self): + """Test scan_extended() for finding boards matching extended criteria""" + brds = boards.Boards() + + # Test with CONFIG-based selection (value=y) + ext = Extended( + name='test_ext', + desc='Test extended board', + fragments=['test_frag'], + targets=[['CONFIG_ARM', 'y']]) + + with mock.patch('qconfig.find_config') as mock_find, \ + mock.patch.object(tools, 'read_file', return_value='CONFIG_TEST=y'): + mock_find.return_value = {'board0', 'board1'} + result = brds.scan_extended(None, ext) + self.assertEqual({'board0', 'board1'}, result) + mock_find.assert_called_once_with(None, ['CONFIG_ARM']) + + # Test with CONFIG-based selection (value=n) + ext = Extended( + name='test_ext2', + desc='Test extended board 2', + fragments=['test_frag'], + targets=[['CONFIG_DEBUG', 'n']]) + + with mock.patch('qconfig.find_config') as mock_find, \ + mock.patch.object(tools, 'read_file', return_value=''): + mock_find.return_value = {'board2'} + result = brds.scan_extended(None, ext) + self.assertEqual({'board2'}, result) + mock_find.assert_called_once_with(None, ['~CONFIG_DEBUG']) + + # Test with CONFIG-based selection (specific value) + ext = Extended( + name='test_ext3', + desc='Test extended board 3', + fragments=['test_frag'], + targets=[['CONFIG_SYS_SOC', '"k3"']]) + + with mock.patch('qconfig.find_config') as mock_find, \ + mock.patch.object(tools, 'read_file', return_value=''): + mock_find.return_value = {'board4'} + result = brds.scan_extended(None, ext) + self.assertEqual({'board4'}, result) + mock_find.assert_called_once_with(None, ['CONFIG_SYS_SOC="k3"']) + + # Test with regex pattern - intersection of glob and find_config + ext = Extended( + name='test_ext4', + desc='Test extended board 4', + fragments=['test_frag'], + targets=[['regex', 'configs/board*_defconfig']]) + + with mock.patch('qconfig.find_config') as mock_find, \ + mock.patch.object(tools, 'read_file', return_value=''), \ + mock.patch('glob.glob') as mock_glob: + mock_glob.return_value = ['configs/board0_defconfig', + 'configs/board2_defconfig'] + mock_find.return_value = {'board0', 'board1', 'board2'} + result = brds.scan_extended(None, ext) + # Should be intersection: {board0, board2} & {board0, board1, board2} + self.assertEqual({'board0', 'board2'}, result) + + def test_parse_extended(self): + """Test parse_extended() for creating extended board entries""" + brds = boards.Boards() + for brd in BOARDS: + brds.add_board(board.Board(*brd)) + + # Create a .buildman file + buildman_file = os.path.join(self._base_dir, 'test.buildman') + content = '''name: test_acpi +desc: Test ACPI boards +fragment: acpi +targets: + CONFIG_ARM=y +''' + tools.write_file(buildman_file, content, binary=False) + + # Mock scan_extended to return specific boards + with mock.patch.object(brds, 'scan_extended') as mock_scan: + mock_scan.return_value = {'board0', 'board1'} + brds.parse_extended(None, buildman_file) + + # Check that new extended boards were added + board_list = brds.get_list() + # Original 4 boards + 2 extended boards + self.assertEqual(6, len(board_list)) + + # Find the extended boards + ext_boards = [b for b in board_list if ',' in b.target] + self.assertEqual(2, len(ext_boards)) + + # Check the extended board properties + ext_board = next(b for b in ext_boards if 'board0' in b.target) + self.assertEqual('test_acpi,board0', ext_board.target) + self.assertEqual('arm', ext_board.arch) + self.assertEqual('board0', ext_board.orig_target) + self.assertIsNotNone(ext_board.extended) + self.assertEqual('test_acpi', ext_board.extended.name) + + def test_try_remove_other_error(self): + """Test try_remove() re-raises non-ENOENT errors""" + with mock.patch('os.remove') as mock_remove: + # Simulate a permission error (not ENOENT) + err = OSError(errno.EACCES, 'Permission denied') + mock_remove.side_effect = err + with self.assertRaises(OSError) as exc: + boards.try_remove('/some/file') + self.assertEqual(errno.EACCES, exc.exception.errno) + + def test_output_is_new_other_error(self): + """Test output_is_new() re-raises non-ENOENT errors""" + with mock.patch('os.path.getctime') as mock_ctime: + err = OSError(errno.EACCES, 'Permission denied') + mock_ctime.side_effect = err + with self.assertRaises(OSError) as exc: + boards.output_is_new('/some/file', 'configs', '.') + self.assertEqual(errno.EACCES, exc.exception.errno) + + def test_output_is_new_hidden_files(self): + """Test output_is_new() skips hidden defconfig files""" + base = self._base_dir + src = self._git_dir + config_dir = os.path.join(src, 'configs') + + # Create boards.cfg + boards_cfg = os.path.join(base, 'boards_hidden.cfg') + content = b'''# +# List of boards +# Automatically generated by buildman/boards.py: don't edit +# +# Status, Arch, CPU, SoC, Vendor, Board, Target, Config, Maintainers + +Active arm armv7 - Tester Board board0 config0 maint +''' + tools.write_file(boards_cfg, content) + + # Create a hidden defconfig file (should be skipped) + hidden = os.path.join(config_dir, '.hidden_defconfig') + tools.write_file(hidden, b'# hidden') + + try: + # Touch boards.cfg to make it newer + time.sleep(0.02) + Path(boards_cfg).touch() + # Should return True (hidden file skipped) + self.assertTrue(boards.output_is_new(boards_cfg, config_dir, src)) + finally: + os.remove(hidden) + + def test_kconfig_scanner_destructor(self): + """Test KconfigScanner.__del__() cleans up leftover temp file""" + src = self._git_dir + scanner = boards.KconfigScanner(src) + + # Simulate a leftover temp file + tmpfile = os.path.join(self._base_dir, 'leftover.tmp') + tools.write_file(tmpfile, b'temp') + scanner._tmpfile = tmpfile + + # Delete the scanner - should clean up the temp file + del scanner + self.assertFalse(os.path.exists(tmpfile)) + + def test_kconfig_scanner_aarch64(self): + """Test KconfigScanner.scan() aarch64 fix-up""" + src = self._git_dir + kc_file = os.path.join(src, 'Kconfig') + orig_kc_data = tools.read_file(kc_file) + + # Add aarch64 board to Kconfig + aarch64_kconfig = orig_kc_data + b''' + +config TARGET_AARCH64_BOARD +\tbool "AArch64 Board" +\tdefault n + +if TARGET_AARCH64_BOARD +config SYS_ARCH +\tdefault "arm" + +config SYS_CPU +\tdefault "armv8" + +config SYS_VENDOR +\tdefault "Test" + +config SYS_BOARD +\tdefault "AArch64 Board" + +config SYS_CONFIG_NAME +\tdefault "aarch64_config" +endif +''' + tools.write_file(kc_file, aarch64_kconfig) + + try: + scanner = boards.KconfigScanner(src) + defconfig = os.path.join(src, 'aarch64_defconfig') + tools.write_file(defconfig, 'CONFIG_TARGET_AARCH64_BOARD=y\n', False) + res, warnings = scanner.scan(defconfig, False) + # Should be fixed up to aarch64 + self.assertEqual('aarch64', res['arch']) + finally: + tools.write_file(kc_file, orig_kc_data) + if os.path.exists(defconfig): + os.remove(defconfig) + + def test_read_boards_short_line(self): + """Test Boards.read_boards() pads short lines to 8 fields""" + boards_cfg = os.path.join(self._base_dir, 'boards_veryshort.cfg') + # Create a board with only 7 fields (missing maintainers) + content = '''Active arm armv7 soc Tester Board target +''' + tools.write_file(boards_cfg, content.encode('utf-8')) + + brds = boards.Boards() + brds.read_boards(boards_cfg) + board_list = brds.get_list() + self.assertEqual(1, len(board_list)) + # cfg_name should be empty string (padded) + self.assertEqual('', board_list[0].cfg_name) + + def test_ensure_board_list_up_to_date_message(self): + """Test ensure_board_list() shows up-to-date message""" + outfile = os.path.join(self._output_dir, 'test-boards-uptodate.cfg') + brds = boards.Boards() + + # First generate the file + with terminal.capture() as (stdout, stderr): + brds.ensure_board_list(outfile, jobs=1, force=False, quiet=False) + + # Run again - should say "up to date" + with terminal.capture() as (stdout, stderr): + result = brds.ensure_board_list(outfile, jobs=1, force=False, + quiet=False) + self.assertTrue(result) + self.assertIn('up to date', stdout.getvalue()) + + def test_ensure_board_list_warnings(self): + """Test ensure_board_list() prints warnings to stderr""" + outfile = os.path.join(self._output_dir, 'test-boards-warn.cfg') + brds = boards.Boards() + + # Mock build_board_list to return warnings + with mock.patch.object(brds, 'build_board_list') as mock_build: + mock_build.return_value = ([], ['WARNING: test warning']) + with terminal.capture() as (stdout, stderr): + result = brds.ensure_board_list(outfile, jobs=1, force=True, + quiet=False) + self.assertFalse(result) + self.assertIn('WARNING: test warning', stderr.getvalue()) + + def test_parse_all_extended(self): + """Test parse_all_extended() finds and parses .buildman files""" + brds = boards.Boards() + for brd in BOARDS: + brds.add_board(board.Board(*brd)) + + # Mock glob to return a .buildman file and parse_extended + with mock.patch('glob.glob') as mock_glob, \ + mock.patch.object(brds, 'parse_extended') as mock_parse: + mock_glob.return_value = ['configs/test.buildman'] + brds.parse_all_extended(None) + mock_glob.assert_called_once_with('configs/*.buildman') + mock_parse.assert_called_once_with(None, 'configs/test.buildman') + + def test_scan_extended_no_match_warning(self): + """Test scan_extended() warns when no configs match regex""" + brds = boards.Boards() + + ext = Extended( + name='test_ext', + desc='Test extended board', + fragments=['test_frag'], + targets=[['regex', 'nonexistent*_defconfig']]) + + with mock.patch('qconfig.find_config') as mock_find, \ + mock.patch.object(tools, 'read_file', return_value=''), \ + mock.patch('glob.glob') as mock_glob, \ + terminal.capture() as (stdout, stderr): + mock_glob.return_value = [] # No matches + mock_find.return_value = set() + result = brds.scan_extended(None, ext) + self.assertEqual(set(), result) + # Warning should be printed + self.assertIn('Warning', stdout.getvalue()) + + def test_kconfig_scanner_riscv_no_rv32i(self): + """Test KconfigScanner.scan() when ARCH_RV32I symbol doesn't exist""" + src = self._git_dir + kc_file = os.path.join(src, 'Kconfig') + orig_kc_data = tools.read_file(kc_file) + + # Add RISCV board WITHOUT defining ARCH_RV32I symbol + # This will cause syms.get('ARCH_RV32I') to return None, + # and accessing .str_value on None raises AttributeError + riscv_kconfig = orig_kc_data + b''' + +config RISCV +\tbool + +config TARGET_RISCV_TEST +\tbool "RISC-V Test Board" +\tdefault n + +if TARGET_RISCV_TEST +config SYS_ARCH +\tdefault "riscv" + +config SYS_CPU +\tdefault "generic" + +config SYS_VENDOR +\tdefault "Test" + +config SYS_BOARD +\tdefault "RISCV Test" + +config SYS_CONFIG_NAME +\tdefault "riscv_test" +endif +''' + tools.write_file(kc_file, riscv_kconfig) + defconfig = os.path.join(src, 'riscv_test_defconfig') + + try: + # Create defconfig that enables the riscv board + tools.write_file(defconfig, 'CONFIG_TARGET_RISCV_TEST=y\n', False) + + scanner = boards.KconfigScanner(src) + res, warnings = scanner.scan(defconfig, False) + + # Should default to riscv64 when ARCH_RV32I lookup fails + self.assertEqual('riscv64', res['arch']) + finally: + tools.write_file(kc_file, orig_kc_data) + if os.path.exists(defconfig): + os.remove(defconfig) + + def test_scan_defconfigs_for_multiprocess(self): + """Test scan_defconfigs_for_multiprocess() function directly""" + src = self._git_dir + config_dir = os.path.join(src, 'configs') + + # Get a list of defconfigs + defconfigs = [os.path.join(config_dir, 'board0_defconfig')] + + # Create a queue and call the function + queue = multiprocessing.Queue() + boards.Boards.scan_defconfigs_for_multiprocess(src, queue, defconfigs, + False) + + # Get the result from the queue + result = queue.get(timeout=5) + params, warnings = result + self.assertEqual('board0', params['target']) + self.assertEqual('arm', params['arch']) + + def test_scan_defconfigs_hidden_files(self): + """Test scan_defconfigs() skips hidden defconfig files""" + src = self._git_dir + config_dir = os.path.join(src, 'configs') + + # Create a hidden defconfig + hidden = os.path.join(config_dir, '.hidden_defconfig') + tools.write_file(hidden, b'CONFIG_TARGET_BOARD0=y') + + try: + brds = boards.Boards() + params_list, warnings = brds.scan_defconfigs(config_dir, src, 1) + + # Hidden file should not be in results + targets = [p['target'] for p in params_list] + self.assertNotIn('.hidden', targets) + # But regular boards should be there + self.assertIn('board0', targets) + finally: + os.remove(hidden) + + def test_maintainers_n_tag_non_configs_path(self): + """Test MaintainersDatabase N: tag skips non-configs paths""" + src = self._git_dir + + # Create a MAINTAINERS file with N: tag + maintainers_file = os.path.join(src, 'MAINTAINERS_TEST') + maintainers_content = '''BOARD0 +M: Test <test@test.com> +S: Active +N: .* +''' + tools.write_file(maintainers_file, maintainers_content, binary=False) + + # Mock os.walk to return a path that doesn't start with 'configs/' + # when walking the configs directory. This tests line 443. + def mock_walk(path): + # Return paths with 'configs/' prefix (normal) and without (edge case) + yield (os.path.join(src, 'configs'), [], ['board0_defconfig']) + # This path will have 'other/' prefix after srcdir removal + yield (os.path.join(src, 'other'), [], ['fred_defconfig']) + + try: + database = boards.MaintainersDatabase() + with mock.patch('os.walk', mock_walk): + database.parse_file(src, maintainers_file) + + # board0 should be found (path starts with configs/) + # fred should be skipped (path starts with other/, not configs/) + self.assertIn('board0', database.database) + self.assertNotIn('fred', database.database) + finally: + os.remove(maintainers_file) + + +if __name__ == '__main__': + unittest.main() -- 2.43.0 base-commit: 268ef9933be28828bdf2895327ac12dbd4b0b47c branch: bmh