buildman: Add a partial test for ensure_board_list()

Create a new function which has the non-UI parts of ensure_board_list().
Add some tests for everything except the N: tag.

While we are here, fix the confusing usage of fname inside a loops that
also uses fname.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/tools/buildman/board.py b/tools/buildman/board.py
index 8ef905b..248d8bf 100644
--- a/tools/buildman/board.py
+++ b/tools/buildman/board.py
@@ -17,14 +17,14 @@
             vendor: Name of vendor (e.g. armltd)
             board_name: Name of board (e.g. integrator)
             target: Target name (use make <target>_defconfig to configure)
-            cfg_name: Config name
+            cfg_name: Config-file name (in includes/configs/)
         """
         self.target = target
         self.arch = arch
         self.cpu = cpu
-        self.board_name = board_name
-        self.vendor = vendor
         self.soc = soc
+        self.vendor = vendor
+        self.board_name = board_name
         self.cfg_name = cfg_name
         self.props = [self.target, self.arch, self.cpu, self.board_name,
                       self.vendor, self.soc, self.cfg_name]
diff --git a/tools/buildman/boards.py b/tools/buildman/boards.py
index daf1f03..541c82f 100644
--- a/tools/buildman/boards.py
+++ b/tools/buildman/boards.py
@@ -328,13 +328,21 @@
 
         return ':'.join(self.database[target][1])
 
-    def parse_file(self, fname):
+    def parse_file(self, srcdir, fname):
         """Parse a MAINTAINERS file.
 
         Parse a MAINTAINERS file and accumulate board status and maintainers
         information in the self.database dict.
 
+        defconfig files are used to specify the target, e.g. xxx_defconfig is
+        used for target 'xxx'. If there is no defconfig file mentioned in the
+        MAINTAINERS file F: entries, then this function does nothing.
+
+        The N: name entries can be used to specify a defconfig file using
+        wildcards.
+
         Args:
+            srcdir (str): Directory containing source code (Kconfig files)
             fname (str): MAINTAINERS file to be parsed
         """
         targets = []
@@ -350,9 +358,12 @@
                     maintainers.append(rest)
                 elif tag == 'F:':
                     # expand wildcard and filter by 'configs/*_defconfig'
-                    for item in glob.glob(rest):
+                    glob_path = os.path.join(srcdir, rest)
+                    for item in glob.glob(glob_path):
                         front, match, rear = item.partition('configs/')
-                        if not front and match:
+                        if front.endswith('/'):
+                            front = front[:-1]
+                        if front == srcdir and match:
                             front, match, rear = rear.rpartition('_defconfig')
                             if match and not rear:
                                 targets.append(front)
@@ -361,9 +372,10 @@
                 elif tag == 'N:':
                     # Just scan the configs directory since that's all we care
                     # about
-                    for dirpath, _, fnames in os.walk('configs'):
-                        for fname in fnames:
-                            path = os.path.join(dirpath, fname)
+                    walk_path = os.walk(os.path.join(srcdir, 'configs'))
+                    for dirpath, _, fnames in walk_path:
+                        for cfg in fnames:
+                            path = os.path.join(dirpath, cfg)
                             front, match, rear = path.partition('configs/')
                             if not front and match:
                                 front, match, rear = rear.rpartition('_defconfig')
@@ -693,7 +705,7 @@
         return params_list
 
     @classmethod
-    def insert_maintainers_info(cls, params_list):
+    def insert_maintainers_info(cls, srcdir, params_list):
         """Add Status and Maintainers information to the board parameters list.
 
         Args:
@@ -703,9 +715,10 @@
             list of str: List of warnings collected due to missing status, etc.
         """
         database = MaintainersDatabase()
-        for (dirpath, _, filenames) in os.walk('.'):
+        for (dirpath, _, filenames) in os.walk(srcdir):
             if 'MAINTAINERS' in filenames:
-                database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
+                database.parse_file(srcdir,
+                                    os.path.join(dirpath, 'MAINTAINERS'))
 
         for i, params in enumerate(params_list):
             target = params['target']
@@ -748,6 +761,30 @@
         with open(output, 'w', encoding="utf-8") as outf:
             outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
 
+    def build_board_list(self, config_dir, srcdir, jobs=1):
+        """Generate a board-database file
+
+        This works by reading the Kconfig, then loading each board's defconfig
+        in to get the setting for each option. In particular, CONFIG_TARGET_xxx
+        is typically set by the defconfig, where xxx is the target to build.
+
+        Args:
+            config_dir (str): Directory containing the defconfig files
+            srcdir (str): Directory containing source code (Kconfig files)
+            jobs (int): The number of jobs to run simultaneously
+
+        Returns:
+            tuple:
+                list of dict: List of board parameters, each a dict:
+                    key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'config',
+                         'target'
+                    value: string value of the key
+                list of str: Warnings that came up
+        """
+        params_list = self.scan_defconfigs(config_dir, srcdir, jobs)
+        warnings = self.insert_maintainers_info(srcdir, params_list)
+        return params_list, warnings
+
     def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
         """Generate a board database file if needed.
 
@@ -767,8 +804,7 @@
             if not quiet:
                 print(f'{output} is up to date. Nothing to do.')
             return True
-        params_list = self.scan_defconfigs(CONFIG_DIR, os.getcwd(), jobs)
-        warnings = self.insert_maintainers_info(params_list)
+        params_list, warnings = self.build_board_list(CONFIG_DIR, '.', jobs)
         for warn in warnings:
             print(warn, file=sys.stderr)
         self.format_and_output(params_list, output)
diff --git a/tools/buildman/func_test.py b/tools/buildman/func_test.py
index 70e2af1..43087b4 100644
--- a/tools/buildman/func_test.py
+++ b/tools/buildman/func_test.py
@@ -877,3 +877,69 @@
         Path(os.path.join(config_dir, 'board0_defconfig')).unlink()
         self.assertFalse(boards.output_is_new(boards_cfg, config_dir, src))
 
+    def test_maintainers(self):
+        """Test detecting boards without a MAINTAINERS entry"""
+        src = self._git_dir
+        main = os.path.join(src, 'boards', 'board0', 'MAINTAINERS')
+        other = os.path.join(src, 'boards', 'board2', 'MAINTAINERS')
+        config_dir = os.path.join(src, 'configs')
+        params_list, warnings = self._boards.build_board_list(config_dir, src)
+
+        # There should be two boards no warnings
+        self.assertEquals(2, len(params_list))
+        self.assertFalse(warnings)
+
+        # Set an invalid status line in the file
+        orig_data = tools.read_file(main, binary=False)
+        lines = ['S:      Other\n' if line.startswith('S:') else line
+                  for line in orig_data.splitlines(keepends=True)]
+        tools.write_file(main, ''.join(lines), binary=False)
+        params_list, warnings = self._boards.build_board_list(config_dir, src)
+        self.assertEquals(2, len(params_list))
+        params = params_list[0]
+        if params['target'] == 'board2':
+            params = params_list[1]
+        self.assertEquals('-', params['status'])
+        self.assertEquals(["WARNING: Other: unknown status for 'board0'"],
+                          warnings)
+
+        # Remove the status line (S:) from a file
+        lines = [line for line in orig_data.splitlines(keepends=True)
+                 if not line.startswith('S:')]
+        tools.write_file(main, ''.join(lines), binary=False)
+        params_list, warnings = self._boards.build_board_list(config_dir, src)
+        self.assertEquals(2, len(params_list))
+        self.assertEquals(["WARNING: -: unknown status for 'board0'"], warnings)
+
+        # Remove the configs/ line (F:) from a file - this is the last line
+        data = ''.join(orig_data.splitlines(keepends=True)[:-1])
+        tools.write_file(main, data, binary=False)
+        params_list, warnings = self._boards.build_board_list(config_dir, src)
+        self.assertEquals(2, len(params_list))
+        self.assertEquals(
+            ["WARNING: no status info for 'board0'",
+             "WARNING: no maintainers for 'board0'"], warnings)
+
+        # Remove the maintainer line (M:) from a file (this should be fine)
+        lines = [line for line in orig_data.splitlines(keepends=True)
+                 if not line.startswith('M:')]
+        tools.write_file(main, ''.join(lines), binary=False)
+        params_list, warnings = self._boards.build_board_list(config_dir, src)
+        self.assertEquals(2, len(params_list))
+        self.assertFalse(warnings)
+
+        # Move the contents of the second file into this one, removing the
+        # second file, to check multiple records in a single file.
+        data = orig_data + tools.read_file(other, binary=False)
+        tools.write_file(main, data, binary=False)
+        os.remove(other)
+        params_list, warnings = self._boards.build_board_list(config_dir, src)
+        self.assertEquals(2, len(params_list))
+        self.assertFalse(warnings)
+
+        # Add another record, this should be ignored
+        extra = '\n\nAnother\nM: Fred\nF: configs/board9_defconfig\nS: other\n'
+        tools.write_file(main, data + extra, binary=False)
+        params_list, warnings = self._boards.build_board_list(config_dir, src)
+        self.assertEquals(2, len(params_list))
+        self.assertFalse(warnings)
diff --git a/tools/buildman/test/boards/board0/MAINTAINERS b/tools/buildman/test/boards/board0/MAINTAINERS
new file mode 100644
index 0000000..08207ff
--- /dev/null
+++ b/tools/buildman/test/boards/board0/MAINTAINERS
@@ -0,0 +1,5 @@
+ARM Board 0
+M:      Mary Mary <quite@contrary.org>
+S:      Maintained
+F:      boards/board0
+F:      configs/board0_defconfig
diff --git a/tools/buildman/test/boards/board2/MAINTAINERS b/tools/buildman/test/boards/board2/MAINTAINERS
new file mode 100644
index 0000000..c154782
--- /dev/null
+++ b/tools/buildman/test/boards/board2/MAINTAINERS
@@ -0,0 +1,5 @@
+ARM Board 2
+M:      Old Mother <hubbard@cupboard.org>
+S:      Maintained
+F:      boards/board2
+F:      configs/board2_defconfig