import json import subprocess import threading import time import unittest from pathlib import Path import pytest # OPTIMIZED: Disabled by default to avoid expensive imports during test discovery _ENABLED = True from ci.util.paths import PROJECT_ROOT from ci.util.symbol_analysis import ( SymbolInfo, analyze_map_file, analyze_symbols, build_reverse_call_graph, find_board_build_info, generate_report, ) from ci.util.tools import Tools, load_tools HERE = Path(__file__).resolve().parent.absolute() UNO = HERE / "uno" OUTPUT = HERE / "output" ELF_FILE = UNO / "firmware.elf" BUILD_INFO_PATH = ( PROJECT_ROOT / ".build" / "fled" / "examples" / "uno" / "build_info.json" ) PLATFORMIO_PATH = Path.home() / ".platformio" PLATFORMIO_PACKAGES_PATH = PLATFORMIO_PATH / "packages" TOOLCHAIN_AVR = PLATFORMIO_PACKAGES_PATH / "toolchain-atmelavr" # Global lock to prevent multiple threads from running compilation simultaneously _compilation_lock = threading.Lock() _compilation_done = False def init() -> None: global _compilation_done # Use lock to ensure only one thread runs compilation with _compilation_lock: if _compilation_done: print("Compilation already completed by another thread, skipping.") return uno_build = PROJECT_ROOT / ".build" / "uno" print( f"Thread {threading.current_thread().ident}: Checking for Uno build in: {uno_build}" ) print(f"BUILD_INFO_PATH: {BUILD_INFO_PATH}") print(f"TOOLCHAIN_AVR: {TOOLCHAIN_AVR}") print(f"BUILD_INFO_PATH exists: {BUILD_INFO_PATH.exists()}") print(f"TOOLCHAIN_AVR exists: {TOOLCHAIN_AVR.exists()}") if not BUILD_INFO_PATH.exists() or not TOOLCHAIN_AVR.exists(): print("Uno build not found. Running compilation...") print(f"Working directory: {PROJECT_ROOT}") try: print( "Starting compilation command: uv run python-m ci.ci-compile uno --examples Blink" ) start_time = time.time() result = subprocess.run( "uv run python -m ci.ci-compile uno --examples Blink", shell=True, check=True, cwd=str(PROJECT_ROOT), capture_output=True, text=True, ) end_time = time.time() print( f"Compilation completed successfully in {end_time - start_time:.2f} seconds." ) print(f"STDOUT: {result.stdout}") if result.stderr: print(f"STDERR: {result.stderr}") _compilation_done = True except subprocess.CalledProcessError as e: print(f"Error during compilation (returncode: {e.returncode}): {e}") if e.stdout: print(f"STDOUT: {e.stdout}") if e.stderr: print(f"STDERR: {e.stderr}") raise else: print("Uno build found, skipping compilation.") _compilation_done = True @pytest.mark.full class TestSymbolAnalysis(unittest.TestCase): @classmethod @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") def setUpClass(cls): """Set up test fixtures before running tests.""" if not _ENABLED: return init() # Import Tools dynamically to avoid import errors when disabled from ci.util.tools import Tools, load_tools cls.tools: Tools = load_tools(BUILD_INFO_PATH) # Load build info for testing with open(BUILD_INFO_PATH) as f: cls.build_info = json.load(f) # Get the board key (should be 'uno' for UNO board) cls.board_key = "uno" if cls.board_key not in cls.build_info: # Fallback to first available key cls.board_key = list(cls.build_info.keys())[0] cls.board_info = cls.build_info[cls.board_key] @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") def test_analyze_symbols_basic(self) -> None: """Test basic symbol analysis functionality.""" print("Testing basic symbol analysis...") # Test with the test ELF file symbols = analyze_symbols( str(ELF_FILE), str(self.tools.nm_path), str(self.tools.cpp_filt_path) ) # Verify we got some symbols self.assertGreater(len(symbols), 0, "Should find some symbols in ELF file") # Verify symbol structure symbols_with_size = 0 for symbol in symbols: self.assertIsInstance(symbol.address, str) self.assertIsInstance(symbol.size, int) self.assertIsInstance(symbol.type, str) self.assertIsInstance(symbol.name, str) self.assertIsInstance(symbol.demangled_name, str) self.assertGreaterEqual( symbol.size, 0, "Symbol size should be non-negative" ) if symbol.size > 0: symbols_with_size += 1 # Ensure we have at least some symbols with actual size (not all zero-size) self.assertGreater( symbols_with_size, 0, "Should have at least some symbols with positive size" ) print(f"Found {len(symbols)} symbols") # Check that we have some common symbols we'd expect in a compiled program symbol_names = [s.demangled_name for s in symbols] # Should have main function main_symbols = [name for name in symbol_names if "main" in name.lower()] self.assertGreater(len(main_symbols), 0, "Should find main function") print(f"Sample symbols: {symbol_names[:5]}") @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") def test_analyze_symbols_from_build_info(self) -> None: """Test symbol analysis using actual build info paths.""" print("Testing symbol analysis with build_info.json paths...") # Get paths from build info nm_path = self.board_info["aliases"]["nm"] cppfilt_path = self.board_info["aliases"]["c++filt"] elf_file = self.board_info["prog_path"] # If the ELF file from build_info doesn't exist, try the actual build location if not Path(elf_file).exists(): # Fallback to the actual ELF file location actual_elf_file = ( PROJECT_ROOT / ".build" / "fled" / "examples" / "uno" / "Blink" / ".pio" / "build" / "uno" / "firmware.elf" ) if actual_elf_file.exists(): elf_file = str(actual_elf_file) print(f"Using actual ELF file location: {elf_file}") # Verify the ELF file exists self.assertTrue(Path(elf_file).exists(), f"ELF file should exist: {elf_file}") # Run analysis symbols = analyze_symbols(elf_file, nm_path, cppfilt_path) # Verify results self.assertGreater(len(symbols), 0, "Should find symbols") # Check that we have symbols with reasonable sizes sizes = [s.size for s in symbols] symbols_with_size = sum(1 for size in sizes if size > 0) self.assertGreater( symbols_with_size, 0, "Should have at least some symbols with positive size" ) # All sizes should be non-negative (zero-size symbols are valid for undefined symbols, labels, etc.) self.assertTrue( all(size >= 0 for size in sizes), "All symbols should have non-negative sizes", ) print(f"Analyzed {len(symbols)} symbols from build ELF: {elf_file}") @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") def test_generate_report_basic(self) -> None: """Test report generation functionality.""" print("Testing basic report generation...") # Create some test symbols using SymbolInfo dataclass test_symbols = [ SymbolInfo( address="0x1000", size=1000, type="T", name="test_function_1", demangled_name="test_function_1()", source="test", ), SymbolInfo( address="0x2000", size=500, type="T", name="_Z12test_func_2v", demangled_name="test_function_2()", source="test", ), SymbolInfo( address="0x3000", size=200, type="D", name="test_data", demangled_name="test_data", source="test", ), ] test_dependencies = {"test_module.o": ["test_function_1", "test_data"]} # Generate report report = generate_report("TEST_BOARD", test_symbols, test_dependencies) # Verify report structure - use dataclass field access self.assertIsInstance(report.board, str) self.assertIsInstance(report.total_symbols, int) self.assertIsInstance(report.total_size, int) self.assertIsInstance(report.largest_symbols, list) self.assertIsInstance(report.type_breakdown, list) self.assertIsInstance(report.dependencies, dict) # Verify values self.assertEqual(report.board, "TEST_BOARD") self.assertEqual(report.total_symbols, 3) self.assertEqual(report.total_size, 1700) # 1000 + 500 + 200 # Verify type breakdown - it's now a list of TypeBreakdown dataclasses type_breakdown_dict = {tb.type: tb for tb in report.type_breakdown} self.assertIn("T", type_breakdown_dict) self.assertIn("D", type_breakdown_dict) self.assertEqual(type_breakdown_dict["T"].count, 2) self.assertEqual(type_breakdown_dict["D"].count, 1) print("Report generation test passed") @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") def test_find_board_build_info(self) -> None: """Test the board build info detection functionality.""" print("Testing board build info detection...") # Test finding UNO board specifically try: build_info_path, board_name = find_board_build_info("uno") self.assertEqual(board_name, "uno") self.assertTrue(build_info_path.exists()) self.assertEqual(build_info_path.name, "build_info.json") print(f"Found UNO build info: {build_info_path}") except SystemExit: self.skipTest("UNO build not available for testing") # Test auto-detection (should find any available board) try: build_info_path, board_name = find_board_build_info(None) self.assertTrue(build_info_path.exists()) self.assertEqual(build_info_path.name, "build_info.json") print(f"Auto-detected board: {board_name} at {build_info_path}") except SystemExit: self.skipTest("No builds available for auto-detection testing") @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") def test_analyze_map_file(self) -> None: """Test map file analysis if available.""" print("Testing map file analysis...") # Try to find a map file elf_file_path = Path(self.board_info["prog_path"]) map_file = elf_file_path.with_suffix(".map") if not map_file.exists(): print(f"Map file not found at {map_file}, skipping map analysis test") return # Analyze the map file dependencies = analyze_map_file(map_file) # Verify result structure self.assertIsInstance(dependencies, dict) if dependencies: print(f"Found {len(dependencies)} modules in map file") # Verify structure of dependencies for module, symbols in dependencies.items(): self.assertIsInstance(module, str) self.assertIsInstance(symbols, list) # Print a sample for debugging sample_modules = list(dependencies.keys())[:3] for module in sample_modules: print(f" {module}: {len(dependencies[module])} symbols") else: print("No dependencies found in map file (this may be normal)") @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") def test_build_reverse_call_graph(self) -> None: """Test reverse call graph building.""" print("Testing reverse call graph building...") # Create test call graph test_call_graph = { "function_a": ["function_b", "function_c"], "function_b": ["function_c", "function_d"], "function_c": ["function_d"], } # Build reverse call graph reverse_graph = build_reverse_call_graph(test_call_graph) # Verify structure expected_reverse = { "function_b": ["function_a"], "function_c": ["function_a", "function_b"], "function_d": ["function_b", "function_c"], } self.assertEqual(reverse_graph, expected_reverse) print("Reverse call graph building test passed") @unittest.skipUnless(_ENABLED, "Tests disabled - set _ENABLED = True to run") def test_full_symbol_analysis_workflow(self) -> None: """Test the complete symbol analysis workflow.""" print("Testing complete symbol analysis workflow...") # Run full analysis on actual build elf_file = self.board_info["prog_path"] # If the ELF file from build_info doesn't exist, try the actual build location if not Path(elf_file).exists(): # Fallback to the actual ELF file location actual_elf_file = ( PROJECT_ROOT / ".build" / "fled" / "examples" / "uno" / "Blink" / ".pio" / "build" / "uno" / "firmware.elf" ) if actual_elf_file.exists(): elf_file = str(actual_elf_file) print(f"Using actual ELF file location: {elf_file}") nm_path = self.board_info["aliases"]["nm"] cppfilt_path = self.board_info["aliases"]["c++filt"] # Analyze symbols symbols = analyze_symbols(elf_file, nm_path, cppfilt_path) # Analyze map file if available map_file = Path(elf_file).with_suffix(".map") dependencies = analyze_map_file(map_file) # Generate report report = generate_report("UNO", symbols, dependencies) # Verify the complete workflow produced valid results - use dataclass field access self.assertGreater(report.total_symbols, 0) self.assertGreater(report.total_size, 0) self.assertGreater(len(report.largest_symbols), 0) self.assertGreater(len(report.type_breakdown), 0) # Print summary for verification print("Complete analysis results:") print(f" Board: {report.board}") print(f" Total symbols: {report.total_symbols}") print( f" Total size: {report.total_size} bytes ({report.total_size / 1024:.1f} KB)" ) print( f" Largest symbol: {report.largest_symbols[0].demangled_name} ({report.largest_symbols[0].size} bytes)" ) # Verify we have expected symbol types for a typical embedded program type_breakdown_dict = {tb.type: tb for tb in report.type_breakdown} self.assertIn("T", type_breakdown_dict, "Should have text/code symbols") print("Full workflow test completed successfully") if __name__ == "__main__": unittest.main()