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

352 lines
13 KiB
Python

# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Results handler for building, saving, and uploading test results."""
import sys
import json
import requests
import time
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Any
from datetime import datetime
from prettytable import PrettyTable
from decimal import Decimal, InvalidOperation
from ..logger import log
from .results_api import ResultsAPI, build_results_payload, validate_payload
from ..constants import Constants, SEPARATOR_LINE
class ResultsHandler:
"""Static methods for building, saving, and uploading test results."""
@staticmethod
def build_deployment_info(
config: Optional[Any] = None,
deployed_by: str = "",
execution_label: str = "",
ci_group: str = "therock_pr",
) -> Dict[str, str]:
"""Build deployment information dict for test execution.
Args:
config: Configuration object (optional)
deployed_by: Username who ran the test
execution_label: Execution label
ci_group: CI/CD group identifier (default: therock_pr)
Returns:
Dict: Deployment info with timestamp, user, label, command, and CI group
"""
# Build deployment info
deployment_info = {
"test_deployed_by": deployed_by,
"test_deployed_on": datetime.now().isoformat(),
"execution_label": execution_label,
"test_flag": "prod_test",
"testcase_command": " ".join(sys.argv),
"execution_type": "automated",
"ci_group": ci_group,
}
log.debug(
f"Deployment info: deployed_by={deployed_by}, ci_group={ci_group}, command={deployment_info['testcase_command']}"
)
return deployment_info
@staticmethod
def build_system_info_dict(system_context: Any) -> Dict[str, Any]:
"""Build system info dict from SystemContext for API payload.
Args:
system_context: SystemContext object with detected system info
Returns:
Dict: System information (OS, CPU, GPU details)
"""
return {
"os": f"{system_context.os_name} {system_context.os_version}",
"os_version": system_context.os_version,
"kernel": system_context.kernel,
"hostname": system_context.hostname,
"system_ip": system_context.system_ip,
"cpu": {
"model": system_context.cpu_model,
"cores": system_context.cpu_cores,
"sockets": system_context.cpu_sockets,
"ram_size": system_context.cpu_ram_size,
"numa_nodes": system_context.cpu_numa_nodes,
"clock_speed": system_context.cpu_clock_speed,
"l1_cache": system_context.cpu_l1_cache,
"l2_cache": system_context.cpu_l2_cache,
"l3_cache": system_context.cpu_l3_cache,
},
"gpu": {
"count": system_context.gpu_count,
"name": system_context.gpu_name,
"marketing_name": system_context.gpu_marketing_name,
"device_id": system_context.gpu_device_id,
"revision_id": system_context.gpu_revision_id,
"vram_size": system_context.gpu_vram_size,
"sys_clock": system_context.gpu_sys_clock,
"mem_clock": system_context.gpu_mem_clock,
"vbios": system_context.gpu_vbios,
"partition_mode": system_context.gpu_partition_mode,
"xgmi_type": system_context.gpu_xgmi_type,
"host_driver": system_context.gpu_host_driver,
"firmwares": system_context.gpu_firmwares,
"no_of_nodes": system_context.gpu_count,
"devices": system_context.gpu_devices,
},
}
@staticmethod
def save_local_results(
results_data: Dict[str, Any], output_dir: str, timestamp: Optional[str] = None
) -> Optional[Path]:
"""Save results to local JSON file with timestamp.
Args:
results_data: Results data dict
output_dir: Output directory path
timestamp: Optional timestamp (auto-generated if None)
Returns:
Path: Saved file path or None if failed
"""
try:
results_dir = Path(output_dir)
results_dir.mkdir(parents=True, exist_ok=True)
if timestamp is None:
timestamp = time.strftime("%Y%m%d_%H%M%S")
output_file = results_dir / f"results_{timestamp}.json"
with open(output_file, "w") as f:
json.dump(results_data, f, indent=2)
log.info(f"Results saved: {output_file}")
return output_file
except Exception as e:
log.error(f"Failed to save local results: {e}")
return None
@staticmethod
def upload_to_api(
system_info: Dict[str, Any],
test_results: List[Dict[str, Any]],
timestamp: str,
api_config: Dict[str, Any],
rocm_info: Dict[str, Any],
deployment_info: Dict[str, str],
test_environment: str = Constants.TEST_ENV_BARE_METAL,
) -> bool:
"""Upload test results to API with system context.
Args:
system_info: System info dict
test_results: List of test result dicts
timestamp: Execution timestamp
api_config: API config from ConfigHelper
rocm_info: ROCm info dict
deployment_info: Deployment info dict
test_environment: Environment type (bm/vm/docker)
Returns:
bool: True if successful, False otherwise
"""
# Check if API submission is enabled
if not api_config.get("enabled", False):
log.debug("API submission disabled")
return False
api_url = api_config.get("url", "")
fallback_url = api_config.get("fallback_url", "")
api_key = api_config.get("api_key", "")
if not api_url:
log.warning("API URL not configured, skipping submission")
return False
log.info(SEPARATOR_LINE)
log.info("Submitting results to API...")
log.info(SEPARATOR_LINE)
try:
# Build API payload
test_results_for_api = []
for result in test_results:
test_results_for_api.append(
{
"test_name": result.get("test_name", ""),
"success": result.get("status", "PASS") == "PASS"
or result.get("success", False),
"duration": result.get("duration", 0),
"error_message": result.get("error_message", ""),
"score": result.get("score"), # Test score/value
"unit": result.get("unit", ""), # Unit of measurement
"flag": result.get(
"flag", "H"
), # H (Higher is better) or L (Lower is better)
"test_config": result.get(
"test_config", {}
), # Test-specific configuration
"start_time": result.get(
"start_time", ""
), # Test start timestamp
"log_path": result.get("log_path", ""), # Log file path
}
)
payload = build_results_payload(
system_info=system_info,
test_results=test_results_for_api,
execution_time=timestamp,
test_environment=test_environment,
build_info=rocm_info,
deployment_info=deployment_info,
)
log.debug(payload)
# Validate payload
if not validate_payload(payload):
log.error("Payload validation failed, skipping API submission")
return False
# Submit to API
api_client = ResultsAPI(api_url, api_key, fallback_url)
success = api_client.submit_results(payload)
if success:
log.info("Results submitted to API successfully")
return True
else:
log.warning("Failed to submit results to API")
return False
except Exception as e:
log.error(f"Unexpected error submitting to API: {e}")
log.warning("Results not submitted - unexpected error")
return False
@staticmethod
def fetch_lkg_scores_from_api(
test_name: str, api_config: Dict[str, Any], rocm_info: Dict[str, Any]
) -> Dict[Tuple[str, str], float]:
"""Fetch Last Known Good (LKG) scores from API for comparison.
Args:
test_name: Test name for LKG score lookup
api_config: API config from ConfigHelper
rocm_info: ROCm info dict
Returns:
Dict: Mapping of (test_name, sub_test_name) tuples to LKG scores
"""
try:
# Get ROCm version
rocm_version = rocm_info.get("rocm_version", "")
# Validate API URL
api_url = api_config.get("url", "")
if not api_url:
raise ValueError("API URL not configured in api_config.")
# Build query string
fetch_api_url = f"{api_url}/api/v1/rock-ci-results?skip=0&limit=600&rocm_version={rocm_version}&{test_name}&lkg_score=true"
# Make API request
response = requests.get(
fetch_api_url, headers={"accept": "application/json"}
)
response.raise_for_status() # Raise error for HTTP issues
data = response.json()
# Validate response structure
if "results" not in data:
raise ValueError("Invalid API response: 'results' key not found.")
# Build dictionary of LKG scores
lkg_scores = {}
for result in data.get("results", []):
t_name = result.get("test_config", {}).get("test_name")
sub_test_name = result.get("test_config", {}).get("sub_test_name")
for metric in result.get("test_metrics", []):
lkg_score = metric.get("lkg_score")
if t_name and sub_test_name and lkg_score is not None:
lkg_scores[(t_name, sub_test_name)] = float(lkg_score)
return lkg_scores
except requests.exceptions.RequestException as e:
raise RuntimeError(f"API request failed: {e}")
except ValueError as e:
raise ValueError(f"Data error: {e}")
except Exception as e:
raise RuntimeError(f"Unexpected error: {e}")
@staticmethod
def get_final_result_table(
table: PrettyTable, lkg_scores: Dict[Tuple[str, str], float]
) -> PrettyTable:
"""Augment PrettyTable with LKG comparison columns.
Args:
table: PrettyTable with test results
lkg_scores: Mapping of (TestName, SubTests) to LKG scores
Returns:
PrettyTable: New table with LKGScores, %Diff, and FinalResult columns
"""
# Validate required columns
required_cols = ["TestName", "SubTests", "Scores", "Flag"]
for col in required_cols:
if col not in table.field_names:
raise ValueError(f"Missing required column '{col}' in PrettyTable.")
# Add new columns
new_field_names = table.field_names + ["LKGScores", "%Diff", "FinalResult"]
new_table = PrettyTable(new_field_names)
for row in table._rows: # Consider using table.rows if available
row_dict = dict(zip(table.field_names, row))
test_name = row_dict["TestName"]
sub_test_name = row_dict["SubTests"]
flag = row_dict["Flag"]
try:
score = Decimal(str(row_dict["Scores"]))
except InvalidOperation:
raise ValueError(f"Invalid score value: {row_dict['Scores']}")
# Get LKG score
lkg_score = lkg_scores.get((test_name, sub_test_name))
diff: Optional[Decimal] = None
final_result = "UNKNOWN"
if lkg_score is not None:
lkg_score_dec = Decimal(str(lkg_score))
if flag == "H" and lkg_score_dec != 0:
diff = ((score - lkg_score_dec) / lkg_score_dec) * 100
elif flag == "L" and lkg_score_dec != 0:
diff = ((lkg_score_dec - score) / lkg_score_dec) * 100
# Determine FinalResult
if diff is not None:
final_result = "FAIL" if diff < -5 else "PASS"
# Append new values
new_row = row + [
float(lkg_score) if lkg_score is not None else None,
round(float(diff), 2) if diff is not None else None,
final_result,
]
new_table.add_row(new_row)
return new_table