From patchwork Mon Nov 24 13:49:19 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 765 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1763992231; bh=BONWzZ6Z8jmh1OC1CpAt8XWn2tFix1K2CGeI9Sojy+o=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=UeAzDjpDAktHlO/wQqPzG+NggM69/uG5LamxJk/TfdsddrHc003JuGs6bH+7UE9LK G8forPO5K/pfDZbC8q6uAuzqfGReutQEZNO2FiHZUi4hPAe4LVteRnaFyIsCWCcgcE GoY44I3cHf8QQotr8PsBvqGp/HQL7cjAYmecyZx5xKdlSj8LAJ+xEQgn3WuKiF8IXb 3pJQNNjjHWBtlRe+oEELiPqpFD2S8wBzwslmVKFDwamAmLszomyMr4jtQ7Yh7UnVVp 91ckgYrRrinsliuaJaIN7zcCN5FtNcs+TJPey0xrstbYefN24ylxIb7GCtDJ/wg/ot Pz/Mj0q9/SIhw== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id F07FF68768 for ; Mon, 24 Nov 2025 06:50:31 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id 8LasDlprMHzQ for ; Mon, 24 Nov 2025 06:50:31 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1763992231; bh=BONWzZ6Z8jmh1OC1CpAt8XWn2tFix1K2CGeI9Sojy+o=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=UeAzDjpDAktHlO/wQqPzG+NggM69/uG5LamxJk/TfdsddrHc003JuGs6bH+7UE9LK G8forPO5K/pfDZbC8q6uAuzqfGReutQEZNO2FiHZUi4hPAe4LVteRnaFyIsCWCcgcE GoY44I3cHf8QQotr8PsBvqGp/HQL7cjAYmecyZx5xKdlSj8LAJ+xEQgn3WuKiF8IXb 3pJQNNjjHWBtlRe+oEELiPqpFD2S8wBzwslmVKFDwamAmLszomyMr4jtQ7Yh7UnVVp 91ckgYrRrinsliuaJaIN7zcCN5FtNcs+TJPey0xrstbYefN24ylxIb7GCtDJ/wg/ot Pz/Mj0q9/SIhw== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id DC9DC6874E for ; Mon, 24 Nov 2025 06:50:31 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1763992229; bh=nSwZnGLNm4MxSMvzXZULhjHQFUofErl2N8gwRT+etSk=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=ZSjCJ0qb1oMKoEGgkcPRcotzGW44CxAJe6go2mI2ABcZ3dgzqYyuqWtSprtEmZMmV saSMVrX+ViULB8hx9OLdGE6p45Pg81bgrpJ6/+2uJjQRGzN1Stob4vEG35eGSeF1k3 4OZSKoMeEvMl9Z1ZYhNwo8yMR5G+XChValwLsCWuSJAVI2ayuK2bpS1VZhDN83dvxO Oaq4UZL2NCERC6AVup8IkJ7j2rFih5yJeVkqIO0d40enRTHhPkSuNkq5Hgl8GLIRzF fkT/03jkGSiLn5LwxbBGYgrRGhMv+kS73pAcHV4wq3qKthI1uwU6Hj54le1Ffa95V0 emBsUPQjE9E9w== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id DD7626874E; Mon, 24 Nov 2025 06:50:29 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id 63r-Fu8mVp3H; Mon, 24 Nov 2025 06:50:29 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1763992225; bh=aB50dJZ0RzXWdrD2KVm1SBvY0Xb9MM7qOWLXN4hDYNQ=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=UYrM5TsN8jsTx/RGc7Hy+z7SQNL6Ft9+bEyamgeHSOf7u1zmbUN4qfOYCmLCsN3OI +c2m3ov3a2RKG5W8Y2pvs6CLUrmqqYVl0KWDKttLTwsSSwU6arWrwZ9kHzedbKI3Gy A1v/N7ZkSmgPMHJNbTL9lVALaXTcOj9f1Cak1Ucw3uhnpLgsHjkxRmJ30zy9P1Lw3l NykMC/QPahJ8ahzwbYlk3FEUSzdgGZ5VU3z2oEwP09VHIeHKecHgeCTGAu8y4SmGRx Hds4oI8Lvg4iIFupG/1eBqxa/tIRvqXlraOj5Poa86HOUj3pLD8EbJt+NvecLFDDCN zvprDfOMFeHtw== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id B296C6873C; Mon, 24 Nov 2025 06:50:25 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Mon, 24 Nov 2025 06:49:19 -0700 Message-ID: <20251124134932.1991031-9-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20251124134932.1991031-1-sjg@u-boot.org> References: <20251124134932.1991031-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: 5AZIKBJQ22AU4JSORRQURL4UUBHCPEFX X-Message-ID-Hash: 5AZIKBJQ22AU4JSORRQURL4UUBHCPEFX X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Simon Glass X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 8/9] codman: Add some basic tests List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass Add some rudimentary tests of the codman functionality. Signed-off-by: Simon Glass --- tools/codman/test_codman.py | 470 ++++++++++++++++++++++++++++++++++++ 1 file changed, 470 insertions(+) create mode 100755 tools/codman/test_codman.py diff --git a/tools/codman/test_codman.py b/tools/codman/test_codman.py new file mode 100755 index 00000000000..ed387c82472 --- /dev/null +++ b/tools/codman/test_codman.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0+ +# +# Copyright 2025 Canonical Ltd +# +"""Very basic tests for codman.py script""" + +import os +import shutil +import subprocess +import sys +import tempfile +import unittest + +# Test configuration +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) + +# Import the module to test +sys.path.insert(0, SCRIPT_DIR) +sys.path.insert(0, os.path.join(SCRIPT_DIR, '..')) +# pylint: disable=wrong-import-position +from u_boot_pylib import terminal, tools +import output # pylint: disable=wrong-import-position +import codman # pylint: disable=wrong-import-position + + +class TestSourceUsage(unittest.TestCase): + """Test cases for codman.py""" + + def setUp(self): + """Set up test environment with fake source tree and build""" + self.test_dir = tempfile.mkdtemp(prefix='test_source_usage_') + self.src_dir = os.path.join(self.test_dir, 'src') + self.build_dir = os.path.join(self.test_dir, 'build') + os.makedirs(self.src_dir) + os.makedirs(self.build_dir) + + # Create fake source files + self._create_fake_sources() + + # Create fake Makefile + self._create_makefile() + + # Create fake .config + self._create_config() + + def tearDown(self): + """Clean up test environment""" + if os.path.exists(self.test_dir): + shutil.rmtree(self.test_dir) + + def _create_fake_sources(self): + """Create a fake source tree with various files""" + # Create directory structure + dirs = [ + 'common', + 'drivers/video', + 'drivers/serial', + 'lib', + 'arch/sandbox', + ] + for dir_path in dirs: + os.makedirs(os.path.join(self.src_dir, dir_path), exist_ok=True) + + # Create source files + # common/main.c - will be compiled + self._write_file('common/main.c', '''#include + +void board_init(void) +{ +#ifdef CONFIG_FEATURE_A + feature_a_init(); +#endif +#ifdef CONFIG_FEATURE_B + feature_b_init(); +#endif + common_init(); +} +''') + + # common/unused.c - will NOT be compiled + self._write_file('common/unused.c', '''#include + +void unused_function(void) +{ + /* This file is never compiled */ +} +''') + + # drivers/video/display.c - will be compiled + self._write_file('drivers/video/display.c', '''#include + +#ifdef CONFIG_VIDEO_LOGO +static void show_logo(void) +{ + /* Show boot logo */ +} +#endif + +void display_init(void) +{ +#ifdef CONFIG_VIDEO_LOGO + show_logo(); +#endif + /* Init display */ +} +''') + + # drivers/serial/serial.c - will be compiled + self._write_file('drivers/serial/serial.c', '''#include + +void serial_init(void) +{ + /* Init serial port */ +} +''') + + # lib/string.c - will be compiled + self._write_file('lib/string.c', '''#include + +int strlen(const char *s) +{ + int len = 0; + while (*s++) + len++; + return len; +} +''') + + # arch/sandbox/cpu.c - will be compiled + self._write_file('arch/sandbox/cpu.c', '''#include + +void cpu_init(void) +{ + /* Sandbox CPU init */ +} +''') + + # Create header files + self._write_file('include/common.h', '''#ifndef __COMMON_H +#define __COMMON_H +void board_init(void); +#endif +''') + + self._write_file('include/video.h', '''#ifndef __VIDEO_H +#define __VIDEO_H +void display_init(void); +#endif +''') + + self._write_file('include/serial.h', '''#ifndef __SERIAL_H +#define __SERIAL_H +void serial_init(void); +#endif +''') + + self._write_file('include/linux/string.h', '''#ifndef __LINUX_STRING_H +#define __LINUX_STRING_H +int strlen(const char *s); +#endif +''') + + def _create_makefile(self): + """Create a simple Makefile that generates .cmd files""" + makefile = f'''# Simple test Makefile +SRCDIR := {self.src_dir} +O ?= . +BUILD_DIR = $(O) + +# Compiler flags +CFLAGS := -Iinclude +ifeq ($(DEBUG),1) +CFLAGS += -g +endif + +# Source files to compile +OBJS = $(BUILD_DIR)/common/main.o \\ + $(BUILD_DIR)/drivers/video/display.o \\ + $(BUILD_DIR)/drivers/serial/serial.o \\ + $(BUILD_DIR)/lib/string.o \\ + $(BUILD_DIR)/arch/sandbox/cpu.o + +all: $(OBJS) +\t@echo "Build complete" + +# Rule to compile .c files +$(BUILD_DIR)/%.o: %.c +\t@mkdir -p $(dir $@) +\t@echo " CC $<" +\t@gcc $(CFLAGS) -c -o $@ $(SRCDIR)/$< +\t@echo "cmd_$@ := gcc $(CFLAGS) -c -o $@ $<" > $(dir $@).$(notdir $@).cmd +\t@echo "source_$@ := $(SRCDIR)/$<" >> $(dir $@).$(notdir $@).cmd +\t@echo "deps_$@ := \\\\" >> $(dir $@).$(notdir $@).cmd +\t@echo " $(SRCDIR)/$< \\\\" >> $(dir $@).$(notdir $@).cmd +\t@echo "" >> $(dir $@).$(notdir $@).cmd + +clean: +\t@rm -rf $(BUILD_DIR) + +.PHONY: all clean +''' + self._write_file('Makefile', makefile) + + def _create_config(self): + """Create a fake .config file""" + config = '''CONFIG_FEATURE_A=y +# CONFIG_FEATURE_B is not set +CONFIG_VIDEO_LOGO=y +''' + self._write_file(os.path.join(self.build_dir, '.config'), config) + + def _write_file(self, rel_path, content): + """Write a file relative to src_dir""" + if rel_path.startswith('/'): + # Absolute path for build dir files + file_path = rel_path + else: + file_path = os.path.join(self.src_dir, rel_path) + os.makedirs(os.path.dirname(file_path), exist_ok=True) + tools.write_file(file_path, content.encode('utf-8')) + + def _build(self, debug=False): + """Run the test build. + + Args: + debug (bool): If True, build with debug symbols (DEBUG=1) + """ + cmd = ['make', '-C', self.src_dir, f'O={self.build_dir}'] + if debug: + cmd.append('DEBUG=1') + result = subprocess.run(cmd, capture_output=True, text=True, + check=False) + if result.returncode != 0: + print(f'Build failed: {result.stderr}') + print(f'Build stdout: {result.stdout}') + self.fail('Test build failed') + + def test_basic_file_stats(self): + """Test basic file-level statistics""" + self._build() + + # Call select_sources() directly + _all_srcs, used, skipped = codman.select_sources( + self.src_dir, self.build_dir, None) + + # Verify counts - we have 5 compiled .c files + self.assertEqual(len(used), 5, + f'Expected 5 used files, got {len(used)}') + + # Should have 1 unused .c file (common/unused.c) + unused_c_files = [f for f in skipped if f.endswith('.c')] + self.assertEqual(len(unused_c_files), 1, + f'Expected 1 unused .c file, got {len(unused_c_files)}') + + # Check that specific files are in used set + used_basenames = {os.path.basename(f) for f in used} + self.assertIn('main.c', used_basenames) + self.assertIn('display.c', used_basenames) + self.assertIn('serial.c', used_basenames) + self.assertIn('string.c', used_basenames) + self.assertIn('cpu.c', used_basenames) + + # Check that unused.c is not in used set + self.assertNotIn('unused.c', used_basenames) + + def test_list_unused(self): + """Test listing unused files""" + self._build() + + _all_srcs, _used, skipped = codman.select_sources( + self.src_dir, self.build_dir, None) + + # Check that unused.c is in skipped set + skipped_basenames = {os.path.basename(f) for f in skipped} + self.assertIn('unused.c', skipped_basenames) + + # Check that used files are not in skipped set + self.assertNotIn('main.c', skipped_basenames) + self.assertNotIn('display.c', skipped_basenames) + + def test_by_dir(self): + """Test directory breakdown by collecting stats""" + self._build() + + all_srcs, used, _skipped = codman.select_sources( + self.src_dir, self.build_dir, None) + + # Collect directory stats + dir_stats = output.collect_dir_stats( + all_srcs, used, None, self.src_dir, False, False) + + # Should have stats for top-level directories + self.assertIn('common', dir_stats) + self.assertIn('drivers', dir_stats) + self.assertIn('lib', dir_stats) + self.assertIn('arch', dir_stats) + + # Check common directory has 2 files (main.c and unused.c) + self.assertEqual(dir_stats['common'].total, 2) + # Only 1 is used (main.c) + self.assertEqual(dir_stats['common'].used, 1) + + def test_subdirs(self): + """Test subdirectory breakdown""" + self._build() + + all_srcs, used, _skipped = codman.select_sources( + self.src_dir, self.build_dir, None) + + # Collect subdirectory stats (by_subdirs=True) + dir_stats = output.collect_dir_stats( + all_srcs, used, None, self.src_dir, True, False) + + # Should have stats for subdirectories + self.assertIn('drivers/video', dir_stats) + self.assertIn('drivers/serial', dir_stats) + self.assertIn('arch/sandbox', dir_stats) + + def test_filter(self): + """Test filtering by pattern""" + self._build() + + # Apply video filter + all_srcs, _used, _skipped = codman.select_sources( + self.src_dir, self.build_dir, '*video*') + + # Should only have video-related files + all_basenames = {os.path.basename(f) for f in all_srcs} + self.assertIn('display.c', all_basenames) + self.assertIn('video.h', all_basenames) + + # Should not have non-video files + self.assertNotIn('main.c', all_basenames) + self.assertNotIn('serial.c', all_basenames) + + def test_no_build_required(self): + """Test that analysis works with existing build""" + self._build() + + # Should work without building + all_srcs, used, _skipped = codman.select_sources( + self.src_dir, self.build_dir, None) + + # Verify we got results + self.assertGreater(len(all_srcs), 0) + self.assertGreater(len(used), 0) + + def test_do_analysis_unifdef(self): + """Test do_analysis() with unifdef""" + self._build() + + _all_srcs, used, _skipped = codman.select_sources( + self.src_dir, self.build_dir, None) + + # Run unifdef analysis + unifdef_path = shutil.which('unifdef') or '/usr/bin/unifdef' + results = codman.do_analysis(used, self.build_dir, self.src_dir, + unifdef_path, include_headers=False, + jobs=1, use_lsp=False) + + # Should get results + self.assertIsNotNone(results) + self.assertGreater(len(results), 0) + + # Check that results have the expected structure + for _file_path, result in results.items(): + self.assertGreater(result.total_lines, 0) + self.assertGreaterEqual(result.active_lines, 0) + self.assertGreaterEqual(result.inactive_lines, 0) + self.assertEqual(result.total_lines, + result.active_lines + result.inactive_lines) + + def test_do_analysis_dwarf(self): + """Test do_analysis() with DWARF""" + # Build with debug symbols + self._build(debug=True) + + _all_srcs, used, _skipped = codman.select_sources( + self.src_dir, self.build_dir, None) + + # Run DWARF analysis (unifdef_path=None) + results = codman.do_analysis(used, self.build_dir, self.src_dir, + unifdef_path=None, include_headers=False, + jobs=1, use_lsp=False) + + # Should get results + self.assertIsNotNone(results) + self.assertGreater(len(results), 0) + + # Check that results have the expected structure + for _file_path, result in results.items(): + self.assertGreater(result.total_lines, 0) + self.assertGreaterEqual(result.active_lines, 0) + self.assertGreaterEqual(result.inactive_lines, 0) + self.assertEqual(result.total_lines, + result.active_lines + result.inactive_lines) + + def test_do_analysis_unifdef_missing_config(self): + """Test do_analysis() with unifdef when config file is missing""" + self._build() + + _all_srcs, used, _skipped = codman.select_sources( + self.src_dir, self.build_dir, None) + + # Remove .config file + config_file = os.path.join(self.build_dir, '.config') + if os.path.exists(config_file): + os.remove(config_file) + + # Capture terminal output + with terminal.capture() as (_stdout, stderr): + # Run unifdef analysis - should return None + unifdef_path = shutil.which('unifdef') or '/usr/bin/unifdef' + results = codman.do_analysis(used, self.build_dir, self.src_dir, + unifdef_path, + include_headers=False, jobs=1, + use_lsp=False) + + # Should return None when config is missing + self.assertIsNone(results) + + # Check that error message was printed to stderr + error_text = stderr.getvalue() + self.assertIn('Config file not found', error_text) + self.assertIn('.config', error_text) + + def test_do_analysis_lsp(self): + """Test do_analysis() with LSP (clangd)""" + # Disabled for now + self.skipTest('LSP test disabled') + # Check if clangd is available + if not shutil.which('clangd'): + self.skipTest('clangd not found - skipping LSP test') + + # Build with compile commands + self._build() + + _all_srcs, used, _skipped = codman.select_sources( + self.src_dir, self.build_dir, None) + + # Run LSP analysis (unifdef_path=None, use_lsp=True) + results = codman.do_analysis(used, self.build_dir, self.src_dir, + unifdef_path=None, include_headers=False, + jobs=1, use_lsp=True) + + # Should get results + self.assertIsNotNone(results) + self.assertGreater(len(results), 0) + + # Check that results have the expected structure + for _file_path, result in results.items(): + self.assertGreater(result.total_lines, 0) + self.assertGreaterEqual(result.active_lines, 0) + self.assertGreaterEqual(result.inactive_lines, 0) + self.assertEqual(result.total_lines, + result.active_lines + result.inactive_lines) + + # Check specific file results + main_file = os.path.join(self.src_dir, 'common/main.c') + if main_file in results: + result = results[main_file] + # main.c has some conditional code, so should have some lines + self.assertGreater(result.total_lines, 0) + # Should have identified some active lines + self.assertGreater(result.active_lines, 0) + + +if __name__ == '__main__': + unittest.main(argv=['test_codman.py'], verbosity=2)