Files
u-boot/tools/buildman/test_boards.py
Simon Glass f40c1031e4 buildman: Increase test coverage for boards.py
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>
2025-12-22 01:07:09 +00:00

740 lines
27 KiB
Python

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