mirror of
https://github.com/ROCm/composable_kernel.git
synced 2026-05-03 21:21:22 +00:00
[CK] [CK_TILE] Improve build and test time of CI with smart dependency parser (#5249) ## Motivation Existing dependency parser needs full build of tests to determine which tests are affected by code changes in a PR. This still takes 2-4 hours for building the tests which slows down the CI as the number of tests grow. To resolve this issue we implemented a smart dependency parser which uses CMake Configure to parse dependencies and build only the affected test cases. We have ensured that two approaches are available 1) CMake pre-build analysis for each PR to ensure fast build and test. 2) Ninja post-build analysis to enable full build for nightly tests. ## Technical Details ```bash ### 1. Configure the project with CMake cmake -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .. ### 2. Analyze dependencies (no build required!) python3 ../script/dependency-parser/main.py cmake-parse compile_commands.json build.ninja \ --workspace-root .. --output cmake_dependency_mapping.json --parallel 8 ### 3. Find tests affected by changes python3 ../script/dependency-parser/main.py select cmake_dependency_mapping.json origin/develop \ HEAD --test-prefix --output tests_to_run.json ### 4. Build only affected tests ninja $(jq -r '.executables[]' tests_to_run.json | tr '\n' ' ') ### 5. Run affected tests ctest -R "$(jq -r '.regex' tests_to_run.json)" ``` ### Jenkins Integration - Added `buildMode` to jenkinsfile to integrate both `selective` and `full` build methods ### Known Limitations ### 1. Build-Time Generated Headers (HIGH RISK) **Problem:** Files generated during the build process (e.g., via `add_custom_command`) cannot be analyzed before building. **Example:** ```cmake add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/generated/config.hpp COMMAND generate_config.sh DEPENDS template.hpp.in ) ``` **Impact:** If a source file includes `generated/config.hpp`, the dependency won't be detected until after building. **Mitigation:** - CK analysis shows **no generated headers** currently used - If generated headers are added in the future, they must be built first - Recommendation: Generate headers in CMake configure phase (not build phase) when possible ## Test Plan **1. Modified Files:** ``` include/ck_tile/ops/common.hpp include/ck_tile/ops/gemm.hpp include/ck_tile/ops/gemm/warp/warp_gemm.hpp ``` **2. Compare tests selected between `build.ninja` and `cmake-parse` methods** ## Test Result - 1. The test completed in 5-6 minutes finding about 8000+ executables that should be built. - 2. We selected a commit 5ccc1387ea which resulted in same 7 tests with both legacy and new methods. - PR | Legacy tests | Smart tests | Notes -- | -- | -- | -- 5261 | 453 | 455 | Only 2 tests (test_amdgcn_mma and test_amdgcn_sparse_mma) 5168 | 0 | 0 | Changes in dispatcher only. No CK tests invoked. 5249 | 0 | 0 | Changes to dependency parser. No CK tests invoked 5260 | 0 | 0 | Changes in dispatcher only. No CK tests invoked. 5174 | 1 | 1 | One test from FMHA affected by this PR in both cases 5383 | 0 | 0 | Changes are only in benchmark files. Did not trigger any tests 5445 | 1 | 1 | Changes are only to tests/ck_tile/gemm_streamk. Only triggered one streamk test in both cases. 5454 | 3 | 3 | Both methods identified same test_grouped_conv_bwd tests 5427 | 234 | 234 | Core infrastructure header changes. Detected exactly same tests 5388 | 85 | 85 | modifies warp-level GEMM operations (warp_gemm.hpp, warp_gemm_dispatcher.hpp). Correctly identified all the streamK gemm tests ## Submission Checklist - [x ] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests.
513 lines
19 KiB
Python
513 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""
|
|
Test-Driven Development tests for CMake Dependency Analyzer.
|
|
|
|
This module tests the new pre-build dependency analysis approach that uses
|
|
compile_commands.json and clang -MM instead of requiring a full ninja build.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
import shutil
|
|
import sys
|
|
|
|
# Add parent directory to path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
|
|
class TestCompileCommandsParser(unittest.TestCase):
|
|
"""Tests for parsing compile_commands.json."""
|
|
|
|
def setUp(self):
|
|
"""Create temporary directory and sample compile_commands.json."""
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
self.compile_commands_path = os.path.join(self.temp_dir, "compile_commands.json")
|
|
|
|
def tearDown(self):
|
|
"""Clean up temporary directory."""
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_parse_empty_compile_commands(self):
|
|
"""Parser should handle empty compile_commands.json gracefully."""
|
|
from cmake_dependency_analyzer import CompileCommandsParser
|
|
|
|
with open(self.compile_commands_path, "w") as f:
|
|
json.dump([], f)
|
|
|
|
parser = CompileCommandsParser(self.compile_commands_path)
|
|
commands = parser.parse()
|
|
|
|
self.assertEqual(len(commands), 0)
|
|
|
|
def test_parse_single_command(self):
|
|
"""Parser should correctly parse a single compile command."""
|
|
from cmake_dependency_analyzer import CompileCommandsParser
|
|
|
|
sample_commands = [
|
|
{
|
|
"directory": "/build",
|
|
"command": "/opt/rocm/bin/amdclang++ -DFOO=1 -I/include -c test.cpp -o test.o",
|
|
"file": "/src/test.cpp",
|
|
}
|
|
]
|
|
with open(self.compile_commands_path, "w") as f:
|
|
json.dump(sample_commands, f)
|
|
|
|
parser = CompileCommandsParser(self.compile_commands_path)
|
|
commands = parser.parse()
|
|
|
|
self.assertEqual(len(commands), 1)
|
|
self.assertEqual(commands[0]["file"], "/src/test.cpp")
|
|
self.assertEqual(commands[0]["directory"], "/build")
|
|
|
|
def test_parse_multiple_commands(self):
|
|
"""Parser should correctly parse multiple compile commands."""
|
|
from cmake_dependency_analyzer import CompileCommandsParser
|
|
|
|
sample_commands = [
|
|
{
|
|
"directory": "/build",
|
|
"command": "/opt/rocm/bin/amdclang++ -c test1.cpp -o test1.o",
|
|
"file": "/src/test1.cpp",
|
|
},
|
|
{
|
|
"directory": "/build",
|
|
"command": "/opt/rocm/bin/amdclang++ -c test2.cpp -o test2.o",
|
|
"file": "/src/test2.cpp",
|
|
},
|
|
]
|
|
with open(self.compile_commands_path, "w") as f:
|
|
json.dump(sample_commands, f)
|
|
|
|
parser = CompileCommandsParser(self.compile_commands_path)
|
|
commands = parser.parse()
|
|
|
|
self.assertEqual(len(commands), 2)
|
|
|
|
def test_filter_by_extension(self):
|
|
"""Parser should filter commands by file extension."""
|
|
from cmake_dependency_analyzer import CompileCommandsParser
|
|
|
|
sample_commands = [
|
|
{"directory": "/build", "command": "clang++ -c test.cpp -o test.o", "file": "/src/test.cpp"},
|
|
{"directory": "/build", "command": "clang++ -c test.cc -o test.o", "file": "/src/test.cc"},
|
|
{"directory": "/build", "command": "clang -c test.c -o test.o", "file": "/src/test.c"},
|
|
]
|
|
with open(self.compile_commands_path, "w") as f:
|
|
json.dump(sample_commands, f)
|
|
|
|
parser = CompileCommandsParser(self.compile_commands_path)
|
|
commands = parser.parse(extensions=[".cpp"])
|
|
|
|
self.assertEqual(len(commands), 1)
|
|
self.assertEqual(commands[0]["file"], "/src/test.cpp")
|
|
|
|
def test_handles_arguments_format(self):
|
|
"""Parser should handle both 'command' and 'arguments' formats."""
|
|
from cmake_dependency_analyzer import CompileCommandsParser
|
|
|
|
sample_commands = [
|
|
{
|
|
"directory": "/build",
|
|
"arguments": ["/opt/rocm/bin/amdclang++", "-c", "test.cpp", "-o", "test.o"],
|
|
"file": "/src/test.cpp",
|
|
}
|
|
]
|
|
with open(self.compile_commands_path, "w") as f:
|
|
json.dump(sample_commands, f)
|
|
|
|
parser = CompileCommandsParser(self.compile_commands_path)
|
|
commands = parser.parse()
|
|
|
|
self.assertEqual(len(commands), 1)
|
|
self.assertIn("command", commands[0])
|
|
|
|
|
|
class TestDependencyExtractor(unittest.TestCase):
|
|
"""Tests for extracting dependencies using clang -MM."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
"""Clean up."""
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_convert_compile_to_dependency_command(self):
|
|
"""Should convert compile command to dependency extraction command."""
|
|
from cmake_dependency_analyzer import DependencyExtractor
|
|
|
|
compile_cmd = "/opt/rocm/bin/amdclang++ -DFOO=1 -I/include -O3 -c /src/test.cpp -o /build/test.o"
|
|
|
|
extractor = DependencyExtractor()
|
|
dep_cmd = extractor.convert_to_dependency_command(compile_cmd, "/tmp/deps.d")
|
|
|
|
# Should have -MM flag
|
|
self.assertIn("-MM", dep_cmd)
|
|
# Should have -MF flag with output file
|
|
self.assertIn("-MF", dep_cmd)
|
|
self.assertIn("/tmp/deps.d", dep_cmd)
|
|
# Should NOT have -c flag
|
|
self.assertNotIn(" -c ", dep_cmd)
|
|
# Should NOT have -o flag with output
|
|
self.assertNotIn(" -o ", dep_cmd)
|
|
# Should preserve includes and defines
|
|
self.assertIn("-DFOO=1", dep_cmd)
|
|
self.assertIn("-I/include", dep_cmd)
|
|
# Should preserve source file
|
|
self.assertIn("/src/test.cpp", dep_cmd)
|
|
|
|
def test_parse_makefile_deps_simple(self):
|
|
"""Should parse simple makefile-style dependency output."""
|
|
from cmake_dependency_analyzer import DependencyExtractor
|
|
|
|
deps_content = "test.o: test.cpp header1.hpp header2.hpp\n"
|
|
|
|
extractor = DependencyExtractor()
|
|
deps = extractor.parse_makefile_deps(deps_content)
|
|
|
|
self.assertEqual(len(deps), 3)
|
|
self.assertIn("test.cpp", deps)
|
|
self.assertIn("header1.hpp", deps)
|
|
self.assertIn("header2.hpp", deps)
|
|
|
|
def test_parse_makefile_deps_multiline(self):
|
|
"""Should parse multiline makefile-style dependency output."""
|
|
from cmake_dependency_analyzer import DependencyExtractor
|
|
|
|
deps_content = """test.o: test.cpp \\
|
|
/include/header1.hpp \\
|
|
/include/header2.hpp \\
|
|
/include/header3.hpp
|
|
"""
|
|
|
|
extractor = DependencyExtractor()
|
|
deps = extractor.parse_makefile_deps(deps_content)
|
|
|
|
self.assertEqual(len(deps), 4)
|
|
self.assertIn("test.cpp", deps)
|
|
self.assertIn("/include/header1.hpp", deps)
|
|
self.assertIn("/include/header2.hpp", deps)
|
|
self.assertIn("/include/header3.hpp", deps)
|
|
|
|
def test_parse_makefile_deps_empty(self):
|
|
"""Should handle empty dependency output."""
|
|
from cmake_dependency_analyzer import DependencyExtractor
|
|
|
|
extractor = DependencyExtractor()
|
|
deps = extractor.parse_makefile_deps("")
|
|
|
|
self.assertEqual(len(deps), 0)
|
|
|
|
@patch("subprocess.run")
|
|
def test_extract_dependencies_success(self, mock_run):
|
|
"""Should successfully extract dependencies using clang -MM."""
|
|
from cmake_dependency_analyzer import DependencyExtractor
|
|
|
|
# Mock successful clang -MM execution
|
|
mock_run.return_value = Mock(returncode=0, stdout="", stderr="")
|
|
|
|
extractor = DependencyExtractor()
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".d", delete=False) as f:
|
|
f.write("test.o: test.cpp header.hpp\n")
|
|
deps_file = f.name
|
|
|
|
with patch.object(extractor, "_get_deps_file", return_value=deps_file):
|
|
deps = extractor.extract("/build", "clang++ -c test.cpp -o test.o", "/src/test.cpp")
|
|
|
|
self.assertIn("test.cpp", deps)
|
|
self.assertIn("header.hpp", deps)
|
|
# Note: The implementation cleans up the temp file, so we don't need to
|
|
|
|
@patch("subprocess.run")
|
|
def test_extract_dependencies_compiler_error(self, mock_run):
|
|
"""Should handle compiler errors gracefully."""
|
|
from cmake_dependency_analyzer import DependencyExtractor
|
|
|
|
# Mock failed clang -MM execution
|
|
mock_run.return_value = Mock(returncode=1, stdout="", stderr="error: file not found")
|
|
|
|
extractor = DependencyExtractor()
|
|
deps = extractor.extract("/build", "clang++ -c test.cpp -o test.o", "/src/test.cpp")
|
|
|
|
# Should return empty list on error, not crash
|
|
self.assertEqual(deps, [])
|
|
|
|
|
|
class TestNinjaTargetParser(unittest.TestCase):
|
|
"""Tests for parsing ninja build files to get target mappings."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
self.ninja_file = os.path.join(self.temp_dir, "build.ninja")
|
|
|
|
def tearDown(self):
|
|
"""Clean up."""
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_parse_executable_to_objects(self):
|
|
"""Should parse executable -> object file mappings from build.ninja."""
|
|
from cmake_dependency_analyzer import NinjaTargetParser
|
|
|
|
ninja_content = """
|
|
rule CXX_EXECUTABLE_LINKER__test_gemm
|
|
command = /opt/rocm/bin/amdclang++ $in -o $out
|
|
|
|
build bin/test_gemm: CXX_EXECUTABLE_LINKER__test_gemm test/test_gemm.cpp.o library/gemm.cpp.o | lib.so
|
|
"""
|
|
with open(self.ninja_file, "w") as f:
|
|
f.write(ninja_content)
|
|
|
|
parser = NinjaTargetParser(self.ninja_file)
|
|
exe_to_objects = parser.parse_executable_mappings()
|
|
|
|
self.assertIn("bin/test_gemm", exe_to_objects)
|
|
self.assertIn("test/test_gemm.cpp.o", exe_to_objects["bin/test_gemm"])
|
|
self.assertIn("library/gemm.cpp.o", exe_to_objects["bin/test_gemm"])
|
|
|
|
def test_parse_object_to_source(self):
|
|
"""Should parse object -> source file mappings from build.ninja."""
|
|
from cmake_dependency_analyzer import NinjaTargetParser
|
|
|
|
ninja_content = """
|
|
rule CXX_COMPILER__test_gemm
|
|
command = /opt/rocm/bin/amdclang++ -c $in -o $out
|
|
|
|
build test/test_gemm.cpp.o: CXX_COMPILER__test_gemm /src/test/test_gemm.cpp
|
|
build library/gemm.cpp.o: CXX_COMPILER__test_gemm /src/library/gemm.cpp
|
|
"""
|
|
with open(self.ninja_file, "w") as f:
|
|
f.write(ninja_content)
|
|
|
|
parser = NinjaTargetParser(self.ninja_file)
|
|
obj_to_source = parser.parse_object_to_source()
|
|
|
|
self.assertIn("test/test_gemm.cpp.o", obj_to_source)
|
|
self.assertEqual(obj_to_source["test/test_gemm.cpp.o"], "/src/test/test_gemm.cpp")
|
|
|
|
def test_filter_test_executables(self):
|
|
"""Should correctly filter test executables by prefix."""
|
|
from cmake_dependency_analyzer import NinjaTargetParser
|
|
|
|
ninja_content = """
|
|
build bin/test_gemm: CXX_EXECUTABLE_LINKER__test_gemm test.o
|
|
build bin/example_gemm: CXX_EXECUTABLE_LINKER__example_gemm example.o
|
|
build bin/ckProfiler: CXX_EXECUTABLE_LINKER__ckProfiler profiler.o
|
|
"""
|
|
with open(self.ninja_file, "w") as f:
|
|
f.write(ninja_content)
|
|
|
|
parser = NinjaTargetParser(self.ninja_file)
|
|
exe_to_objects = parser.parse_executable_mappings()
|
|
|
|
test_exes = [exe for exe in exe_to_objects if "test_" in exe]
|
|
self.assertEqual(len(test_exes), 1)
|
|
self.assertIn("bin/test_gemm", test_exes)
|
|
|
|
|
|
class TestDependencyMapper(unittest.TestCase):
|
|
"""Tests for building the file -> executable dependency mapping."""
|
|
|
|
def test_build_file_to_executable_mapping(self):
|
|
"""Should build correct file -> executable mapping."""
|
|
from cmake_dependency_analyzer import DependencyMapper
|
|
|
|
# Simulated data
|
|
exe_to_objects = {
|
|
"bin/test_gemm": ["test/test_gemm.cpp.o", "lib/gemm.cpp.o"],
|
|
"bin/test_conv": ["test/test_conv.cpp.o", "lib/conv.cpp.o"],
|
|
}
|
|
obj_to_source = {
|
|
"test/test_gemm.cpp.o": "test/test_gemm.cpp",
|
|
"lib/gemm.cpp.o": "lib/gemm.cpp",
|
|
"test/test_conv.cpp.o": "test/test_conv.cpp",
|
|
"lib/conv.cpp.o": "lib/conv.cpp",
|
|
}
|
|
source_to_deps = {
|
|
"test/test_gemm.cpp": ["test/test_gemm.cpp", "include/gemm.hpp", "include/common.hpp"],
|
|
"lib/gemm.cpp": ["lib/gemm.cpp", "include/gemm.hpp"],
|
|
"test/test_conv.cpp": ["test/test_conv.cpp", "include/conv.hpp", "include/common.hpp"],
|
|
"lib/conv.cpp": ["lib/conv.cpp", "include/conv.hpp"],
|
|
}
|
|
|
|
mapper = DependencyMapper()
|
|
file_to_exes = mapper.build_mapping(exe_to_objects, obj_to_source, source_to_deps)
|
|
|
|
# common.hpp should map to both test executables
|
|
self.assertIn("include/common.hpp", file_to_exes)
|
|
self.assertIn("bin/test_gemm", file_to_exes["include/common.hpp"])
|
|
self.assertIn("bin/test_conv", file_to_exes["include/common.hpp"])
|
|
|
|
# gemm.hpp should only map to test_gemm
|
|
self.assertIn("include/gemm.hpp", file_to_exes)
|
|
self.assertIn("bin/test_gemm", file_to_exes["include/gemm.hpp"])
|
|
self.assertNotIn("bin/test_conv", file_to_exes["include/gemm.hpp"])
|
|
|
|
def test_normalize_paths(self):
|
|
"""Should normalize paths relative to workspace root."""
|
|
from cmake_dependency_analyzer import DependencyMapper
|
|
|
|
mapper = DependencyMapper(workspace_root="/workspace/rocm-libraries/projects/composablekernel")
|
|
|
|
# Test monorepo-style path
|
|
normalized = mapper.normalize_path(
|
|
"/workspace/rocm-libraries/projects/composablekernel/include/ck/ck.hpp"
|
|
)
|
|
self.assertEqual(normalized, "include/ck/ck.hpp")
|
|
|
|
# Test already relative path
|
|
normalized = mapper.normalize_path("include/ck/ck.hpp")
|
|
self.assertEqual(normalized, "include/ck/ck.hpp")
|
|
|
|
def test_filter_system_files(self):
|
|
"""Should filter out system files."""
|
|
from cmake_dependency_analyzer import DependencyMapper
|
|
|
|
mapper = DependencyMapper()
|
|
|
|
self.assertFalse(mapper.is_project_file("/usr/include/stdio.h"))
|
|
self.assertFalse(mapper.is_project_file("/opt/rocm/include/hip/hip_runtime.h"))
|
|
self.assertTrue(mapper.is_project_file("include/ck/ck.hpp"))
|
|
self.assertTrue(mapper.is_project_file("test/test_gemm.cpp"))
|
|
|
|
|
|
class TestCMakeDependencyAnalyzer(unittest.TestCase):
|
|
"""Integration tests for the full CMake dependency analyzer."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
"""Clean up."""
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_output_format_compatibility(self):
|
|
"""Output JSON should be compatible with selective_test_filter.py."""
|
|
from cmake_dependency_analyzer import CMakeDependencyAnalyzer
|
|
|
|
# Create minimal test data
|
|
analyzer = CMakeDependencyAnalyzer(
|
|
compile_commands_path=None,
|
|
ninja_path=None,
|
|
workspace_root=self.temp_dir,
|
|
)
|
|
|
|
# Manually set internal state for testing output format
|
|
analyzer.file_to_executables = {
|
|
"include/ck/ck.hpp": {"bin/test_gemm", "bin/test_conv"},
|
|
"test/test_gemm.cpp": {"bin/test_gemm"},
|
|
}
|
|
analyzer.executable_to_files = {
|
|
"bin/test_gemm": {"include/ck/ck.hpp", "test/test_gemm.cpp"},
|
|
"bin/test_conv": {"include/ck/ck.hpp"},
|
|
}
|
|
|
|
output_file = os.path.join(self.temp_dir, "output.json")
|
|
analyzer.export_to_json(output_file)
|
|
|
|
with open(output_file) as f:
|
|
data = json.load(f)
|
|
|
|
# Check required fields for selective_test_filter.py compatibility
|
|
self.assertIn("file_to_executables", data)
|
|
self.assertIn("executable_to_files", data)
|
|
self.assertIn("statistics", data)
|
|
|
|
# Check file_to_executables format (should be lists, not sets)
|
|
self.assertIsInstance(data["file_to_executables"]["include/ck/ck.hpp"], list)
|
|
|
|
def test_statistics_calculation(self):
|
|
"""Should calculate correct statistics."""
|
|
from cmake_dependency_analyzer import CMakeDependencyAnalyzer
|
|
|
|
analyzer = CMakeDependencyAnalyzer(
|
|
compile_commands_path=None,
|
|
ninja_path=None,
|
|
workspace_root=self.temp_dir,
|
|
)
|
|
|
|
analyzer.file_to_executables = {
|
|
"include/common.hpp": {"bin/test1", "bin/test2", "bin/test3"},
|
|
"include/specific.hpp": {"bin/test1"},
|
|
"test/test1.cpp": {"bin/test1"},
|
|
}
|
|
|
|
stats = analyzer.calculate_statistics()
|
|
|
|
self.assertEqual(stats["total_files"], 3)
|
|
self.assertEqual(stats["files_with_multiple_executables"], 1)
|
|
|
|
|
|
class TestParallelDependencyExtraction(unittest.TestCase):
|
|
"""Tests for parallel dependency extraction."""
|
|
|
|
def test_batch_extraction_preserves_results(self):
|
|
"""Parallel extraction should produce same results as serial."""
|
|
from cmake_dependency_analyzer import DependencyExtractor
|
|
|
|
extractor = DependencyExtractor(parallel_workers=4)
|
|
|
|
# This is more of an integration test placeholder
|
|
# Real parallel testing would require actual compiler invocations
|
|
self.assertIsNotNone(extractor)
|
|
|
|
|
|
class TestEdgeCases(unittest.TestCase):
|
|
"""Tests for edge cases and error handling."""
|
|
|
|
def test_handles_missing_compile_commands(self):
|
|
"""Should raise appropriate error for missing compile_commands.json."""
|
|
from cmake_dependency_analyzer import CompileCommandsParser
|
|
|
|
with self.assertRaises(FileNotFoundError):
|
|
parser = CompileCommandsParser("/nonexistent/compile_commands.json")
|
|
parser.parse()
|
|
|
|
def test_handles_malformed_json(self):
|
|
"""Should handle malformed JSON gracefully."""
|
|
from cmake_dependency_analyzer import CompileCommandsParser
|
|
|
|
temp_dir = tempfile.mkdtemp()
|
|
try:
|
|
path = os.path.join(temp_dir, "compile_commands.json")
|
|
with open(path, "w") as f:
|
|
f.write("not valid json {{{")
|
|
|
|
parser = CompileCommandsParser(path)
|
|
with self.assertRaises(json.JSONDecodeError):
|
|
parser.parse()
|
|
finally:
|
|
shutil.rmtree(temp_dir)
|
|
|
|
def test_handles_empty_ninja_file(self):
|
|
"""Should handle empty ninja file gracefully."""
|
|
from cmake_dependency_analyzer import NinjaTargetParser
|
|
|
|
temp_dir = tempfile.mkdtemp()
|
|
try:
|
|
ninja_file = os.path.join(temp_dir, "build.ninja")
|
|
with open(ninja_file, "w") as f:
|
|
f.write("")
|
|
|
|
parser = NinjaTargetParser(ninja_file)
|
|
exe_to_objects = parser.parse_executable_mappings()
|
|
|
|
self.assertEqual(len(exe_to_objects), 0)
|
|
finally:
|
|
shutil.rmtree(temp_dir)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|