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
2482 lines
87 KiB
Python
2482 lines
87 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright Advanced Micro Devices, Inc.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import logging
|
|
import os
|
|
import platform
|
|
import re
|
|
import resource
|
|
import shlex
|
|
import shutil
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import sysconfig
|
|
import time
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
from typing import Dict, List, NoReturn, Optional, Set, Tuple, Union
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO, format="%(asctime)s - %(levelname)s: %(message)s"
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Status symbols used throughout.
|
|
STATUS_PASS = "[✓]"
|
|
STATUS_FAIL = "[X]"
|
|
STATUS_WARN = "[!]"
|
|
STATUS_FLAKY = "[~]"
|
|
|
|
# Test result categories.
|
|
TEST_CATEGORIES = [
|
|
"PASS",
|
|
"FAIL",
|
|
"ERROR",
|
|
"UNRESOLVED",
|
|
"TIMEOUT",
|
|
"UNTESTED",
|
|
"UNSUPPORTED",
|
|
"XFAIL",
|
|
"KFAIL",
|
|
"FLAKY", # Tests that failed initially but passed on retry.
|
|
]
|
|
|
|
# Default wall-clock timeout (seconds) for each `make check` invocation in
|
|
# --one-by-one mode when --one-by-one-test-timeout is not provided.
|
|
ONE_BY_ONE_DEFAULT_WALL_CLOCK_TIMEOUT = 3600
|
|
|
|
# Sanitizers we pre-configure on every run. If rocgdb wasn't built with the
|
|
# corresponding -fsanitize=..., the matching *_OPTIONS env var is silently
|
|
# ignored at runtime, so listing extras here is harmless.
|
|
SANITIZERS = ("asan", "tsan", "ubsan", "msan", "lsan", "hwasan")
|
|
|
|
# Patterns for parsing DejaGnu .sum lines.
|
|
RESULT_LINE_RE = re.compile(
|
|
r"^(PASS|FAIL|XFAIL|UNTESTED|UNSUPPORTED|KFAIL|UNRESOLVED): (.+)$"
|
|
)
|
|
TIMEOUT_RE = re.compile(r"\(timeout\)")
|
|
RUNNING_EXP_RE = re.compile(r"Running\s+(\S+\.exp)")
|
|
|
|
# Category display configuration: prefix symbol.
|
|
CATEGORY_DISPLAY = {
|
|
"PASS": STATUS_PASS,
|
|
"FAIL": STATUS_FAIL,
|
|
"ERROR": STATUS_FAIL,
|
|
"UNRESOLVED": STATUS_FAIL,
|
|
"TIMEOUT": STATUS_WARN,
|
|
"UNTESTED": STATUS_WARN,
|
|
"UNSUPPORTED": STATUS_WARN,
|
|
"XFAIL": STATUS_WARN,
|
|
"KFAIL": STATUS_WARN,
|
|
"FLAKY": STATUS_FLAKY,
|
|
}
|
|
|
|
|
|
def _log_error_and_exit(message: str, exit_code: int = 1) -> NoReturn:
|
|
"""
|
|
Log an error message and exit the program.
|
|
|
|
Args:
|
|
message: Error message to log.
|
|
exit_code: Exit code (default 1).
|
|
"""
|
|
logger.error(f"{STATUS_FAIL} Error: {message}")
|
|
sys.exit(exit_code)
|
|
|
|
|
|
def _run_command(
|
|
cmd: List[str],
|
|
cwd: Optional[Path] = None,
|
|
env: Optional[Dict[str, str]] = None,
|
|
capture_output: bool = False,
|
|
check: bool = True,
|
|
error_msg: Optional[str] = None,
|
|
timeout: Optional[int] = None,
|
|
kill_process_group: bool = False,
|
|
) -> subprocess.CompletedProcess:
|
|
"""
|
|
Run a command with consistent error handling.
|
|
|
|
Args:
|
|
cmd: Command and arguments to execute.
|
|
cwd: Working directory for command execution.
|
|
env: Environment variables for command.
|
|
capture_output: Whether to capture stdout/stderr.
|
|
check: Whether to raise on non-zero exit.
|
|
error_msg: Custom error message on failure.
|
|
timeout: Optional wall-clock timeout in seconds; raises
|
|
subprocess.TimeoutExpired if exceeded.
|
|
kill_process_group: Run the command in a new process group/session and,
|
|
on TimeoutExpired, SIGKILL the entire group so descendants
|
|
(e.g. runtest/expect/gdb spawned by `make`) are not orphaned.
|
|
|
|
Returns:
|
|
CompletedProcess result.
|
|
|
|
Exits:
|
|
Code 1 if check=True, command exits with non-zero status, and error_msg is provided.
|
|
|
|
Raises:
|
|
CalledProcessError if check=True, command exits with non-zero status, and error_msg is None.
|
|
TimeoutExpired if timeout is set and exceeded.
|
|
"""
|
|
stdout = subprocess.PIPE if capture_output else None
|
|
stderr = subprocess.PIPE if capture_output else None
|
|
try:
|
|
process = subprocess.Popen(
|
|
cmd,
|
|
cwd=str(cwd) if cwd else None,
|
|
env=env,
|
|
text=True,
|
|
stdout=stdout,
|
|
stderr=stderr,
|
|
start_new_session=kill_process_group,
|
|
)
|
|
except OSError as e:
|
|
if error_msg:
|
|
_log_error_and_exit(f"{error_msg}: {e}")
|
|
else:
|
|
_log_error_and_exit(f"Failed to execute {cmd[0]!r}: {e}")
|
|
try:
|
|
out, err = process.communicate(timeout=timeout)
|
|
except BaseException:
|
|
# Cover TimeoutExpired and any other escaping exception
|
|
# (KeyboardInterrupt, OSError mid-read, etc.) so the make /
|
|
# runtest / expect / gdb process tree is never orphaned. With a new
|
|
# session we can SIGKILL the whole group; otherwise just the child.
|
|
if kill_process_group:
|
|
try:
|
|
os.killpg(process.pid, signal.SIGKILL)
|
|
except ProcessLookupError:
|
|
pass
|
|
else:
|
|
process.kill()
|
|
try:
|
|
out, err = process.communicate()
|
|
except Exception:
|
|
out, err = None, None
|
|
raise
|
|
result = subprocess.CompletedProcess(cmd, process.returncode, out, err)
|
|
if check and process.returncode != 0:
|
|
exc = subprocess.CalledProcessError(process.returncode, cmd, out, err)
|
|
if error_msg:
|
|
_log_error_and_exit(f"{error_msg}: {exc}")
|
|
raise exc
|
|
return result
|
|
|
|
|
|
def _read_file_lines(file_path: Path, error_context: str = "") -> List[str]:
|
|
"""
|
|
Read file lines with error handling.
|
|
|
|
Args:
|
|
file_path: Path to file to read.
|
|
error_context: Context for error message.
|
|
|
|
Returns:
|
|
List of stripped lines from file.
|
|
"""
|
|
try:
|
|
with open(file_path, encoding="utf-8") as f:
|
|
return [line.strip() for line in f]
|
|
except OSError as e:
|
|
_log_error_and_exit(
|
|
f"Failed to read {file_path}: {e}"
|
|
f"{'. ' + error_context if error_context else ''}"
|
|
)
|
|
|
|
|
|
def _extract_test_file_from_name(test_name: str) -> str:
|
|
"""
|
|
Extract test file path from full test name.
|
|
|
|
Args:
|
|
test_name: Full test name (e.g., "gdb.rocm/foo.exp: test description").
|
|
|
|
Returns:
|
|
Test file path without description (e.g., "gdb.rocm/foo.exp").
|
|
"""
|
|
return test_name.split(": ", 1)[0]
|
|
|
|
|
|
def load_ignore_list_from_json(json_path: Path) -> Dict[str, List[str]]:
|
|
"""
|
|
Load test ignore list from JSON file.
|
|
|
|
Args:
|
|
json_path: Path to JSON file containing ignore list.
|
|
|
|
Returns:
|
|
Dictionary mapping compiler labels to lists of ignored test paths.
|
|
Returns empty dict if file doesn't exist.
|
|
|
|
Exits:
|
|
Code 1 if JSON file exists but is malformed.
|
|
"""
|
|
if not json_path.exists():
|
|
logger.warning(
|
|
f"Ignore list file not found: {json_path}. Using empty ignore list."
|
|
)
|
|
return {}
|
|
|
|
try:
|
|
with open(json_path, encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
except OSError as e:
|
|
_log_error_and_exit(f"Failed to read {json_path}: {e}")
|
|
except json.JSONDecodeError as e:
|
|
_log_error_and_exit(f"Failed to parse JSON from {json_path}: {e}")
|
|
|
|
if not isinstance(data, dict):
|
|
_log_error_and_exit(
|
|
f"Invalid ignore list format in {json_path}: expected a JSON object"
|
|
)
|
|
|
|
# Validate structure: each key should map to a list of strings.
|
|
for key, value in data.items():
|
|
if not isinstance(value, list):
|
|
_log_error_and_exit(
|
|
f"Invalid ignore list format in {json_path}: "
|
|
f"key '{key}' should map to a list"
|
|
)
|
|
if not all(isinstance(item, str) for item in value):
|
|
_log_error_and_exit(
|
|
f"Invalid ignore list format in {json_path}: "
|
|
f"all items in list for key '{key}' should be strings"
|
|
)
|
|
|
|
return data
|
|
|
|
|
|
def _non_negative_int(value: str) -> int:
|
|
"""
|
|
Validate argument is a non-negative integer.
|
|
|
|
Args:
|
|
value: String value from command line.
|
|
|
|
Returns:
|
|
Non-negative integer value.
|
|
|
|
Raises:
|
|
argparse.ArgumentTypeError: If value is negative.
|
|
"""
|
|
ivalue = int(value)
|
|
if ivalue < 0:
|
|
raise argparse.ArgumentTypeError(f"{value} is not a non-negative integer")
|
|
return ivalue
|
|
|
|
|
|
def _timeout_value(value: str) -> int:
|
|
"""
|
|
Validate timeout is a positive integer (seconds).
|
|
|
|
Args:
|
|
value: String value from command line.
|
|
|
|
Returns:
|
|
Integer timeout value.
|
|
|
|
Raises:
|
|
argparse.ArgumentTypeError: If value is not a positive integer.
|
|
"""
|
|
ivalue = int(value)
|
|
if ivalue <= 0:
|
|
raise argparse.ArgumentTypeError(
|
|
f"timeout must be a positive integer (got {ivalue})"
|
|
)
|
|
return ivalue
|
|
|
|
|
|
def _positive_nonzero_int(value: str) -> int:
|
|
"""
|
|
Validate argument is a positive non-zero integer.
|
|
|
|
Args:
|
|
value: String value from command line.
|
|
|
|
Returns:
|
|
Positive non-zero integer value.
|
|
|
|
Raises:
|
|
argparse.ArgumentTypeError: If value is not positive and non-zero.
|
|
"""
|
|
ivalue = int(value)
|
|
if ivalue <= 0:
|
|
raise argparse.ArgumentTypeError(f"{value} is not a positive non-zero integer")
|
|
return ivalue
|
|
|
|
|
|
def parse_arguments() -> argparse.Namespace:
|
|
"""
|
|
Parse and validate command-line arguments for the ROCgdb test suite.
|
|
|
|
Returns:
|
|
Parsed arguments with validated paths, timeout, and retry settings.
|
|
|
|
Exits:
|
|
Code 1 if arguments are invalid or paths don't exist.
|
|
"""
|
|
parser = argparse.ArgumentParser(
|
|
description="Run ROCgdb test suite with different compilers.",
|
|
formatter_class=lambda prog: argparse.RawDescriptionHelpFormatter(
|
|
prog, width=100
|
|
),
|
|
epilog="""Examples:
|
|
python %(prog)s --testsuite-dir /path/to/testsuite --rocgdb-bin /path/to/rocgdb
|
|
python %(prog)s --rocgdb-bin /path/to/rocgdb
|
|
python %(prog)s --tests gdb.base/break.exp gdb.base/call-ar-st.exp
|
|
python %(prog)s --gpu-tests
|
|
python %(prog)s --cpu-tests
|
|
python %(prog)s --parallel
|
|
python %(prog)s --group-results
|
|
python %(prog)s --default-timeout 300
|
|
python %(prog)s --retry-timeout 600
|
|
python %(prog)s --max-failed-retries 2
|
|
python %(prog)s --optimization="-O0"
|
|
python %(prog)s --runtestflags="--target_board=hip -debug"
|
|
python %(prog)s --no-xfail
|
|
python %(prog)s --quiet
|
|
python %(prog)s --skip-failed-test-log
|
|
python %(prog)s --output-ignore-list-file custom_ignore_list.json
|
|
python %(prog)s --one-by-one --tests gdb.rocm/foo.exp
|
|
|
|
""",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--skip-failed-test-log",
|
|
action="store_false",
|
|
dest="dump_failed_test_log",
|
|
help="Skip dumping gdb.log to the console for failed tests. Default is to dump the log.",
|
|
)
|
|
parser.add_argument(
|
|
"--group-results",
|
|
action="store_true",
|
|
help="Group test results in summary output. Default is off.",
|
|
)
|
|
parser.add_argument(
|
|
"--max-failed-retries",
|
|
type=_non_negative_int,
|
|
default=3,
|
|
help="Maximum number of times to retry failed tests. Default is 3.",
|
|
)
|
|
parser.add_argument(
|
|
"--no-xfail",
|
|
action="store_true",
|
|
help="Do not use ignore lists for failed tests. Default is to use the ignore lists.",
|
|
)
|
|
parser.add_argument(
|
|
"--retry-ignored-tests",
|
|
action="store_true",
|
|
help="Retry tests that are on the ignore list when they fail. "
|
|
"Default is off: ignored tests that fail on the first run are not retried.",
|
|
)
|
|
parser.add_argument(
|
|
"--optimization",
|
|
type=str,
|
|
default="",
|
|
help="Optimization level to pass to compiler (e.g., -O0, -Os, -Og).",
|
|
)
|
|
parser.add_argument(
|
|
"--parallel", action="store_true", help="Run tests in parallel. Default is off."
|
|
)
|
|
parser.add_argument(
|
|
"-j",
|
|
"--jobs",
|
|
type=_positive_nonzero_int,
|
|
default=None,
|
|
metavar="N",
|
|
help="Number of parallel jobs for make (e.g., -j4). Only valid with --parallel. "
|
|
"If --parallel is used without -j, defaults to os.cpu_count().",
|
|
)
|
|
parser.add_argument(
|
|
"--quiet",
|
|
action="store_true",
|
|
help="Do not output configure/testsuite commands. Default is off.",
|
|
)
|
|
parser.add_argument(
|
|
"--rocgdb-bin",
|
|
type=Path,
|
|
help="Path to the ROCgdb executable. Validated for existence when "
|
|
"supplied. Default: <rocm_dir>/bin/rocgdb, where rocm_dir is "
|
|
"auto-discovered (see --try-rocm-path).",
|
|
)
|
|
parser.add_argument(
|
|
"--runtestflags",
|
|
type=str,
|
|
default="",
|
|
help="Additional flags for RUNTESTFLAGS (e.g., '--target_board=hip -debug').",
|
|
)
|
|
|
|
# Create mutually exclusive group for test selection
|
|
test_group = parser.add_mutually_exclusive_group()
|
|
test_group.add_argument(
|
|
"--tests",
|
|
nargs="+",
|
|
default=None,
|
|
help="List of tests to run (individual .exp files or directories). "
|
|
"For directories, the /*.exp suffix is automatically added. "
|
|
"Mutually exclusive with --gpu-tests and --cpu-tests. "
|
|
"Default (if no test option specified) is gdb.rocm/*.exp and gdb.dwarf2/*.exp",
|
|
)
|
|
test_group.add_argument(
|
|
"--gpu-tests",
|
|
action="store_true",
|
|
help="Run GPU-specific tests (gdb.rocm directory only). "
|
|
"Mutually exclusive with --tests and --cpu-tests.",
|
|
)
|
|
test_group.add_argument(
|
|
"--cpu-tests",
|
|
action="store_true",
|
|
help="Run all CPU tests (all testsuite directories except gdb.rocm). "
|
|
"Automatically discovers all gdb.* directories with .exp files. "
|
|
"Mutually exclusive with --tests and --gpu-tests.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--testsuite-dir",
|
|
type=Path,
|
|
help="Path to the GDB testsuite directory. Validated for existence "
|
|
"when supplied. Default: <rocm_dir>/tests/rocgdb/gdb/testsuite, "
|
|
"where rocm_dir is auto-discovered (see --try-rocm-path).",
|
|
)
|
|
parser.add_argument(
|
|
"--default-timeout",
|
|
type=_timeout_value,
|
|
default=None,
|
|
help="Default timeout in seconds for all test runs from the start. "
|
|
"If not specified, no timeout is set initially.",
|
|
)
|
|
parser.add_argument(
|
|
"--retry-timeout",
|
|
type=_timeout_value,
|
|
default=100,
|
|
help="Timeout in seconds for timeout-based retry runs only. "
|
|
"Only applies when retrying tests that timed out. Default is 100.",
|
|
)
|
|
parser.add_argument(
|
|
"--try-rocm-path",
|
|
action="store_true",
|
|
help="Try to use ROCM_PATH environment variable to determine the ROCm tree root. "
|
|
"If ROCM_PATH is set and valid, it takes precedence over OUTPUT_ARTIFACTS_DIR and script location detection. "
|
|
"If ROCM_PATH is not set, falls back to OUTPUT_ARTIFACTS_DIR or script location detection. "
|
|
"If ROCM_PATH is invalid, issues an error.",
|
|
)
|
|
parser.add_argument(
|
|
"--check-type",
|
|
type=str,
|
|
choices=["check", "check-read1", "check-readmore"],
|
|
default="check",
|
|
help="Which GDB testsuite target to invoke. 'check' is the normal run. "
|
|
"'check-read1' runs under an LD_PRELOAD shim that forces read(2) to "
|
|
"return 1 byte at a time (stress-tests GDB's incremental I/O parsing). "
|
|
"'check-readmore' forces large reads (the opposite extreme). These "
|
|
"are upstream GDB testsuite modes and can surface latent testcase "
|
|
"regex bugs that 'check' alone misses. Default: 'check'.",
|
|
)
|
|
parser.add_argument(
|
|
"--ignore-list-file",
|
|
type=Path,
|
|
required=False,
|
|
metavar="PATH",
|
|
help="Path to JSON file containing test ignore lists. "
|
|
"If not specified, uses rocgdb_ignore_list.json in the same directory as this script. "
|
|
"This option requires a value.",
|
|
)
|
|
parser.add_argument(
|
|
"--output-ignore-list-file",
|
|
type=Path,
|
|
required=False,
|
|
metavar="PATH",
|
|
help="Path to output JSON file for generating a new ignore list from test failures. "
|
|
"When specified, the testsuite runs normally and all failures are written to this file "
|
|
"at the end of the run. Common failures go into 'Generic' category, compiler-specific "
|
|
"failures go into compiler-specific categories (e.g., 'GCC', 'LLVM'). "
|
|
"This option requires a value.",
|
|
)
|
|
parser.add_argument(
|
|
"--one-by-one",
|
|
action="store_true",
|
|
help="Run each test in its own `make check` invocation and save its gdb.log "
|
|
"under --one-by-one-log-dir. Each invocation is subject to a wall-clock limit; "
|
|
"see --one-by-one-test-timeout. Mutually exclusive with --parallel.",
|
|
)
|
|
parser.add_argument(
|
|
"--one-by-one-log-dir",
|
|
type=Path,
|
|
default=None,
|
|
metavar="PATH",
|
|
help="Root directory for per-test gdb.log captures in --one-by-one mode. "
|
|
"Default: <testsuite_dir>/one_by_one_logs.",
|
|
)
|
|
parser.add_argument(
|
|
"--one-by-one-test-timeout",
|
|
type=_positive_nonzero_int,
|
|
default=None,
|
|
metavar="SECS",
|
|
help="Wall-clock timeout (seconds) per individual `make check` invocation "
|
|
"in --one-by-one mode. Defaults to 3600.",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Validate that -j/--jobs is only used with --parallel.
|
|
if args.jobs is not None and not args.parallel:
|
|
_log_error_and_exit(
|
|
"-j/--jobs can only be specified when --parallel is provided."
|
|
)
|
|
|
|
# Validate --one-by-one constraints.
|
|
if args.one_by_one and args.parallel:
|
|
_log_error_and_exit("--one-by-one cannot be combined with --parallel.")
|
|
if (
|
|
args.one_by_one_log_dir is not None or args.one_by_one_test_timeout is not None
|
|
) and not args.one_by_one:
|
|
_log_error_and_exit(
|
|
"--one-by-one-log-dir / --one-by-one-test-timeout require --one-by-one."
|
|
)
|
|
|
|
# Validate paths independently if provided. Either flag may be supplied
|
|
# alone; whichever is omitted is auto-discovered in _resolve_rocm_paths.
|
|
if args.testsuite_dir is not None:
|
|
args.testsuite_dir = validate_path(args.testsuite_dir, is_dir=True)
|
|
if args.rocgdb_bin is not None:
|
|
args.rocgdb_bin = validate_path(args.rocgdb_bin, is_file=True)
|
|
|
|
# Adjust retry-timeout if default-timeout is larger.
|
|
if args.default_timeout is not None and args.default_timeout > args.retry_timeout:
|
|
logger.warning(
|
|
f"--retry-timeout is being overridden from {args.retry_timeout}s to "
|
|
f"{args.default_timeout}s because --default-timeout is larger."
|
|
)
|
|
args.retry_timeout = args.default_timeout
|
|
|
|
return args
|
|
|
|
|
|
class TestResults:
|
|
"""Class to store and manage test results across multiple compiler runs."""
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize test results storage with empty data structures."""
|
|
# Flag to control whether to group results in output.
|
|
self.group_results: bool = False
|
|
|
|
# Our main data structure for storing results. The mapping goes like this:
|
|
#
|
|
# - 1 compiler label maps to N labels (PASS, FAIL etc).
|
|
# - 1 label maps to N test files (gdb.rocm/simple.exp for example).
|
|
# - 1 test file maps to N test descriptions (the complete test
|
|
# line output by dejagnu).
|
|
self.test_data = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
|
|
|
|
# Track currently failed tests by compiler_label.
|
|
self.failed_tests: Dict[str, Set[str]] = defaultdict(set)
|
|
|
|
# Track flaky tests (failed initially but passed on retry) by compiler_label.
|
|
self.flaky_tests: Dict[str, Set[str]] = defaultdict(set)
|
|
|
|
# Track harness-level errors (ERROR lines with no preceding
|
|
# "Running ....exp") per compiler_label. Replaced on each
|
|
# update_results call so it reflects the most recent run's state.
|
|
self.harness_errors: Dict[str, List[str]] = defaultdict(list)
|
|
|
|
def cleanup_old_entries(self, compiler_label: str, tests: List[str]) -> None:
|
|
"""
|
|
Remove stale test results before retry runs to prevent duplicate entries.
|
|
|
|
Args:
|
|
compiler_label: Compiler identifier (e.g., "GCC", "LLVM").
|
|
tests: List of test file names to remove.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
# Case: first run for this compiler — nothing to clean. Use
|
|
# `in` rather than indexing to avoid the defaultdict factory
|
|
# materializing a phantom empty entry for the compiler.
|
|
if compiler_label not in self.test_data:
|
|
return
|
|
|
|
logger.info(f"Retry run. Cleaning dictionary entries for {' '.join(tests)}.")
|
|
|
|
# Clear the failed tests set for this compiler label.
|
|
self.failed_tests[compiler_label].clear()
|
|
|
|
# Clear tests entries from the main test results database for this
|
|
# compiler label.
|
|
for test in tests:
|
|
for category in TEST_CATEGORIES:
|
|
category_results = self.test_data[compiler_label].get(category)
|
|
if category_results and test in category_results:
|
|
del category_results[test]
|
|
|
|
def update_results(
|
|
self, compiler_label: str, tests: List[str], results_file: Path
|
|
) -> None:
|
|
"""
|
|
Parse and update test results from a DejaGnu results file.
|
|
|
|
A single pass over the file extracts both result lines (PASS/FAIL/...)
|
|
and ERROR lines, attributing each ERROR to the most recently seen
|
|
`Running ....exp` line. ERRORs without a preceding Running line are
|
|
recorded as harness-level errors.
|
|
|
|
Args:
|
|
compiler_label: Compiler name (e.g., "GCC", "LLVM").
|
|
tests: List of test file names to clear before updating.
|
|
results_file: Path to results file with lines like "STATUS: test_description".
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
self.cleanup_old_entries(compiler_label, tests)
|
|
|
|
new_errors: Dict[str, List[str]] = defaultdict(list)
|
|
orphan_errors: List[str] = []
|
|
current_test_file: Optional[str] = None
|
|
|
|
for line in _read_file_lines(results_file, "during results update"):
|
|
if not line:
|
|
continue
|
|
|
|
running_match = RUNNING_EXP_RE.search(line)
|
|
if running_match:
|
|
segments = os.path.normpath(running_match.group(1)).split(os.path.sep)
|
|
current_test_file = "/".join(segments[-2:])
|
|
|
|
if line.startswith("ERROR:"):
|
|
msg = line[len("ERROR:") :].strip()
|
|
if current_test_file is None:
|
|
orphan_errors.append(msg)
|
|
else:
|
|
new_errors[current_test_file].append(f"{current_test_file}: {msg}")
|
|
continue
|
|
|
|
match = RESULT_LINE_RE.match(line)
|
|
if match:
|
|
status, test_name = match.groups()
|
|
test_file = _extract_test_file_from_name(test_name)
|
|
is_timeout = TIMEOUT_RE.search(test_name)
|
|
|
|
# Add the entry to our test results database.
|
|
self.test_data[compiler_label][status][test_file].append(test_name)
|
|
|
|
# For timeouts, we add a duplicate since we want to
|
|
# explicitly list tests that ran into timeouts.
|
|
if is_timeout:
|
|
self.test_data[compiler_label]["TIMEOUT"][test_file].append(
|
|
test_name
|
|
)
|
|
|
|
# Track the current list of failed tests. Timeouts on
|
|
# statuses other than FAIL/UNRESOLVED (e.g. a KFAIL that
|
|
# happened to time out) are harmless and intentionally
|
|
# excluded.
|
|
if status in ["FAIL", "UNRESOLVED"]:
|
|
self.failed_tests[compiler_label].add(test_file)
|
|
|
|
# Merge ERROR entries into the existing dict so that ERRORs for tests
|
|
# not being rerun are preserved (cleanup_old_entries has already
|
|
# cleared the slots for the tests in the current run).
|
|
for test_file, messages in new_errors.items():
|
|
self.test_data[compiler_label]["ERROR"][test_file] = messages
|
|
self.failed_tests[compiler_label].add(test_file)
|
|
|
|
# Replace harness errors with this run's orphans.
|
|
self.harness_errors[compiler_label] = orphan_errors
|
|
|
|
def get_failed_tests(self, compiler_label: str) -> List[str]:
|
|
"""
|
|
Get currently failed tests for a compiler.
|
|
|
|
Args:
|
|
compiler_label: Compiler identifier.
|
|
|
|
Returns:
|
|
List of failed test file paths.
|
|
"""
|
|
# Use .get to avoid materializing a phantom empty entry for
|
|
# compilers that ran without any failures.
|
|
return list(self.failed_tests.get(compiler_label, set()))
|
|
|
|
def mark_flaky_tests(self, compiler_label: str, test_files: Set[str]) -> None:
|
|
"""
|
|
Mark tests as flaky (failed initially but passed on retry).
|
|
|
|
Args:
|
|
compiler_label: Compiler identifier (e.g., "GCC", "LLVM").
|
|
test_files: Set of test file paths that were flaky.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not test_files:
|
|
return
|
|
|
|
# Add to flaky tests set.
|
|
self.flaky_tests[compiler_label].update(test_files)
|
|
|
|
# Add to test_data under FLAKY category with marker.
|
|
for test_file in test_files:
|
|
flaky_description = f"{test_file}: initially failed, passed on retry"
|
|
self.test_data[compiler_label]["FLAKY"][test_file].append(flaky_description)
|
|
|
|
logger.info(
|
|
f"{STATUS_FLAKY} {len(test_files)} test(s) became stable after retry for {compiler_label}"
|
|
)
|
|
|
|
def print_all_summaries(self) -> None:
|
|
"""
|
|
Print test summaries for all compilers.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not self.test_data:
|
|
logger.info(f"\n{STATUS_FAIL} No test results to display.")
|
|
return
|
|
|
|
print_section("COMBINED GDB TESTSUITE ANALYSIS")
|
|
|
|
for compiler_label in sorted(self.test_data.keys()):
|
|
self._print_summary(compiler_label, self.test_data[compiler_label])
|
|
|
|
self.print_comparison()
|
|
|
|
def _print_summary(
|
|
self, compiler_label: str, details: Dict[str, Dict[str, List[str]]]
|
|
) -> None:
|
|
"""
|
|
Print test summary for a single compiler.
|
|
|
|
Args:
|
|
compiler_label: Compiler identifier.
|
|
details: Mapping from status category to test files and descriptions.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
print_section(f"{compiler_label}", border_char="-", inline=True)
|
|
for cat, prefix_indicator in CATEGORY_DISPLAY.items():
|
|
count = sum(len(tests) for tests in details.get(cat, {}).values())
|
|
|
|
if count > 0:
|
|
header = f" {prefix_indicator} {cat}: {count}"
|
|
logger.info(header)
|
|
|
|
# Print details for non-PASS tests.
|
|
if count > 0 and cat != "PASS":
|
|
if self.group_results:
|
|
self._print_grouped_tests(details[cat])
|
|
else:
|
|
for test_file in details[cat]:
|
|
for test_name in details[cat][test_file]:
|
|
logger.info(f" {test_name}")
|
|
|
|
harness_errors = self.harness_errors.get(compiler_label, [])
|
|
if harness_errors:
|
|
logger.info(f" {STATUS_FAIL} HARNESS ERRORS: {len(harness_errors)}")
|
|
for msg in harness_errors:
|
|
logger.info(f" {msg}")
|
|
|
|
def _print_grouped_tests(self, test_list: Dict[str, List[str]]) -> None:
|
|
"""
|
|
Print tests grouped by directory, file, and description.
|
|
|
|
Args:
|
|
test_list: Mapping of test file paths to descriptions.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
grouped = defaultdict(lambda: defaultdict(list))
|
|
|
|
for file, descriptions in test_list.items():
|
|
test_dir, test_file = os.path.split(file)
|
|
prefix_len = len(file) + 2
|
|
for test_description in descriptions:
|
|
grouped[test_dir][test_file].append(test_description[prefix_len:])
|
|
|
|
for directory in sorted(grouped.keys()):
|
|
logger.info(f" * {directory}")
|
|
for filename in sorted(grouped[directory].keys()):
|
|
descriptions = grouped[directory][filename]
|
|
if len(descriptions) == 1 and descriptions[0]:
|
|
logger.info(f" * {filename}: {descriptions[0]}")
|
|
elif len(descriptions) == 1:
|
|
logger.info(f" * {filename}")
|
|
else:
|
|
logger.info(f" * {filename}")
|
|
for desc in descriptions:
|
|
if desc:
|
|
logger.info(f" * {desc}")
|
|
|
|
def print_comparison(self) -> None:
|
|
"""
|
|
Print failing tests exclusive to each compiler.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if len(self.failed_tests) <= 1:
|
|
return
|
|
|
|
print_section("EXCLUSIVE FAILING TESTS COMPARISON")
|
|
|
|
for compiler_label in sorted(self.failed_tests.keys()):
|
|
other_failed = set.union(
|
|
*(
|
|
self.failed_tests[other]
|
|
for other in self.failed_tests
|
|
if other != compiler_label
|
|
)
|
|
)
|
|
exclusive_tests = self.failed_tests[compiler_label] - other_failed
|
|
|
|
print_section(f"{compiler_label}", border_char="-", inline=True)
|
|
if exclusive_tests:
|
|
for test in sorted(exclusive_tests):
|
|
logger.info(f" {STATUS_FAIL} {test}")
|
|
else:
|
|
logger.info(" No exclusive failing tests.")
|
|
|
|
def check_all_pass_or_xfailed(
|
|
self, xfailed_tests: Dict[str, List[str]]
|
|
) -> Tuple[bool, Dict[str, Dict]]:
|
|
"""
|
|
Check if all tests passed or are in the expected failure list.
|
|
|
|
Args:
|
|
xfailed_tests: Compiler labels mapped to expected failing test paths.
|
|
Must include "Generic" key for cross-compiler failures.
|
|
|
|
Returns:
|
|
Tuple of (overall_pass, details) where:
|
|
- overall_pass: True if no unexpected failures.
|
|
- details: Per-compiler dict with passed status, total_failed count,
|
|
unexpected_failures, unused_xfails, expected_generic,
|
|
expected_compiler_specific lists, and flaky_tests count.
|
|
"""
|
|
overall_pass = True
|
|
details = {}
|
|
generic_xfailed = set(xfailed_tests.get("Generic", []))
|
|
|
|
# Include all compilers that ran tests, so all-passing compilers also
|
|
# show up in the final status report.
|
|
all_compilers = set(self.test_data.keys())
|
|
|
|
for compiler_label in all_compilers:
|
|
failed_test_files = set(self.failed_tests.get(compiler_label, set()))
|
|
compiler_xfailed = set(xfailed_tests.get(compiler_label, []))
|
|
all_xfailed = generic_xfailed | compiler_xfailed
|
|
|
|
unexpected_failures = failed_test_files - all_xfailed
|
|
unused_xfails = all_xfailed - failed_test_files
|
|
expected_generic = failed_test_files & generic_xfailed
|
|
expected_compiler_specific = failed_test_files & compiler_xfailed
|
|
|
|
harness_errors = self.harness_errors.get(compiler_label, [])
|
|
has_harness_errors = bool(harness_errors)
|
|
compiler_pass = len(unexpected_failures) == 0 and not has_harness_errors
|
|
overall_pass = overall_pass and compiler_pass
|
|
|
|
flaky_count = len(self.flaky_tests.get(compiler_label, set()))
|
|
|
|
details[compiler_label] = {
|
|
"passed": compiler_pass,
|
|
"total_failed": len(failed_test_files),
|
|
"unexpected_failures": sorted(unexpected_failures),
|
|
"unused_xfails": sorted(unused_xfails),
|
|
"expected_generic": sorted(expected_generic),
|
|
"expected_compiler_specific": sorted(expected_compiler_specific),
|
|
"flaky_tests": sorted(self.flaky_tests.get(compiler_label, set())),
|
|
"flaky_count": flaky_count,
|
|
"harness_errors": list(harness_errors),
|
|
}
|
|
|
|
return overall_pass, details
|
|
|
|
def generate_ignore_list(self) -> Dict[str, List[str]]:
|
|
"""
|
|
Generate ignore list from current test failures.
|
|
|
|
Analyzes failures across all compilers and categorizes them as:
|
|
- Generic: Tests that failed for all compilers
|
|
- Compiler-specific: Tests that failed only for specific compilers
|
|
|
|
Returns:
|
|
Dictionary with keys "Generic" and compiler labels (e.g., "GCC", "LLVM"),
|
|
each mapping to a sorted list of failed test file paths.
|
|
"""
|
|
# Collect all failed test files per compiler that ran tests.
|
|
all_compiler_failures = {}
|
|
|
|
for compiler_label in sorted(self.test_data.keys()):
|
|
failed_test_files = set(self.failed_tests.get(compiler_label, set()))
|
|
all_compiler_failures[compiler_label] = failed_test_files
|
|
|
|
# Determine which failures are common to all compilers.
|
|
if len(all_compiler_failures) > 1:
|
|
generic_failures = set.intersection(*all_compiler_failures.values())
|
|
else:
|
|
# 0 compilers (no runs) or 1 compiler (all failures are compiler-specific).
|
|
generic_failures = set()
|
|
|
|
# Build the ignore list structure.
|
|
ignore_list = {"Generic": sorted(generic_failures)}
|
|
|
|
# Add compiler-specific failures.
|
|
for compiler_label in sorted(all_compiler_failures.keys()):
|
|
compiler_specific = all_compiler_failures[compiler_label] - generic_failures
|
|
ignore_list[compiler_label] = sorted(compiler_specific)
|
|
|
|
return ignore_list
|
|
|
|
def print_final_status(
|
|
self, xfailed_tests: Dict[str, List[str]], no_xfail: bool
|
|
) -> bool:
|
|
"""
|
|
Print final PASS/FAIL status for all compilers.
|
|
|
|
Args:
|
|
xfailed_tests: Compiler labels mapped to expected failures.
|
|
no_xfail: If True, ignore the expected failure lists.
|
|
|
|
Returns:
|
|
True if all tests passed or are expected failures, False otherwise.
|
|
"""
|
|
overall_pass, details = self.check_all_pass_or_xfailed(
|
|
{} if no_xfail else xfailed_tests
|
|
)
|
|
|
|
print_section("FINAL TEST STATUS")
|
|
|
|
for compiler_label in sorted(details.keys()):
|
|
detail = details[compiler_label]
|
|
status_symbol = STATUS_PASS if detail["passed"] else STATUS_FAIL
|
|
status_text = "PASS" if detail["passed"] else "FAIL"
|
|
|
|
logger.info(f"{status_symbol} {compiler_label}: {status_text}")
|
|
|
|
if detail["total_failed"]:
|
|
logger.info(
|
|
f"{STATUS_FAIL} Total Failed Tests: {detail['total_failed']}"
|
|
)
|
|
|
|
if detail["flaky_count"] > 0:
|
|
logger.info(
|
|
f"{STATUS_FLAKY} Flaky Tests (passed after retry) ({detail['flaky_count']}):"
|
|
)
|
|
for test in detail["flaky_tests"]:
|
|
logger.info(f" {test}")
|
|
|
|
if detail["expected_generic"]:
|
|
logger.info(
|
|
f"{STATUS_WARN} Ignored Failures (Generic) ({len(detail['expected_generic'])}):"
|
|
)
|
|
for test in detail["expected_generic"]:
|
|
logger.info(f" {test}")
|
|
|
|
if detail["expected_compiler_specific"]:
|
|
logger.info(
|
|
f"{STATUS_WARN} Ignored Failures ({compiler_label}) ({len(detail['expected_compiler_specific'])}):"
|
|
)
|
|
for test in detail["expected_compiler_specific"]:
|
|
logger.info(f" {test}")
|
|
|
|
if detail["unexpected_failures"]:
|
|
logger.info(
|
|
f"{STATUS_FAIL} Unexpected Failures ({len(detail['unexpected_failures'])}):"
|
|
)
|
|
for test in detail["unexpected_failures"]:
|
|
logger.info(f" {test}")
|
|
|
|
if detail["harness_errors"]:
|
|
logger.info(
|
|
f"{STATUS_FAIL} Harness Errors ({len(detail['harness_errors'])}):"
|
|
)
|
|
for msg in detail["harness_errors"]:
|
|
logger.info(f" {msg}")
|
|
|
|
if detail["unused_xfails"]:
|
|
logger.info(
|
|
f"{STATUS_WARN} Unused Ignored Failures ({len(detail['unused_xfails'])}):"
|
|
)
|
|
for test in detail["unused_xfails"]:
|
|
logger.info(f" {test}")
|
|
logger.info("")
|
|
|
|
overall_status = "PASS" if overall_pass else "FAIL"
|
|
overall_symbol = STATUS_PASS if overall_pass else STATUS_FAIL
|
|
print_section(f"{overall_symbol} OVERALL STATUS: {overall_status}")
|
|
logger.info("")
|
|
|
|
return overall_pass
|
|
|
|
|
|
def print_section(
|
|
title: str,
|
|
border_char: str = "=",
|
|
width: int = 80,
|
|
center: bool = True,
|
|
inline: bool = False,
|
|
color: Optional[str] = None,
|
|
) -> None:
|
|
"""
|
|
Print a formatted section header to console.
|
|
|
|
Args:
|
|
title: Text to display in the header.
|
|
border_char: Character for border line (default "=").
|
|
width: Total header width (default 80).
|
|
center: Center title in multi-line mode (default True).
|
|
inline: Single-line format if True, multi-line if False (default False).
|
|
color: ANSI color code (e.g., "\\033[92m" for green).
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
reset = "\033[0m"
|
|
apply_color = (
|
|
(lambda text: f"{color}{text}{reset}") if color else (lambda text: text)
|
|
)
|
|
|
|
# Always add a newline to the beginning of the section.
|
|
logger.info("")
|
|
|
|
if inline:
|
|
# Prepare inline title.
|
|
title_str = f" {title} "
|
|
remaining = width - len(title_str)
|
|
if remaining < 0:
|
|
remaining = 0
|
|
left = border_char * (remaining // 2)
|
|
right = border_char * (remaining - len(left))
|
|
logger.info(apply_color(f"{left}{title_str}{right}"))
|
|
else:
|
|
# Multi-line section style.
|
|
border = border_char * width
|
|
title_line = f"{title:^{width}}" if center else title
|
|
logger.info(apply_color(border))
|
|
logger.info(apply_color(title_line))
|
|
logger.info(apply_color(border))
|
|
|
|
|
|
def _log_aligned_fields(
|
|
fields: List[Union[str, Tuple[str, str]]],
|
|
indent: str = " ",
|
|
) -> None:
|
|
"""
|
|
Log (label, value) pairs with colons aligned to the longest label with an associated value.
|
|
|
|
Plain string entries are logged as-is (pass-through lines), skipping the
|
|
colon and padding — useful for pass-through lines mixed in with key/value rows.
|
|
|
|
Args:
|
|
fields: List of (label, value) tuples or plain strings for pass-through lines.
|
|
indent: Leading whitespace prefix for every row.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not fields:
|
|
return
|
|
labelled = [entry[0] for entry in fields if isinstance(entry, tuple)]
|
|
width = max(len(label) for label in labelled) if labelled else 0
|
|
for entry in fields:
|
|
if isinstance(entry, str):
|
|
logger.info(f"{indent}{entry}")
|
|
else:
|
|
label, value = entry
|
|
logger.info(f"{indent}{label:<{width}} : {value}")
|
|
|
|
|
|
def validate_required_files(required_files: Dict[str, Path]) -> None:
|
|
"""
|
|
Validate presence of required files and print status.
|
|
|
|
Args:
|
|
required_files: File descriptions mapped to filesystem paths.
|
|
|
|
Returns:
|
|
None
|
|
|
|
Exits:
|
|
Code 1 if any required file is missing.
|
|
"""
|
|
print_section("Required files")
|
|
all_valid = True
|
|
|
|
fields: List[Tuple[str, str]] = []
|
|
for name, path in required_files.items():
|
|
present = path.is_file()
|
|
status = STATUS_PASS if present else STATUS_FAIL
|
|
fields.append((f"{status} {name}", str(path)))
|
|
if not present:
|
|
all_valid = False
|
|
_log_aligned_fields(fields)
|
|
|
|
if not all_valid:
|
|
_log_error_and_exit("One or more required files are missing.")
|
|
|
|
|
|
def _prepend_to_path_var(env_vars: Dict[str, str], name: str, parts: List[str]) -> None:
|
|
"""
|
|
Prepend `parts` to a path-style env var in `env_vars`.
|
|
|
|
The caller is responsible for logging the change (so multiple env-var
|
|
updates can be printed as a single aligned block).
|
|
|
|
Args:
|
|
env_vars: Environment variable dictionary to mutate.
|
|
name: Name of the path-style variable (e.g., "PATH", "LD_LIBRARY_PATH").
|
|
parts: New entries to prepend, in order.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
existing = env_vars.get(name, "")
|
|
env_vars[name] = os.pathsep.join(parts + ([existing] if existing else []))
|
|
|
|
|
|
def setup_sanitizer_environment(env_vars: Dict[str, str], testsuite_dir: Path) -> None:
|
|
"""
|
|
Pre-configure *SAN_OPTIONS for every sanitizer we know about.
|
|
|
|
This runs unconditionally. When rocgdb is not sanitizer-instrumented,
|
|
the matching env vars are ignored by the (absent) sanitizer runtime,
|
|
so the only side effect of a normal run is the creation of an empty
|
|
sanitizer log directory.
|
|
|
|
For each sanitizer in SANITIZERS we set:
|
|
halt_on_error=0 - sanitizer findings do not abort the run
|
|
detect_leaks=0 - asan only. ASan ships LSan integrated;
|
|
silence leak reports by default. Override
|
|
with ASAN_OPTIONS=detect_leaks=1 for a
|
|
dedicated leak run.
|
|
log_path=<dir>/log - reports go to <dir>/log.<pid>; the
|
|
sanitizer runtime appends .<pid> per
|
|
process, so one log file is produced
|
|
per process. All sanitizers share a
|
|
single directory because log_path lives
|
|
in the sanitizer common flags - in a
|
|
combined build (e.g. ASan + UBSan) the
|
|
last-initialized runtime would overwrite
|
|
any per-sanitizer log_path. Reports
|
|
self-identify in their content.
|
|
suppressions=<file> - only when the .supp template exists;
|
|
empty suppression files are still
|
|
attached intentionally so users can
|
|
populate the templates without code
|
|
changes. suppressions is per-runtime
|
|
and does not collide.
|
|
|
|
If the shared log directory cannot be created (e.g. read-only
|
|
testsuite mount), the log_path option is omitted and the sanitizer
|
|
falls back to stderr - still non-fatal.
|
|
"""
|
|
print_section("Sanitizer environment")
|
|
|
|
suppressions_dir = Path(__file__).resolve().parent / "sanitizers"
|
|
log_dir = testsuite_dir / "sanitizers_output"
|
|
log_dir_ready = True
|
|
try:
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
except OSError as exc:
|
|
log_dir_ready = False
|
|
logger.warning(
|
|
f"{STATUS_WARN} Could not create sanitizer log dir "
|
|
f"{log_dir}: {exc}. Sanitizer reports will go to stderr."
|
|
)
|
|
|
|
fields: List[Tuple[str, str]] = []
|
|
for sanitizer in SANITIZERS:
|
|
options = ["halt_on_error=0"]
|
|
if sanitizer == "asan":
|
|
options.append("detect_leaks=0")
|
|
if log_dir_ready:
|
|
options.append(f"log_path={log_dir}/log")
|
|
|
|
suppression_file = suppressions_dir / f"{sanitizer}.supp"
|
|
if suppression_file.is_file():
|
|
options.append(f"suppressions={suppression_file}")
|
|
|
|
env_var_name = f"{sanitizer.upper()}_OPTIONS"
|
|
# Preserve any pre-existing options from the caller's environment.
|
|
# Sanitizer option parsing is last-wins, so appending the existing
|
|
# value after our defaults lets the user override them.
|
|
existing = env_vars.get(env_var_name, "")
|
|
if existing:
|
|
options.append(existing)
|
|
env_vars[env_var_name] = ":".join(options)
|
|
fields.append((env_var_name, env_vars[env_var_name]))
|
|
|
|
_log_aligned_fields(fields)
|
|
|
|
|
|
def setup_environment(artifacts_dir: Path) -> Dict[str, str]:
|
|
"""
|
|
Configure environment variables for test suite execution.
|
|
|
|
Args:
|
|
artifacts_dir: Path to artifacts directory with binaries and libraries.
|
|
|
|
Returns:
|
|
Updated environment variables dictionary.
|
|
"""
|
|
print_section("Setting up environment variables")
|
|
|
|
# Copy current environment.
|
|
env_vars = os.environ.copy()
|
|
|
|
# Add ROCgdb and LLVM binaries to PATH and libraries to LD_LIBRARY_PATH.
|
|
#
|
|
# Please note LLVM has switched location for its executables/libraries.
|
|
# It used to be llvm/bin (and llvm/lib) but now is lib/llvm/bin (and
|
|
# lib/llvm/lib). A llvm -> lib/llvm symlink is kept for backwards
|
|
# compatibility, but the new path should be used moving forward.
|
|
path_parts = [
|
|
f"{artifacts_dir}/bin",
|
|
f"{artifacts_dir}/llvm/bin",
|
|
f"{artifacts_dir}/lib/llvm/bin",
|
|
]
|
|
ld_library_parts = [
|
|
f"{artifacts_dir}/lib",
|
|
f"{artifacts_dir}/llvm/lib",
|
|
f"{artifacts_dir}/lib/llvm/lib",
|
|
]
|
|
|
|
# Determine target triple
|
|
target_triple = None
|
|
amdclang_path = artifacts_dir / "lib" / "llvm" / "bin" / "amdclang++"
|
|
if amdclang_path.exists():
|
|
try:
|
|
target_triple = subprocess.run(
|
|
[str(amdclang_path), "--print-target-triple"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
check=True,
|
|
).stdout.strip()
|
|
except (subprocess.SubprocessError, OSError) as exc:
|
|
raise RuntimeError(
|
|
f"'{amdclang_path} --print-target-triple' failed; "
|
|
"this suggests a broken toolchain."
|
|
) from exc
|
|
|
|
if target_triple:
|
|
ld_library_parts += [f"{artifacts_dir}/lib/llvm/lib/{target_triple}"]
|
|
|
|
_prepend_to_path_var(env_vars, "PATH", path_parts)
|
|
_prepend_to_path_var(env_vars, "LD_LIBRARY_PATH", ld_library_parts)
|
|
|
|
# Check if we are running within a github actions context, where a
|
|
# non-system version of Python is being used. If so, we have
|
|
# pythonLocation set to the base location of the Python interpreter.
|
|
if "pythonLocation" in env_vars:
|
|
# Set PYTHONHOME so rocgdb can initialize Python properly.
|
|
env_vars["PYTHONHOME"] = env_vars["pythonLocation"]
|
|
pythonhome_value = env_vars["PYTHONHOME"]
|
|
else:
|
|
pythonhome_value = "not set (no pythonLocation env var)"
|
|
|
|
_log_aligned_fields(
|
|
[
|
|
("PATH (prepended)", os.pathsep.join(path_parts)),
|
|
("LD_LIBRARY_PATH (prepended)", os.pathsep.join(ld_library_parts)),
|
|
("PYTHONHOME", pythonhome_value),
|
|
]
|
|
)
|
|
|
|
return env_vars
|
|
|
|
|
|
def check_executables(executables: List[str], env_vars: Dict[str, str]) -> None:
|
|
"""
|
|
Verify required executables are in the prepared PATH.
|
|
|
|
Args:
|
|
executables: List of executable names to check.
|
|
env_vars: Prepared environment supplying the PATH used for the lookup.
|
|
|
|
Returns:
|
|
None
|
|
|
|
Exits:
|
|
Code 1 if any executable is missing.
|
|
"""
|
|
print_section("Required executables")
|
|
missing = []
|
|
search_path = env_vars.get("PATH")
|
|
|
|
for exe in executables:
|
|
path = shutil.which(exe, path=search_path)
|
|
if path:
|
|
logger.info(f"{STATUS_PASS} {exe:15} found at: {path}")
|
|
else:
|
|
missing.append(exe)
|
|
logger.info(f"{STATUS_FAIL} {exe:15} NOT found on PATH")
|
|
|
|
if missing:
|
|
_log_error_and_exit(f"Missing {len(missing)} executables required for testing.")
|
|
|
|
|
|
def setup_core_file_info() -> None:
|
|
"""
|
|
Set system core file size limit to unlimited and display core file pattern.
|
|
|
|
The outcome is communicated to the user via logger.info / logger.warning
|
|
so the result is visible in the run banner.
|
|
"""
|
|
print_section("Core file information")
|
|
|
|
# Display current core file pattern.
|
|
core_pattern_file = Path("/proc/sys/kernel/core_pattern")
|
|
try:
|
|
if core_pattern_file.exists():
|
|
core_pattern = core_pattern_file.read_text().strip()
|
|
logger.info(f"System core file pattern: {core_pattern}")
|
|
else:
|
|
logger.info("System core file pattern: N/A (file not found)")
|
|
except OSError as e:
|
|
logger.warning(f"System core file pattern: N/A (unable to read: {e})")
|
|
|
|
try:
|
|
resource.setrlimit(
|
|
resource.RLIMIT_CORE, (resource.RLIM_INFINITY, resource.RLIM_INFINITY)
|
|
)
|
|
soft, hard = resource.getrlimit(resource.RLIMIT_CORE)
|
|
|
|
if soft == resource.RLIM_INFINITY:
|
|
logger.info(f"{STATUS_PASS} Core file size limit set to unlimited")
|
|
else:
|
|
logger.warning(f"Core file size limit is {soft}, not unlimited")
|
|
logger.warning("Core file tests may not execute properly")
|
|
|
|
except (OSError, ValueError) as e:
|
|
logger.error(f"{STATUS_FAIL} Unable to set core file size limit: {e}")
|
|
logger.warning("Core file tests will not be executed")
|
|
|
|
|
|
def cleanup_test_suite(
|
|
test_suite_dir: Path, env_vars: Dict[str, str], quiet: bool = False
|
|
) -> None:
|
|
"""
|
|
Clean build artifacts and recreate site.exp configuration.
|
|
|
|
Args:
|
|
test_suite_dir: Path to test suite directory.
|
|
env_vars: Environment variables for cleanup commands.
|
|
quiet: Suppress command output if True (default False).
|
|
|
|
Returns:
|
|
None
|
|
|
|
Exits:
|
|
Code 1 if cleanup fails.
|
|
"""
|
|
logger.info("Cleaning test suite directory...")
|
|
cmd = ["make", "clean"]
|
|
logger.info(f"Executing cleanup: {shlex.join(cmd)}")
|
|
_run_command(
|
|
cmd,
|
|
cwd=test_suite_dir,
|
|
env=env_vars,
|
|
capture_output=quiet,
|
|
error_msg="Failed to clean test directory",
|
|
)
|
|
|
|
logger.info("Removing old site.exp and site.bak...")
|
|
for filename in ["site.exp", "site.bak"]:
|
|
(test_suite_dir / filename).unlink(missing_ok=True)
|
|
|
|
logger.info("Creating site.exp...")
|
|
cmd = ["make", "site.exp"]
|
|
logger.info(f"Executing: {shlex.join(cmd)}")
|
|
_run_command(
|
|
cmd,
|
|
cwd=test_suite_dir,
|
|
env=env_vars,
|
|
capture_output=quiet,
|
|
error_msg="Failed to create site.exp",
|
|
)
|
|
|
|
|
|
def configure_test_suite(
|
|
test_suite_dir: Path, env_vars: Dict[str, str], quiet: bool = False
|
|
) -> None:
|
|
"""
|
|
Run test suite configuration script.
|
|
|
|
Args:
|
|
test_suite_dir: Path to test suite directory with configure script.
|
|
env_vars: Environment variables for configuration.
|
|
quiet: Suppress command output if True (default False).
|
|
|
|
Returns:
|
|
None
|
|
|
|
Exits:
|
|
Code 1 if configuration fails.
|
|
"""
|
|
configure_script = test_suite_dir / "configure"
|
|
cmd = ["sh", str(configure_script)]
|
|
logger.info(f"Executing: {shlex.join(cmd)}")
|
|
_run_command(
|
|
cmd,
|
|
cwd=test_suite_dir,
|
|
env=env_vars,
|
|
capture_output=quiet,
|
|
error_msg="Test suite configuration failed",
|
|
)
|
|
cleanup_test_suite(test_suite_dir, env_vars, quiet)
|
|
|
|
|
|
def set_test_timeout(test_suite_dir: Path, timeout_value: int) -> None:
|
|
"""
|
|
Set gdb_test_timeout in site.exp configuration file.
|
|
|
|
Args:
|
|
test_suite_dir: Path to test suite directory with site.exp file.
|
|
timeout_value: Timeout in seconds for test runs.
|
|
|
|
Returns:
|
|
None
|
|
|
|
Exits:
|
|
Code 1 if site.exp is missing or cannot be written.
|
|
"""
|
|
site_exp_file = test_suite_dir / "site.exp"
|
|
if not site_exp_file.is_file():
|
|
_log_error_and_exit(f"site.exp not found at {site_exp_file}")
|
|
try:
|
|
with open(site_exp_file, "a", encoding="utf-8") as f:
|
|
f.write(f"\nset gdb_test_timeout {timeout_value}\n")
|
|
logger.info(
|
|
f"{STATUS_PASS} Successfully set gdb_test_timeout to {timeout_value} in {site_exp_file}"
|
|
)
|
|
except OSError as e:
|
|
_log_error_and_exit(f"Failed to write timeout to {site_exp_file}: {e}")
|
|
|
|
|
|
def discover_test_directories(
|
|
testsuite_dir: Path, exclude: Optional[List[str]] = None
|
|
) -> List[str]:
|
|
"""
|
|
Discover all test directories in the testsuite.
|
|
|
|
Args:
|
|
testsuite_dir: Path to testsuite directory.
|
|
exclude: List of directory names to exclude (default None).
|
|
|
|
Returns:
|
|
Sorted list of test directory names (e.g., ["gdb.base", "gdb.threads"]).
|
|
"""
|
|
if exclude is None:
|
|
exclude = []
|
|
|
|
test_dirs = []
|
|
for item in testsuite_dir.iterdir():
|
|
if item.is_dir() and item.name.startswith("gdb.") and item.name not in exclude:
|
|
if any(item.glob("*.exp")):
|
|
test_dirs.append(item.name)
|
|
|
|
return sorted(test_dirs)
|
|
|
|
|
|
def expand_test_paths(test_list: List[str], testsuite_dir: Path) -> List[str]:
|
|
"""
|
|
Expand test paths to a list of .exp files, validating existence.
|
|
|
|
Args:
|
|
test_list: Test paths (individual .exp files or directories).
|
|
testsuite_dir: Base directory to resolve relative paths.
|
|
|
|
Returns:
|
|
List of .exp test file paths.
|
|
|
|
Exits:
|
|
Code 1 if any test file is missing.
|
|
"""
|
|
expanded_tests: List[str] = []
|
|
|
|
for test_path in test_list:
|
|
if not test_path.endswith(".exp"):
|
|
pattern = f"{test_path}/*.exp"
|
|
matches = sorted(glob.glob(pattern, root_dir=str(testsuite_dir)))
|
|
if matches:
|
|
expanded_tests.extend(matches)
|
|
logger.info(
|
|
f"Expanded directory '{test_path}' to {len(matches)} test files"
|
|
)
|
|
else:
|
|
logger.warning(f"No test files found matching pattern: {pattern}")
|
|
else:
|
|
expanded_tests.append(test_path)
|
|
logger.info(f"Added test file: {test_path}")
|
|
|
|
# Drop duplicates while preserving first-occurrence order so that
|
|
# overlapping --tests arguments do not silently run the same test twice.
|
|
deduped_tests = list(dict.fromkeys(expanded_tests))
|
|
if len(deduped_tests) != len(expanded_tests):
|
|
dropped = len(expanded_tests) - len(deduped_tests)
|
|
logger.warning(
|
|
f"Dropped {dropped} duplicate test path(s) from the expanded test list"
|
|
)
|
|
expanded_tests = deduped_tests
|
|
|
|
# Verify that all expanded files exist.
|
|
for file_path in expanded_tests:
|
|
abs_path = testsuite_dir / file_path
|
|
if not abs_path.is_file():
|
|
_log_error_and_exit(f"Missing or invalid test file: {file_path}")
|
|
|
|
return expanded_tests
|
|
|
|
|
|
def print_env_variables() -> None:
|
|
"""
|
|
Print all environment variables for debugging.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
print_section("Environment Variables")
|
|
_log_aligned_fields([(key, value) for key, value in os.environ.items()])
|
|
|
|
|
|
def validate_path(path: Path, is_dir: bool = False, is_file: bool = False) -> Path:
|
|
"""
|
|
Validate path exists and matches expected type.
|
|
|
|
Args:
|
|
path: Path to validate.
|
|
is_dir: Require directory if True (default False).
|
|
is_file: Require file if True (default False).
|
|
|
|
Returns:
|
|
The resolved (absolute, symlink-followed) path.
|
|
|
|
Exits:
|
|
Code 1 if path doesn't exist or is wrong type.
|
|
"""
|
|
resolved_path = path.resolve()
|
|
|
|
if is_dir and not resolved_path.is_dir():
|
|
_log_error_and_exit(f"Directory does not exist: {resolved_path}")
|
|
if is_file and not resolved_path.is_file():
|
|
_log_error_and_exit(f"File does not exist: {resolved_path}")
|
|
|
|
return resolved_path
|
|
|
|
|
|
def validate_rocgdb(rocgdb_bin: Path, env_vars: Dict[str, str]) -> None:
|
|
"""
|
|
Validate ROCgdb executable can run successfully.
|
|
|
|
Args:
|
|
rocgdb_bin: Path to ROCgdb executable.
|
|
env_vars: Prepared environment (PATH/LD_LIBRARY_PATH/sanitizer options)
|
|
to use for the validation subprocesses.
|
|
|
|
Returns:
|
|
None
|
|
|
|
Exits:
|
|
Code 1 if ROCgdb fails to execute or its python validation fails.
|
|
"""
|
|
print_section("ROCgdb launcher data")
|
|
|
|
try:
|
|
# First invoke the rocgdb launcher in debug mode.
|
|
print_section("ROCgdb launcher start", border_char="-", inline=True)
|
|
env = env_vars.copy()
|
|
env["ROCGDB_WRAPPER_DEBUG"] = "1"
|
|
result = _run_command(
|
|
[str(rocgdb_bin), "--version"], env=env, capture_output=True
|
|
)
|
|
|
|
for line in result.stderr.splitlines():
|
|
logger.info(line)
|
|
logger.info("ROCgdb launcher ran successfully.")
|
|
|
|
# Now validate that we can launch rocgdb at all.
|
|
print_section("ROCgdb executable start", border_char="-", inline=True)
|
|
result = _run_command(
|
|
[str(rocgdb_bin), "--version"], env=env_vars, capture_output=True
|
|
)
|
|
|
|
for line in result.stdout.splitlines():
|
|
logger.info(line)
|
|
logger.info("ROCgdb executable ran successfully.")
|
|
except subprocess.CalledProcessError as e:
|
|
_log_error_and_exit(f"ROCgdb did not run successfully: {e.stderr}")
|
|
|
|
# Validate internal Python environment.
|
|
print_section("ROCgdb internal python data", border_char="-", inline=True)
|
|
|
|
py_cmd = (
|
|
"import sys, os, sysconfig, gdb; "
|
|
"v = sys.version_info; "
|
|
"version = f'{v.major}.{v.minor}.{v.micro}'; "
|
|
"supports = bool(sysconfig.get_config_var('Py_ENABLE_SHARED')); "
|
|
"lib_dir = sysconfig.get_config_var('LIBDIR'); "
|
|
"ld_lib = sysconfig.get_config_var('LDLIBRARY'); "
|
|
"lib_path = os.path.join(lib_dir, ld_lib) if (supports and lib_dir and ld_lib) else 'N/A'; "
|
|
"print(f'Executable: {sys.executable}'); "
|
|
"print(f'Version: {version}'); "
|
|
"print(f'Supports libpython: {supports}'); "
|
|
"print(f'libpython Path: {lib_path}'); "
|
|
'print(f\'libpython Version: {sysconfig.get_config_var("VERSION") or "N/A"}\'); '
|
|
"print(f'GDB python modules path: {gdb.PYTHONDIR}'); "
|
|
)
|
|
|
|
result = _run_command(
|
|
[str(rocgdb_bin), "-batch", "-ex", f"python {py_cmd}"],
|
|
env=env_vars,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
# Parse "label: value" pairs printed by the embedded script and
|
|
# re-emit them with colons aligned, matching the surrounding
|
|
# banner blocks. Lines without a colon are passed through verbatim.
|
|
parsed: List[Union[str, Tuple[str, str]]] = []
|
|
for line in result.stdout.splitlines():
|
|
label, sep, value = line.partition(":")
|
|
if sep:
|
|
parsed.append((label.strip(), value.strip()))
|
|
else:
|
|
parsed.append(line)
|
|
_log_aligned_fields(parsed)
|
|
logger.info("ROCgdb internal python validation successful.")
|
|
elif "Python scripting is not supported in this copy of GDB" in result.stderr:
|
|
logger.warning(
|
|
"Python scripting is not supported in this copy of GDB. Testing will proceed without Python support."
|
|
)
|
|
else:
|
|
_log_error_and_exit(f"ROCgdb python validation failed: {result.stderr}")
|
|
|
|
|
|
def run_tests(
|
|
test_suite_dir: Path,
|
|
rocgdb_bin: Path,
|
|
env_vars: Dict[str, str],
|
|
tests: List[str],
|
|
cc: str,
|
|
cxx: str,
|
|
fc: str,
|
|
compiler_label: str,
|
|
test_results: "TestResults",
|
|
args: argparse.Namespace,
|
|
xfailed_tests: Optional[Dict[str, List[str]]] = None,
|
|
) -> None:
|
|
"""
|
|
Run ROCgdb test suite with retry logic for failed tests.
|
|
|
|
Args:
|
|
test_suite_dir: Path to test suite directory.
|
|
rocgdb_bin: Path to ROCgdb binary.
|
|
env_vars: Environment variables for test execution.
|
|
tests: Test file names to run.
|
|
cc: C compiler command.
|
|
cxx: C++ compiler command.
|
|
fc: Fortran compiler command.
|
|
compiler_label: Compiler identifier (e.g., "GCC").
|
|
test_results: TestResults object for storing results.
|
|
args: Parsed command-line arguments.
|
|
xfailed_tests: Compiler labels mapped to expected failing test paths.
|
|
Used to skip retrying ignored tests when --retry-ignored-tests is off.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
max_iterations = 1 + args.max_failed_retries
|
|
current_tests = tests
|
|
|
|
for iteration in range(1, max_iterations + 1):
|
|
print_section(f"ROCgdb tests - {compiler_label} - Iteration {iteration}")
|
|
logger.info(f"Number of tests to run: {len(current_tests)}")
|
|
|
|
# Track which tests we're running in this iteration (for flaky detection).
|
|
tests_in_iteration = set(current_tests)
|
|
|
|
configure_test_suite(test_suite_dir, env_vars, args.quiet)
|
|
|
|
# default_timeout applies on every iteration. On retry iterations
|
|
# following a TIMEOUT, use retry_timeout instead (parse_arguments
|
|
# guarantees it is >= default_timeout).
|
|
timeout_to_set = args.default_timeout
|
|
if iteration > 1 and test_results.test_data.get(compiler_label, {}).get(
|
|
"TIMEOUT"
|
|
):
|
|
timeout_to_set = args.retry_timeout
|
|
|
|
if timeout_to_set is not None:
|
|
set_test_timeout(test_suite_dir, timeout_to_set)
|
|
|
|
start_time = time.perf_counter()
|
|
|
|
runtestflags_str = _build_runtestflags(
|
|
rocgdb_bin, cc, cxx, fc, args.optimization, args.runtestflags
|
|
)
|
|
|
|
if args.one_by_one:
|
|
if args.one_by_one_test_timeout is not None:
|
|
wall_clock = args.one_by_one_test_timeout
|
|
else:
|
|
# gdb_test_timeout is per-testcase, not per-.exp. Default
|
|
# generously and let the user override via --one-by-one-test-timeout.
|
|
wall_clock = ONE_BY_ONE_DEFAULT_WALL_CLOCK_TIMEOUT
|
|
logger.info(
|
|
f"Using default one-by-one wall-clock timeout: {wall_clock}s "
|
|
f"(override with --one-by-one-test-timeout)"
|
|
)
|
|
log_dir = args.one_by_one_log_dir or (test_suite_dir / "one_by_one_logs")
|
|
_execute_one_by_one(
|
|
test_suite_dir,
|
|
current_tests,
|
|
runtestflags_str,
|
|
args.check_type,
|
|
args.quiet,
|
|
env_vars,
|
|
log_dir,
|
|
compiler_label,
|
|
iteration,
|
|
wall_clock,
|
|
)
|
|
else:
|
|
cmd = [
|
|
"make",
|
|
args.check_type,
|
|
f"RUNTESTFLAGS={runtestflags_str}",
|
|
f"TESTS={' '.join(current_tests)}",
|
|
]
|
|
if args.parallel:
|
|
jobs = args.jobs or os.cpu_count()
|
|
cmd += ["FORCE_PARALLEL=1", f"-j{jobs}"]
|
|
|
|
logger.info(
|
|
f"Executing tests with {compiler_label} - Iteration {iteration}: {shlex.join(cmd)}"
|
|
)
|
|
_run_command(
|
|
cmd,
|
|
cwd=test_suite_dir,
|
|
env=env_vars,
|
|
capture_output=args.quiet,
|
|
check=False,
|
|
kill_process_group=True,
|
|
)
|
|
|
|
duration = time.perf_counter() - start_time
|
|
|
|
print_section(
|
|
f"Tests with {compiler_label} - Iteration {iteration} completed in {duration:.4f} seconds."
|
|
)
|
|
|
|
# Parse test results from gdb.sum.
|
|
results_file = test_suite_dir / "gdb.sum"
|
|
test_results.update_results(compiler_label, current_tests, results_file)
|
|
|
|
failed_tests = test_results.get_failed_tests(compiler_label)
|
|
|
|
# Detect flaky tests: tests we ran this iteration that now have PASS
|
|
# entries (and are no longer in failed_tests). Restrict to tests we
|
|
# actually ran so tests missing from gdb.sum aren't misreported as flaky.
|
|
if iteration > 1:
|
|
passed_this_iteration = {
|
|
test_file
|
|
for test_file in test_results.test_data.get(compiler_label, {}).get(
|
|
"PASS", {}
|
|
)
|
|
if test_file in tests_in_iteration
|
|
}
|
|
failed_test_files = set(failed_tests)
|
|
newly_passed = passed_this_iteration - failed_test_files
|
|
if newly_passed:
|
|
test_results.mark_flaky_tests(compiler_label, newly_passed)
|
|
|
|
if not failed_tests:
|
|
logger.info(
|
|
f"{STATUS_PASS} No failing tests for {compiler_label}. Stopping iterations."
|
|
)
|
|
break
|
|
elif iteration < max_iterations:
|
|
# Collect the failed tests for the next iteration. By default,
|
|
# tests on the ignore list are not retried: they are known failures
|
|
# and re-running them (potentially many times, for two compilers)
|
|
# adds significant wall-clock time for no diagnostic value.
|
|
# --retry-ignored-tests or --no-xfail restore the old behaviour.
|
|
retry_candidates = set(failed_tests)
|
|
if not args.retry_ignored_tests and not args.no_xfail and xfailed_tests:
|
|
all_xfailed = set(xfailed_tests.get("Generic", [])) | set(
|
|
xfailed_tests.get(compiler_label, [])
|
|
)
|
|
skipped = retry_candidates & all_xfailed
|
|
retry_candidates -= all_xfailed
|
|
if skipped:
|
|
logger.info(
|
|
f"{STATUS_WARN} Skipping retry of {len(skipped)} ignored test(s) "
|
|
f"for {compiler_label} (use --retry-ignored-tests to retry them):"
|
|
)
|
|
for t in sorted(skipped):
|
|
logger.info(f" {t}")
|
|
if not retry_candidates:
|
|
logger.info(
|
|
f"{STATUS_PASS} All remaining failures for {compiler_label} are on the "
|
|
f"ignore list. Stopping iterations."
|
|
)
|
|
break
|
|
current_tests = sorted(retry_candidates)
|
|
logger.info(
|
|
f"{STATUS_FAIL} {len(current_tests)} failing test(s) found for {compiler_label}. "
|
|
f"Proceeding to iteration {iteration + 1} with only failed tests."
|
|
)
|
|
else:
|
|
logger.info(
|
|
f"{STATUS_FAIL} {len(failed_tests)} failing test(s) remain for {compiler_label} "
|
|
f"after {max_iterations} iterations."
|
|
)
|
|
|
|
# Print the contents of gdb.log in a visually uncluttered way.
|
|
# Skipped in --one-by-one mode since per-test logs are already on disk.
|
|
if args.dump_failed_test_log and not args.one_by_one:
|
|
gdb_log_file = test_suite_dir / "gdb.log"
|
|
if gdb_log_file.is_file():
|
|
print_section("Contents of gdb.log")
|
|
# Read directly rather than via _read_file_lines: gdb.log
|
|
# uses leading whitespace for nesting, which .strip() would
|
|
# flatten and make the failure dump much harder to read.
|
|
try:
|
|
contents = gdb_log_file.read_text(
|
|
encoding="utf-8", errors="replace"
|
|
)
|
|
except OSError as e:
|
|
logger.warning(f"Failed to read {gdb_log_file}: {e}")
|
|
else:
|
|
for line in contents.splitlines():
|
|
logger.info(line)
|
|
else:
|
|
logger.warning(
|
|
f"gdb.log not found at {gdb_log_file}; skipping dump."
|
|
)
|
|
|
|
|
|
def _resolve_rocm_paths(args: argparse.Namespace) -> Tuple[Path, Path, Path]:
|
|
"""
|
|
Resolve the ROCm tree root, ROCgdb binary, and testsuite directory.
|
|
|
|
The ROCm tree root is always auto-discovered from:
|
|
ROCM_PATH (if --try-rocm-path) > OUTPUT_ARTIFACTS_DIR > script location.
|
|
|
|
The binary and testsuite paths are then derived from that root unless
|
|
the user overrode either with --rocgdb-bin / --testsuite-dir, in which
|
|
case the supplied path is honored verbatim. The two override flags are
|
|
independent — supplying one does not require the other.
|
|
|
|
Args:
|
|
args: Parsed command-line arguments.
|
|
|
|
Returns:
|
|
Tuple of (rocm_dir, rocgdb_bin, rocgdb_testsuite_dir).
|
|
|
|
Exits:
|
|
Code 1 if a configured environment-variable path does not exist.
|
|
"""
|
|
rocm_dir = None
|
|
|
|
if args.try_rocm_path:
|
|
rocm_path_env = os.getenv("ROCM_PATH")
|
|
if rocm_path_env is not None:
|
|
tmp_dir = Path(rocm_path_env).resolve()
|
|
if not tmp_dir.is_dir():
|
|
_log_error_and_exit(
|
|
f"ROCM_PATH is set to '{tmp_dir}' but the path does not exist"
|
|
)
|
|
rocm_dir = tmp_dir
|
|
logger.info(f"Using ROCM_PATH: {rocm_dir}")
|
|
else:
|
|
logger.info(
|
|
"--try-rocm-path flag set but ROCM_PATH environment variable is not set. "
|
|
"Falling back to OUTPUT_ARTIFACTS_DIR or script location."
|
|
)
|
|
|
|
if rocm_dir is None:
|
|
output_artifacts_dir = os.getenv("OUTPUT_ARTIFACTS_DIR")
|
|
if output_artifacts_dir is not None:
|
|
# OUTPUT_ARTIFACTS_DIR is set, verify it exists.
|
|
rocm_dir = Path(output_artifacts_dir).resolve()
|
|
if not rocm_dir.is_dir():
|
|
_log_error_and_exit(
|
|
f"OUTPUT_ARTIFACTS_DIR is set to '{rocm_dir}' but the path does not exist"
|
|
)
|
|
logger.info(f"Using OUTPUT_ARTIFACTS_DIR: {rocm_dir}")
|
|
else:
|
|
# OUTPUT_ARTIFACTS_DIR is not set. Fall back to the script's own
|
|
# location: when installed, this file lives at
|
|
# <rocm_tree>/tests/rocgdb/test_rocgdb.py, so three .parent hops
|
|
# land on <rocm_tree>. This branch only produces a usable path
|
|
# when run from the installed location, not from the source repo.
|
|
script_path = Path(__file__).resolve()
|
|
rocm_dir = script_path.parent.parent.parent
|
|
logger.info(f"Using script-based path: {rocm_dir}")
|
|
|
|
rocgdb_bin = (
|
|
args.rocgdb_bin if args.rocgdb_bin is not None else rocm_dir / "bin" / "rocgdb"
|
|
)
|
|
rocgdb_testsuite_dir = (
|
|
args.testsuite_dir
|
|
if args.testsuite_dir is not None
|
|
else rocm_dir / "tests" / "rocgdb" / "gdb" / "testsuite"
|
|
)
|
|
return rocm_dir, rocgdb_bin, rocgdb_testsuite_dir
|
|
|
|
|
|
def main() -> None:
|
|
"""
|
|
Main entry point for ROCgdb test suite runner.
|
|
|
|
Orchestrates argument parsing, environment setup, test execution,
|
|
and result reporting for GCC and LLVM compilers.
|
|
|
|
Returns:
|
|
None
|
|
|
|
Exits:
|
|
Code 0 if all tests pass or are expected failures, Code 1 otherwise.
|
|
"""
|
|
args = parse_arguments()
|
|
|
|
# Determine ignore list file path.
|
|
if args.ignore_list_file is None:
|
|
# Default to rocgdb_ignore_list.json in the same directory as this script.
|
|
script_dir = Path(__file__).parent
|
|
args.ignore_list_file = script_dir / "rocgdb_ignore_list.json"
|
|
else:
|
|
# User explicitly supplied a path — it must exist.
|
|
args.ignore_list_file = validate_path(args.ignore_list_file, is_file=True)
|
|
|
|
# Determine ignore list file status for configuration display.
|
|
if args.ignore_list_file.exists():
|
|
ignore_list_file_status = f"Reading from {args.ignore_list_file}"
|
|
else:
|
|
ignore_list_file_status = "disabled"
|
|
|
|
# Determine output ignore list file status for configuration display.
|
|
if args.output_ignore_list_file is not None:
|
|
output_ignore_list_file_status = f"Outputting to {args.output_ignore_list_file}"
|
|
else:
|
|
output_ignore_list_file_status = "disabled"
|
|
|
|
start_time = time.perf_counter()
|
|
|
|
rocm_dir, rocgdb_bin, rocgdb_testsuite_dir = _resolve_rocm_paths(args)
|
|
|
|
rocgdb_configure_script = rocgdb_testsuite_dir / "configure"
|
|
|
|
# Handle test selection based on command-line options.
|
|
# This must happen before print_configuration() since it displays args.tests.
|
|
if args.gpu_tests:
|
|
args.tests = ["gdb.rocm"]
|
|
logger.info("Using --gpu-tests: running gdb.rocm tests only")
|
|
elif args.cpu_tests:
|
|
args.tests = discover_test_directories(
|
|
rocgdb_testsuite_dir, exclude=["gdb.rocm"]
|
|
)
|
|
if not args.tests:
|
|
_log_error_and_exit("No CPU test directories found in testsuite")
|
|
logger.info(
|
|
f"Using --cpu-tests: discovered {len(args.tests)} CPU test directories"
|
|
)
|
|
elif args.tests is None:
|
|
# Default case: no test option was specified.
|
|
args.tests = ["gdb.rocm", "gdb.dwarf2"]
|
|
logger.info("Using default tests: gdb.rocm and gdb.dwarf2")
|
|
|
|
# Print env variables.
|
|
print_env_variables()
|
|
|
|
# Print Python information.
|
|
print_python_info()
|
|
|
|
# Show configuration summary.
|
|
print_configuration(
|
|
rocgdb_bin,
|
|
rocgdb_testsuite_dir,
|
|
rocgdb_configure_script,
|
|
rocm_dir,
|
|
args,
|
|
ignore_list_file_status,
|
|
output_ignore_list_file_status,
|
|
)
|
|
|
|
# Load ignore list from JSON file.
|
|
xfailed_tests = load_ignore_list_from_json(args.ignore_list_file)
|
|
|
|
# Validate critical files exist.
|
|
validate_required_files(
|
|
{
|
|
"ROCgdb launcher": rocgdb_bin,
|
|
"ROCgdb configure script": rocgdb_configure_script,
|
|
}
|
|
)
|
|
|
|
if platform.system() == "Linux":
|
|
# Set core dump file limit and display info.
|
|
setup_core_file_info()
|
|
|
|
# Prepare environment for tests.
|
|
env_vars = setup_environment(rocm_dir)
|
|
setup_sanitizer_environment(env_vars, rocgdb_testsuite_dir)
|
|
|
|
# Validate that we can run rocgdb.
|
|
validate_rocgdb(rocgdb_bin, env_vars)
|
|
|
|
# Verify executables presence.
|
|
check_executables(
|
|
[
|
|
"make",
|
|
"amdclang++",
|
|
"gcc",
|
|
"g++",
|
|
"gfortran",
|
|
"clang",
|
|
"clang++",
|
|
"flang",
|
|
"runtest",
|
|
],
|
|
env_vars,
|
|
)
|
|
|
|
print_section("Expanding test paths")
|
|
tests = expand_test_paths(args.tests, rocgdb_testsuite_dir)
|
|
|
|
if not tests:
|
|
_log_error_and_exit("No test files found")
|
|
|
|
# Compiler configurations.
|
|
compilers = [
|
|
("gcc", "g++", "gfortran", "GCC"),
|
|
("clang", "clang++", "flang", "LLVM"),
|
|
]
|
|
|
|
# Initialize test result tracking.
|
|
test_results = TestResults()
|
|
test_results.group_results = args.group_results
|
|
|
|
# Run tests for all configured compilers.
|
|
for cc, cxx, fc, compiler_label in compilers:
|
|
run_tests(
|
|
rocgdb_testsuite_dir,
|
|
rocgdb_bin,
|
|
env_vars,
|
|
tests,
|
|
cc,
|
|
cxx,
|
|
fc,
|
|
compiler_label,
|
|
test_results,
|
|
args,
|
|
xfailed_tests,
|
|
)
|
|
|
|
# Final summaries.
|
|
test_results.print_all_summaries()
|
|
overall_pass = test_results.print_final_status(xfailed_tests, args.no_xfail)
|
|
|
|
# Generate and write output ignore list if requested.
|
|
if args.output_ignore_list_file is not None:
|
|
ignore_list = test_results.generate_ignore_list()
|
|
try:
|
|
with open(args.output_ignore_list_file, "w", encoding="utf-8") as f:
|
|
json.dump(ignore_list, f, indent=2, ensure_ascii=False)
|
|
f.write("\n") # Add trailing newline.
|
|
logger.info(
|
|
f"{STATUS_PASS} Generated ignore list written to {args.output_ignore_list_file}"
|
|
)
|
|
except OSError as e:
|
|
print_section("Generated ignore list (write failed)")
|
|
for line in json.dumps(
|
|
ignore_list, indent=2, ensure_ascii=False
|
|
).splitlines():
|
|
logger.info(line)
|
|
_log_error_and_exit(
|
|
f"Failed to write ignore list to {args.output_ignore_list_file}: {e}"
|
|
)
|
|
|
|
logger.info("")
|
|
duration = time.perf_counter() - start_time
|
|
logger.info(f"Total test run duration: {duration:.4f} seconds.")
|
|
|
|
sys.exit(0 if overall_pass else 1)
|
|
|
|
|
|
def _build_runtestflags(
|
|
rocgdb_bin: Path,
|
|
cc: str,
|
|
cxx: str,
|
|
fc: str,
|
|
optimization: str,
|
|
runtestflags: str,
|
|
) -> str:
|
|
"""
|
|
Build RUNTESTFLAGS string for test execution.
|
|
|
|
Args:
|
|
rocgdb_bin: Path to ROCgdb binary.
|
|
cc: C compiler command.
|
|
cxx: C++ compiler command.
|
|
fc: Fortran compiler command.
|
|
optimization: Optimization flags.
|
|
runtestflags: Additional flags.
|
|
|
|
Returns:
|
|
Complete RUNTESTFLAGS string.
|
|
"""
|
|
parts = [
|
|
f"GDB={shlex.quote(str(rocgdb_bin))}",
|
|
f"CC_FOR_TARGET={shlex.quote(cc)}",
|
|
f"CXX_FOR_TARGET={shlex.quote(cxx)}",
|
|
f"F77_FOR_TARGET={shlex.quote(fc)}",
|
|
f"F90_FOR_TARGET={shlex.quote(fc)}",
|
|
"HIP_COMPILER_FOR_TARGET=amdclang++",
|
|
]
|
|
if optimization:
|
|
parts.append(f"CFLAGS_FOR_TARGET={shlex.quote(optimization)}")
|
|
if runtestflags:
|
|
parts.append(runtestflags)
|
|
|
|
return " ".join(parts)
|
|
|
|
|
|
def _execute_one_by_one(
|
|
test_suite_dir: Path,
|
|
tests: List[str],
|
|
runtestflags_str: str,
|
|
check_type: str,
|
|
quiet: bool,
|
|
env_vars: Dict[str, str],
|
|
log_dir: Path,
|
|
compiler_label: str,
|
|
iteration: int,
|
|
wall_clock_timeout: int,
|
|
) -> None:
|
|
"""
|
|
Run each test in its own `make check` invocation, capturing per-test
|
|
gdb.log under <log_dir>/<compiler>/{pass,fail}/<group>/ and writing a
|
|
sibling .status file with timing and iteration metadata. Per-test gdb.sum
|
|
content is aggregated into the canonical gdb.sum so the existing
|
|
result-parsing path consumes it unchanged.
|
|
|
|
A test is bucketed into "fail/" if its per-test gdb.sum contains any line
|
|
starting with FAIL:, UNRESOLVED:, or ERROR:, or if the wall-clock timeout
|
|
fires; otherwise "pass/".
|
|
|
|
Args:
|
|
test_suite_dir: Path to test suite directory.
|
|
tests: Test files to run individually (e.g. ["gdb.rocm/foo.exp"]).
|
|
runtestflags_str: RUNTESTFLAGS string built by _build_runtestflags.
|
|
check_type: GDB testsuite target to invoke (check / check-read1 / check-readmore).
|
|
quiet: Capture subprocess output if True.
|
|
env_vars: Environment variables for test execution.
|
|
log_dir: Root directory for per-test gdb.log captures.
|
|
compiler_label: Compiler identifier (e.g., "GCC").
|
|
iteration: Current retry iteration (1-based) — controls log filename suffix.
|
|
wall_clock_timeout: Wall-clock timeout (seconds) for each `make check`.
|
|
|
|
Returns:
|
|
None
|
|
|
|
Exits:
|
|
Code 1 if the aggregated gdb.sum cannot be written.
|
|
"""
|
|
aggregated_lines: List[str] = []
|
|
retry_number = iteration - 1
|
|
base_suffix = "" if retry_number == 0 else f".retry-{retry_number}"
|
|
|
|
for test in tests:
|
|
cmd = [
|
|
"make",
|
|
check_type,
|
|
f"RUNTESTFLAGS={runtestflags_str}",
|
|
f"TESTS={test}",
|
|
]
|
|
logger.info(f"[one-by-one] {compiler_label} - {test}: {shlex.join(cmd)}")
|
|
|
|
# Purge stale gdb.sum / gdb.log so a killed or failing `make` cannot
|
|
# cause this iteration to read the previous test's leftovers.
|
|
for stale in (test_suite_dir / "gdb.sum", test_suite_dir / "gdb.log"):
|
|
try:
|
|
stale.unlink()
|
|
except FileNotFoundError:
|
|
pass
|
|
except OSError as e:
|
|
logger.warning(
|
|
f"{STATUS_WARN} {test}: failed to clear stale {stale.name}: {e}"
|
|
)
|
|
|
|
wall_clock_timed_out = False
|
|
start = time.perf_counter()
|
|
try:
|
|
_run_command(
|
|
cmd,
|
|
cwd=test_suite_dir,
|
|
env=env_vars,
|
|
capture_output=quiet,
|
|
check=False,
|
|
timeout=wall_clock_timeout,
|
|
kill_process_group=True,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
wall_clock_timed_out = True
|
|
logger.warning(
|
|
f"{STATUS_WARN} {test}: wall-clock timeout after {wall_clock_timeout}s"
|
|
)
|
|
duration = time.perf_counter() - start
|
|
|
|
# Read per-test gdb.sum for both bucketing and aggregation.
|
|
gdb_sum = test_suite_dir / "gdb.sum"
|
|
per_test_lines: List[str] = []
|
|
if gdb_sum.is_file():
|
|
per_test_lines = _read_file_lines(
|
|
gdb_sum, f"during one-by-one aggregation of {test}"
|
|
)
|
|
elif not wall_clock_timed_out:
|
|
# `make` either exited before runtest could produce gdb.sum or
|
|
# runtest itself failed without writing one. Without this
|
|
# synthesized line the test would contribute nothing to the
|
|
# aggregated gdb.sum and silently count as a pass.
|
|
logger.warning(
|
|
f"{STATUS_WARN} {test}: gdb.sum not produced; "
|
|
f"recording as UNRESOLVED."
|
|
)
|
|
per_test_lines.append(
|
|
f"UNRESOLVED: {test}: one-by-one make produced no gdb.sum"
|
|
)
|
|
|
|
# On wall-clock timeout, synthesize a FAIL line so update_results
|
|
# tracks the timeout. The "(timeout)" suffix triggers the existing
|
|
# TIMEOUT classification via TIMEOUT_RE.
|
|
if wall_clock_timed_out:
|
|
per_test_lines.append(
|
|
f"FAIL: {test}: one-by-one wall-clock timeout (timeout)"
|
|
)
|
|
|
|
aggregated_lines.extend(per_test_lines)
|
|
|
|
# Determine pass/fail using the same definition as TestResults, and
|
|
# pick the most informative status label by scanning for the highest
|
|
# severity prefix present (TIMEOUT > ERROR > UNRESOLVED > FAIL > PASS).
|
|
has_error = has_unresolved = has_fail = False
|
|
for line in per_test_lines:
|
|
if line.startswith("ERROR:"):
|
|
has_error = True
|
|
elif line.startswith("UNRESOLVED:"):
|
|
has_unresolved = True
|
|
elif line.startswith("FAIL:"):
|
|
has_fail = True
|
|
test_failed = wall_clock_timed_out or has_fail or has_unresolved or has_error
|
|
status_dir = "fail" if test_failed else "pass"
|
|
if wall_clock_timed_out:
|
|
status_text = "TIMEOUT"
|
|
elif has_error:
|
|
status_text = "ERROR"
|
|
elif has_fail:
|
|
status_text = "FAIL"
|
|
elif has_unresolved:
|
|
status_text = "UNRESOLVED"
|
|
else:
|
|
status_text = "PASS"
|
|
|
|
test_path = Path(test)
|
|
target_dir = log_dir / compiler_label / status_dir / test_path.parent
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
log_target = target_dir / f"{test_path.stem}{base_suffix}.log"
|
|
status_target = target_dir / f"{test_path.stem}{base_suffix}.status"
|
|
|
|
gdb_log = test_suite_dir / "gdb.log"
|
|
if gdb_log.is_file():
|
|
try:
|
|
shutil.copy2(gdb_log, log_target)
|
|
except OSError as e:
|
|
logger.warning(
|
|
f"{STATUS_WARN} {test}: failed to copy gdb.log to {log_target}: {e}"
|
|
)
|
|
else:
|
|
logger.warning(
|
|
f"{STATUS_WARN} {test}: gdb.log not produced; skipping log capture."
|
|
)
|
|
|
|
status_content = (
|
|
f"test: {test}\n"
|
|
f"compiler: {compiler_label}\n"
|
|
f"iteration: {iteration}\n"
|
|
f"retry_number: {retry_number}\n"
|
|
f"status: {status_text}\n"
|
|
f"duration_seconds: {duration:.3f}\n"
|
|
f"wall_clock_timeout: {bool(wall_clock_timed_out)}\n"
|
|
)
|
|
try:
|
|
status_target.write_text(status_content, encoding="utf-8")
|
|
except OSError as e:
|
|
logger.warning(
|
|
f"{STATUS_WARN} {test}: failed to write status file {status_target}: {e}"
|
|
)
|
|
|
|
aggregated_sum = test_suite_dir / "gdb.sum"
|
|
try:
|
|
aggregated_sum.write_text("\n".join(aggregated_lines) + "\n", encoding="utf-8")
|
|
except OSError as e:
|
|
_log_error_and_exit(f"Failed to write aggregated gdb.sum: {e}")
|
|
|
|
|
|
def print_python_info() -> None:
|
|
"""
|
|
Print Python interpreter information including version and libpython details.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
print_section("Python information")
|
|
|
|
version = (
|
|
f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
)
|
|
supports_libpython = bool(sysconfig.get_config_var("Py_ENABLE_SHARED"))
|
|
|
|
lib_path = "N/A"
|
|
lib_version = "N/A"
|
|
|
|
if supports_libpython:
|
|
lib_dir = sysconfig.get_config_var("LIBDIR")
|
|
ld_library = sysconfig.get_config_var("LDLIBRARY")
|
|
if lib_dir and ld_library:
|
|
lib_path = os.path.join(lib_dir, ld_library)
|
|
lib_version = str(sysconfig.get_config_var("VERSION") or "N/A")
|
|
|
|
lib_path_exists = lib_path != "N/A" and os.path.exists(lib_path)
|
|
|
|
if not supports_libpython:
|
|
supports_value = "No"
|
|
elif lib_path_exists:
|
|
supports_value = "Yes"
|
|
else:
|
|
supports_value = "Supported but libpython is missing"
|
|
|
|
lib_path_value = (
|
|
f"{lib_path} (missing)"
|
|
if lib_path != "N/A" and not lib_path_exists
|
|
else lib_path
|
|
)
|
|
|
|
_log_aligned_fields(
|
|
[
|
|
("Executable", sys.executable),
|
|
("Version", version),
|
|
("Supports libpython", supports_value),
|
|
("libpython Path", lib_path_value),
|
|
("libpython Version", lib_version),
|
|
]
|
|
)
|
|
|
|
|
|
def print_configuration(
|
|
rocgdb_bin: Path,
|
|
testsuite_dir: Path,
|
|
configure_script: Path,
|
|
rocm_tree_root: Path,
|
|
args: argparse.Namespace,
|
|
ignore_list_file_status: str,
|
|
output_ignore_list_file_status: str,
|
|
) -> None:
|
|
"""
|
|
Display the ROCgdb test configuration in a formatted table.
|
|
|
|
Args:
|
|
rocgdb_bin: Path to the ROCgdb binary.
|
|
testsuite_dir: Path to the test suite directory.
|
|
configure_script: Path to the configure script for the test suite.
|
|
rocm_tree_root: Path to the root of the ROCm tree.
|
|
args: Parsed command-line arguments containing additional configuration values.
|
|
ignore_list_file_status: Status message for ignore list file.
|
|
output_ignore_list_file_status: Status message for output ignore list file.
|
|
|
|
Returns:
|
|
None
|
|
|
|
Notes:
|
|
- Prints an ASCII table summarizing binary location, directories, test selection,
|
|
and execution settings.
|
|
- Fields come directly from parsed CLI arguments.
|
|
"""
|
|
|
|
if not args.parallel:
|
|
parallel_info = "Disabled"
|
|
elif args.jobs is None:
|
|
parallel_info = f"Enabled ({os.cpu_count()} jobs, from cpu_count)"
|
|
else:
|
|
parallel_info = f"Enabled ({args.jobs} jobs)"
|
|
|
|
default_timeout_display = (
|
|
f"{args.default_timeout} seconds"
|
|
if args.default_timeout is not None
|
|
else "Dejagnu's default"
|
|
)
|
|
|
|
one_by_one_timeout_display = (
|
|
f"{args.one_by_one_test_timeout} seconds"
|
|
if args.one_by_one_test_timeout is not None
|
|
else f"{ONE_BY_ONE_DEFAULT_WALL_CLOCK_TIMEOUT} seconds (auto)"
|
|
)
|
|
|
|
fields = [
|
|
("OS", platform.system()),
|
|
("ROCm Directory", rocm_tree_root),
|
|
("ROCgdb Binary", rocgdb_bin),
|
|
("Testsuite Directory", testsuite_dir),
|
|
("Configure Script", configure_script),
|
|
("Tests", " ".join(args.tests)),
|
|
("Check Type", args.check_type),
|
|
("Parallel Execution", parallel_info),
|
|
("One-by-one Mode", "Enabled" if args.one_by_one else "Disabled"),
|
|
]
|
|
if args.one_by_one:
|
|
fields.append(
|
|
(
|
|
"One-by-one Log Dir",
|
|
args.one_by_one_log_dir or (testsuite_dir / "one_by_one_logs"),
|
|
)
|
|
)
|
|
fields.append(("One-by-one Test Timeout", one_by_one_timeout_display))
|
|
fields.extend(
|
|
[
|
|
("Use FAIL ignore list", "Not using" if args.no_xfail else "Using"),
|
|
(
|
|
"Retry ignored tests",
|
|
"Enabled" if args.retry_ignored_tests else "Disabled",
|
|
),
|
|
("Group Results", "Enabled" if args.group_results else "Disabled"),
|
|
("Default Timeout", default_timeout_display),
|
|
("Retry Timeout", f"{args.retry_timeout} seconds"),
|
|
("Max Failed Retries", args.max_failed_retries),
|
|
("Optimization", args.optimization or "None"),
|
|
("Additional Runtest Flags", args.runtestflags or "None"),
|
|
("Ignore List", ignore_list_file_status),
|
|
("Output Ignore List", output_ignore_list_file_status),
|
|
]
|
|
)
|
|
print_section("ROCgdb Test Suite Configuration")
|
|
_log_aligned_fields([(label, str(value)) for label, value in fields])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|