Files
TheRock/rocm-systems/projects/rocprofiler-systems/tests/pytest/rocprofsys/validators.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

491 lines
15 KiB
Python

# Copyright (c) Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""
Output validators for rocprofiler-systems test results.
This module wraps the existing validation scripts from the tests/ directory:
- validate-perfetto-proto.py
- validate-rocpd.py
- validate-timemory-json.py
- validate-causal-json.py
We also provide the following validators:
- validate_file_exists
"""
from __future__ import annotations
import os
import re
import shlex
import shutil
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Optional
def _python_for_validation_scripts() -> str:
"""Return the Python executable to use when running validation scripts.
When running inside a PyInstaller/frozen binary, sys.executable is the
binary itself, which does not accept script paths and validation args.
Use system Python instead.
"""
if getattr(sys, "frozen", False):
env_py = os.environ.get("ROCPROFSYS_VALIDATION_PYTHON")
if env_py:
return env_py
path = shutil.which("python3") or shutil.which("python")
return path or "python3"
return sys.executable
@dataclass
class ValidationResult:
"""Result of a validation operation.
Attributes:
is_valid: Whether the validation passed
message: Description of result or error
details: Additional details (e.g., query results)
stdout: Standard output from validation script
stderr: Standard error from validation script
command: The command that was executed
"""
is_valid: bool
message: str
details: Optional[dict[str, Any]] = None
stdout: str = ""
stderr: str = ""
command: str = ""
ROCPROFSYS_ABORT_FAIL_REGEX = [
r"### ERROR ###",
r"unknown-hash=",
r"address of faulting memory reference",
r"exiting with non-zero exit code",
r"terminate called after throwing an instance",
r"calling abort\.\. in ",
r"Exit code: [1-9]",
r"Failed to open Symtab for",
]
from rocprofsys.runners import TestResult
def _validate_regex(
text: str,
pass_regex: Optional[list[str]] = None,
fail_regex: Optional[list[str]] = None,
use_abort_fail_regex: bool = False,
) -> ValidationResult:
"""Validate the regex patterns in some given text.
Args:
text: Text to validate
pass_regex: Optional list of regex patterns that must be found for success
fail_regex: Optional list of regex patterns that must NOT be found
use_abort_fail_regex: Whether to validate against ROCPROFSYS_ABORT_FAIL_REGEX (default: True)
Returns:
ValidationResult with is_valid=True if all patterns pass, False otherwise
"""
# Build fail regex list
fail_patterns: list[str] = []
if fail_regex:
fail_patterns.extend(fail_regex)
if use_abort_fail_regex:
fail_patterns.extend(ROCPROFSYS_ABORT_FAIL_REGEX)
if not fail_patterns and not pass_regex:
return ValidationResult(is_valid=True, message="No patterns to validate")
# Use re.DOTALL so '.' matches newlines (like CMake regex behavior)
flags = re.DOTALL
# Fail patterns: one combined alternation, short-circuit on first hit
if fail_patterns:
fail_re = re.compile(
"|".join(f"(?P<f{i}>{p})" for i, p in enumerate(fail_patterns)), flags
)
m = fail_re.search(text)
if m is not None:
idx = int(m.lastgroup[1:])
return ValidationResult(
is_valid=False,
message=f"Fail pattern matched: {fail_patterns[idx]}",
)
# Pass patterns: individual re.search per pattern
if pass_regex:
for pattern in pass_regex:
if re.search(pattern, text, flags) is None:
return ValidationResult(
is_valid=False,
message=f"Pass pattern not found: {pattern}",
)
return ValidationResult(is_valid=True, message="All patterns validated successfully")
def validate_regex(
test_result: TestResult,
pass_regex: Optional[list[str]] = None,
fail_regex: Optional[list[str]] = None,
use_abort_fail_regex: bool = True,
) -> ValidationResult:
return _validate_regex(
test_result.test_output, pass_regex, fail_regex, use_abort_fail_regex
)
def validate_file_regex(
file_path: Path,
pass_regex: Optional[list[str]] = None,
fail_regex: Optional[list[str]] = None,
use_abort_fail_regex: bool = True,
) -> ValidationResult:
if not file_path.exists():
return ValidationResult(False, f"File not found: {file_path}")
with open(file_path, "r") as f:
text = f.read()
return _validate_regex(text, pass_regex, fail_regex, use_abort_fail_regex)
def validate_file_exists(path: Path, description: str = "File") -> ValidationResult:
"""Validate that a file exists and is non-empty.
Args:
path: Path to check
description: Description for error messages
Returns:
ValidationResult
"""
if not path.exists():
return ValidationResult(False, f"{description} not found: {path}")
if path.stat().st_size == 0:
return ValidationResult(False, f"{description} is empty: {path}")
return ValidationResult(True, f"{description} exists: {path}")
def _run_validation_script(
script_name: str,
args: list[str],
tests_dir: Path,
timeout: int = 60,
) -> ValidationResult:
"""Run an existing validation script from the tests directory.
Args:
script_name: Name of the script (e.g., 'validate-perfetto-proto.py')
args: Arguments to pass to the script
tests_dir: Path to directory containing validation scripts
timeout: Timeout in seconds
Returns:
ValidationResult with script output
"""
script_path = tests_dir / script_name
if not script_path.exists():
return ValidationResult(False, f"Validation script not found: {script_path}")
python_exe = _python_for_validation_scripts()
cmd = [python_exe, str(script_path)] + args
cmd_str = " ".join(shlex.quote(arg) for arg in cmd)
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
)
if result.returncode == 0:
message = result.stdout.strip()
else:
message = (
result.stderr.strip()
or result.stdout.strip()
or f"Exit code: {result.returncode}"
)
return ValidationResult(
is_valid=(result.returncode == 0),
message=message,
stdout=result.stdout,
stderr=result.stderr,
command=cmd_str,
)
except subprocess.TimeoutExpired:
return ValidationResult(
False, f"Validation timed out after {timeout}s", command=cmd_str
)
except Exception as e:
return ValidationResult(False, f"Validation error: {e}", command=cmd_str)
# ============================================================================
# Perfetto Validation - wraps validate-perfetto-proto.py
# ============================================================================
def validate_perfetto_trace(
trace_path: Path,
tests_dir: Path,
categories: Optional[list[str]] = None,
labels: Optional[list[str]] = None,
counts: Optional[list[int]] = None,
depths: Optional[list[int]] = None,
label_substrings: Optional[list[str]] = None,
counter_names: Optional[list[str]] = None,
key_names: Optional[list[str]] = None,
key_counts: Optional[list[int]] = None,
trace_processor_path: Optional[Path] = None,
print_output: bool = False,
check_counter_pairing: bool = False,
timeout: int = 120,
) -> ValidationResult:
"""Validate a Perfetto trace file using validate-perfetto-proto.py.
Slice validation mode is inferred by validate-perfetto-proto.py: pass ``depths``
(-d) for positional row-by-row checks; omit ``depths`` for aggregate-by-name
(sum counts across depths). Omit ``counts`` (-c) in aggregate mode for
presence-only checks.
Args:
trace_path: Path to perfetto-trace.proto file
tests_dir: Path to directory containing validation scripts
categories: List of categories to filter by (-m flag)
labels: Expected labels (-l flag)
counts: Expected counts (-c flag)
depths: Expected depths (-d flag); omit for aggregate-by-name validation
label_substrings: Expected label substrings (-s flag)
counter_names: Counter names to validate (--counter-names flag)
key_names: Debug key names to check (--key-names flag)
key_counts: Expected counts for debug keys (--key-counts flag)
trace_processor_path: Path to trace_processor_shell (-t flag)
print_output: Whether to print trace data (-p flag)
check_counter_pairing: Verify counter tracks have paired start/end entries
timeout: Validation timeout in seconds
Returns:
ValidationResult with validation status
"""
if not trace_path.exists():
return ValidationResult(False, f"Trace file not found: {trace_path}")
# Allow override of trace_processor_path to allow perfetto validation using older GLIBC versions
env_path = os.environ.get("ROCPROFSYS_TRACE_PROC_SHELL")
if env_path:
trace_processor_path = Path(env_path)
args = ["-i", str(trace_path)]
if categories:
args.extend(["-m"] + categories)
if labels:
args.extend(["-l"] + labels)
elif label_substrings:
args.extend(["-s"] + label_substrings)
if counts:
args.extend(["-c"] + [str(c) for c in counts])
if depths:
args.extend(["-d"] + [str(d) for d in depths])
if counter_names:
args.extend(["--counter-names"] + counter_names)
if check_counter_pairing:
args.append("--check-counter-pairing")
if key_names:
args.extend(["--key-names"] + key_names)
if key_counts:
args.extend(["--key-counts"] + [str(k) for k in key_counts])
if trace_processor_path:
args.extend(["-t", str(trace_processor_path)])
if print_output:
args.append("-p")
return _run_validation_script("validate-perfetto-proto.py", args, tests_dir, timeout)
# ============================================================================
# ROCpd Database Validation - wraps validate-rocpd.py
# ============================================================================
def validate_rocpd_database(
db_path: Path,
tests_dir: Path,
rules_files: Optional[list[Path]] = None,
timeout: int = 60,
gpu_category_to_skip: Optional[list[str]] = None,
) -> ValidationResult:
"""Validate a ROCpd database file using validate-rocpd.py.
Args:
db_path: Path to rocpd.db file
tests_dir: Path to directory containing validation scripts
rules_files: List of JSON rules files to use for validation
timeout: Validation timeout in seconds
gpu_category_to_skip: GPU categories to skip tagged validation queries for
(instinct, radeon, apu). Omit or pass empty to run all queries
Returns:
ValidationResult with validation status
"""
if not db_path.exists():
return ValidationResult(False, f"Database not found: {db_path}")
args = ["-db", str(db_path)]
if rules_files:
existing_rules = [str(r) for r in rules_files if r.exists()]
if existing_rules:
args.extend(["-r"] + existing_rules)
if gpu_category_to_skip:
args.extend(["--gpu-category-to-skip"] + gpu_category_to_skip)
return _run_validation_script("validate-rocpd.py", args, tests_dir, timeout)
# ============================================================================
# Timemory JSON Validation - wraps validate-timemory-json.py
# ============================================================================
def validate_timemory_json(
json_path: Path,
tests_dir: Path,
metric: str,
labels: Optional[list[str]] = None,
counts: Optional[list[int]] = None,
depths: Optional[list[int]] = None,
print_output: bool = False,
timeout: int = 60,
) -> ValidationResult:
"""Validate a timemory JSON output file using validate-timemory-json.py.
Args:
json_path: Path to JSON file
metric: Metric name to validate (-m flag)
tests_dir: Path to directory containing validation scripts
labels: Expected labels (-l flag)
counts: Expected counts (-c flag)
depths: Expected depths (-d flag)
print_output: Whether to print data (-p flag)
timeout: Validation timeout in seconds
Returns:
ValidationResult with validation status
"""
if not json_path.exists():
return ValidationResult(False, f"JSON file not found: {json_path}")
args = ["-i", str(json_path), "-m", metric]
if labels:
args.extend(["-l"] + labels)
if counts:
args.extend(["-c"] + [str(c) for c in counts])
if depths:
args.extend(["-d"] + [str(d) for d in depths])
if print_output:
args.append("-p")
return _run_validation_script("validate-timemory-json.py", args, tests_dir, timeout)
# ============================================================================
# Causal JSON Validation - wraps validate-causal-json.py
# ============================================================================
def validate_causal_json(
json_path: Path,
tests_dir: Path,
ci_mode: bool = False,
additional_args: Optional[list[str]] = None,
timeout: int = 60,
) -> ValidationResult:
"""Validate a causal profiling JSON output file using validate-causal-json.py.
Args:
json_path: Path to causal JSON file
tests_dir: Path to directory containing validation scripts
ci_mode: Whether running in CI mode (--ci flag)
additional_args: Additional arguments to pass to the script
timeout: Validation timeout in seconds
Returns:
ValidationResult with validation status
"""
if not json_path.exists():
return ValidationResult(False, f"JSON file not found: {json_path}")
args = ["-i", str(json_path)]
if ci_mode:
args.append("--ci")
if additional_args:
args.extend(additional_args)
return _run_validation_script("validate-causal-json.py", args, tests_dir, timeout)
def validate_unified_memory_outputs(
output_dir: Path,
tests_dir: Path,
timeout: int = 60,
) -> ValidationResult:
"""Validate unified-memory text and JSON outputs in a test output tree."""
txt_matches = sorted(output_dir.rglob("unified_memory*.txt"))
json_matches = sorted(output_dir.rglob("unified_memory*.json"))
if not txt_matches:
return ValidationResult(False, f"No unified_memory*.txt found under {output_dir}")
if not json_matches:
return ValidationResult(
False, f"No unified_memory*.json found under {output_dir}"
)
txt_file = txt_matches[0]
json_file = json_matches[0]
if txt_file.parent != json_file.parent:
return ValidationResult(
False,
"Unified-memory outputs landed in different directories: "
f"{txt_file.parent} vs {json_file.parent}",
)
return _run_validation_script(
"validate-unified-memory.py",
["--output-dir", str(txt_file.parent)],
tests_dir,
timeout,
)