From: Simon Glass <simon.glass@canonical.com> Add some rudimentary tests of the codman functionality. Signed-off-by: Simon Glass <simon.glass@canonical.com> --- 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 <common.h> + +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 <common.h> + +void unused_function(void) +{ + /* This file is never compiled */ +} +''') + + # drivers/video/display.c - will be compiled + self._write_file('drivers/video/display.c', '''#include <video.h> + +#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 <serial.h> + +void serial_init(void) +{ + /* Init serial port */ +} +''') + + # lib/string.c - will be compiled + self._write_file('lib/string.c', '''#include <linux/string.h> + +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 <common.h> + +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) -- 2.43.0