Files
u-boot/tools/codman/test_codman.py
Simon Glass b7b6a731f5 codman: Add --html option to generate colored HTML reports
Add HTML output support with collapsible hierarchical directory
structure, color-coded metrics, and build info in the header.

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
2025-12-08 17:37:35 -07:00

477 lines
15 KiB
Python
Executable File

#!/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, method = 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)
self.assertEqual(method, 'unifdef')
# 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, method = 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)
self.assertEqual(method, 'dwarf')
# 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 results
unifdef_path = shutil.which('unifdef') or '/usr/bin/unifdef'
results, method = codman.do_analysis(used, self.build_dir,
self.src_dir, unifdef_path,
include_headers=False, jobs=1,
use_lsp=False)
# Should return None results when config is missing
self.assertIsNone(results)
self.assertEqual(method, 'unifdef')
# 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, method = 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)
self.assertEqual(method, 'lsp')
# 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)