diff --git a/.github/scripts/coverage_runner.py b/.github/scripts/coverage_runner.py new file mode 100644 index 000000000000..993245613f2c --- /dev/null +++ b/.github/scripts/coverage_runner.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +"""Run tests with coverage profiling and/or generate coverage reports. + +This is the report-time half of the coverage flow described in TheRock's coverage +design docs. It is driven by the ``coverage_metadata.json`` produced by +``export_coverage_metadata.py`` and can either: + + * run tests to produce profraw files (default), or + * skip tests and merge pre-collected profraw files (``--skip-tests``), which is + how the GitHub Actions flow uses it (tests run in the shared package-test + workflow and upload their profraw). + +Object and llvm tool locations are resolved by name within ``--build-dir`` so the +script works whether it is pointed at a full build/dist tree or a staged artifact. +""" +import argparse +import json +import logging +import os +import subprocess +from pathlib import Path + +logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + + +def find_tool(build_dir: Path, name: str) -> Path | None: + """Locate an executable ``name`` inside ``build_dir`` (prefer bin/ dirs).""" + candidates = [p for p in build_dir.rglob(name) if p.is_file()] + if not candidates: + return None + candidates.sort(key=lambda p: (0 if p.parent.name == "bin" else 1, len(str(p)))) + return candidates[0] + + +def resolve_objects(build_dir: Path, metadata: dict) -> list[Path]: + """Resolve coverage object files (libraries + test binaries) within build_dir.""" + objects: list[Path] = [] + + # Prefer the explicit relative paths recorded at export time, if present. + rel_objects = [] + for kind in ("libraries", "test_binaries"): + rel_objects.extend(metadata.get("coverage_objects", {}).get(kind, []) or []) + for rel in rel_objects: + candidate = build_dir / rel + if candidate.is_file(): + objects.append(candidate) + + # Fall back to resolving by basename (handles a different layout than export). + if not objects: + basenames = [] + for kind in ("libraries", "test_binaries"): + basenames.extend(metadata.get("object_basenames", {}).get(kind, []) or []) + for basename in basenames: + hits = [p for p in build_dir.rglob(f"{basename}*") if p.is_file()] + # Prefer concrete files (e.g. libhiprand.so.1.1) over bare symlinks, + # and the most specific (longest) name. + hits.sort(key=lambda p: (not p.is_symlink(), len(p.name)), reverse=True) + if hits: + objects.append(hits[0]) + + # De-duplicate while preserving order. + seen, unique = set(), [] + for obj in objects: + key = obj.resolve() + if key not in seen: + seen.add(key) + unique.append(obj) + return unique + + +def set_coverage_environment(metadata: dict, coverage_dir: Path): + pattern = metadata.get("llvm_profile_pattern", "%m") + profraw_dir = coverage_dir / "profraw" + profraw_dir.mkdir(parents=True, exist_ok=True) + profile_file = str(profraw_dir / f"{pattern}.profraw") + os.environ["LLVM_PROFILE_FILE"] = profile_file + logging.info("Set LLVM_PROFILE_FILE=%s", profile_file) + + +def run_tests(test_dir: Path, metadata: dict) -> int: + category = metadata.get("test_category", "") + cmd = [ + "ctest", + "--test-dir", + str(test_dir), + "--output-on-failure", + "--parallel", + "8", + "--timeout", + "7200", + ] + if category: + cmd += ["-L", category] + logging.info("Running tests: %s", " ".join(cmd)) + return subprocess.run(cmd).returncode + + +def merge_profraw_files(llvm_profdata: Path, profraw_dir: Path, out_path: Path) -> Path: + profraw_files = [str(p) for p in profraw_dir.rglob("*.profraw")] + if not profraw_files: + raise RuntimeError(f"No .profraw files found under {profraw_dir}") + logging.info("Merging %d profraw file(s)", len(profraw_files)) + subprocess.run( + [str(llvm_profdata), "merge", "-sparse", "-o", str(out_path), *profraw_files], + check=True, + ) + logging.info("Created %s", out_path) + return out_path + + +def generate_reports( + llvm_cov: Path, + llvm_cxxfilt: Path | None, + objects: list[Path], + ignore_regex: str, + profdata: Path, + coverage_dir: Path, + project: str, +): + object_args: list[str] = [] + for obj in objects: + object_args += ["-object", str(obj)] + ignore_args = [f"-ignore-filename-regex={ignore_regex}"] if ignore_regex else [] + + text_report = coverage_dir / f"code_cov_{project}.report" + logging.info("Generating text report -> %s", text_report) + with open(text_report, "w") as f: + subprocess.run( + [ + str(llvm_cov), + "report", + *object_args, + f"-instr-profile={profdata}", + *ignore_args, + ], + stdout=f, + check=True, + ) + print(text_report.read_text()) + + logging.info("Generating HTML report -> %s", coverage_dir) + show_cmd = [ + str(llvm_cov), + "show", + *object_args, + f"-instr-profile={profdata}", + *ignore_args, + "--format=html", + f"--output-dir={coverage_dir}", + ] + if llvm_cxxfilt is not None: + show_cmd.insert(2, f"-Xdemangler={llvm_cxxfilt}") + subprocess.run(show_cmd, check=True) + + lcov_file = coverage_dir / "coverage.info" + logging.info("Generating LCOV export -> %s", lcov_file) + with open(lcov_file, "w") as f: + subprocess.run( + [ + str(llvm_cov), + "export", + *object_args, + f"-instr-profile={profdata}", + *ignore_args, + "--format=lcov", + ], + stdout=f, + check=True, + ) + + +def main(): + parser = argparse.ArgumentParser(description="Run/aggregate coverage and report") + parser.add_argument( + "--build-dir", + type=Path, + required=True, + help="Tree to resolve coverage objects and llvm tools from", + ) + parser.add_argument( + "--metadata", + type=Path, + required=True, + help="coverage_metadata.json from export_coverage_metadata.py", + ) + parser.add_argument( + "--coverage-dir", type=Path, required=True, help="Output directory for reports" + ) + parser.add_argument( + "--profraw-dir", + type=Path, + default=None, + help="Directory of profraw files (default: /profraw)", + ) + parser.add_argument( + "--test-dir", + type=Path, + default=None, + help="ctest directory (default: --build-dir) when running tests", + ) + parser.add_argument( + "--skip-tests", + action="store_true", + help="Do not run tests; merge existing profraw files", + ) + args = parser.parse_args() + + with open(args.metadata) as f: + metadata = json.load(f) + project = metadata.get("project", "project") + logging.info("Coverage for project: %s", project) + + args.coverage_dir.mkdir(parents=True, exist_ok=True) + profraw_dir = args.profraw_dir or (args.coverage_dir / "profraw") + + # Resolve tooling. Tools link libLLVM and bundled sysdeps that sit next to + # them, so add their directory to LD_LIBRARY_PATH. + llvm_profdata = find_tool(args.build_dir, "llvm-profdata") + llvm_cov = find_tool(args.build_dir, "llvm-cov") + llvm_cxxfilt = find_tool(args.build_dir, "llvm-cxxfilt") + if llvm_profdata is None or llvm_cov is None: + raise FileNotFoundError( + f"llvm-profdata/llvm-cov not found under {args.build_dir}" + ) + tool_dirs = {str(t.parent) for t in (llvm_profdata, llvm_cov, llvm_cxxfilt) if t} + os.environ["LD_LIBRARY_PATH"] = os.pathsep.join( + [*tool_dirs, os.environ.get("LD_LIBRARY_PATH", "")] + ) + for t in (llvm_profdata, llvm_cov, llvm_cxxfilt): + if t: + t.chmod(0o755) + + objects = resolve_objects(args.build_dir, metadata) + if not objects: + raise RuntimeError("No coverage object files could be resolved.") + logging.info("Coverage objects: %s", ", ".join(str(o) for o in objects)) + + if not args.skip_tests: + set_coverage_environment(metadata, args.coverage_dir) + rc = run_tests(args.test_dir or args.build_dir, metadata) + if rc != 0: + logging.warning("Tests exited with %d; continuing with coverage.", rc) + + # Some test runners (e.g. GPU families whose machines have no matching GPU) + # run only host-side quick tests that never exercise the instrumented + # library, so they upload no profraw. With nothing to merge there is no + # coverage to report; treat that as a no-op success rather than failing the + # job, so the absence of data on one GPU family does not break CI. + if not list(profraw_dir.rglob("*.profraw")): + logging.warning( + "No .profraw files found under %s; skipping coverage report " + "(no instrumented test data was produced for this build).", + profraw_dir, + ) + return + + profdata = merge_profraw_files( + llvm_profdata, profraw_dir, args.coverage_dir / f"{project}.profdata" + ) + generate_reports( + llvm_cov, + llvm_cxxfilt, + objects, + metadata.get("ignore_filename_regex", ""), + profdata, + args.coverage_dir, + project, + ) + logging.info("Coverage generation complete.") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/export_coverage_metadata.py b/.github/scripts/export_coverage_metadata.py new file mode 100644 index 000000000000..74772d3cae0a --- /dev/null +++ b/.github/scripts/export_coverage_metadata.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +"""Export coverage metadata from a build tree to JSON for use in the report job. + +This is the build-time half of the coverage flow described in TheRock's coverage +design docs. It reads ``test_categories_coverage.yaml``, verifies the configured +coverage objects exist in the build/dist tree, and writes a ``coverage_metadata.json`` +that ``coverage_runner.py`` consumes later to merge profraw files and produce a report. + +Object/tool names are recorded by file name (basename). The report job locates the +actual files in whatever layout it has (build dist or staged artifact), so version +suffixes such as ``libhiprand.so.1.1`` are handled by the runner. +""" +import argparse +import json +import logging +import shutil +from pathlib import Path + +import yaml + +logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + + +def load_coverage_config(config_path: Path) -> dict: + with open(config_path) as f: + return yaml.safe_load(f) + + +def resolve_project_key(config: dict, project: str) -> str: + """Return the config key for ``project`` (case-insensitive).""" + projects = config.get("projects", {}) + if project in projects: + return project + lowered = project.lower() + if lowered in projects: + return lowered + raise ValueError( + f"Project '{project}' not found in coverage config " + f"(known: {sorted(projects)})" + ) + + +def find_by_basename(build_dir: Path, basename: str) -> list[str]: + """Find files in ``build_dir`` whose name starts with ``basename``. + + Matches versioned shared libraries (e.g. libhiprand.so -> libhiprand.so.1.1). + Returns paths relative to ``build_dir`` so the metadata is location independent. + """ + matches: list[str] = [] + for path in build_dir.rglob(f"{basename}*"): + if path.is_file(): + matches.append(str(path.relative_to(build_dir))) + return sorted(set(matches)) + + +def _looks_like_executable(path: Path) -> bool: + """Heuristic: a built test executable (ELF), not a source/object/CMake file.""" + if not path.is_file() or path.is_symlink(): + return False + if path.suffix: # test executables have no extension (test_foo, not test_foo.cpp) + return False + if "CMakeFiles" in path.parts: + return False + try: + with open(path, "rb") as f: + return f.read(4) == b"\x7fELF" + except OSError: + return False + + +def find_test_binaries(build_dir: Path, prefix: str, scope: str | None) -> list[Path]: + """Resolve instrumented test executables whose name starts with ``prefix``. + + Header-only libraries have no shared object to instrument; coverage instead + comes from the test binaries. ``scope`` (e.g. the component build subdir name + like ``rocPRIM``) restricts matches to that component so a shared group build + (PRIM builds rocprim + hipcub + rocthrust together) does not pull in the other + components' binaries. De-duplicates by basename, preferring the copy under a + ``test`` directory. + """ + by_name: dict[str, Path] = {} + for path in build_dir.rglob(f"{prefix}*"): + if not _looks_like_executable(path): + continue + # Match the component build subdir as a substring of a path part, so the + # dedicated test subproject TheRock generates for some components (e.g. + # rocPRIM's tests live under "rocPRIM_tests") is still scoped correctly. + if scope and not any(scope in part for part in path.parts): + continue + name = path.name + prev = by_name.get(name) + if prev is None or ("test" in path.parts and "test" not in prev.parts): + by_name[name] = path + return [by_name[k] for k in sorted(by_name)] + + +def export_metadata( + build_dir: Path, + project: str, + config_path: Path, + output_path: Path, + cmake_target: str | None = None, + stage_dir: Path | None = None, +): + config = load_coverage_config(config_path) + key = resolve_project_key(config, project) + project_config = config["projects"][key] + + if not project_config.get("enabled", False): + logging.info("Coverage disabled for %s; nothing to export.", key) + return + + objects = project_config.get("coverage_objects", {}) + + # Shared libraries are located by basename and staged separately (by the + # workflow) from dist/lib; the runner re-resolves them by basename. + found = {"libraries": [], "test_binaries": []} + for basename in objects.get("libraries", []) or []: + hits = find_by_basename(build_dir, basename) + if hits: + found["libraries"].extend(hits) + else: + logging.warning("Coverage object not found for libraries: %s", basename) + + # Header-only libraries: the configured test_binaries entries are name + # prefixes. Resolve them to the actual instrumented executables, then copy + # them into the staged coverage-objects dir so the report job can pass them + # to llvm-cov as -object. We record their paths relative to the stage dir so + # the runner finds them directly. + staged_test_binaries: list[str] = [] + staged_basenames: list[str] = [] + test_stage = (stage_dir / "test") if stage_dir is not None else None + total_bytes = 0 + for prefix in objects.get("test_binaries", []) or []: + binaries = find_test_binaries(build_dir, prefix, cmake_target) + if not binaries: + logging.warning( + "No instrumented test binaries found for prefix '%s' (scope=%s)", + prefix, + cmake_target, + ) + continue + logging.info( + "Resolved %d test binary(ies) for prefix '%s'", len(binaries), prefix + ) + for binary in binaries: + if test_stage is not None: + test_stage.mkdir(parents=True, exist_ok=True) + dest = test_stage / binary.name + if not dest.exists(): + shutil.copy2(binary, dest) + total_bytes += dest.stat().st_size + staged_test_binaries.append(f"test/{binary.name}") + else: + staged_test_binaries.append(str(binary.relative_to(build_dir))) + staged_basenames.append(binary.name) + found["test_binaries"] = sorted(set(staged_test_binaries)) + if test_stage is not None and staged_basenames: + logging.info( + "Staged %d test binary(ies) (%.1f MiB) into %s", + len(set(staged_basenames)), + total_bytes / (1024 * 1024), + test_stage, + ) + + metadata = { + "project": key, + "coverage_objects": found, + # Basenames are kept so the runner can re-resolve in a different layout. + "object_basenames": { + "libraries": objects.get("libraries", []) or [], + "test_binaries": sorted(set(staged_basenames)), + }, + "ignore_filename_regex": project_config.get("ignore_filename_regex", ""), + "llvm_profile_pattern": project_config.get("llvm_profile_pattern", "%m"), + "test_category": project_config.get("test_category", ""), + "llvm_tools": { + "llvm_profdata": "llvm-profdata", + "llvm_cov": "llvm-cov", + "llvm_cxxfilt": "llvm-cxxfilt", + }, + } + + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w") as f: + json.dump(metadata, f, indent=2) + + logging.info("Exported coverage metadata to %s", output_path) + logging.info(" Libraries found: %d", len(found["libraries"])) + logging.info(" Test binaries found: %d", len(found["test_binaries"])) + + +def main(): + parser = argparse.ArgumentParser(description="Export coverage metadata to JSON") + parser.add_argument( + "--build-dir", + type=Path, + required=True, + help="Build/dist tree to search for coverage objects", + ) + parser.add_argument( + "--project", + type=str, + required=True, + help="Project key or name (e.g. hiprand / HIPRAND)", + ) + parser.add_argument( + "--config", + type=Path, + required=True, + help="Path to test_categories_coverage.yaml", + ) + parser.add_argument( + "--output", type=Path, required=True, help="Output coverage_metadata.json path" + ) + parser.add_argument( + "--cmake-target", + type=str, + default=None, + help="Component build subdir name used to scope header-only test binaries " + "(e.g. rocPRIM), so a shared group build does not pull in siblings.", + ) + parser.add_argument( + "--stage-dir", + type=Path, + default=None, + help="Coverage-objects dir to copy resolved test binaries into " + "(header-only libs). Defaults to the output file's directory.", + ) + args = parser.parse_args() + stage_dir = args.stage_dir or args.output.parent + export_metadata( + args.build_dir, + args.project, + args.config, + args.output, + cmake_target=args.cmake_target, + stage_dir=stage_dir, + ) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/therock_configure_coverage.py b/.github/scripts/therock_configure_coverage.py new file mode 100644 index 000000000000..e169359f6a50 --- /dev/null +++ b/.github/scripts/therock_configure_coverage.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +import json +import logging +import os +from pathlib import Path + +from therock_matrix import collect_projects_to_run +from pr_detect_changed_subtrees import get_valid_prefixes, find_matched_subtrees +from config_loader import load_repo_config +from therock_configure_ci import get_modified_paths # reuse existing helper + +logging.basicConfig(level=logging.INFO) +SCRIPT_DIR = Path(__file__).resolve().parent + +# Coverage-enabled projects: project key -> (cmake_target, build_subdir, cmake_options) +# Only projects listed here will get coverage jobs. cmake_options pins the build +# to just this project so the coverage job does not inherit the (possibly merged) +# mega-group options that would otherwise build unrelated components. +COVERAGE_PROJECT_METADATA = { + "rocblas": ( + "rocBLAS", + "math-libs/rocBLAS", + "-DTHEROCK_ENABLE_BLAS=ON -DTHEROCK_ENABLE_ALL=OFF", + ), +} + + +def get_build_metadata(project_key: str, base_dir: str = "TheRock/build-coverage"): + """Get CMake target and build directory for a coverage-enabled project. + + Returns: + Tuple of (uppercase_name, cmake_target, build_dir, cmake_options) or None if not + coverage-enabled + """ + if project_key not in COVERAGE_PROJECT_METADATA: + return None + + cmake_target, build_subdir, cmake_options = COVERAGE_PROJECT_METADATA[project_key] + build_dir = f"{base_dir}/{build_subdir}/build" + return project_key.upper(), cmake_target, build_dir, cmake_options + + +def get_changed_subtrees_only(): + repo_config_path = SCRIPT_DIR / ".." / "repos-config.json" + config = load_repo_config(str(repo_config_path)) + valid_prefixes = get_valid_prefixes(config) + + base_ref = os.environ.get("BASE_REF", "HEAD^") + modified_paths = get_modified_paths(base_ref) + + matched_subtrees = find_matched_subtrees(modified_paths, valid_prefixes) + return matched_subtrees + + +def main(): + subtrees = get_changed_subtrees_only() + projects = collect_projects_to_run(subtrees) + + # Emit one INDEPENDENT coverage job per coverage-enabled project in each + # group. Several coverage-enabled projects can share a group (e.g. the rand + # group contains both rocrand and hiprand); each gets its own build -> test + # -> report pipeline so the components are tracked separately. + coverage_projects = [] + seen_projects = set() + for proj in projects: + pts_list = [p for p in proj.get("projects_to_test", "").split(",") if p] + + covered = [p for p in pts_list if p in COVERAGE_PROJECT_METADATA] + if not covered: + logging.info( + "Skipping group with tests %s - no coverage-enabled project", pts_list + ) + continue + + for project_key in covered: + if project_key in seen_projects: + continue # avoid duplicate jobs if a project appears in multiple groups + seen_projects.add(project_key) + + uppercase_name, cmake_target, build_dir, cmake_options = get_build_metadata( + project_key + ) + # Copy the group entry so each coverage project gets its own job. + entry = dict(proj) + entry["project_name"] = uppercase_name + entry["cmake_target"] = cmake_target + entry["build_dir"] = build_dir + # Pin to this project's own options so we don't build the merged + # mega-group (which pulls in unrelated components like hipdnn/providers). + entry["cmake_options"] = cmake_options + # Only run this project's own tests, so the test stage matches the + # pinned (single-project) build. + entry["projects_to_test"] = project_key + coverage_projects.append(entry) + + # Output for GitHub Actions + output = { + "coverage_projects": json.dumps(coverage_projects), + } + github_output = os.environ.get("GITHUB_OUTPUT") + if github_output: + with open(github_output, "a") as f: + for k, v in output.items(): + f.write(f"{k}={v}\n") + else: + logging.warning("GITHUB_OUTPUT not set; printing to stdout instead") + print(json.dumps(output)) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/therock-ci-coverage.yml b/.github/workflows/therock-ci-coverage.yml new file mode 100644 index 000000000000..5a4749e73b31 --- /dev/null +++ b/.github/workflows/therock-ci-coverage.yml @@ -0,0 +1,422 @@ +name: TheRock CI Linux Coverage + +on: + workflow_call: + inputs: + cmake_options: + type: string + project_name: + type: string + cmake_target: + type: string + build_dir: + type: string + amdgpu_families: + type: string + test_runs_on: + type: string + projects_to_test: + type: string + test_type: + type: string + # TheRock revision for the BUILD stage. Pinned to a commit that exports + # llvm-cov / llvm-profdata in the dist (a feature branch not yet on main). + therock_ref: + type: string + default: 6afcd5ac5298e9a5cdb3d2c45ae7605739ad2b94 + # TheRock revision for the TEST stage. Uses a current commit (which has + # build_tools/github_actions/cleanup_processes.sh) so the shared test flow + # does not fail in its post-job cleanup. The build ref above lacks that + # script, and current TheRock lacks the llvm export, so the two stages use + # different refs until the export work lands on main. + test_therock_ref: + type: string + default: 2d417fa49bc4e657bcbcf0ae549238784175f0fe + +permissions: + contents: read + +jobs: + therock-coverage-build: + name: Build Coverage (${{ inputs.project_name }} | ${{ inputs.amdgpu_families }}) + runs-on: azure-linux-scale-rocm + permissions: + id-token: write + container: + image: ghcr.io/rocm/therock_build_manylinux_x86_64@sha256:48492540591673fdc8d51beb89bde41e7ba13cbb528f643c0a481ba42c4058f2 + options: -v /runner/config:/home/awsconfig/ + env: + AMDGPU_FAMILIES: ${{ inputs.amdgpu_families }} + CACHE_DIR: ${{ github.workspace }}/.container-cache + CCACHE_CONFIGPATH: ${{ github.workspace }}/.ccache/ccache.conf + TEATIME_FORCE_INTERACTIVE: 0 + AWS_SHARED_CREDENTIALS_FILE: /home/awsconfig/credentials.ini + steps: + - name: "Checking out repository for rocm-libraries" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Checkout TheRock repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: "ROCm/TheRock" + path: TheRock + ref: ${{ inputs.therock_ref }} + + - name: Install python deps + run: | + pip install -r TheRock/requirements.txt + + - name: Adjust git config + run: | + git config --global --add safe.directory $PWD + git config fetch.parallel 10 + + - name: Setup ccache + run: | + ./TheRock/build_tools/setup_ccache.py \ + --config-preset "github-oss-dev" \ + --dir "$(dirname $CCACHE_CONFIGPATH)" \ + --local-path "$CACHE_DIR/ccache" + + - name: Runner health status + run: | + ./TheRock/build_tools/health_status.py + + - name: Pull DVC files for rocm-libraries # LOGNAME details here https://github.com/ROCm/rocm-libraries/pull/1617 + run: | + if command -v dvc &> /dev/null; then + echo "dvc detected" + else + echo "Warning, dvc not detected!" + fi + LOGNAME=github-runner dvc pull -v + + - name: Fetch sources + timeout-minutes: 30 + run: | + ./TheRock/build_tools/fetch_sources.py --jobs 12 --no-include-rocm-libraries --no-include-ml-frameworks ${{ contains(inputs.cmake_options, 'THEROCK_ENABLE_IREE_LIBS=ON') && '--include-iree-libs' || '' }} + + - name: Configure coverage build + env: + amdgpu_families: ${{ env.AMDGPU_FAMILIES }} + package_version: ADHOCBUILD + extra_cmake_options: >- + -DTHEROCK_ROCM_LIBRARIES_SOURCE_DIR=../ + -D${{ inputs.project_name }}_ENABLE_COVERAGE=ON + -DLLVM_TOOLS_SEARCH_PREFIX=${{ github.workspace }}/TheRock/build-coverage/dist/rocm/lib/llvm + ${{ inputs.cmake_options }} + BUILD_DIR: build-coverage + run: | + python3 TheRock/build_tools/github_actions/build_configure.py + + - name: Build therock-archives and therock-dist + run: cmake --build TheRock/build-coverage --target therock-archives therock-dist -- -k 0 + + - name: Stage coverage objects and llvm tools + run: | + set -euo pipefail + # Use $GITHUB_WORKSPACE (container path) rather than the github.workspace + # expression (host path) so the staged dir matches what upload-artifact reads. + STAGE="${GITHUB_WORKSPACE}/coverage-objects" + DIST="TheRock/build-coverage/dist/rocm" + LLVM_BIN="${DIST}/lib/llvm/bin" + LIB_NAME="lib$(echo '${{ inputs.cmake_target }}' | tr '[:upper:]' '[:lower:]')" + mkdir -p "${STAGE}/lib" "${STAGE}/bin" + # Instrumented shared libraries for the target component (used as llvm-cov -object). + find "${DIST}" -type f -name "${LIB_NAME}*.so*" -exec cp -av {} "${STAGE}/lib/" \; + # Version-matched coverage tooling, so the report job does not depend on the dist. + cp -av "${LLVM_BIN}/llvm-profdata" "${STAGE}/bin/" + cp -av "${LLVM_BIN}/llvm-cov" "${STAGE}/bin/" + # The llvm tools dynamically link libLLVM plus TheRock's bundled sysdeps + # (e.g. librocm_sysdeps_z). Stage the whole llvm lib dir and all sysdeps + # next to the tools so the report job can run them via LD_LIBRARY_PATH. + if [[ -d "${DIST}/lib/llvm/lib" ]]; then + find "${DIST}/lib/llvm/lib" -maxdepth 1 -name '*.so*' -exec cp -av {} "${STAGE}/bin/" \; + fi + # Note: do NOT use -type f here, otherwise the SONAME symlinks (e.g. + # librocm_sysdeps_z.so.1 -> librocm_sysdeps_z.so.1.3.2) are skipped and + # the loader cannot resolve the dependency. + find "${DIST}" -name 'libLLVM.so*' -exec cp -av {} "${STAGE}/bin/" \; + find "${DIST}" -name 'librocm_sysdeps_*.so*' -exec cp -av {} "${STAGE}/bin/" \; + # Demangler used by llvm-cov show for readable HTML. + cp -av "${LLVM_BIN}/llvm-cxxfilt" "${STAGE}/bin/" 2>/dev/null || true + echo "Staged coverage objects:" + ls -lR "${STAGE}" + echo "llvm-profdata dependencies (in build env):" + ldd "${STAGE}/bin/llvm-profdata" || true + + - name: Export coverage metadata + run: | + set -euo pipefail + python3 -m pip install --quiet pyyaml || true + # Written into the staged dir so it travels with the coverage objects. + python3 .github/scripts/export_coverage_metadata.py \ + --build-dir TheRock/build-coverage \ + --project "${{ inputs.project_name }}" \ + --config test_categories_coverage.yaml \ + --cmake-target "${{ inputs.cmake_target }}" \ + --output "${GITHUB_WORKSPACE}/coverage-objects/coverage_metadata.json" + cat "${GITHUB_WORKSPACE}/coverage-objects/coverage_metadata.json" + + - name: Upload coverage objects and llvm tools + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: coverage-objects-${{ inputs.project_name }}-${{ inputs.amdgpu_families }} + # Relative path resolves against the container workspace, matching the staging step. + path: coverage-objects + if-no-files-found: error + retention-days: 7 + + - name: Report + if: ${{ !cancelled() }} + run: | + echo "Full SDK du:" + echo "------------" + du -h -d 1 TheRock/build-coverage/dist/rocm + echo "Artifact Archives:" + echo "------------------" + ls -lh TheRock/build-coverage/artifacts/*.tar.xz + echo "Artifacts:" + echo "----------" + du -h -d 1 TheRock/build-coverage/artifacts + echo "CCache Stats:" + echo "-------------" + ccache -s -v + + - name: Configure AWS Credentials for non-forked repos + if: ${{ always() && !github.event.pull_request.head.repo.fork }} + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 + with: + aws-region: us-east-2 + role-to-assume: arn:aws:iam::692859939525:role/therock-ci-external + + - name: Post Build Upload + if: always() + run: | + python3 TheRock/build_tools/github_actions/post_build_upload.py \ + --run-id ${{ github.run_id }} \ + --artifact-group ${{ env.AMDGPU_FAMILIES }} \ + --build-dir TheRock/build-coverage \ + --upload + + # Run the standard package test flow against the coverage-instrumented build. + # Each sharded component test uploads its llvm profraw data (see + # therock-test-component.yml) which the report job below merges. + therock-coverage-test: + name: Test Coverage (${{ inputs.project_name }} | ${{ inputs.amdgpu_families }}) + needs: [therock-coverage-build] + if: ${{ inputs.test_runs_on != '' }} + uses: ./.github/workflows/therock-test-packages.yml + secrets: inherit + with: + therock_ref: ${{ inputs.test_therock_ref }} + projects_to_test: ${{ inputs.projects_to_test }} + amdgpu_families: ${{ inputs.amdgpu_families }} + test_runs_on: ${{ inputs.test_runs_on }} + platform: "linux" + test_type: ${{ inputs.test_type }} + coverage: true + + # Merge the profraw files from all test shards and produce the coverage report + # using the version-matched llvm tools staged by the build job. + therock-coverage-report: + name: Coverage Report (${{ inputs.project_name }} | ${{ inputs.amdgpu_families }}) + needs: [therock-coverage-test] + if: ${{ !cancelled() && needs.therock-coverage-test.result != 'skipped' }} + runs-on: azure-linux-scale-rocm + # Run in the build container so the staged llvm tools (and libLLVM) have a + # compatible base environment. + container: + image: ghcr.io/rocm/therock_build_manylinux_x86_64@sha256:48492540591673fdc8d51beb89bde41e7ba13cbb528f643c0a481ba42c4058f2 + env: + REPORT_ARTIFACT: coverage-report-${{ inputs.project_name }}-${{ inputs.amdgpu_families }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + steps: + # Check out the sources so llvm-cov can annotate the HTML report. The build + # embeds absolute source paths under the workspace, so checking out here (at + # the workspace root) makes those paths resolve. + - name: Checkout rocm-libraries (for source annotation) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Download coverage objects and llvm tools + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: coverage-objects-${{ inputs.project_name }}-${{ inputs.amdgpu_families }} + path: coverage-objects + + - name: Download coverage profraw artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + # Scope to THIS component's profraw (projects_to_test is pinned to the + # single coverage project) so multiple components on the same GPU + # family don't cross-contaminate each other's reports. + pattern: coverage-profraw-${{ inputs.amdgpu_families }}-${{ inputs.projects_to_test }}-* + path: coverage-profraw + merge-multiple: true + + - name: Merge profraw and generate coverage report + shell: bash + env: + AMDGPU_FAMILIES: ${{ inputs.amdgpu_families }} + PROJECT_NAME: ${{ inputs.project_name }} + CMAKE_TARGET: ${{ inputs.cmake_target }} + PROJECTS_TO_TEST: ${{ inputs.projects_to_test }} + BUILD_DIR: ${{ inputs.build_dir }} + BUILD_THEROCK_REF: ${{ inputs.therock_ref }} + TEST_THEROCK_REF: ${{ inputs.test_therock_ref }} + REPO: ${{ github.repository }} + COMMIT_SHA: ${{ github.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + OBJECTS_DIR="${GITHUB_WORKSPACE}/coverage-objects" + PROFRAW_DIR="${GITHUB_WORKSPACE}/coverage-profraw" + REPORT_DIR="${GITHUB_WORKSPACE}/coverage-report" + mkdir -p "${REPORT_DIR}" + METADATA="${OBJECTS_DIR}/coverage_metadata.json" + + # Drive merge + report from the build-time metadata. --skip-tests: the + # tests already ran in the package-test stage and uploaded their profraw. + python3 .github/scripts/coverage_runner.py \ + --build-dir "${OBJECTS_DIR}" \ + --metadata "${METADATA}" \ + --coverage-dir "${REPORT_DIR}" \ + --profraw-dir "${PROFRAW_DIR}" \ + --skip-tests + + # If no profraw was produced (e.g. a GPU family whose machines run only + # host-side tests that never exercise the instrumented library), the + # runner skips report generation and no coverage.info exists. Treat that + # as a clean no-op so the absence of data on one family does not fail CI. + if [[ ! -f "${REPORT_DIR}/coverage.info" ]]; then + echo "No coverage.info produced; skipping report post-processing for this build." + exit 0 + fi + + # Make lcov paths repo-relative so Codecov can map them to the source tree. + if [[ -f "${REPORT_DIR}/coverage.info" ]]; then + sed -i "s|${GITHUB_WORKSPACE}/||g" "${REPORT_DIR}/coverage.info" + fi + + # Collect details for the reference links file. Header-only components + # have no .so; their instrumented objects are the staged test binaries. + mapfile -t SOURCES < <(sed -n 's|^SF:||p' "${REPORT_DIR}/coverage.info" 2>/dev/null | sort -u) + mapfile -t OBJECTS < <( { find "${OBJECTS_DIR}/lib" -type f -name "*.so*" 2>/dev/null; find "${OBJECTS_DIR}/test" -type f 2>/dev/null; } | sort) + PROFRAW_COUNT=$(find "${PROFRAW_DIR}" -type f -name '*.profraw' 2>/dev/null | wc -l) + + # Generate the reference text file with all relevant paths and links. + LINKS_FILE="${REPORT_DIR}/coverage-report-links.txt" + { + echo "${PROJECT_NAME} Code Coverage - Paths and Links" + echo "==================================================" + echo "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "Repository: ${REPO}" + echo "Commit: ${COMMIT_SHA}" + echo "Pull request: ${GITHUB_SERVER_URL}/${REPO}/pull/${PR_NUMBER:-N/A}" + echo "Workflow run: ${RUN_URL}" + echo "GPU family: ${AMDGPU_FAMILIES}" + echo + echo "[Coverage config]" + echo " Config: test_categories_coverage.yaml -> coverage_metadata.json" + echo " Runner: .github/scripts/coverage_runner.py (--skip-tests)" + echo + echo "[TheRock - where ${PROJECT_NAME} was packed for coverage]" + echo " Build TheRock ref: ${BUILD_THEROCK_REF}" + echo " Test TheRock ref: ${TEST_THEROCK_REF}" + echo " Build dir: ${BUILD_DIR}" + echo + echo "[Instrumented code - source file(s)]" + for s in ${SOURCES[@]+"${SOURCES[@]}"}; do echo " ${s}"; done + echo + echo "[Instrumented code - object(s) used by llvm-cov]" + for o in ${OBJECTS[@]+"${OBJECTS[@]}"}; do echo " ${o#${GITHUB_WORKSPACE}/}"; done + echo + echo "[Libraries used by the coverage tooling]" + for f in $(find "${OBJECTS_DIR}/bin" -maxdepth 1 -type f | sort); do echo " ${f#${GITHUB_WORKSPACE}/}"; done + echo + echo "[Tests executed against the instrumented build]" + echo " projects_to_test: ${PROJECTS_TO_TEST}" + echo " Test driver: ctest (via .github/workflows/therock-test-component.yml)" + echo " profraw merged: ${PROFRAW_COUNT} file(s)" + echo " Test stage: ${RUN_URL} (job 'Test Coverage (${PROJECT_NAME} | ${AMDGPU_FAMILIES})')" + echo + echo "[Coverage report outputs]" + echo " HTML report: coverage-report/index.html" + echo " Text summary: coverage-report/code_cov_$(echo "${CMAKE_TARGET}" | tr '[:upper:]' '[:lower:]').report" + echo " LCOV: coverage-report/coverage.info" + echo " Merged profile: coverage-report/$(echo "${CMAKE_TARGET}" | tr '[:upper:]' '[:lower:]').profdata" + echo + echo "[Report upload / download]" + echo " Artifact name: ${REPORT_ARTIFACT}" + echo " Download from: ${RUN_URL} (Artifacts section)" + echo " Codecov: https://app.codecov.io/gh/${REPO}/pull/${PR_NUMBER:-} (flag: ${CMAKE_TARGET})" + } > "${LINKS_FILE}" + + echo "----- ${LINKS_FILE} -----" + cat "${LINKS_FILE}" + + - name: Upload coverage report + if: ${{ always() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ env.REPORT_ARTIFACT }} + path: coverage-report + if-no-files-found: warn + # Keep the shareable report around well beyond the build inputs. + retention-days: 90 + + - name: Upload coverage to Codecov + id: codecov + if: ${{ always() }} + continue-on-error: true + shell: bash + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + run: | + set -x + # The standalone Codecov CLI binary is built against a newer glibc than + # the manylinux build container provides (it failed to load libpython + # with "GLIBC_2.29 not found"). Install via pip so it runs under the + # container's own Python interpreter instead. + if ! python3 -m pip install --quiet codecov-cli; then + echo "Could not install codecov-cli; skipping upload." + echo "uploaded=false" >> "${GITHUB_OUTPUT}" + exit 0 + fi + if [[ -z "${CODECOV_TOKEN:-}" ]]; then + echo "CODECOV_TOKEN is empty (expected for fork PRs, which do not receive" + echo "repository secrets); attempting a tokenless upload (public repo)." + fi + if codecovcli upload-process \ + --disable-search \ + --file coverage-report/coverage.info \ + --flag "${{ inputs.cmake_target }}" \ + --name "${{ inputs.cmake_target }}-${{ inputs.amdgpu_families }}" \ + --slug "${{ github.repository }}"; then + echo "uploaded=true" >> "${GITHUB_OUTPUT}" + else + echo "Codecov upload failed (non-fatal)." + echo "uploaded=false" >> "${GITHUB_OUTPUT}" + fi + + - name: Coverage report summary + if: ${{ always() }} + shell: bash + run: | + { + echo "### ${{ inputs.project_name }} coverage (${{ inputs.amdgpu_families }})" + echo "" + report_file="$(find coverage-report -maxdepth 1 -name 'code_cov_*.report' 2>/dev/null | head -1)" + if [[ -n "${report_file}" ]]; then + echo '```' + tail -n 3 "${report_file}" + echo '```' + fi + echo "- Report artifact: \`${REPORT_ARTIFACT}\` (download from [run artifacts](${RUN_URL}))" + if [[ "${{ steps.codecov.outputs.uploaded }}" == "true" ]]; then + echo "- Codecov: https://app.codecov.io/gh/${{ github.repository }}/pull/${{ github.event.pull_request.number }} (flag: ${{ inputs.cmake_target }})" + else + echo "- Codecov: upload unavailable for this run (fork PRs do not receive CODECOV_TOKEN, and tokenless upload is disabled for this repo). Use the report artifact above; Codecov populates when run in a non-fork context." + fi + # tee so the summary is also visible in this step's log, not only the job summary page. + } | tee -a "${GITHUB_STEP_SUMMARY}" diff --git a/.github/workflows/therock-ci-linux.yml b/.github/workflows/therock-ci-linux.yml index 0948b1e4ee59..4e1ef73f2f48 100644 --- a/.github/workflows/therock-ci-linux.yml +++ b/.github/workflows/therock-ci-linux.yml @@ -25,6 +25,7 @@ on: build_runs_on: type: string + permissions: contents: read @@ -160,8 +161,6 @@ jobs: therock-test-linux: name: Test (${{ inputs.amdgpu_families }}) needs: [therock-build-linux] - # TODO(TheRock#3288): Re-enable gfx950-dcgpu runners once we get more capacity - if: ${{ inputs.amdgpu_families != 'gfx950-dcgpu' }} uses: ./.github/workflows/therock-test-packages.yml with: therock_ref: ${{ inputs.therock_ref }} diff --git a/.github/workflows/therock-ci.yml b/.github/workflows/therock-ci.yml index c2379f99d73d..c53844afae77 100644 --- a/.github/workflows/therock-ci.yml +++ b/.github/workflows/therock-ci.yml @@ -53,6 +53,7 @@ jobs: test_type: ${{ steps.linux_projects.outputs.test_type }} linux_package_targets: ${{ steps.configure_linux.outputs.package_targets }} windows_package_targets: ${{ steps.configure_windows.outputs.package_targets }} + coverage_projects: ${{ steps.coverage_projects.outputs.coverage_projects }} steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -114,6 +115,13 @@ jobs: run: | python .github/scripts/therock_configure_ci.py + - name: Determine Linux projects for coverage (changed subtrees only) + id: coverage_projects + env: + PLATFORM: "linux" + run: | + python .github/scripts/therock_configure_coverage.py + - name: Determine Windows projects to run id: windows_projects env: @@ -145,7 +153,7 @@ jobs: contents: read id-token: write needs: setup - if: ${{ needs.setup.outputs.linux_projects != '[]' }} + if: ${{ false && needs.setup.outputs.linux_projects != '[]' }} strategy: fail-fast: false matrix: @@ -170,7 +178,7 @@ jobs: contents: read id-token: write needs: setup - if: ${{ needs.setup.outputs.windows_projects != '[]' }} + if: ${{ false && needs.setup.outputs.windows_projects != '[]' }} strategy: fail-fast: false matrix: @@ -188,6 +196,31 @@ jobs: test_runs_on: ${{ matrix.target_bundle.test_machine }} build_runs_on: ${{ needs.setup.outputs.windows_build_runs_on }} + therock-ci-coverage: + name: Coverage (${{ matrix.projects.project_name }} | ${{ matrix.target_bundle.amdgpu_family }}) + permissions: + contents: read + id-token: write + needs: setup + if: ${{ needs.setup.outputs.coverage_projects != '[]' }} + strategy: + fail-fast: false + matrix: + projects: ${{ fromJSON(needs.setup.outputs.coverage_projects) }} + target_bundle: ${{ fromJSON(needs.setup.outputs.linux_package_targets) }} + uses: ./.github/workflows/therock-ci-coverage.yml + secrets: inherit + with: + cmake_options: ${{ matrix.projects.cmake_options }} + project_name: ${{ matrix.projects.project_name }} + cmake_target: ${{ matrix.projects.cmake_target }} + build_dir: ${{ matrix.projects.build_dir }} + amdgpu_families: ${{ matrix.target_bundle.amdgpu_family }} + test_runs_on: ${{ matrix.target_bundle.test_machine }} + projects_to_test: ${{ matrix.projects.projects_to_test }} + test_type: ${{ needs.setup.outputs.test_type }} + + therock_ci_summary: name: TheRock CI Summary if: always() @@ -195,6 +228,7 @@ jobs: - setup - therock-ci-linux - therock-ci-windows + - therock-ci-coverage runs-on: ubuntu-24.04 steps: - name: Output failed jobs diff --git a/.github/workflows/therock-test-component.yml b/.github/workflows/therock-test-component.yml index b37bb11739ca..4e455a0c093b 100644 --- a/.github/workflows/therock-test-component.yml +++ b/.github/workflows/therock-test-component.yml @@ -17,6 +17,10 @@ on: type: string component: type: string + coverage: + description: "When true, collect llvm profraw data from the test run and upload it as an artifact." + type: boolean + default: false permissions: @@ -106,6 +110,14 @@ jobs: run: | python ./build_tools/print_driver_gpu_info.py + - name: Prepare coverage profile directory + if: ${{ inputs.coverage }} + run: | + # Use $GITHUB_WORKSPACE (container path) so the dir matches what + # upload-artifact reads. Instrumented binaries emit one profraw file + # per process/binary here (%p/%m); non-instrumented binaries ignore it. + mkdir -p "${GITHUB_WORKSPACE}/coverage-profraw" + echo "LLVM_PROFILE_FILE=${GITHUB_WORKSPACE}/coverage-profraw/${TEST_COMPONENT}-shard${{ matrix.shard }}-%p-%m.profraw" >> "${GITHUB_ENV}" # Set thread limits from KUBE_CPU_REQUEST (Kubernetes-injected) to avoid oversubscription. # Must be done at runtime since container env vars aren't in GitHub's env context. - name: Set thread limits @@ -133,6 +145,16 @@ jobs: python ./build_tools/memory_monitor.py --phase "Test ${{ fromJSON(inputs.component).job_name }}" -- \ bash -c '${{ fromJSON(inputs.component).test_script }}' 2>&1 | tee ./test_logs/test_output.log + - name: Upload coverage profraw + if: ${{ always() && inputs.coverage }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: coverage-profraw-${{ inputs.amdgpu_families }}-${{ fromJSON(inputs.component).job_name }}-shard${{ matrix.shard }} + # Relative path resolves against the container workspace, matching the prepare step. + path: coverage-profraw/*.profraw + if-no-files-found: warn + retention-days: 7 + - name: Notify Teams Channel on Failure if: ${{ failure() && !github.event.pull_request.head.repo.fork }} run: | diff --git a/.github/workflows/therock-test-packages.yml b/.github/workflows/therock-test-packages.yml index 8f290a8e82f3..330192f3b8db 100644 --- a/.github/workflows/therock-test-packages.yml +++ b/.github/workflows/therock-test-packages.yml @@ -16,6 +16,10 @@ on: type: string test_type: type: string + coverage: + description: "When true, collect llvm profraw data from the test runs for a coverage report." + type: boolean + default: false permissions: contents: read @@ -98,4 +102,5 @@ jobs: }} platform: ${{ inputs.platform }} component: ${{ toJSON(matrix.components) }} + coverage: ${{ inputs.coverage }} secrets: inherit diff --git a/projects/rocblas/CMakeLists.txt b/projects/rocblas/CMakeLists.txt index f39e3516e539..e4194443bb57 100644 --- a/projects/rocblas/CMakeLists.txt +++ b/projects/rocblas/CMakeLists.txt @@ -328,6 +328,11 @@ if( BUILD_CLIENTS_SAMPLES OR BUILD_CLIENTS_TESTS OR BUILD_CLIENTS_BENCHMARKS ) endif() endif() +# CI passes -DROCBLAS_ENABLE_COVERAGE=ON (the standardized coverage flag); it is +# gated directly at the instrumentation site (library/) without toggling the +# shared BUILD_CODE_COVERAGE / ROCBLAS_BUILD_COVERAGE variables. +option(ROCBLAS_ENABLE_COVERAGE "Enable code coverage" OFF) + if( NOT SKIP_LIBRARY ) add_subdirectory( library ) endif() diff --git a/projects/rocblas/library/CMakeLists.txt b/projects/rocblas/library/CMakeLists.txt index cb87d6107316..a6b1103a6b79 100644 --- a/projects/rocblas/library/CMakeLists.txt +++ b/projects/rocblas/library/CMakeLists.txt @@ -140,7 +140,7 @@ function( rocblas_library_settings lib_target_ ) set_target_properties( ${lib_target_} PROPERTIES PREFIX "lib" ) endif() - if(BUILD_CODE_COVERAGE) + if(ROCBLAS_ENABLE_COVERAGE) target_compile_options(${lib_target_} PRIVATE -fprofile-instr-generate -fcoverage-mapping) target_link_options(${lib_target_} PRIVATE -fprofile-instr-generate) endif() diff --git a/test_categories_coverage.yaml b/test_categories_coverage.yaml new file mode 100644 index 000000000000..41aa3aaed7ac --- /dev/null +++ b/test_categories_coverage.yaml @@ -0,0 +1,26 @@ +# Coverage configuration for rocm-libraries projects. +# +# Defines which projects have coverage enabled and the metadata used to drive +# the coverage report (see .github/scripts/export_coverage_metadata.py and +# .github/scripts/coverage_runner.py). +# +# Notes: +# * `libraries` / `test_binaries` are matched by file name (basename); the +# export/runner scripts locate the actual instrumented files in the build +# tree, so version suffixes (e.g. libhiprand.so.1.1) are handled +# automatically. +# * `ignore_filename_regex` is passed to `llvm-cov` as -ignore-filename-regex. +# * `llvm_profile_pattern` is the LLVM_PROFILE_FILE pattern; %m keeps one file +# per instrumented binary so parallel tests do not clobber each other. +# * `test_category` is the ctest label coverage tests run under. +projects: + rocblas: + enabled: true + coverage_objects: + # Coverage is collected from the rocBLAS library. + libraries: + - librocblas.so + test_binaries: [] + ignore_filename_regex: '.*/test/.*|.*/tests/.*|.*/clients/.*|.*[Tt]ensile.*|.*/_deps/.*|.*deps.*' + llvm_profile_pattern: '%m' + test_category: quick