[Concept] buildman: Increase test coverage for boards.py

Message ID 20251222122134.724607-1-sjg@u-boot.org
State New
Headers
Series [Concept] buildman: Increase test coverage for boards.py |

Commit Message

Simon Glass Dec. 22, 2025, 12:21 p.m. UTC
  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
  

Patch

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()