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
1363 lines
55 KiB
Python
1363 lines
55 KiB
Python
#!/usr/bin/env python3
|
|
#
|
|
# Copyright (C) Advanced Micro Devices. All rights reserved.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
# this software and associated documentation files (the "Software"), to deal in
|
|
# the Software without restriction, including without limitation the rights to
|
|
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
# the Software, and to permit persons to whom the Software is furnished to do so,
|
|
# subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in all
|
|
# copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
"""Core shared infrastructure for the AMD SMI Python test suite.
|
|
|
|
Owns three responsibilities:
|
|
* amdsmi module discovery -- resolves the install via
|
|
AMDSMI_PATH -> ROCM_HOME -> ROCM_PATH -> /opt/rocm, inserts it on
|
|
sys.path, and guards against a shadowing site-packages copy
|
|
(see ROCM-1552 / PR #6359).
|
|
* the ``Common`` helper base -- enum tables, ``Test_API_*`` drivers,
|
|
``check_ret`` and friends, held as ``self.common`` by the API tests
|
|
(intentionally NOT a unittest.TestCase).
|
|
* the unittest runner machinery -- ``run_test_dir``,
|
|
``GTestSummaryRunner``, verbosity/-k/-l parsing and the help/legend
|
|
printers used by the three top-level runners.
|
|
|
|
Imported by every functional/unit leaf test and by all three runners
|
|
(integration_test.py, cli_unit_test.py, unit_tests.py).
|
|
"""
|
|
|
|
import contextlib
|
|
import inspect
|
|
import io
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import sys
|
|
import time
|
|
import unittest
|
|
|
|
|
|
def _print_path_remediation(script, file):
|
|
"""Print the shared 'fix with one of:' remediation block.
|
|
|
|
Used by both print_amdsmi_path_help() (-h context) and print_shadow_error()
|
|
(import-error context) to avoid duplicating the step list.
|
|
"""
|
|
print(
|
|
"\t 1. Run the tests from the installed amd-smi-lib-tests package location (RECOMMENDED):\n"
|
|
f"\t\t `sudo $ROCM_PATH/share/amd_smi/tests/python_unittest/{script} -v`\n"
|
|
f"\t\t e.g. `sudo /opt/rocm/share/amd_smi/tests/python_unittest/{script} -v`\n\n"
|
|
"\t 2. Set AMDSMI_PATH to point at the correct build:\n"
|
|
"\t\t `export AMDSMI_PATH=$ROCM_PATH/share/amd_smi`\n\n"
|
|
"\t 3. Reinstall the correct amd-smi-lib package (removes the stale Python package too):\n"
|
|
"\t\t `sudo apt remove amd-smi-lib && sudo apt install amd-smi-lib`\n"
|
|
"\t\t or, if amd-smi was installed directly via pip:\n"
|
|
"\t\t `sudo pip uninstall amdsmi`",
|
|
file=file,
|
|
)
|
|
|
|
|
|
def print_amdsmi_path_help(file=sys.stdout):
|
|
"""Print env-var documentation, path remediation guidance, and usage examples for -h output."""
|
|
_script = os.path.basename(sys.argv[0]) or "<script>"
|
|
print("\nAdditional options:", file=file)
|
|
print(" -l, --list List all available tests and exit", file=file)
|
|
print(" -x, --exclude PAT Skip tests whose id contains PAT (inverse of -k)", file=file)
|
|
print(file=file)
|
|
print("Environment variables:", file=file)
|
|
print(" AMDSMI_PATH Path to amd_smi share dir (overrides ROCM_HOME/ROCM_PATH)", file=file)
|
|
print(" ROCM_HOME ROCm install root, used when AMDSMI_PATH is unset", file=file)
|
|
print(" ROCM_PATH Fallback ROCm install root (default: /opt/rocm)", file=file)
|
|
print(file=file)
|
|
print("If amd-smi loads from the wrong path, fix with one of:", file=file)
|
|
_print_path_remediation(_script, file)
|
|
_full = "sudo " + (sys.argv[0] or _script)
|
|
_base = sys.argv[0] or _script
|
|
_cmds = [
|
|
(_base + " -l", "list all available tests"),
|
|
(_full + " -v", "run all tests, verbose with print statements (RECOMMENDED)"),
|
|
(_full + ' -k "test_name" -v', "run only tests matching substring"),
|
|
(_full + " -x performance -v", "run everything except the performance tests"),
|
|
(_full + " -q", "run all tests, quiet with no print statements"),
|
|
]
|
|
_w = max(len(cmd) for cmd, _ in _cmds) + 2
|
|
print(file=file)
|
|
print("Examples:", file=file)
|
|
for cmd, desc in _cmds:
|
|
print(f" {cmd:<{_w}} - {desc}", file=file)
|
|
|
|
|
|
def print_shadow_error(script, loaded_from, expected_path, file=sys.stderr):
|
|
"""Print an actionable error when amdsmi was loaded from the wrong path.
|
|
|
|
Prints an ERROR header identifying the unexpected source, the shadowing
|
|
diagnosis, the shared remediation steps, and a pointer to -h for more details.
|
|
"""
|
|
print(
|
|
f"ERROR: amdsmi loaded from '{loaded_from}' instead of expected path '{expected_path}'.",
|
|
file=file,
|
|
)
|
|
print("A system-installed amdsmi package is shadowing the test target.", file=file)
|
|
print("Fix with one of:", file=file)
|
|
_print_path_remediation(script, file)
|
|
print(f"\nRefer to `{script} -h` for more details.", file=file)
|
|
|
|
|
|
amdsmi_path = os.environ.get("AMDSMI_PATH") or os.path.join(
|
|
os.environ.get("ROCM_HOME") or os.environ.get("ROCM_PATH") or "/opt/rocm", "share/amd_smi"
|
|
)
|
|
if not os.path.exists(amdsmi_path):
|
|
raise FileNotFoundError(
|
|
f'amdsmi path "{amdsmi_path}" does not exist. '
|
|
"Set ROCM_HOME, ROCM_PATH, or AMDSMI_PATH to the correct install location."
|
|
)
|
|
sys.path.insert(0, amdsmi_path)
|
|
try:
|
|
import amdsmi
|
|
except ImportError as e:
|
|
raise ImportError(f'Could not import the "amdsmi" module from "{amdsmi_path}"') from e
|
|
|
|
# Verify the imported amdsmi package came from amdsmi_path, not a system-installed
|
|
# package in dist-packages. A stale system install wins over sys.path.append() because
|
|
# dist-packages is already on sys.path at interpreter startup; insert(0,...) above
|
|
# prevents this, but error explicitly in case amdsmi was already cached in sys.modules.
|
|
#
|
|
# Example of how to trigger this error output (for test purposes):
|
|
# sudo AMDSMI_PATH=/tmp /opt/rocm/share/amd_smi/tests/python_unittest/cli_unit_test.py -v
|
|
_amdsmi_file = getattr(amdsmi, "__file__", None) or ""
|
|
if not os.path.realpath(_amdsmi_file).startswith(os.path.realpath(amdsmi_path) + os.sep):
|
|
_script = os.path.basename(sys.argv[0]) or "unit_tests.py"
|
|
print_shadow_error(_script, _amdsmi_file, amdsmi_path)
|
|
# For direct test-script invocations use sys.exit so no Python traceback
|
|
# clutters the remediation output. For anything else (pytest, IDE runners,
|
|
# ad-hoc imports) raise so the caller gets a clear error with location.
|
|
_known_scripts = ("unit_tests.py", "integration_test.py", "cli_unit_test.py")
|
|
_main_file = getattr(sys.modules.get("__main__"), "__file__", "") or ""
|
|
if os.path.basename(_main_file) in _known_scripts:
|
|
sys.exit(1)
|
|
raise RuntimeError(
|
|
f"amdsmi loaded from wrong path: '{_amdsmi_file}' (expected under '{amdsmi_path}')"
|
|
)
|
|
|
|
#################################################
|
|
# Module level functions, not part of the class #
|
|
#################################################
|
|
|
|
# Canonical status-string sentinels, shared by the Common class and the
|
|
# standalone benchmark suites (single source of truth).
|
|
PASS = "AMDSMI_STATUS_SUCCESS"
|
|
FAIL = "AMDSMI_STATUS_INVAL"
|
|
|
|
# Status-code -> name map, derived once from the AmdSmiStatus enum so the table
|
|
# never drifts from the library. Shared by the Common class and the standalone
|
|
# performance benchmark suites (which intentionally do not inherit Common).
|
|
ERROR_MAP = {str(member.value): f"AMDSMI_STATUS_{member.name}" for member in amdsmi.AmdSmiStatus}
|
|
|
|
VERBOSITY_QUIET = 0 # -q / --quiet
|
|
VERBOSITY_NORMAL = 1 # default (dot-per-test)
|
|
VERBOSITY_VERBOSE = 2 # -v / --verbose (per-test result lines)
|
|
|
|
|
|
def build_type_lists():
|
|
"""Enum-derived test-parameter lists, each ``[(name, enum_value, expected)]``.
|
|
|
|
Pure: built solely from the amdsmi enums/wrapper with no ``amdsmi_init`` or
|
|
hardware access, so it is safe to call at import or test-discovery time.
|
|
Shared by the ``Common`` class and the standalone performance benchmark
|
|
suites (which intentionally avoid constructing ``Common`` to keep hardware
|
|
init out of collection), so this enum-to-list logic lives in one place.
|
|
"""
|
|
status_types = [
|
|
(member.name, amdsmi.AmdSmiStatus(member.value), PASS) for member in amdsmi.AmdSmiStatus
|
|
]
|
|
|
|
clk_types = []
|
|
for member in amdsmi.AmdSmiClkType:
|
|
cond = PASS
|
|
if member.name in ["DCEF", "PCIE"]:
|
|
cond = [PASS, FAIL]
|
|
clk_types.append((member.name, amdsmi.AmdSmiClkType(member.value), cond))
|
|
|
|
clk_limit_types = [
|
|
(member.name, amdsmi.AmdSmiClkLimitType(member.value), PASS)
|
|
for member in amdsmi.AmdSmiClkLimitType
|
|
]
|
|
|
|
# amdsmi_get_cpu_current_io_bandwidth(handle, encoding: int, link_name: str) takes a
|
|
# bandwidth-type int and a link-ID string (see amdsmi_interface). The valid values come
|
|
# from the amdsmi_link_id_bw_type_t struct in amdsmi.h: link names P0-P4/G0-G7, and
|
|
# bandwidth types 1 (Aggregate), 2 (Read), 4 (Write). For IO bandwidth only AGG (1) is
|
|
# used; Read/Write (2/4) apply to XGMI bandwidth. Each tuple is
|
|
# (link_name, bw_type, expected): the consumers pass element 0 as link_name and element
|
|
# 1 as the encoding int. This mirrors the public interface contract rather than scraping
|
|
# wrapper *_BW0 symbols (the old scrape passed the symbol name, e.g. "AGG_BW0", as
|
|
# link_name, which is not a valid link ID).
|
|
io_bw_agg_type = 1 # AGG: the bandwidth type used for IO bandwidth
|
|
io_bw_encodings = [
|
|
(link_name, io_bw_agg_type, PASS)
|
|
for link_name in (
|
|
"P0",
|
|
"P1",
|
|
"P2",
|
|
"P3",
|
|
"P4",
|
|
"G0",
|
|
"G1",
|
|
"G2",
|
|
"G3",
|
|
"G4",
|
|
"G5",
|
|
"G6",
|
|
"G7",
|
|
)
|
|
]
|
|
|
|
gpu_blocks = []
|
|
for member in amdsmi.AmdSmiGpuBlock:
|
|
cond = PASS
|
|
if member.name in ["INVALID", "RESERVED"]:
|
|
cond = FAIL
|
|
gpu_blocks.append((member.name, amdsmi.AmdSmiGpuBlock(member.value), cond))
|
|
|
|
memory_types = [
|
|
(member.name, amdsmi.AmdSmiMemoryType(member.value), PASS)
|
|
for member in amdsmi.AmdSmiMemoryType
|
|
]
|
|
|
|
reg_types = [
|
|
(member.name, amdsmi.AmdSmiRegType(member.value), PASS) for member in amdsmi.AmdSmiRegType
|
|
]
|
|
|
|
voltage_metrics = [
|
|
(member.name, amdsmi.AmdSmiVoltageMetric(member.value), PASS)
|
|
for member in amdsmi.AmdSmiVoltageMetric
|
|
]
|
|
|
|
voltage_types = []
|
|
for member in amdsmi.AmdSmiVoltageType:
|
|
cond = PASS
|
|
if member.name in ["INVALID"]:
|
|
cond = FAIL
|
|
voltage_types.append((member.name, amdsmi.AmdSmiVoltageType(member.value), cond))
|
|
|
|
link_types = []
|
|
for member in amdsmi.AmdSmiLinkType:
|
|
cond = PASS
|
|
if member.name in ["AMDSMI_LINK_TYPE_NOT_APPLICABLE", "AMDSMI_LINK_TYPE_UNKNOWN"]:
|
|
cond = FAIL
|
|
link_types.append((member.name, amdsmi.AmdSmiLinkType(member.value), cond))
|
|
|
|
temperature_types = [
|
|
(member.name, amdsmi.AmdSmiTemperatureType(member.value), PASS)
|
|
for member in amdsmi.AmdSmiTemperatureType
|
|
]
|
|
|
|
temperature_metrics = [
|
|
(member.name, amdsmi.AmdSmiTemperatureMetric(member.value), PASS)
|
|
for member in amdsmi.AmdSmiTemperatureMetric
|
|
]
|
|
|
|
utilization_counter_types: list = [
|
|
(member.name, amdsmi.AmdSmiUtilizationCounterType(member.value), PASS)
|
|
for member in amdsmi.AmdSmiUtilizationCounterType
|
|
]
|
|
# Negative test: integer 100 is out of range for AmdSmiUtilizationCounterType;
|
|
# the API must reject it. Not a real enum member so must be added explicitly.
|
|
utilization_counter_types.append(("UTILIZATION_COUNTER_BAD", 100, FAIL))
|
|
|
|
event_groups = []
|
|
for member in amdsmi.AmdSmiEventGroup:
|
|
cond = PASS
|
|
if member.name in ["GRP_INVALID"]:
|
|
cond = FAIL
|
|
event_groups.append((member.name, amdsmi.AmdSmiEventGroup(member.value), cond))
|
|
|
|
event_types = [
|
|
(member.name, amdsmi.AmdSmiEventType(member.value), PASS)
|
|
for member in amdsmi.AmdSmiEventType
|
|
]
|
|
|
|
counter_commands = [
|
|
(member.name, amdsmi.AmdSmiCounterCommand(member.value), PASS)
|
|
for member in amdsmi.AmdSmiCounterCommand
|
|
]
|
|
|
|
compute_partition_types = []
|
|
for member in amdsmi.AmdSmiComputePartitionType:
|
|
cond = PASS
|
|
if member.name in ["INVALID"]:
|
|
cond = FAIL
|
|
compute_partition_types.append(
|
|
(member.name, amdsmi.AmdSmiComputePartitionType(member.value), cond)
|
|
)
|
|
|
|
memory_partition_types = []
|
|
for member in amdsmi.AmdSmiMemoryPartitionType:
|
|
cond = PASS
|
|
if member.name in ["UNKNOWN"]:
|
|
cond = FAIL
|
|
elif member.name in ["NPS4", "NPS8"]:
|
|
# NPS4/NPS8 are hardware-dependent; accept success or invalid depending on support
|
|
# BTW - no asic supports NPS8...
|
|
cond = [PASS, FAIL]
|
|
memory_partition_types.append(
|
|
(member.name, amdsmi.AmdSmiMemoryPartitionType(member.value), cond)
|
|
)
|
|
|
|
accelerator_partition_mem_alloc_mode_types = []
|
|
for member in amdsmi.AmdSmiAcceleratorPartitionMemAllocModeType:
|
|
cond = PASS
|
|
if member.name in ["INVALID"]:
|
|
cond = FAIL
|
|
accelerator_partition_mem_alloc_mode_types.append(
|
|
(member.name, amdsmi.AmdSmiAcceleratorPartitionMemAllocModeType(member.value), cond)
|
|
)
|
|
|
|
freq_inds = []
|
|
for member in amdsmi.AmdSmiFreqInd:
|
|
cond = PASS
|
|
if member.name in ["INVALID"]:
|
|
cond = FAIL
|
|
freq_inds.append((member.name, amdsmi.AmdSmiFreqInd(member.value), cond))
|
|
|
|
power_profile_preset_masks = []
|
|
for member in amdsmi.AmdSmiPowerProfilePresetMasks:
|
|
cond = PASS
|
|
if member.name in ["INVALID"]:
|
|
# INVALID (0xFFFFFFFFFFFFFFFF) is intentionally tested with a specific expected
|
|
# status rather than the generic FAIL (AMDSMI_STATUS_INVAL). The API returns
|
|
# AMDSMI_STATUS_INPUT_OUT_OF_BOUNDS for this sentinel, not AMDSMI_STATUS_INVAL.
|
|
cond = "AMDSMI_STATUS_INPUT_OUT_OF_BOUNDS"
|
|
power_profile_preset_masks.append(
|
|
(member.name, amdsmi.AmdSmiPowerProfilePresetMasks(member.value), cond)
|
|
)
|
|
|
|
processor_types = []
|
|
for member in amdsmi.AmdSmiProcessorType:
|
|
cond = PASS
|
|
if member.name in ["UNKNOWN"]:
|
|
cond = FAIL
|
|
processor_types.append((member.name, amdsmi.AmdSmiProcessorType(member.value), cond))
|
|
|
|
dev_perf_levels = []
|
|
for member in amdsmi.AmdSmiDevPerfLevel:
|
|
cond = PASS
|
|
if member.name in ["UNKNOWN"]:
|
|
cond = FAIL
|
|
dev_perf_levels.append((member.name, amdsmi.AmdSmiDevPerfLevel(member.value), cond))
|
|
|
|
return {
|
|
"status_types": status_types,
|
|
"clk_types": clk_types,
|
|
"clk_limit_types": clk_limit_types,
|
|
"io_bw_encodings": io_bw_encodings,
|
|
"gpu_blocks": gpu_blocks,
|
|
"memory_types": memory_types,
|
|
"reg_types": reg_types,
|
|
"voltage_metrics": voltage_metrics,
|
|
"voltage_types": voltage_types,
|
|
"link_types": link_types,
|
|
"temperature_types": temperature_types,
|
|
"temperature_metrics": temperature_metrics,
|
|
"utilization_counter_types": utilization_counter_types,
|
|
"event_groups": event_groups,
|
|
"event_types": event_types,
|
|
"counter_commands": counter_commands,
|
|
"compute_partition_types": compute_partition_types,
|
|
"memory_partition_types": memory_partition_types,
|
|
"accelerator_partition_mem_alloc_mode_types": accelerator_partition_mem_alloc_mode_types,
|
|
"freq_inds": freq_inds,
|
|
"power_profile_preset_masks": power_profile_preset_masks,
|
|
"processor_types": processor_types,
|
|
"dev_perf_levels": dev_perf_levels,
|
|
}
|
|
|
|
|
|
# Module-level views of the enum-derived parameter lists, computed once at import.
|
|
# They resolve natively (e.g. ``common.GPU_BLOCKS``) so the standalone benchmark
|
|
# suites can consume them directly instead of via dynamic ``setattr`` a type checker
|
|
# can't follow. Read-only test parameters, so sharing a single instance is safe.
|
|
_TYPE_LISTS = build_type_lists()
|
|
STATUS_TYPES = _TYPE_LISTS["status_types"]
|
|
CLK_TYPES = _TYPE_LISTS["clk_types"]
|
|
CLK_LIMIT_TYPES = _TYPE_LISTS["clk_limit_types"]
|
|
IO_BW_ENCODINGS = _TYPE_LISTS["io_bw_encodings"]
|
|
GPU_BLOCKS = _TYPE_LISTS["gpu_blocks"]
|
|
MEMORY_TYPES = _TYPE_LISTS["memory_types"]
|
|
REG_TYPES = _TYPE_LISTS["reg_types"]
|
|
VOLTAGE_METRICS = _TYPE_LISTS["voltage_metrics"]
|
|
VOLTAGE_TYPES = _TYPE_LISTS["voltage_types"]
|
|
LINK_TYPES = _TYPE_LISTS["link_types"]
|
|
TEMPERATURE_TYPES = _TYPE_LISTS["temperature_types"]
|
|
TEMPERATURE_METRICS = _TYPE_LISTS["temperature_metrics"]
|
|
UTILIZATION_COUNTER_TYPES = _TYPE_LISTS["utilization_counter_types"]
|
|
EVENT_GROUPS = _TYPE_LISTS["event_groups"]
|
|
EVENT_TYPES = _TYPE_LISTS["event_types"]
|
|
COUNTER_COMMANDS = _TYPE_LISTS["counter_commands"]
|
|
COMPUTE_PARTITION_TYPES = _TYPE_LISTS["compute_partition_types"]
|
|
MEMORY_PARTITION_TYPES = _TYPE_LISTS["memory_partition_types"]
|
|
ACCELERATOR_PARTITION_MEM_ALLOC_MODE_TYPES = _TYPE_LISTS[
|
|
"accelerator_partition_mem_alloc_mode_types"
|
|
]
|
|
FREQ_INDS = _TYPE_LISTS["freq_inds"]
|
|
POWER_PROFILE_PRESET_MASKS = _TYPE_LISTS["power_profile_preset_masks"]
|
|
PROCESSOR_TYPES = _TYPE_LISTS["processor_types"]
|
|
DEV_PERF_LEVELS = _TYPE_LISTS["dev_perf_levels"]
|
|
|
|
|
|
def _parse_verbose(argv=None):
|
|
"""Resolve the verbosity selector from *argv* (defaults to ``sys.argv``).
|
|
|
|
``-q``/``--quiet`` -> QUIET; ``-v``/``-vv``/``--verbose`` -> VERBOSE;
|
|
otherwise NORMAL. Parsed once at import as the module-level ``verbose`` so
|
|
every runner and test module shares a single source of truth instead of
|
|
re-parsing ``sys.argv`` in each file.
|
|
"""
|
|
argv = sys.argv if argv is None else argv
|
|
if "-q" in argv or "--quiet" in argv:
|
|
return VERBOSITY_QUIET
|
|
if any(a in ("-v", "-vv", "--verbose") for a in argv):
|
|
return VERBOSITY_VERBOSE
|
|
return VERBOSITY_NORMAL
|
|
|
|
|
|
# Shared verbosity for the whole process (runners + the test modules they import).
|
|
verbose = _parse_verbose()
|
|
|
|
|
|
def _print_test_ids(suite):
|
|
for test in suite:
|
|
if isinstance(test, unittest.TestSuite):
|
|
_print_test_ids(test)
|
|
else:
|
|
# Print the full dotted test id (e.g. "cli.test_event.TestEvent.test_command")
|
|
# so -l output matches the GTestSummaryRunner's per-test labels. Strip the
|
|
# "__main__." prefix the same way so module-run and discover-run agree.
|
|
#
|
|
# The -l/--list output is the data the user asked for, so it goes to stdout
|
|
# (lets `... -l | grep xgmi` work without 2>&1). Test-run chatter (legend,
|
|
# progress, summary, errors, help) stays on stderr.
|
|
test_id = test.id()
|
|
if test_id.startswith("__main__."):
|
|
test_id = test_id[len("__main__.") :]
|
|
print(f"\t{test_id}", file=sys.stdout)
|
|
return
|
|
|
|
|
|
def print_test_ids(suite):
|
|
"""Print every test ID in an already-loaded *suite* to stdout.
|
|
|
|
Public entry point for the discover()-based runners (cli_unit_test.py,
|
|
integration_test.py, unit_tests.py), which build their suite from a
|
|
directory rather than from a single module. Emits an "Available tests:"
|
|
header followed by one dotted test id per line. Output goes to stdout so
|
|
the listing can be captured/piped independently of the normal test-run
|
|
output on stderr (e.g. ``... -l | grep xgmi``).
|
|
"""
|
|
print("Available tests:", file=sys.stdout)
|
|
_print_test_ids(suite)
|
|
return
|
|
|
|
|
|
def print_legend():
|
|
"""Print the dot-character legend for unittest output to stderr.
|
|
|
|
Call this before running tests in non-verbose mode so users know what
|
|
the single-character result indicators (., s, F, E) mean.
|
|
"""
|
|
print("=" * 70, file=sys.stderr)
|
|
print("Legend: . = pass, s = skipped, F = fail, E = error", file=sys.stderr)
|
|
print("=" * 70, file=sys.stderr)
|
|
return
|
|
|
|
|
|
def print_unittest_help():
|
|
"""Print unittest's -h output with its built-in Examples epilog stripped.
|
|
|
|
unittest (via argparse) appends its own Examples block; we capture stdout,
|
|
remove everything from the last 'Examples:' onward, and reprint — leaving
|
|
our print_amdsmi_path_help() as the sole Examples section at the bottom.
|
|
"""
|
|
buf = io.StringIO()
|
|
try:
|
|
with contextlib.redirect_stdout(buf):
|
|
unittest.main(argv=[sys.argv[0] or "unit_tests.py", "--help"])
|
|
except SystemExit:
|
|
pass
|
|
output = buf.getvalue()
|
|
examples_idx = output.rfind("\nExamples:")
|
|
if examples_idx != -1:
|
|
output = output[:examples_idx]
|
|
sys.stdout.write(output)
|
|
sys.stdout.flush()
|
|
|
|
|
|
def make_runner_verbosity(verbose):
|
|
"""Map our three verbosity levels to the correct unittest.TextTestRunner verbosity.
|
|
|
|
In verbose mode, test bodies self-report each result via print_func_name() /
|
|
check_ret(), so we suppress the runner's own per-test lines (VERBOSITY_QUIET)
|
|
to avoid printing each test name twice. In normal and quiet modes we keep the
|
|
runner at VERBOSITY_NORMAL (dots) so CI has a per-test progress indicator and
|
|
hung tests are detectable.
|
|
|
|
verbose value → runner verbosity
|
|
VERBOSITY_QUIET (0) → VERBOSITY_NORMAL (dots only)
|
|
VERBOSITY_NORMAL (1) → VERBOSITY_NORMAL (dots only)
|
|
VERBOSITY_VERBOSE (2) → VERBOSITY_QUIET (runner silent; test bodies own output)
|
|
"""
|
|
if verbose >= VERBOSITY_VERBOSE:
|
|
return VERBOSITY_QUIET # test bodies print their own per-test output
|
|
return VERBOSITY_NORMAL # dots keep CI informed of progress
|
|
|
|
|
|
def _parse_k_pattern(argv=None):
|
|
"""Return the ``-k``/``--keyword`` filter substring from *argv*, or ``None``.
|
|
|
|
Accepts both the separate ("-k", "pattern") and joined ("-kpattern") forms,
|
|
matching the spellings unittest itself understands.
|
|
"""
|
|
argv = sys.argv if argv is None else argv
|
|
for i, arg in enumerate(argv):
|
|
if arg in ("-k", "--keyword") and i + 1 < len(argv):
|
|
return argv[i + 1]
|
|
if arg.startswith("-k") and arg != "-k":
|
|
return arg[2:]
|
|
return None
|
|
|
|
|
|
def _parse_x_pattern(argv=None):
|
|
"""Return the ``-x``/``--exclude`` filter substring from *argv*, or ``None``.
|
|
|
|
Tests whose id contains this substring are skipped (the inverse of ``-k``).
|
|
Accepts both the separate ("-x", "pattern") and joined ("-xpattern") forms.
|
|
"""
|
|
argv = sys.argv if argv is None else argv
|
|
for i, arg in enumerate(argv):
|
|
if arg in ("-x", "--exclude") and i + 1 < len(argv):
|
|
return argv[i + 1]
|
|
if arg.startswith("-x") and arg != "-x":
|
|
return arg[2:]
|
|
return None
|
|
|
|
|
|
def _filter_suite_exclude(suite, pattern):
|
|
"""Return a copy of *suite* with every test whose id contains *pattern* removed.
|
|
|
|
unittest's ``-k`` can only include matching tests; this provides the inverse so
|
|
callers can run everything except, e.g., the performance suites (``-x performance``).
|
|
"""
|
|
filtered = unittest.TestSuite()
|
|
for test in suite:
|
|
if isinstance(test, unittest.TestSuite):
|
|
filtered.addTest(_filter_suite_exclude(test, pattern))
|
|
elif pattern not in test.id():
|
|
filtered.addTest(test)
|
|
return filtered
|
|
|
|
|
|
def run_test_dir(subdir, title, top_level_dir):
|
|
"""Discover and run every test under *top_level_dir*/*subdir*, then exit.
|
|
|
|
Single implementation of the runner boilerplate shared by the three entry
|
|
scripts (integration_test.py / cli_unit_test.py / unit_tests.py): it handles
|
|
``-h``/``--help``, ``-k``/``--keyword`` filtering, ``-l``/``--list``, the
|
|
root-privilege check, the legend/title preamble, output buffering
|
|
(``-b``/``--buffer``) and the GTest-style summary runner. Never returns —
|
|
always calls ``sys.exit()`` (0 on success, 1 on failure).
|
|
"""
|
|
argv = sys.argv
|
|
|
|
# Skip legend/title/"Running" preamble when the user just wants help text.
|
|
if "-h" in argv or "--help" in argv:
|
|
print_unittest_help()
|
|
print_amdsmi_path_help()
|
|
sys.exit(0)
|
|
|
|
loader = unittest.TestLoader()
|
|
# -k does substring matching; wrapping in * lets globs (test_ttm*) match too.
|
|
k_pattern = _parse_k_pattern(argv)
|
|
if k_pattern:
|
|
loader.testNamePatterns = [f"*{k_pattern}*"]
|
|
|
|
suite = loader.discover(
|
|
start_dir=os.path.join(top_level_dir, subdir),
|
|
pattern="test_*.py",
|
|
top_level_dir=top_level_dir,
|
|
)
|
|
|
|
# -x/--exclude drops any test whose id contains the substring (the inverse of
|
|
# -k), e.g. run everything but the perf suites with `-x performance`.
|
|
x_pattern = _parse_x_pattern(argv)
|
|
if x_pattern:
|
|
suite = _filter_suite_exclude(suite, x_pattern)
|
|
|
|
if "-l" in argv or "--list" in argv:
|
|
print_test_ids(suite)
|
|
sys.exit(0)
|
|
|
|
# Detect if ran without sudo or root privileges
|
|
if os.geteuid() != 0:
|
|
print(
|
|
"Warning: Some tests may require elevated privileges (sudo/root) to run completely.\n",
|
|
file=sys.stderr,
|
|
)
|
|
print("Please relaunch with elevated privileges.\n", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Only show the dot-character legend when not in verbose mode; in verbose
|
|
# mode each test prints its own result line so the dot legend is irrelevant.
|
|
if verbose < VERBOSITY_VERBOSE:
|
|
print_legend()
|
|
|
|
if verbose > VERBOSITY_QUIET:
|
|
print(f"{title}\n")
|
|
print("Running tests...\n")
|
|
|
|
runner = GTestSummaryRunner(
|
|
stream=sys.stderr,
|
|
verbosity=make_runner_verbosity(verbose),
|
|
buffer="-b" in argv or "--buffer" in argv,
|
|
)
|
|
result = runner.run(suite)
|
|
sys.exit(0 if result.wasSuccessful() else 1)
|
|
|
|
|
|
class GTestSummaryRunner(unittest.TextTestRunner):
|
|
"""TextTestRunner that appends a GTest-style pass/skip/fail summary.
|
|
|
|
After the standard unittest output, prints a colored block like::
|
|
|
|
[----------] 40 tests ran.
|
|
[ PASSED ] 39 tests.
|
|
[ SKIPPED ] 1 test, listed below:
|
|
[ SKIPPED ] TestClass.test_method_name
|
|
|
|
Color scheme mirrors GTest:
|
|
cyan = separator line ([----------])
|
|
green = PASSED
|
|
yellow = SKIPPED
|
|
red = FAILED
|
|
|
|
Colors are automatically suppressed when the output stream is not a TTY
|
|
(e.g. when piped to a file or CI log capture), so file-based runners such
|
|
as those in perf_tests.py will never receive ANSI escape codes in their logs.
|
|
|
|
Example::
|
|
|
|
runner = common.GTestSummaryRunner(verbosity=common.make_runner_verbosity(verbose))
|
|
unittest.main(testRunner=runner)
|
|
|
|
# To redirect output to stderr (e.g. when stdout is used for data):
|
|
runner = common.GTestSummaryRunner(stream=sys.stderr, verbosity=common.make_runner_verbosity(verbose))
|
|
"""
|
|
|
|
_CYAN = "\033[36m"
|
|
_GREEN = "\033[32m"
|
|
_YELLOW = "\033[33m"
|
|
_RED = "\033[31m"
|
|
_RESET = "\033[0m"
|
|
|
|
@staticmethod
|
|
def _plural(n):
|
|
return "s" if n != 1 else ""
|
|
|
|
@staticmethod
|
|
def _test_label(test):
|
|
"""Return the GTest-format label for *test*.
|
|
|
|
``TestCase.id()`` is the public API for a test's full dotted name
|
|
(e.g. ``"__main__.ClassName.method_name"``). When the id starts with
|
|
the standard ``__main__.`` prefix we strip it, yielding
|
|
``ClassName.method_name`` without risking label collisions between
|
|
tests from different modules that share a class name.
|
|
"""
|
|
test_id = test.id()
|
|
if test_id.startswith("__main__."):
|
|
return test_id[len("__main__.") :]
|
|
return test_id
|
|
|
|
def _color(self, code, text):
|
|
"""Wrap *text* in *code* only when colors are appropriate.
|
|
|
|
Colors are suppressed when the ``NO_COLOR`` environment variable is set
|
|
(https://no-color.org/) or when the output stream is not a real TTY.
|
|
"""
|
|
if os.environ.get("NO_COLOR"):
|
|
return text
|
|
underlying = getattr(self.stream, "stream", self.stream)
|
|
if hasattr(underlying, "isatty") and underlying.isatty():
|
|
return f"{code}{text}{self._RESET}"
|
|
return text
|
|
|
|
def run(self, test):
|
|
start = time.perf_counter()
|
|
result = super().run(test)
|
|
elapsed_ms = round((time.perf_counter() - start) * 1000)
|
|
self._print_gtest_summary(result, elapsed_ms)
|
|
return result
|
|
|
|
def _print_gtest_summary(self, result, elapsed_ms):
|
|
"""Write the GTest-style pass/skip/fail block to *self.stream*."""
|
|
stream = self.stream # unittest._WritelnDecorator, supports .writeln()
|
|
skipped = len(result.skipped)
|
|
# unexpectedSuccesses are tests marked @expectedFailure that passed instead.
|
|
# They cause wasSuccessful() to return False, so count them as failures so
|
|
# the summary accurately reflects the overall run status.
|
|
unexpectedSuccesses = len(getattr(result, "unexpectedSuccesses", []))
|
|
failures = len(result.failures) + len(result.errors) + unexpectedSuccesses
|
|
passed = result.testsRun - skipped - failures
|
|
|
|
stream.writeln()
|
|
stream.writeln(
|
|
self._color(
|
|
self._CYAN,
|
|
f"[----------] {result.testsRun} test{self._plural(result.testsRun)} ran. ({elapsed_ms} ms total)",
|
|
)
|
|
)
|
|
stream.writeln(
|
|
self._color(self._GREEN, f"[ PASSED ] {passed} test{self._plural(passed)}.")
|
|
)
|
|
|
|
if failures:
|
|
stream.writeln(
|
|
self._color(
|
|
self._RED,
|
|
f"[ FAILED ] {failures} test{self._plural(failures)}, listed below:",
|
|
)
|
|
)
|
|
for t, _ in result.failures:
|
|
stream.writeln(self._color(self._RED, f"[ FAILED ] {self._test_label(t)}"))
|
|
for t, _ in result.errors:
|
|
stream.writeln(self._color(self._RED, f"[ FAILED ] {self._test_label(t)}"))
|
|
# unexpectedSuccesses items are bare TestCase instances (not (test, traceback) tuples).
|
|
for t in getattr(result, "unexpectedSuccesses", []):
|
|
stream.writeln(
|
|
self._color(
|
|
self._RED, f"[ FAILED ] {self._test_label(t)} (unexpected success)"
|
|
)
|
|
)
|
|
if skipped:
|
|
stream.writeln(
|
|
self._color(
|
|
self._YELLOW,
|
|
f"[ SKIPPED ] {skipped} test{self._plural(skipped)}, listed below:",
|
|
)
|
|
)
|
|
for t, _ in result.skipped:
|
|
stream.writeln(self._color(self._YELLOW, f"[ SKIPPED ] {self._test_label(t)}"))
|
|
stream.writeln()
|
|
|
|
|
|
def has_gpu_od_interface(bdf):
|
|
"""Check if a GPU has the gpu_od sysfs interface.
|
|
|
|
Delegates to amdsmi_helpers.AMDSMIHelpers.detect_gpu_od(). Requires the
|
|
AMD-SMI CLI to be installed (amdsmi_helpers is imported on first call).
|
|
|
|
Args:
|
|
bdf: PCI Bus/Device/Function string (e.g. '0000:26:00.0')
|
|
|
|
Returns:
|
|
bool: True if gpu_od directory exists for this GPU
|
|
"""
|
|
# TODO(amdsmi_team): Refactor to create an amdsmi_get_gpu_fan_speed_range() API
|
|
# amdsmi_get_gpu_fan_speed_range(
|
|
# amdsmi_processor_handle processor_handle,
|
|
# uint32_t sensor_ind,
|
|
# amdsmi_range_t* fan_speed_range)
|
|
# and begin deprecation of amdsmi_get_gpu_fan_speed_max(). This will allow us
|
|
# to remove the dependency on amdsmi_helpers from this common module.
|
|
# Exposing non-public SYSFS API interfaces in the CLI (and in general)
|
|
# is a bad design pattern and needs to be addressed in the future.
|
|
amdsmi_cli_path = os.path.normpath(
|
|
os.path.join(amdsmi_path, "..", "..", "libexec", "amdsmi_cli")
|
|
)
|
|
if not os.path.exists(amdsmi_cli_path):
|
|
raise FileNotFoundError(
|
|
f'amdsmi_cli path "{amdsmi_cli_path}" does not exist. '
|
|
f"Ensure the AMD-SMI CLI is installed, or set AMDSMI_PATH correctly."
|
|
)
|
|
if amdsmi_cli_path not in sys.path:
|
|
sys.path.append(amdsmi_cli_path)
|
|
try:
|
|
import amdsmi_helpers # type: ignore
|
|
except ImportError as e:
|
|
raise ImportError(
|
|
f'Could not import the "amdsmi_helpers" module from "{amdsmi_cli_path}"'
|
|
) from e
|
|
has_gpu_od, _ = amdsmi_helpers.AMDSMIHelpers.detect_gpu_od(bdf)
|
|
return has_gpu_od
|
|
|
|
|
|
class Common:
|
|
VIRTUALIZATION_MODE_MAP = {}
|
|
for member in amdsmi.AmdSmiVirtualizationMode:
|
|
VIRTUALIZATION_MODE_MAP[amdsmi.AmdSmiVirtualizationMode(member.value)] = member.name
|
|
|
|
def __init__(self, verbose, *args, **kwargs):
|
|
self.verbose = verbose
|
|
self.max_num_physical_devices = (
|
|
amdsmi.amdsmi_interface.AMDSMI_MAX_NUM_XCP * amdsmi.amdsmi_interface.AMDSMI_MAX_DEVICES
|
|
)
|
|
self.PASS = PASS
|
|
self.FAIL = FAIL
|
|
self.ANY_FAIL = "ANY_FAIL"
|
|
|
|
# Tests marked with either of these flags will be skipped
|
|
# and need to be implemented later.
|
|
self.TODO_SKIP_FAIL = True
|
|
self.TODO_SKIP_NOT_COMPLETE = True
|
|
|
|
try:
|
|
self.amdsmi_smart_init()
|
|
|
|
# Get gpus
|
|
self.processors = amdsmi.amdsmi_get_processor_handles()
|
|
# Set bad gpu: None is rejected by the isinstance check in amdsmi_interface.py,
|
|
# raising AmdSmiParameterException(INVAL). Platform-agnostic and unambiguous.
|
|
self.bad_gpu = None
|
|
|
|
self.virt_mode = []
|
|
self.asic_info = []
|
|
self.board_info = []
|
|
for gpu in self.processors:
|
|
# Get virtualization mode info
|
|
try:
|
|
ret = amdsmi.amdsmi_get_gpu_virtualization_mode(gpu)
|
|
mode_name = self.VIRTUALIZATION_MODE_MAP.get(ret["mode"], "UNKNOWN")
|
|
self.virt_mode.append({"mode": mode_name})
|
|
except amdsmi.AmdSmiLibraryException:
|
|
# Virtualization mode is not supported on every ASIC; record
|
|
# UNKNOWN silently (it is still surfaced via print_device_header).
|
|
self.virt_mode.append({"mode": "UNKNOWN"})
|
|
|
|
# Get asic info
|
|
self.asic_info.append(amdsmi.amdsmi_get_gpu_asic_info(gpu))
|
|
# Get board info
|
|
self.board_info.append(amdsmi.amdsmi_get_gpu_board_info(gpu))
|
|
|
|
amdsmi.amdsmi_shut_down()
|
|
except amdsmi.AmdSmiLibraryException as e:
|
|
print(f"In class Common, Cannot get processor information, {e}")
|
|
|
|
self.not_supported_error_codes = [
|
|
("2", "AMDSMI_STATUS_NOT_SUPPORTED"),
|
|
("3", "AMDSMI_STATUS_NOT_YET_IMPLEMENTED"),
|
|
("49", "AMDSMI_STATUS_NO_HSMP_MSG_SUP"),
|
|
]
|
|
|
|
self.error_map = ERROR_MAP
|
|
# All enum-derived parameter lists are built by the shared, hardware-free
|
|
# build_type_lists() so the logic lives in one place (also importable by
|
|
# the standalone benchmark suites).
|
|
for _attr, _value in build_type_lists().items():
|
|
setattr(self, _attr, _value)
|
|
return
|
|
|
|
def print(self, msg, data=None):
|
|
if self.verbose > VERBOSITY_QUIET:
|
|
if data is None:
|
|
print(msg, flush=True)
|
|
elif any(data in value for value in self.not_supported_error_codes):
|
|
print(f"{msg} {data}", flush=True)
|
|
else:
|
|
if isinstance(data, str) and data in self.error_map.values():
|
|
print(msg, end="")
|
|
else:
|
|
print(msg)
|
|
if isinstance(data, dict) or isinstance(data, list):
|
|
print(json.dumps(data, sort_keys=False, indent=4), flush=True)
|
|
else:
|
|
print(data)
|
|
return
|
|
|
|
def print_func_name(self, msg=None):
|
|
if self.verbose == VERBOSITY_VERBOSE:
|
|
stk = inspect.stack()
|
|
if stk[1].function == "_callSetUp":
|
|
return
|
|
print(f"\n## {stk[1].function}()", flush=True)
|
|
if msg:
|
|
print(msg, flush=True)
|
|
return
|
|
|
|
def print_device_header(self, i):
|
|
# Guard against stale caches: processors may be refreshed in setUp without
|
|
# refreshing these display-only lists (which are populated once at __init__).
|
|
# Print virtualization mode info
|
|
msg = f"virtualization mode(gpu={i})"
|
|
self.print(f"\t{msg}")
|
|
if i < len(self.virt_mode):
|
|
mode = self.virt_mode[i]["mode"]
|
|
self.print(f"\t\tmode : {mode}")
|
|
# Print asic info
|
|
msg = f"asic info(gpu={i})"
|
|
self.print(f"\t{msg}")
|
|
if i < len(self.asic_info):
|
|
for key, value in self.asic_info[i].items():
|
|
self.print(f"\t\t{key} : {value}")
|
|
# Print board info
|
|
msg = f"board info(gpu={i})"
|
|
self.print(f"\t{msg}")
|
|
if i < len(self.board_info):
|
|
for key, value in self.board_info[i].items():
|
|
self.print(f"\t\t{key} : {value}")
|
|
return
|
|
|
|
def get_error_code(self, exc):
|
|
error_code = "-1"
|
|
error_code_name = "UNKNOWN_ERROR"
|
|
if hasattr(exc, "get_error_code"):
|
|
error_code = str(exc.get_error_code())
|
|
if error_code in self.error_map:
|
|
error_code_name = self.error_map[error_code]
|
|
return (error_code, error_code_name)
|
|
|
|
def get_dict_key_from_value(self, _value, _dict):
|
|
if _value not in _dict.values():
|
|
return None
|
|
for key, value in _dict.items():
|
|
if value == _value:
|
|
return key
|
|
return None
|
|
|
|
def get_error_code_from_name(self, error_code_name):
|
|
error_code = self.get_dict_key_from_value(error_code_name, self.error_map)
|
|
if error_code is None:
|
|
error_code = -1
|
|
return error_code
|
|
|
|
def check_ret(self, msg, exc, expected_code_name=None, printIt=True):
|
|
# Returns True if the test FAILED (i.e. the result did not match expected).
|
|
# Callers use the pattern: `if self.check_ret(...): raise_exception = e`
|
|
if isinstance(exc, str) and len(exc) == 0:
|
|
error_code_name = expected_code_name
|
|
error_code = "-1"
|
|
if error_code_name in self.error_map.values():
|
|
for key, value in self.error_map.items():
|
|
if value == error_code_name:
|
|
error_code = key
|
|
break
|
|
elif hasattr(exc, "get_error_code"):
|
|
error_code, error_code_name = self.get_error_code(exc)
|
|
else:
|
|
error_code = str(exc).split(":", maxsplit=1)[0]
|
|
error_code_name = "AMDSMI_STATUS_INVAL"
|
|
|
|
# Check for when there are multiple passing conditions
|
|
if isinstance(expected_code_name, list):
|
|
for ec_name in expected_code_name:
|
|
if not self.check_ret(msg, exc, ec_name, False): # check without printing
|
|
# This expected code matched - print once and return success
|
|
if self.verbose > VERBOSITY_QUIET and printIt:
|
|
if msg:
|
|
print(f"{msg}\n", end="")
|
|
ec_code = self.get_error_code_from_name(ec_name)
|
|
print(f"\tTEST SUCCESS, AMDSMI API Returned {ec_code:>2s}, {ec_name}")
|
|
return False
|
|
|
|
# No expected result matched - print failure (respects same guards as single-condition path)
|
|
if self.verbose > VERBOSITY_QUIET and printIt:
|
|
if msg:
|
|
print(f"{msg}\n", end="")
|
|
status_msg = (
|
|
f"\tTEST FAILURE, AMDSMI API Returned {error_code:>2s}, {error_code_name}\n"
|
|
)
|
|
for ec_name in expected_code_name:
|
|
ec_code = self.get_error_code_from_name(ec_name)
|
|
status_msg += (
|
|
f"\t AMDSMI API Expected {ec_code:>2s}, {expected_code_name}"
|
|
)
|
|
return True
|
|
|
|
# Check for single passing condition
|
|
status_msg = ""
|
|
status_ret = False
|
|
if any(error_code in value for value in self.not_supported_error_codes):
|
|
status_msg = f"\tAMDSMI API Returned {error_code:>2s}, {error_code_name}"
|
|
elif error_code_name == expected_code_name:
|
|
status_msg = f"\tTEST SUCCESS, AMDSMI API Returned expected {error_code:>2s}, {expected_code_name}"
|
|
elif error_code_name != self.PASS and expected_code_name == self.ANY_FAIL:
|
|
status_msg = f"\tTEST SUCCESS, AMDSMI API Returned expected fail {error_code:>2s}, {expected_code_name}"
|
|
else:
|
|
expected_error_code = self.get_error_code_from_name(expected_code_name)
|
|
status_msg = (
|
|
f"\tTEST FAILURE, AMDSMI API Returned {error_code:>2s}, {error_code_name}\n"
|
|
)
|
|
status_msg += f"\t AMDSMI API Expected {expected_error_code:>2s}, {expected_code_name}"
|
|
status_ret = True
|
|
if self.verbose > VERBOSITY_QUIET and printIt:
|
|
if msg:
|
|
print(f"{msg}\n", end="")
|
|
print(f"{status_msg}", flush=True)
|
|
return status_ret
|
|
|
|
def _check_amdgpu_driver(self):
|
|
"""Returns true if amdgpu is found in the list of initialized modules"""
|
|
amd_gpu_status_file = pathlib.Path("/sys/module/amdgpu/initstate")
|
|
if amd_gpu_status_file.exists():
|
|
try:
|
|
return amd_gpu_status_file.read_text(encoding="ascii").strip() == "live"
|
|
except OSError:
|
|
pass
|
|
|
|
# If the driver is loaded either as a module OR built in, this dir will be populated
|
|
drv = pathlib.Path("/sys/bus/pci/drivers/amdgpu")
|
|
if not drv.exists():
|
|
return False
|
|
|
|
# Check if a symlink exists that loosely matches PCI BDF format
|
|
# ex: 0000:03:00.0
|
|
for p in drv.iterdir():
|
|
if p.is_symlink() and ":" in p.name and "." in p.name:
|
|
return True
|
|
return False
|
|
|
|
def _check_amd_hsmp_driver(self):
|
|
"""Returns true if amd_hsmp or hsmp_acpi is found in the list of initialized modules"""
|
|
return pathlib.Path("/dev/hsmp").exists()
|
|
|
|
def _init_with_flag(self, init_flag, driver_msg):
|
|
ret = None
|
|
try:
|
|
ret = amdsmi.amdsmi_init(init_flag)
|
|
except (amdsmi.AmdSmiLibraryException, amdsmi.AmdSmiParameterException) as e:
|
|
# AmdSmiLibraryException exposes get_error_code(); AmdSmiParameterException does not.
|
|
# Accessing .err_code on AmdSmiParameterException raises AttributeError.
|
|
err_code = e.get_error_code() if hasattr(e, "get_error_code") else None
|
|
if err_code in (amdsmi.AmdSmiStatus.NOT_INIT, amdsmi.AmdSmiStatus.DRIVER_NOT_LOADED):
|
|
self.print(driver_msg)
|
|
raise unittest.SkipTest(driver_msg)
|
|
raise
|
|
return ret
|
|
|
|
def _skip_if_missing(self, names):
|
|
# All public APIs must be re-exported at the top-level amdsmi namespace.
|
|
# If an API is missing there, it belongs in amdsmi_interface.py, not a sub-module.
|
|
# Note: self.id() is NOT used here — Common is not a TestCase subclass, so self.id()
|
|
# does not exist when called through a Common instance. Instead, walk the call stack to
|
|
# find the name of the enclosing test_* method, which works regardless of calling convention.
|
|
missing = [name for name in names if not hasattr(amdsmi, name)]
|
|
if missing:
|
|
test_name = next(
|
|
(frame.function for frame in inspect.stack() if frame.function.startswith("test_")),
|
|
"<unknown>",
|
|
)
|
|
msg = f"{test_name} | Missing amdsmi API(s) in amdsmi_interface.py: " + ", ".join(
|
|
missing
|
|
)
|
|
# self may be a Common instance (has .verbose directly) or a TestCase instance
|
|
# that stores Common as self.common (when called via common.Common._skip_if_missing(self, ...)).
|
|
verbose = getattr(self, "verbose", None)
|
|
if verbose is None:
|
|
verbose = getattr(getattr(self, "common", None), "verbose", VERBOSITY_NORMAL)
|
|
if verbose > VERBOSITY_QUIET:
|
|
print(msg, file=sys.stderr)
|
|
raise unittest.SkipTest(msg)
|
|
return
|
|
|
|
def _build_call_msg(self, func_name, i, j, params):
|
|
msg = f"\t### {func_name}("
|
|
if i is not None:
|
|
msg += f"gpu={i}"
|
|
if j is not None:
|
|
msg += f", gpu={j}"
|
|
for param_name, param_value in params.items():
|
|
if isinstance(param_value, list):
|
|
msg += f", {param_name}={{value}}"
|
|
else:
|
|
msg += f", {param_name}={param_value}"
|
|
msg += ")"
|
|
return msg
|
|
|
|
def amdsmi_smart_init(self):
|
|
"""Initializes AMDSMI Library based on live drivers found in the system.
|
|
|
|
Checks for the presence of the amdgpu, amd_hsmp or hsmp_acpi drivers and initializes the
|
|
AMD SMI library based on the live drivers found. Mirrors the flag-building logic in the
|
|
CLI (amdsmi_init.py): flags are OR-combined additively so that systems with both a
|
|
discrete GPU and CPU power management (non-APU servers) work correctly. INIT_AMD_APUS
|
|
is intentionally not used here; it targets integrated APU configurations only.
|
|
|
|
Return:
|
|
tuple: A tuple containing:
|
|
- ret: The return value from amdsmi_init() (typically None on success)
|
|
- init_flag: The combined flag used to initialize the AMD SMI library
|
|
(bitwise OR of INIT_AMD_GPUS and/or INIT_AMD_CPUS)
|
|
|
|
Raises:
|
|
AmdSmiLibraryException: If initialization fails for reasons other than driver not loaded
|
|
AmdSmiParameterException: If invalid parameters are passed to amdsmi_init
|
|
unittest.SkipTest: If no compatible AMD drivers are detected on the system
|
|
"""
|
|
# Build init_flag additively from detected drivers (mirrors CLI amdsmi_init.py pattern).
|
|
# Using INIT_AMD_APUS when both GPU and CPU drivers are present is wrong on discrete-GPU
|
|
# servers — use OR-combined flags instead, exactly as the CLI does.
|
|
init_flag = 0
|
|
if self._check_amdgpu_driver():
|
|
init_flag |= amdsmi.AmdSmiInitFlags.INIT_AMD_GPUS
|
|
if self._check_amd_hsmp_driver():
|
|
init_flag |= amdsmi.AmdSmiInitFlags.INIT_AMD_CPUS
|
|
if init_flag == 0:
|
|
msg = "Drivers not loaded (amdgpu, amd_hsmp or hsmp_acpi drivers not found in modules)"
|
|
self.print(msg)
|
|
raise unittest.SkipTest(msg)
|
|
|
|
msg = "AMDSMI init failed for detected drivers"
|
|
ret = self._init_with_flag(init_flag, msg)
|
|
return (ret, init_flag)
|
|
|
|
def Test_API(self, **kwargs):
|
|
"""
|
|
Tests API with zero or more arguments
|
|
|
|
Arguments:
|
|
func_name: API to be executed
|
|
Optional:
|
|
param1_name: Name of parameter 1
|
|
"""
|
|
|
|
params = kwargs
|
|
iterator = iter(params.items())
|
|
func_name, func = next(iterator)
|
|
del params[func_name]
|
|
|
|
raise_exception = None
|
|
cond = self.PASS
|
|
msg = self._build_call_msg(func_name, None, None, params)
|
|
try:
|
|
data = func(*[value for value in params.values()])
|
|
self.print(msg, data)
|
|
self.check_ret("", "", cond)
|
|
except (amdsmi.AmdSmiLibraryException, amdsmi.AmdSmiParameterException) as e:
|
|
if self.check_ret(msg, e, cond):
|
|
raise_exception = e
|
|
if raise_exception:
|
|
raise raise_exception
|
|
return
|
|
|
|
def Test_API_Per_GPU(self, **kwargs):
|
|
"""
|
|
Tests API per GPU with zero or more arguments
|
|
|
|
Arguments:
|
|
func_name: API to be executed
|
|
Optional:
|
|
param1_name: Name of parameter 1
|
|
param2_name: Name of parameter 2
|
|
param3_name: Name of parameter 3
|
|
"""
|
|
|
|
params = kwargs
|
|
iterator = iter(params.items())
|
|
func_name, func = next(iterator)
|
|
del params[func_name]
|
|
|
|
raise_exception = None
|
|
for i in range(len(self.processors) + 1):
|
|
cond = self.PASS
|
|
if i < len(self.processors):
|
|
gpu = self.processors[i]
|
|
self.print_device_header(i)
|
|
else:
|
|
# bad gpu
|
|
gpu = self.bad_gpu
|
|
i = "invalid"
|
|
cond = self.FAIL
|
|
|
|
msg = self._build_call_msg(func_name, i, None, params)
|
|
try:
|
|
data = func(gpu, *[value for value in params.values()])
|
|
self.print(msg, data)
|
|
self.check_ret("", "", cond)
|
|
except (amdsmi.AmdSmiLibraryException, amdsmi.AmdSmiParameterException) as e:
|
|
if self.check_ret(msg, e, cond):
|
|
raise_exception = e
|
|
self.print("")
|
|
if raise_exception:
|
|
raise raise_exception
|
|
return
|
|
|
|
def Test_Per_GPU_With_One_Enum(self, **kwargs):
|
|
"""
|
|
Tests API per GPU per Enum with zero or more arguments
|
|
|
|
Arguments:
|
|
func_name: API to be executed
|
|
value1_name=[(value_name, value, value_cond), ...]
|
|
Optional:
|
|
param1_name: Name of parameter 1
|
|
param2_name: Name of parameter 2
|
|
"""
|
|
|
|
params = kwargs
|
|
iterator = iter(params.items())
|
|
func_name, func = next(iterator)
|
|
_, values1 = next(iterator)
|
|
del params[func_name]
|
|
|
|
raise_exception = None
|
|
for i in range(len(self.processors) + 1):
|
|
if i < len(self.processors):
|
|
gpu = self.processors[i]
|
|
self.print_device_header(i)
|
|
else:
|
|
# bad gpu
|
|
gpu = self.bad_gpu
|
|
i = "invalid"
|
|
|
|
for value1_name, value1, value1_cond in values1:
|
|
cond = self.PASS
|
|
if i == "invalid" or value1_cond == self.FAIL:
|
|
cond = self.FAIL
|
|
msg = self._build_call_msg(func_name, i, None, params)
|
|
msg = msg.replace("{value}", value1_name, 1)
|
|
try:
|
|
data = func(
|
|
gpu,
|
|
*[
|
|
value if not isinstance(value, list) else value1
|
|
for value in params.values()
|
|
],
|
|
)
|
|
self.print(msg, data)
|
|
self.check_ret("", "", cond)
|
|
except (amdsmi.AmdSmiLibraryException, amdsmi.AmdSmiParameterException) as e:
|
|
if value1_cond != self.PASS:
|
|
if self.check_ret(msg, e, value1_cond):
|
|
raise_exception = e
|
|
else:
|
|
if self.check_ret(msg, e, cond):
|
|
raise_exception = e
|
|
self.print("")
|
|
if raise_exception:
|
|
raise raise_exception
|
|
return
|
|
|
|
def Test_Per_GPU_With_Two_Enums(self, **kwargs):
|
|
"""
|
|
Tests API per GPU per 2 Enums with zero or more arguments
|
|
|
|
Arguments:
|
|
func_name: API to be executed
|
|
value1_name=[(value_name, value, value_cond), ...]
|
|
value2_name=[(value_name, value, value_cond), ...]
|
|
Optional:
|
|
param1_name: Name of parameter 1
|
|
param2_name: Name of parameter 2
|
|
"""
|
|
|
|
params = kwargs
|
|
iterator = iter(params.items())
|
|
func_name, func = next(iterator)
|
|
_, values1 = next(iterator)
|
|
_, values2 = next(iterator)
|
|
del params[func_name]
|
|
|
|
raise_exception = None
|
|
for i in range(len(self.processors) + 1):
|
|
if i < len(self.processors):
|
|
gpu = self.processors[i]
|
|
self.print_device_header(i)
|
|
else:
|
|
# bad gpu
|
|
gpu = self.bad_gpu
|
|
i = "invalid"
|
|
|
|
for value1_name, value1, value1_cond in values1:
|
|
for value2_name, value2, value2_cond in values2:
|
|
cond = self.PASS
|
|
if i == "invalid" or value1_cond == self.FAIL or value2_cond == self.FAIL:
|
|
cond = self.FAIL
|
|
msg = self._build_call_msg(func_name, i, None, params)
|
|
msg = msg.replace("{value}", value1_name, 1)
|
|
msg = msg.replace("{value}", value2_name, 1)
|
|
try:
|
|
data = func(
|
|
gpu,
|
|
*[
|
|
value
|
|
if not isinstance(value, list)
|
|
else value1
|
|
if index == 0
|
|
else value2
|
|
for index, value in enumerate(params.values())
|
|
],
|
|
)
|
|
self.print(msg, data)
|
|
self.check_ret("", "", cond)
|
|
except (amdsmi.AmdSmiLibraryException, amdsmi.AmdSmiParameterException) as e:
|
|
if value1_cond != self.PASS:
|
|
if self.check_ret(msg, e, value1_cond):
|
|
raise_exception = e
|
|
elif value2_cond != self.PASS:
|
|
if self.check_ret(msg, e, value2_cond):
|
|
raise_exception = e
|
|
else:
|
|
if self.check_ret(msg, e, cond):
|
|
raise_exception = e
|
|
self.print("")
|
|
if raise_exception:
|
|
raise raise_exception
|
|
return
|
|
|
|
def Test_Per_GPU_With_GPU(self, **kwargs):
|
|
"""
|
|
Tests API per GPU per GPU with zero or more arguments
|
|
|
|
Arguments:
|
|
func_name: API to be executed
|
|
Optional:
|
|
param1_name: Name of parameter 1
|
|
param2_name: Name of parameter 2
|
|
"""
|
|
params = kwargs
|
|
iterator = iter(params.items())
|
|
func_name, func = next(iterator)
|
|
del params[func_name]
|
|
|
|
raise_exception = None
|
|
for i in range(len(self.processors) + 1):
|
|
if i < len(self.processors):
|
|
gpu_i = self.processors[i]
|
|
self.print_device_header(i)
|
|
else:
|
|
# bad gpu
|
|
gpu_i = self.bad_gpu
|
|
i = "invalid"
|
|
for j in range(len(self.processors) + 1):
|
|
if j < len(self.processors):
|
|
gpu_j = self.processors[j]
|
|
self.print_device_header(j)
|
|
else:
|
|
# bad gpu
|
|
gpu_j = self.bad_gpu
|
|
j = "invalid"
|
|
|
|
cond = self.PASS
|
|
if i == "invalid" or j == "invalid" or i == j:
|
|
cond = self.FAIL
|
|
msg = self._build_call_msg(func_name, i, j, params)
|
|
try:
|
|
data = func(gpu_i, gpu_j, *[value for value in params.values()])
|
|
self.print(msg, data)
|
|
self.check_ret("", "", cond)
|
|
except (amdsmi.AmdSmiLibraryException, amdsmi.AmdSmiParameterException) as e:
|
|
if self.check_ret(msg, e, cond):
|
|
raise_exception = e
|
|
self.print("")
|
|
if raise_exception:
|
|
raise raise_exception
|
|
return
|