[Concept,02/18] buildman: Add tests for builder helper functions

Message ID 20260109183116.3262115-3-sjg@u-boot.org
State New
Headers
Series buildman: Improve test coverage for builder.py |

Commit Message

Simon Glass Jan. 9, 2026, 6:30 p.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

Add TestBuilderFuncs class with 9 new tests to improve coverage of
builder.py methods that were previously uncovered:

- Parsing nm output for function sizes
- Combining static function symbols with the same name
- Parsing .config style defconfig files
- Parsing autoconf.h header files
- Handling missing config files gracefully
- Squashing y-values in config parsing
- Parsing uboot.env environment files
- Handling missing environment files
- Handling malformed environment entries

Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 tools/buildman/main.py |   3 +-
 tools/buildman/test.py | 185 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 187 insertions(+), 1 deletion(-)
  

Patch

diff --git a/tools/buildman/main.py b/tools/buildman/main.py
index d85e8dd428d..04993dcd87b 100755
--- a/tools/buildman/main.py
+++ b/tools/buildman/main.py
@@ -49,7 +49,8 @@  def run_tests(skip_net_tests, debug, verbose, args):
     result = test_util.run_test_suites(
         'buildman', debug, verbose, False, False, args.threads, test_name, [],
         [test.TestBuildOutput, test.TestBuildBoards, test.TestBuild,
-         test.TestBuildConfig, test.TestBuildMisc, func_test.TestFunctional,
+         test.TestBuildConfig, test.TestBuildMisc, test.TestBuilderFuncs,
+         func_test.TestFunctional,
          test_boards.TestBoards, test_bsettings.TestBsettings,
          'buildman.toolchain'])
 
diff --git a/tools/buildman/test.py b/tools/buildman/test.py
index fb5dcd09555..828d05781cc 100644
--- a/tools/buildman/test.py
+++ b/tools/buildman/test.py
@@ -1269,5 +1269,190 @@  class TestBuildMisc(TestBuildBase):
                                                     'other_board'))
 
 
+class TestBuilderFuncs(TestBuildBase):
+    """Tests for individual Builder methods"""
+
+    def test_read_func_sizes(self):
+        """Test read_func_sizes() function"""
+        build = builder.Builder(self.toolchains, self.base_dir, None, 0, 2)
+
+        # Create test data simulating 'nm' output
+        # NM_SYMBOL_TYPES = 'tTdDbBr' - text, data, bss, rodata
+        nm_output = '''00000100 T func_one
+00000200 t func_two
+00000050 T func_three
+00000030 d data_var
+00000010 W weak_func
+'''
+        with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp:
+            tmp.write(nm_output)
+            tmp_name = tmp.name
+
+        try:
+            with open(tmp_name, 'r', encoding='utf-8') as fhandle:
+                sizes = build.read_func_sizes(tmp_name, fhandle)
+
+            # T, t, d are in NM_SYMBOL_TYPES, W is not
+            self.assertEqual(0x100, sizes['func_one'])
+            self.assertEqual(0x200, sizes['func_two'])
+            self.assertEqual(0x50, sizes['func_three'])
+            self.assertEqual(0x30, sizes['data_var'])
+            self.assertNotIn('weak_func', sizes)
+        finally:
+            os.unlink(tmp_name)
+
+    def test_read_func_sizes_static(self):
+        """Test read_func_sizes() with static function symbols"""
+        build = builder.Builder(self.toolchains, self.base_dir, None, 0, 2)
+
+        # Test static functions (have . in name after first char)
+        nm_output = '''00000100 t func.1234
+00000050 t func.5678
+'''
+        with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp:
+            tmp.write(nm_output)
+            tmp_name = tmp.name
+
+        try:
+            with open(tmp_name, 'r', encoding='utf-8') as fhandle:
+                sizes = build.read_func_sizes(tmp_name, fhandle)
+
+            # Static functions should be combined under 'static.func'
+            self.assertEqual(0x100 + 0x50, sizes['static.func'])
+        finally:
+            os.unlink(tmp_name)
+
+    def test_process_config_defconfig(self):
+        """Test _process_config() with .config style file"""
+        build = builder.Builder(self.toolchains, self.base_dir, None, 0, 2)
+
+        config_data = '''# This is a comment
+CONFIG_OPTION_A=y
+CONFIG_OPTION_B="string"
+CONFIG_OPTION_C=123
+# CONFIG_OPTION_D is not set
+'''
+        with tempfile.NamedTemporaryFile(mode='w', delete=False,
+                                         suffix='.config') as tmp:
+            tmp.write(config_data)
+            tmp_name = tmp.name
+
+        try:
+            config = build._process_config(tmp_name)
+
+            self.assertEqual('y', config['CONFIG_OPTION_A'])
+            self.assertEqual('"string"', config['CONFIG_OPTION_B'])
+            self.assertEqual('123', config['CONFIG_OPTION_C'])
+            # Comments should be ignored
+            self.assertNotIn('CONFIG_OPTION_D', config)
+        finally:
+            os.unlink(tmp_name)
+
+    def test_process_config_autoconf_h(self):
+        """Test _process_config() with autoconf.h style file"""
+        build = builder.Builder(self.toolchains, self.base_dir, None, 0, 2)
+
+        config_data = '''/* Auto-generated header */
+#define CONFIG_OPTION_A 1
+#define CONFIG_OPTION_B "value"
+#define CONFIG_OPTION_C
+#define NOT_CONFIG 1
+'''
+        with tempfile.NamedTemporaryFile(mode='w', delete=False,
+                                         suffix='.h') as tmp:
+            tmp.write(config_data)
+            tmp_name = tmp.name
+
+        try:
+            config = build._process_config(tmp_name)
+
+            self.assertEqual('1', config['CONFIG_OPTION_A'])
+            self.assertEqual('"value"', config['CONFIG_OPTION_B'])
+            # #define without value gets empty string (squash_config_y=False)
+            self.assertEqual('', config['CONFIG_OPTION_C'])
+            # Non-CONFIG_ defines should be ignored
+            self.assertNotIn('NOT_CONFIG', config)
+        finally:
+            os.unlink(tmp_name)
+
+    def test_process_config_nonexistent(self):
+        """Test _process_config() with non-existent file"""
+        build = builder.Builder(self.toolchains, self.base_dir, None, 0, 2)
+
+        config = build._process_config('/nonexistent/path/config')
+        self.assertEqual({}, config)
+
+    def test_process_config_squash_y(self):
+        """Test _process_config() with squash_config_y enabled"""
+        build = builder.Builder(self.toolchains, self.base_dir, None, 0, 2)
+        build.squash_config_y = True
+
+        config_data = '''CONFIG_OPTION_A=y
+CONFIG_OPTION_B=n
+#define CONFIG_OPTION_C
+'''
+        with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp:
+            tmp.write(config_data)
+            tmp_name = tmp.name
+
+        try:
+            config = build._process_config(tmp_name)
+
+            # y should be squashed to 1
+            self.assertEqual('1', config['CONFIG_OPTION_A'])
+            # n should remain n
+            self.assertEqual('n', config['CONFIG_OPTION_B'])
+            # Empty #define should get '1' when squash_config_y is True
+            self.assertEqual('1', config['CONFIG_OPTION_C'])
+        finally:
+            os.unlink(tmp_name)
+
+    def test_process_environment(self):
+        """Test _process_environment() function"""
+        build = builder.Builder(self.toolchains, self.base_dir, None, 0, 2)
+
+        # Environment file uses null-terminated strings
+        env_data = 'bootcmd=run bootm\x00bootdelay=3\x00console=ttyS0\x00'
+        with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp:
+            tmp.write(env_data)
+            tmp_name = tmp.name
+
+        try:
+            env = build._process_environment(tmp_name)
+
+            self.assertEqual('run bootm', env['bootcmd'])
+            self.assertEqual('3', env['bootdelay'])
+            self.assertEqual('ttyS0', env['console'])
+        finally:
+            os.unlink(tmp_name)
+
+    def test_process_environment_nonexistent(self):
+        """Test _process_environment() with non-existent file"""
+        build = builder.Builder(self.toolchains, self.base_dir, None, 0, 2)
+
+        env = build._process_environment('/nonexistent/path/uboot.env')
+        self.assertEqual({}, env)
+
+    def test_process_environment_invalid_lines(self):
+        """Test _process_environment() handles invalid lines gracefully"""
+        build = builder.Builder(self.toolchains, self.base_dir, None, 0, 2)
+
+        # Include lines without '=' which should be ignored
+        env_data = 'valid=value\x00invalid_no_equals\x00another=good\x00'
+        with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp:
+            tmp.write(env_data)
+            tmp_name = tmp.name
+
+        try:
+            env = build._process_environment(tmp_name)
+
+            self.assertEqual('value', env['valid'])
+            self.assertEqual('good', env['another'])
+            # Invalid line should be silently ignored
+            self.assertNotIn('invalid_no_equals', env)
+        finally:
+            os.unlink(tmp_name)
+
+
 if __name__ == "__main__":
     unittest.main()