From 3d10c98abe95a7687cb89668e323204dbd0ba127 Mon Sep 17 00:00:00 2001 From: Illia Silin <98187287+illsilin@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:48:00 -0700 Subject: [PATCH] Introduce dependency-based CI test selection. (#2377) * Selective test filter initial commit. * Expanded folder paths for parsing ninja dependencies. * Fixing default branch name in the test evaluation script. * Fixing paths for robustness and adding ctest command to the launch script. * change jenkins file and few tests to upgrade CI * Setting ninja build path. * Fixing typo in Jenkinsfile, and wrong paths. * Fixing typo in launch script. * add few more tests to check CI logic * Fixing header for shell script. * turn off performance test by default, add option to run all unit tests * revert dummy changes in source code to trigger tests * make sure develop branch runs all unit tests --------- Co-authored-by: Vidyasagar Ananthan [ROCm/composable_kernel commit: c3c8c6a10f0842cf52c08f1f99dc31714accaaea] --- Jenkinsfile | 43 ++- script/dependency-parser/README.md | 173 ++++++++++ script/dependency-parser/main.py | 78 +++++ .../src/enhanced_ninja_parser.py | 315 ++++++++++++++++++ .../src/selective_test_filter.py | 136 ++++++++ script/launch_tests.sh | 59 ++++ 6 files changed, 786 insertions(+), 18 deletions(-) create mode 100644 script/dependency-parser/README.md create mode 100644 script/dependency-parser/main.py create mode 100644 script/dependency-parser/src/enhanced_ninja_parser.py create mode 100644 script/dependency-parser/src/selective_test_filter.py create mode 100755 script/launch_tests.sh diff --git a/Jenkinsfile b/Jenkinsfile index f9d7feb77c..b2fda68b70 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -343,15 +343,8 @@ def cmake_build(Map conf=[:]){ def build_cmd def execute_cmd = conf.get("execute_cmd", "") if(!setup_args.contains("NO_CK_BUILD")){ - if (setup_args.contains("gfx9") && params.NINJA_BUILD_TRACE){ - echo "running ninja build trace" - setup_cmd = conf.get("setup_cmd", """${cmake_envs} cmake -G Ninja ${setup_args} -DCMAKE_CXX_FLAGS=" -O3 -ftime-trace " .. """) - build_cmd = conf.get("build_cmd", "${build_envs} ninja -j${nt} ${config_targets}") - } - else{ - setup_cmd = conf.get("setup_cmd", "${cmake_envs} cmake ${setup_args} .. ") - build_cmd = conf.get("build_cmd", "${build_envs} make -j${nt} ${config_targets}") - } + setup_cmd = conf.get("setup_cmd", """${cmake_envs} cmake -G Ninja ${setup_args} -DCMAKE_CXX_FLAGS=" -O3 -ftime-trace " .. """) + build_cmd = conf.get("build_cmd", "${build_envs} ninja -j${nt} ${config_targets}") cmd = conf.get("cmd", """ ${setup_cmd} ${build_cmd} @@ -379,7 +372,12 @@ def cmake_build(Map conf=[:]){ archiveArtifacts "clang_build_analysis.log" // do not run unit tests when building instances only if(!params.BUILD_INSTANCES_ONLY){ - sh "ninja check" + if (!params.RUN_ALL_UNIT_TESTS){ + sh "../script/launch_tests.sh" + } + else{ + sh "ninja check" + } } if(params.BUILD_INSTANCES_ONLY){ // build deb packages @@ -393,7 +391,12 @@ def cmake_build(Map conf=[:]){ else{ // run unit tests unless building library for all targets if (!params.BUILD_INSTANCES_ONLY){ - sh "make check" + if (!params.RUN_ALL_UNIT_TESTS){ + sh "../script/launch_tests.sh" + } + else{ + sh "ninja check" + } } } } @@ -793,10 +796,10 @@ def process_results(Map conf=[:]){ } //launch develop branch daily jobs -CRON_SETTINGS = BRANCH_NAME == "develop" ? '''0 23 * * * % RUN_FULL_QA=true;DISABLE_DL_KERNELS=true;RUN_CK_TILE_FMHA_TESTS=true;RUN_CK_TILE_TRANSPOSE_TESTS=true;RUN_CK_TILE_GEMM_TESTS=true;RUN_TILE_ENGINE_GEMM_TESTS=false - 0 21 * * * % RUN_GROUPED_CONV_LARGE_CASES_TESTS=true;hipTensor_test=true;BUILD_GFX908=true;BUILD_GFX950=true - 0 19 * * * % BUILD_DOCKER=true;COMPILER_VERSION=amd-staging;BUILD_COMPILER=/llvm-project/build/bin/clang++;USE_SCCACHE=false;NINJA_BUILD_TRACE=true - 0 17 * * * % BUILD_DOCKER=true;COMPILER_VERSION=amd-mainline;BUILD_COMPILER=/llvm-project/build/bin/clang++;USE_SCCACHE=false;NINJA_BUILD_TRACE=true +CRON_SETTINGS = BRANCH_NAME == "develop" ? '''0 23 * * * % RUN_FULL_QA=true;DISABLE_DL_KERNELS=true;RUN_CK_TILE_FMHA_TESTS=true;RUN_CK_TILE_TRANSPOSE_TESTS=true;RUN_CK_TILE_GEMM_TESTS=true;RUN_TILE_ENGINE_GEMM_TESTS=false;RUN_PERFORMANCE_TESTS=true;RUN_ALL_UNIT_TESTS=true + 0 21 * * * % RUN_GROUPED_CONV_LARGE_CASES_TESTS=true;hipTensor_test=true;BUILD_GFX908=true;BUILD_GFX950=true;RUN_PERFORMANCE_TESTS=true;RUN_ALL_UNIT_TESTS=true + 0 19 * * * % BUILD_DOCKER=true;COMPILER_VERSION=amd-staging;BUILD_COMPILER=/llvm-project/build/bin/clang++;USE_SCCACHE=false;NINJA_BUILD_TRACE=true;RUN_ALL_UNIT_TESTS=true + 0 17 * * * % BUILD_DOCKER=true;COMPILER_VERSION=amd-mainline;BUILD_COMPILER=/llvm-project/build/bin/clang++;USE_SCCACHE=false;NINJA_BUILD_TRACE=true;RUN_ALL_UNIT_TESTS=true 0 15 * * * % BUILD_INSTANCES_ONLY=true;USE_SCCACHE=false;NINJA_BUILD_TRACE=true 0 13 * * * % BUILD_LEGACY_OS=true;USE_SCCACHE=false;RUN_PERFORMANCE_TESTS=false''' : "" @@ -859,8 +862,8 @@ pipeline { description: "Run the cppcheck static analysis (default: OFF)") booleanParam( name: "RUN_PERFORMANCE_TESTS", - defaultValue: true, - description: "Run the performance tests (default: ON)") + defaultValue: false, + description: "Run the performance tests (default: OFF)") booleanParam( name: "RUN_GROUPED_CONV_LARGE_CASES_TESTS", defaultValue: false, @@ -913,6 +916,10 @@ pipeline { name: "RUN_INDUCTOR_TESTS", defaultValue: true, description: "Run inductor codegen tests (default: ON)") + booleanParam( + name: "RUN_ALL_UNIT_TESTS", + defaultValue: false, + description: "Run all unit tests (default: OFF)") } environment{ dbuser = "${dbuser}" @@ -1025,7 +1032,7 @@ pipeline { { when { beforeAgent true - expression { params.RUN_CODEGEN_TESTS.toBoolean() } + expression { params.RUN_CODEGEN_TESTS.toBoolean() && !params.BUILD_INSTANCES_ONLY.toBoolean() } } agent{ label rocmnode("gfx90a")} environment{ diff --git a/script/dependency-parser/README.md b/script/dependency-parser/README.md new file mode 100644 index 0000000000..ff4a44b9a2 --- /dev/null +++ b/script/dependency-parser/README.md @@ -0,0 +1,173 @@ +# Dependency-based Selective Test Filtering using Static Analysis of Ninja Builds for C++ Projects + +## Overview + +This tool provides advanced dependency-based selective test filtering and build optimization for large C++ monorepos using static parsing of Ninja build files. By analyzing both source and header dependencies, it enables precise identification of which tests and executables are affected by code changes, allowing for efficient CI/CD workflows and faster incremental builds. + +The parser: +- Identifies all executables in the Ninja build. +- Maps object files to their source and header dependencies using `ninja -t deps`. +- Constructs a reverse mapping from each file to all dependent executables. +- Handles multi-executable dependencies and supports parallel processing for scalability. +- Exports results in CSV and JSON formats for integration with other tools. + +## Features + +- **Comprehensive Dependency Tracking**: Captures direct source file dependencies and, critically, all included header files via `ninja -t deps`. +- **Executable to Object Mapping**: Parses the `build.ninja` file to understand how executables are linked from object files. +- **Object to Source/Header Mapping**: Uses `ninja -t deps` for each object file to get a complete list of its dependencies. +- **File to Executable Inversion**: Inverts the dependency graph to map each file to the set of executables that depend on it. +- **Parallel Processing**: Utilizes a `ThreadPoolExecutor` to run `ninja -t deps` commands in parallel, significantly speeding up analysis for projects with many object files. +- **Filtering**: Option to filter out system files and focus on project-specific dependencies. +- **Multiple Output Formats**: + - **CSV**: `enhanced_file_executable_mapping.csv` - A comma-separated values file where each row lists a file and a semicolon-separated list of executables that depend on it. + - **JSON**: `enhanced_dependency_mapping.json` - A JSON file representing a dictionary where keys are file paths and values are lists of dependent executables. +- **Robust Error Handling**: Includes error handling for missing files and failed subprocess commands. + +## Prerequisites + +- **Python 3.7+** +- **Ninja build system**: The `ninja` executable must be in the system's PATH or its path provided as an argument. +- A **Ninja build directory** containing a `build.ninja` file and the compiled object files. The project should have been built at least once. + +## Using CMake with Ninja + +To use this tool effectively, your C++ project should be configured with CMake to generate Ninja build files and dependency information. Follow these steps: + +1. **Configure CMake to use Ninja and generate dependencies:** + ```bash + cmake -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Release /path/to/your/source + ``` + - The `-G Ninja` flag tells CMake to generate Ninja build files. + - `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` is optional but useful for other tooling. + - Ensure your CMakeLists.txt uses `target_include_directories` and proper dependency declarations for accurate results. + +2. **Build your project with Ninja:** + ```bash + ninja + ``` + - This step is required to generate all object files and dependency information (`.d` files) that the parser relies on. + +3. **Run the dependency parser tool:** + ```bash + python main.py parse /path/to/build.ninja --workspace-root /path/to/your/workspace + ``` + +**Note:** Always run Ninja to ensure all dependencies are up to date before invoking the parser. If you change source files or headers, re-run Ninja first. + +## Usage + +All features are available via the unified main.py CLI: + +```bash +# Dependency parsing (now supports --workspace-root) +python main.py parse examples/build-ninja/build.ninja --workspace-root /path/to/your/workspace + +# Selective test filtering +python main.py select enhanced_dependency_mapping.json [--all | --test-prefix] [--output ] + +# Code auditing +python main.py audit enhanced_dependency_mapping.json + +# Build optimization +python main.py optimize enhanced_dependency_mapping.json [ ...] +``` + +**Arguments:** + +1. ``: (Required) The full path to the `build.ninja` file within your Ninja build directory. +2. `[--workspace-root ]`: (Optional, recommended) The root directory of your workspace. +3. `[path_to_ninja_executable]`: (Optional) The path to the `ninja` executable if it's not in your system's PATH. Defaults to `ninja`. + +**Example:** + +```bash +# Assuming your build directory is 'build-ninja' and it contains 'build.ninja' +python src/enhanced_ninja_parser.py build-ninja/build.ninja + +# With custom workspace root +python src/enhanced_ninja_parser.py build-ninja/build.ninja ninja /path/to/your/workspace + +# If ninja is installed in a custom location +python src/enhanced_ninja_parser.py /path/to/project/build/build.ninja /usr/local/bin/ninja +``` + +## How It Works + +1. **Initialization**: + * Takes the path to `build.ninja` and optionally the `ninja` executable. + * Sets up internal data structures to store mappings. + +2. **Build File Parsing (`_parse_build_file`)**: + * Reads the `build.ninja` file. + * Uses regular expressions to identify rules for linking executables (e.g., `build my_exe: link main.o utils.o`) and compiling object files (e.g., `build main.o: cxx ../src/main.cpp`). + * Populates `executable_to_objects` (mapping an executable name to a list of its .o files) and `object_to_source` (mapping an object file to its primary source file). + +3. **Object Dependency Extraction (`_extract_all_object_dependencies`)**: + * Iterates through all unique object files identified in the previous step. + * For each object file, it calls `_get_object_dependencies`. + * This process is parallelized using `ThreadPoolExecutor` for efficiency. Each call to `ninja -t deps` runs in a separate thread. + +4. **Individual Object Dependencies (`_get_object_dependencies`)**: + * For a given object file (e.g., `main.o`), it runs the command: `ninja -t deps main.o` in the build directory. + * This command outputs a list of all files that `main.o` depends on, including its primary source (`main.cpp`) and all headers (`*.h`, `*.hpp`) it includes directly or indirectly. + * The output is parsed, cleaned, and returned as a list of file paths. + +5. **Building Final File-to-Executable Mapping (`_build_file_to_executable_mapping`)**: + * This is the core inversion step. It iterates through each executable and its associated object files. + * For each object file, it looks up the full list of its dependencies (source and headers) obtained in step 3 & 4. + * For every dependent file found, it adds the current executable to that file's entry in the `file_to_executables` dictionary. + * If `filter_project_files` is enabled, it checks each dependency against a list of common system paths (e.g., `/usr/include`, `_deps/`) and excludes them if they match. + +6. **Filtering (`_is_project_file`)**: + * A helper function to determine if a given file path is likely a project file or a system/external library file. This helps in focusing the dependency map on the user's own codebase. + +7. **Output Generation**: + * **`export_to_csv(csv_file)`**: Writes the `file_to_executables` mapping to a CSV file. Each row contains a file path and a semicolon-delimited string of executable names. + * **`export_to_json(json_file)`**: Dumps the `file_to_executables` mapping (where the set of executables is converted to a list) into a JSON file. + * **`print_summary()`**: Prints a summary of the findings, including the number of executables, object files, source files, and header files mapped. + +## Output Files + +Running the script will generate two files in the same directory as the input `build.ninja` file: + +- **`enhanced_file_executable_mapping.csv`**: + ```csv + File,Executables + /path/to/project/src/main.cpp,my_exe_1;my_exe_2 + /path/to/project/include/utils.h,my_exe_1;another_test + ... + ``` + +- **`enhanced_dependency_mapping.json`**: + ```json + { + "/path/to/project/src/main.cpp": ["my_exe_1", "my_exe_2"], + "/path/to/project/include/utils.h": ["my_exe_1", "another_test"], + ... + } + ``` + +## Use Cases + +- **Impact Analysis**: Determine which executables (especially tests) need to be rebuilt or re-run when a specific source or header file changes. +- **Build Optimization**: Understand the dependency structure to potentially optimize build times. +- **Code Auditing**: Get a clear overview of how files are used across different executables. +- **Selective Testing**: Integrate with CI/CD systems to run only the tests affected by a given set of changes. + +## Limitations + +- Relies on the accuracy of Ninja's dependency information (`ninja -t deps`). If the build system doesn't correctly generate `.d` (dependency) files, the header information might be incomplete. +- The definition of "project file" vs. "system file" is based on a simple path-based heuristic and might need adjustment for specific project structures. +- Performance for extremely large projects (tens of thousands of object files) might still be a consideration, though parallelization helps significantly. + +## Troubleshooting + +- **"ninja: command not found"**: Ensure `ninja` is installed and in your PATH, or provide the full path to the executable as the second argument. +- **"build.ninja not found"**: Double-check the path to your `build.ninja` file. +- **Empty or Incomplete Output**: + * Make sure the project has been successfully built at least once. `ninja -t deps` relies on information generated during the build. + * Verify that your CMake (or other meta-build system) is configured to generate dependency files for Ninja. +- **Slow Performance**: For very large projects, the number of `ninja -t deps` calls can be substantial. While parallelized, it can still take time. Consider if all object files truly need to be analyzed or if a subset is sufficient for your needs. + +This tool provides a powerful way to gain deep insights into your Ninja project's dependency structure, enabling more intelligent build and test workflows. diff --git a/script/dependency-parser/main.py b/script/dependency-parser/main.py new file mode 100644 index 0000000000..b8fd67ac49 --- /dev/null +++ b/script/dependency-parser/main.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +""" +Unified CLI for Ninja Dependency Analysis and Selective Testing + +Features: +- Dependency parsing (from build.ninja) +- Selective test filtering (between git refs) +- Code auditing (--audit) +- Build optimization (--optimize-build) +""" + +import argparse +import sys +import os + +def run_dependency_parser(args): + from src.enhanced_ninja_parser import main as ninja_main + sys.argv = ["enhanced_ninja_parser.py"] + args + ninja_main() + +def run_selective_test_filter(args): + from src.selective_test_filter import main as filter_main + sys.argv = ["selective_test_filter.py"] + args + filter_main() + +def main(): + parser = argparse.ArgumentParser(description="Unified Ninja Dependency & Selective Testing Tool") + subparsers = parser.add_subparsers(dest="command", required=True) + + # Dependency parsing + parser_parse = subparsers.add_parser("parse", help="Parse build.ninja and generate dependency mapping") + parser_parse.add_argument("build_ninja", help="Path to build.ninja") + parser_parse.add_argument("--ninja", help="Path to ninja executable", default="ninja") + parser_parse.add_argument("--workspace-root", help="Path to workspace root", default=None) + + # Selective testing + parser_test = subparsers.add_parser("select", help="Selective test filtering between git refs") + parser_test.add_argument("depmap_json", help="Path to dependency mapping JSON") + parser_test.add_argument("ref1", help="Source git ref") + parser_test.add_argument("ref2", help="Target git ref") + parser_test.add_argument("--all", action="store_true", help="Include all executables") + parser_test.add_argument("--test-prefix", action="store_true", help="Only include executables starting with 'test_'") + parser_test.add_argument("--output", help="Output JSON file", default="tests_to_run.json") + + # Code auditing + parser_audit = subparsers.add_parser("audit", help="List all files and their dependent executables") + parser_audit.add_argument("depmap_json", help="Path to dependency mapping JSON") + + # Build optimization + parser_opt = subparsers.add_parser("optimize", help="List affected executables for changed files") + parser_opt.add_argument("depmap_json", help="Path to dependency mapping JSON") + parser_opt.add_argument("changed_files", nargs="+", help="List of changed files") + + args = parser.parse_args() + + if args.command == "parse": + parse_args = [args.build_ninja, args.ninja] + if args.workspace_root: + parse_args.append(args.workspace_root) + run_dependency_parser(parse_args) + elif args.command == "select": + filter_args = [args.depmap_json, args.ref1, args.ref2] + if args.test_prefix: + filter_args.append("--test-prefix") + if args.all: + filter_args.append("--all") + if args.output: + filter_args += ["--output", args.output] + run_selective_test_filter(filter_args) + elif args.command == "audit": + run_selective_test_filter([args.depmap_json, "--audit"]) + elif args.command == "optimize": + run_selective_test_filter([args.depmap_json, "--optimize-build"] + args.changed_files) + else: + parser.print_help() + +if __name__ == "__main__": + main() diff --git a/script/dependency-parser/src/enhanced_ninja_parser.py b/script/dependency-parser/src/enhanced_ninja_parser.py new file mode 100644 index 0000000000..087ab50640 --- /dev/null +++ b/script/dependency-parser/src/enhanced_ninja_parser.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 +""" +Enhanced Ninja Dependency Parser + +This script combines ninja build file parsing with ninja -t deps to create a comprehensive +mapping that includes both source files AND header files, and properly handles files +used by multiple executables. +""" + +import re +import os +import sys +import subprocess +from pathlib import Path +from collections import defaultdict +import json +from concurrent.futures import ThreadPoolExecutor, as_completed +import threading + +class EnhancedNinjaDependencyParser: + def __init__(self, build_file_path, ninja_executable="ninja"): + self.build_file_path = build_file_path + self.build_dir = os.path.dirname(build_file_path) + self.ninja_executable = ninja_executable + + # Core data structures + self.executable_to_objects = {} # exe -> [object_files] + self.object_to_source = {} # object -> primary_source + self.object_to_all_deps = {} # object -> [all_dependencies] + self.file_to_executables = defaultdict(set) # file -> {executables} + + # Thread safety + self.lock = threading.Lock() + + def parse_dependencies(self): + """Main method to parse all dependencies.""" + print(f"Parsing ninja dependencies from: {self.build_file_path}") + + # Step 1: Parse build file for executable -> object mappings + self._parse_build_file() + + # Step 2: Get all object files and their dependencies + print(f"Found {len(self.object_to_source)} object files") + print("Extracting detailed dependencies for all object files...") + self._extract_object_dependencies() + + # Step 3: Build the final file -> executables mapping + self._build_file_to_executable_mapping() + + def _parse_build_file(self): + """Parse the ninja build file to extract executable -> object mappings.""" + print("Parsing ninja build file...") + + with open(self.build_file_path, 'r') as f: + content = f.read() + # Parse executable build rules + exe_pattern = r'^build (bin/[^:]+):\s+\S+\s+([^|]+)' + obj_pattern = r'^build ([^:]+\.(?:cpp|cu|hip)\.o):\s+\S+\s+([^\s|]+)' + + lines = content.split('\n') + + for line in lines: + # Match executable rules + exe_match = re.match(exe_pattern, line) + if exe_match and ('EXECUTABLE' in line or 'test_' in exe_match.group(1) or 'example_' in exe_match.group(1)): + exe = exe_match.group(1) + deps_part = exe_match.group(2).strip() + + object_files = [] + for dep in deps_part.split(): + if dep.endswith('.o') and not dep.startswith('/'): + object_files.append(dep) + + self.executable_to_objects[exe] = object_files + continue + + # Match object compilation rules + obj_match = re.match(obj_pattern, line) + if obj_match: + object_file = obj_match.group(1) + source_file = obj_match.group(2) + self.object_to_source[object_file] = source_file + + print(f"Found {len(self.executable_to_objects)} executables") + print(f"Found {len(self.object_to_source)} object-to-source mappings") + + def _extract_object_dependencies(self): + """Extract detailed dependencies for all object files using ninja -t deps.""" + object_files = list(self.object_to_source.keys()) + # Process object files in parallel for better performance + if not object_files: + print("No object files found - skipping dependency extraction") + return + + max_workers = min(16, len(object_files)) # Limit concurrent processes + + with ThreadPoolExecutor(max_workers=max_workers) as executor: + # Submit all object files for processing + future_to_obj = { + executor.submit(self._get_object_dependencies, obj): obj + for obj in object_files + } + # Process completed futures + completed = 0 + for future in as_completed(future_to_obj): + obj_file = future_to_obj[future] + try: + dependencies = future.result() + with self.lock: + self.object_to_all_deps[obj_file] = dependencies + completed += 1 + if completed % 100 == 0: + print(f"Processed {completed}/{len(object_files)} object files...") + except Exception as e: + print(f"Error processing {obj_file}: {e}") + + print(f"Completed dependency extraction for {len(self.object_to_all_deps)} object files") + + def _get_object_dependencies(self, object_file): + """Get all dependencies for a single object file using ninja -t deps.""" + try: + # Run ninja -t deps for this object file + cmd = [self.ninja_executable, "-t", "deps", object_file] + result = subprocess.run( + cmd, + cwd=self.build_dir, + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + return [] + + dependencies = [] + lines = result.stdout.strip().split('\n') + + for line in lines[1:]: # Skip first line with metadata + line = line.strip() + if line and not line.startswith('#'): + # Convert absolute paths to relative paths from workspace root + dep_file = line + ws_root = getattr(self, "workspace_root", "..") + ws_prefix = ws_root.rstrip("/") + "/" + if dep_file.startswith(ws_prefix): + dep_file = dep_file[len(ws_prefix):] + dependencies.append(dep_file) + + return dependencies + + except Exception as e: + print(f"Error getting dependencies for {object_file}: {e}") + return [] + + def _build_file_to_executable_mapping(self): + """Build the final mapping from files to executables.""" + print("Building file-to-executable mapping...") + + for exe, object_files in self.executable_to_objects.items(): + for obj_file in object_files: + # Add all dependencies of this object file + if obj_file in self.object_to_all_deps: + for dep_file in self.object_to_all_deps[obj_file]: + # Filter out system files and focus on project files + if self._is_project_file(dep_file): + self.file_to_executables[dep_file].add(exe) + + print(f"Built mapping for {len(self.file_to_executables)} files") + + # Show statistics + multi_exe_files = {f: exes for f, exes in self.file_to_executables.items() if len(exes) > 1} + print(f"Files used by multiple executables: {len(multi_exe_files)}") + + if multi_exe_files: + print("Sample files with multiple dependencies:") + for f, exes in sorted(multi_exe_files.items())[:5]: + print(f" {f}: {len(exes)} executables") + + def _is_project_file(self, file_path): + """Determine if a file is part of the project (not system files).""" + # Include files that are clearly part of the project + if any(file_path.startswith(prefix) for prefix in [ + 'include/', 'library/', 'test/', 'example/', 'src/', 'profiler/', + 'build/include/', 'build/_deps/gtest', 'client_example', 'codegen', 'tile_engine' + ]): + return True + + # Exclude system files + if any(file_path.startswith(prefix) for prefix in [ + '/usr/', '/opt/rocm', '/lib/', '/system/', '/local/' + ]): + return False + + # Include files with common source/header extensions + if file_path.endswith(('.cpp', '.hpp', '.h', '.c', '.cc', '.cxx', '.cu', '.hip', '.inc')): + return True + + return False + + def export_to_csv(self, output_file): + """Export the file-to-executable mapping to CSV with proper comma separation.""" + print(f"Exporting mapping to {output_file}") + + with open(output_file, 'w') as f: + f.write("source_file,executables\n") + for file_path in sorted(self.file_to_executables.keys()): + executables = sorted(self.file_to_executables[file_path]) + # Use semicolon to separate multiple executables within the field + exe_list = ';'.join(executables) + f.write(f'"{file_path}","{exe_list}"\n') + + def export_to_json(self, output_file): + """Export the complete mapping to JSON.""" + print(f"Exporting complete mapping to {output_file}") + + # Build reverse mapping (executable -> files) + exe_to_files = defaultdict(set) + for file_path, exes in self.file_to_executables.items(): + for exe in exes: + exe_to_files[exe].add(file_path) + + mapping_data = { + 'file_to_executables': { + file_path: list(exes) for file_path, exes in self.file_to_executables.items() + }, + 'executable_to_files': { + exe: sorted(files) for exe, files in exe_to_files.items() + }, + 'statistics': { + 'total_files': len(self.file_to_executables), + 'total_executables': len(self.executable_to_objects), + 'total_object_files': len(self.object_to_source), + 'files_with_multiple_executables': len([f for f, exes in self.file_to_executables.items() if len(exes) > 1]) + } + } + + with open(output_file, 'w') as f: + json.dump(mapping_data, f, indent=2) + + def print_summary(self): + """Print a summary of the parsed dependencies.""" + print("\n=== Enhanced Dependency Mapping Summary ===") + print(f"Total executables: {len(self.executable_to_objects)}") + print(f"Total files mapped: {len(self.file_to_executables)}") + print(f"Total object files processed: {len(self.object_to_all_deps)}") + + # Files by type + cpp_files = sum(1 for f in self.file_to_executables.keys() if f.endswith('.cpp')) + hpp_files = sum(1 for f in self.file_to_executables.keys() if f.endswith('.hpp')) + h_files = sum(1 for f in self.file_to_executables.keys() if f.endswith('.h')) + + print(f"\nFile types:") + print(f" .cpp files: {cpp_files}") + print(f" .hpp files: {hpp_files}") + print(f" .h files: {h_files}") + + # Multi-executable files + multi_exe_files = {f: exes for f, exes in self.file_to_executables.items() if len(exes) > 1} + print(f"\nFiles used by multiple executables: {len(multi_exe_files)}") + + if multi_exe_files: + print("\nTop files with most dependencies:") + sorted_multi = sorted(multi_exe_files.items(), key=lambda x: len(x[1]), reverse=True) + for file_path, exes in sorted_multi[:10]: + print(f" {file_path}: {len(exes)} executables") + +def main(): + # Accept: build_file, ninja_path, workspace_root + default_workspace_root = ".." + if len(sys.argv) > 3: + build_file = sys.argv[1] + ninja_path = sys.argv[2] + workspace_root = sys.argv[3] + elif len(sys.argv) > 2: + build_file = sys.argv[1] + ninja_path = sys.argv[2] + workspace_root = default_workspace_root + elif len(sys.argv) > 1: + build_file = sys.argv[1] + ninja_path = "ninja" + workspace_root = default_workspace_root + else: + build_file = f"{default_workspace_root}/build/build.ninja" + ninja_path = "ninja" + workspace_root = default_workspace_root + + if not os.path.exists(build_file): + print(f"Error: Build file not found: {build_file}") + sys.exit(1) + + try: + subprocess.run([ninja_path, "--version"], capture_output=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + print(f"Error: ninja executable not found: {ninja_path}") + sys.exit(1) + + parser = EnhancedNinjaDependencyParser(build_file, ninja_path) + parser.workspace_root = workspace_root # Attach for use in _get_object_dependencies + parser.parse_dependencies() + parser.print_summary() + + # Export results + output_dir = os.path.dirname(build_file) + csv_file = os.path.join(output_dir, 'enhanced_file_executable_mapping.csv') + json_file = os.path.join(output_dir, 'enhanced_dependency_mapping.json') + + parser.export_to_csv(csv_file) + parser.export_to_json(json_file) + + print(f"\nResults exported to:") + print(f" CSV: {csv_file}") + print(f" JSON: {json_file}") + +if __name__ == "__main__": + main() diff --git a/script/dependency-parser/src/selective_test_filter.py b/script/dependency-parser/src/selective_test_filter.py new file mode 100644 index 0000000000..f364d60d27 --- /dev/null +++ b/script/dependency-parser/src/selective_test_filter.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Selective Test Filter Tool + +Given two git refs (branches or commit IDs), this tool: +- Identifies changed files between the refs +- Loads the enhanced dependency mapping JSON (from enhanced_ninja_parser.py) +- Maps changed files to affected test executables (optionally filtering for "test_" prefix) +- Exports the list of tests to run to tests_to_run.json + +Usage: + python selective_test_filter.py [--all | --test-prefix] [--output ] + +Arguments: + Path to enhanced_dependency_mapping.json + Source git ref (branch or commit) + Target git ref (branch or commit) + +Options: + --all Include all executables (default) + --test-prefix Only include executables starting with "test_" + --output Output JSON file (default: tests_to_run.json) +""" + +import sys +import subprocess +import json +import os + +def get_changed_files(ref1, ref2): + """Return a set of files changed between two git refs.""" + try: + result = subprocess.run( + ["git", "diff", "--name-only", ref1, ref2], + capture_output=True, text=True, check=True + ) + files = set(line.strip() for line in result.stdout.splitlines() if line.strip()) + return files + except subprocess.CalledProcessError as e: + print(f"Error running git diff: {e}") + sys.exit(1) + +def load_depmap(depmap_json): + """Load the dependency mapping JSON.""" + with open(depmap_json, "r") as f: + data = json.load(f) + # Support both old and new formats + if "file_to_executables" in data: + return data["file_to_executables"] + return data + +def select_tests(file_to_executables, changed_files, filter_mode): + """Return a set of test executables affected by changed files.""" + affected = set() + for f in changed_files: + if f in file_to_executables: + for exe in file_to_executables[f]: + if filter_mode == "all": + affected.add(exe) + elif filter_mode == "test_prefix" and exe.startswith("test_"): + affected.add(exe) + return sorted(affected) + +def main(): + if "--audit" in sys.argv: + if len(sys.argv) < 2: + print("Usage: python selective_test_filter.py --audit") + sys.exit(1) + depmap_json = sys.argv[1] + if not os.path.exists(depmap_json): + print(f"Dependency map JSON not found: {depmap_json}") + sys.exit(1) + file_to_executables = load_depmap(depmap_json) + for f, exes in file_to_executables.items(): + print(f"{f}: {', '.join(exes)}") + print(f"Total files: {len(file_to_executables)}") + sys.exit(0) + + if "--optimize-build" in sys.argv: + if len(sys.argv) < 3: + print("Usage: python selective_test_filter.py --optimize-build [ ...]") + sys.exit(1) + depmap_json = sys.argv[1] + changed_files = set(sys.argv[sys.argv.index("--optimize-build") + 1 :]) + if not os.path.exists(depmap_json): + print(f"Dependency map JSON not found: {depmap_json}") + sys.exit(1) + file_to_executables = load_depmap(depmap_json) + affected_executables = set() + for f in changed_files: + if f in file_to_executables: + affected_executables.update(file_to_executables[f]) + print("Affected executables:") + for exe in sorted(affected_executables): + print(exe) + print(f"Total affected executables: {len(affected_executables)}") + sys.exit(0) + + if len(sys.argv) < 4: + print("Usage: python selective_test_filter.py [--all | --test-prefix] [--output ]") + sys.exit(1) + + depmap_json = sys.argv[1] + ref1 = sys.argv[2] + ref2 = sys.argv[3] + filter_mode = "all" + output_json = "tests_to_run.json" + + if "--test-prefix" in sys.argv: + filter_mode = "test_prefix" + if "--all" in sys.argv: + filter_mode = "all" + if "--output" in sys.argv: + idx = sys.argv.index("--output") + if idx + 1 < len(sys.argv): + output_json = sys.argv[idx + 1] + + if not os.path.exists(depmap_json): + print(f"Dependency map JSON not found: {depmap_json}") + sys.exit(1) + + changed_files = get_changed_files(ref1, ref2) + if not changed_files: + print("No changed files detected.") + tests = [] + else: + file_to_executables = load_depmap(depmap_json) + tests = select_tests(file_to_executables, changed_files, filter_mode) + + with open(output_json, "w") as f: + json.dump({"tests_to_run": tests, "changed_files": sorted(changed_files)}, f, indent=2) + + print(f"Exported {len(tests)} tests to run to {output_json}") + +if __name__ == "__main__": + main() diff --git a/script/launch_tests.sh b/script/launch_tests.sh new file mode 100755 index 0000000000..829ac82378 --- /dev/null +++ b/script/launch_tests.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# Get the directory where the script is located +BUILD_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Go one level up to PACKAGE_HOME +PACKAGE_HOME="$(dirname "$BUILD_DIR")" + +SCRIPT_DIR="$PACKAGE_HOME/script/" + +# Search for build.ninja under PACKAGE_HOME +BUILD_NINJA_FILE="$PACKAGE_HOME/build/build.ninja" + +if [ -z "$BUILD_NINJA_FILE" ]; then + echo "Error: build.ninja not found under $PACKAGE_HOME" + exit 1 +fi + +python3 "$SCRIPT_DIR/dependency-parser/main.py" parse "$BUILD_NINJA_FILE" --workspace-root "$PACKAGE_HOME" + +# Get the directory containing build.ninja +BUILD_DIR=$(dirname "$BUILD_NINJA_FILE") + +# Path to enhanced_dependency_mapping.json in the same directory +JSON_FILE="$BUILD_DIR/enhanced_dependency_mapping.json" + +# Check if the JSON file exists +if [ ! -f "$JSON_FILE" ]; then + echo "Error: $JSON_FILE not found." + exit 1 +fi + +branch=$(git rev-parse --abbrev-ref HEAD) + +# Run the command +python3 "$SCRIPT_DIR/dependency-parser/main.py" select "$JSON_FILE" origin/develop $branch + +# Path to tests_to_run.json in the same directory +TEST_FILE="tests_to_run.json" + +command=$(python3 -c " +import json +import os +with open('$TEST_FILE', 'r') as f: + data = json.load(f) + tests = data.get('tests_to_run', []) + if tests: + # Extract just the filename after the last '/' + clean_tests = [os.path.basename(test) for test in tests] + print('ctest -R \"' + '|'.join(clean_tests) + '\"') + else: + print('# No tests to run') +") + +echo "$command" + +eval "$command" + +