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
554 lines
19 KiB
Python
554 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright © Advanced Micro Devices, Inc., or its affiliates.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from typing import List, Dict, Tuple
|
|
from re import Pattern
|
|
|
|
|
|
class TestNameValidator:
|
|
|
|
KEYWORDS: Dict[str, List[str]] = {
|
|
"test_types": ["Test", "Integration"],
|
|
"gpu": ["Gpu"],
|
|
"datatypes": ["Bfp16", "Fp16", "Fp32", "Fp64"],
|
|
"shapes": ["Nhwc", "Nchw", "Ndhwc", "Ncdhw"],
|
|
}
|
|
|
|
FLATTENED_KEYWORDS: List[str] = [
|
|
kw for sublist in KEYWORDS.values() for kw in sublist
|
|
]
|
|
|
|
POSITIONAL_KEYWORDS: List[str] = (
|
|
KEYWORDS["test_types"] + KEYWORDS["datatypes"] + KEYWORDS["gpu"]
|
|
)
|
|
|
|
FULL_NAME_RE: Pattern[str] = re.compile(
|
|
r"^(?:(?P<prefix>(?:DISABLED_)?[A-Z][A-Za-z0-9]*)/)?"
|
|
r"(?P<suite>[A-Z][A-Za-z0-9]*)"
|
|
r"(?:/\d+)?"
|
|
r"\.(?P<case>(?:DISABLED_[A-Z][A-Za-z0-9]+|[A-Z][A-Za-z0-9]*))"
|
|
r"(?:/.*)?$"
|
|
)
|
|
|
|
def _validate_test_case(self, case_name: str) -> List[str]:
|
|
"""
|
|
Validate a test case name.
|
|
Returns a list of issues found, or empty list if valid.
|
|
"""
|
|
issues = []
|
|
|
|
if case_name.startswith("DISABLED_"):
|
|
case_name = case_name[9:]
|
|
|
|
# Check for disallowed positional keywords
|
|
found_keywords = [kw for kw in self.POSITIONAL_KEYWORDS if kw in case_name]
|
|
if found_keywords:
|
|
issues.append(
|
|
f"Test case name should not contain keywords: {', '.join(found_keywords)}. These belong in the test suite name."
|
|
)
|
|
|
|
return issues
|
|
|
|
def _validate_suite_structure(self, suite_name: str) -> List[str]:
|
|
"""
|
|
Validate the structure of a test suite name.
|
|
Returns a list of issues found, or empty list if valid.
|
|
"""
|
|
issues = []
|
|
|
|
prefix_part = f"({'|'.join(self.KEYWORDS['test_types'])})"
|
|
gpu_part = f"({self.KEYWORDS['gpu'][0]})?"
|
|
feature_part = r"(?P<feature>[A-Z][a-zA-Z0-9]*?)"
|
|
datatypes_part = f"({'|'.join(self.KEYWORDS['datatypes'])})?"
|
|
|
|
structure_regex = re.compile(
|
|
f"^{prefix_part}{gpu_part}{feature_part}{datatypes_part}$"
|
|
)
|
|
|
|
match = structure_regex.match(suite_name)
|
|
|
|
if not match:
|
|
issues.append(
|
|
"Suite name does not follow the structure: (Test|Integration)[Gpu?]FeatureName[Datatype?]"
|
|
)
|
|
return issues
|
|
|
|
feature_name = match.group("feature")
|
|
|
|
for keyword in self.POSITIONAL_KEYWORDS:
|
|
if keyword in feature_name:
|
|
issues.append(
|
|
f"Keyword '{keyword}' is misplaced and should not be in the middle of the suite name."
|
|
)
|
|
|
|
return issues
|
|
|
|
def validate_test_name(self, test_name: str) -> List[str]:
|
|
"""
|
|
Validate a single full test name.
|
|
Returns a list of issues found, or empty list if valid
|
|
"""
|
|
issues = []
|
|
|
|
parsed_match = self.FULL_NAME_RE.match(test_name)
|
|
if not parsed_match:
|
|
issues.append(
|
|
"Test name does not match expected PascalCase format 'TestSuite.TestCase' or 'TestSuite/Instance.TestCase' without special characters."
|
|
)
|
|
return issues
|
|
|
|
prefix = parsed_match.group("prefix") or ""
|
|
suite_name = parsed_match.group("suite")
|
|
case_name = parsed_match.group("case")
|
|
|
|
for keyword in self.FLATTENED_KEYWORDS:
|
|
matches = re.findall(
|
|
re.escape(keyword), f"{prefix}/{suite_name}.{case_name}", re.IGNORECASE
|
|
)
|
|
|
|
valid_matches = [m for m in matches if m == keyword or m == keyword.upper()]
|
|
# Check capitalization
|
|
issues.extend(
|
|
[
|
|
f"Keyword '{match}' should be capitalized as '{keyword}'"
|
|
for match in valid_matches
|
|
if match != keyword
|
|
]
|
|
)
|
|
|
|
# Check duplicates
|
|
if len(valid_matches) > 1:
|
|
issues.append(
|
|
f"Keyword '{keyword}' appears more than once."
|
|
) # Potentially useful to make test names more concise
|
|
|
|
issues.extend(self._validate_suite_structure(suite_name))
|
|
issues.extend(self._validate_test_case(case_name))
|
|
|
|
return issues
|
|
|
|
@staticmethod
|
|
def extract_test_names_from_ctest_json(json_path: Path) -> List[str]:
|
|
"""Extract test names from CTest JSON output."""
|
|
try:
|
|
with open(json_path, "r") as f:
|
|
data = json.load(f)
|
|
|
|
test_names = []
|
|
if "tests" in data:
|
|
for test in data["tests"]:
|
|
if "name" in test:
|
|
test_names.append(test["name"].split("#")[0].strip())
|
|
|
|
return test_names
|
|
except FileNotFoundError:
|
|
print(f"Error: CTest JSON file not found: {json_path}", file=sys.stderr)
|
|
return []
|
|
except json.JSONDecodeError as e:
|
|
print(f"Error: Invalid JSON in file {json_path}: {e}", file=sys.stderr)
|
|
return []
|
|
except Exception as e:
|
|
print(f"Unexpected error reading CTest JSON file: {e}", file=sys.stderr)
|
|
return []
|
|
|
|
@staticmethod
|
|
def extract_test_names_from_executables(
|
|
executables_file: Path, build_dir: Path, verbose: bool = False
|
|
) -> List[str]:
|
|
"""Extract test names by running executables with --gtest_list_tests."""
|
|
test_names = []
|
|
|
|
try:
|
|
with open(executables_file, "r") as f:
|
|
executables = [line.strip() for line in f if line.strip()]
|
|
if verbose:
|
|
print(f"Found {len(executables)} test executables to process.")
|
|
print(f"Executables: {executables}")
|
|
except FileNotFoundError:
|
|
print(
|
|
f"Error: Executables file not found: {executables_file}",
|
|
file=sys.stderr,
|
|
)
|
|
return []
|
|
|
|
for executable in executables:
|
|
exec_path = (build_dir / executable).resolve()
|
|
if not exec_path.exists() or not exec_path.is_file():
|
|
print(f"Warning: Executable not found: {exec_path}", file=sys.stderr)
|
|
continue
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
[str(exec_path), "--gtest_list_tests"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5,
|
|
cwd=str(exec_path.parent),
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
print(
|
|
f"Warning: Failed to list tests from {executable}: {result.stderr}",
|
|
file=sys.stderr,
|
|
)
|
|
continue
|
|
|
|
current_suite = None
|
|
for line in result.stdout.splitlines():
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
|
|
# Strip gtest comments (e.g. "# TypeParam = float")
|
|
line = line.split("#")[0].strip()
|
|
if not line:
|
|
continue
|
|
|
|
if line.endswith(".") and not line.endswith("..."):
|
|
current_suite = line[:-1]
|
|
elif line and current_suite:
|
|
test_names.append(f"{current_suite}.{line}")
|
|
|
|
except subprocess.TimeoutExpired:
|
|
print(f"Warning: Timeout running {executable}", file=sys.stderr)
|
|
except Exception as e:
|
|
print(f"Warning: Error running {executable}: {e}", file=sys.stderr)
|
|
|
|
return test_names
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Validate test names against hipDNN rules",
|
|
epilog="At least one of --ctest-json or --test-executables must be provided.",
|
|
)
|
|
|
|
input_group = parser.add_argument_group(
|
|
"input methods", "Specify one or both methods to gather test names"
|
|
)
|
|
|
|
input_group.add_argument(
|
|
"--ctest-json",
|
|
type=Path,
|
|
help="Path to CTest JSON output (from ctest --show-only=json-v1)",
|
|
)
|
|
|
|
input_group.add_argument(
|
|
"--test-executables",
|
|
type=Path,
|
|
help="Path to file containing list of test executables",
|
|
)
|
|
|
|
input_group.add_argument(
|
|
"--build-dir",
|
|
type=Path,
|
|
help="Build directory where test executables are located. Required if --test-executables is provided.",
|
|
)
|
|
|
|
options_group = parser.add_argument_group("options")
|
|
|
|
options_group.add_argument(
|
|
"--strict",
|
|
action="store_true",
|
|
help="Exit with non-zero status if any test names are invalid",
|
|
default=False,
|
|
)
|
|
|
|
options_group.add_argument(
|
|
"--verbose",
|
|
action="store_true",
|
|
help="Show all test names, not just invalid ones",
|
|
default=False,
|
|
)
|
|
|
|
options_group.add_argument(
|
|
"--run-tests",
|
|
action="store_true",
|
|
help="Run unit tests for this validator",
|
|
default=False,
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not args.run_tests:
|
|
if args.test_executables and not args.build_dir:
|
|
parser.error("--build-dir is required when using --test-executables")
|
|
|
|
if not args.ctest_json and not args.test_executables:
|
|
parser.error(
|
|
"At least one of --ctest-json or --test-executables must be provided"
|
|
)
|
|
|
|
return args
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
|
|
if args.run_tests:
|
|
unittest.main(argv=[sys.argv[0]], exit=True)
|
|
|
|
validator = TestNameValidator()
|
|
test_names = []
|
|
|
|
if args.test_executables and args.build_dir:
|
|
test_names.extend(
|
|
validator.extract_test_names_from_executables(
|
|
args.test_executables, args.build_dir, args.verbose
|
|
)
|
|
)
|
|
|
|
if args.ctest_json:
|
|
test_names.extend(validator.extract_test_names_from_ctest_json(args.ctest_json))
|
|
|
|
if not test_names:
|
|
print("Warning: No test names found to validate", file=sys.stderr)
|
|
return 0
|
|
|
|
invalid_count = 0
|
|
results: Dict[str, List[str]] = {}
|
|
|
|
for test_name in test_names:
|
|
issues = validator.validate_test_name(test_name)
|
|
results[test_name] = issues
|
|
if issues:
|
|
invalid_count += 1
|
|
|
|
print(f"\nTest Name Validation Report")
|
|
print(f"{'=' * 60}")
|
|
print(f"Total tests found: {len(test_names)}")
|
|
print(f"Valid test names: {len(test_names) - invalid_count}")
|
|
print(f"Invalid test names: {invalid_count}\n")
|
|
|
|
if invalid_count == 0:
|
|
return 0
|
|
|
|
print(f"{'Test Name':<50} {'Status':<10}")
|
|
print(f"{'-' * 60}")
|
|
|
|
for test_name, issues in sorted(results.items()):
|
|
is_valid = len(issues) == 0
|
|
if not is_valid or args.verbose:
|
|
status = "[PASS]" if is_valid else "[FAIL]"
|
|
print(f"{test_name:<50} {status:<10}")
|
|
if issues:
|
|
for issue in issues:
|
|
print(f" -> {issue}")
|
|
|
|
print(f"\nWarning: {invalid_count} test(s) have non-conforming names")
|
|
print(
|
|
" - For detailed hipDNN test naming rules, see: docs/CodingStyleAndNamingGuidelines.md\n"
|
|
)
|
|
|
|
return 1 if args.strict else 0
|
|
|
|
|
|
class TestTestNameValidator(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.validator = TestNameValidator()
|
|
|
|
def test_valid_test_names_basic(self) -> None:
|
|
"""Test basic valid test names in PascalCase"""
|
|
valid_names = [
|
|
"TestMyClass.Something",
|
|
"IntegrationConvolution.Forward",
|
|
"TestBatchNorm.Backward",
|
|
"TestExecutionPlanBuilder.Build",
|
|
]
|
|
|
|
for name in valid_names:
|
|
with self.subTest(name=name):
|
|
issues = self.validator.validate_test_name(name)
|
|
self.assertEqual(
|
|
issues, [], f"Expected {name} to be valid, but got issues: {issues}"
|
|
)
|
|
|
|
def test_valid_test_names_with_gpu(self) -> None:
|
|
"""Test valid test names with Gpu keyword"""
|
|
valid_names = [
|
|
"TestGpuConvolution.Forward",
|
|
"IntegrationGpuBatchNorm.Backward",
|
|
"TestGpuExecutionPlanBuilder.Build",
|
|
]
|
|
|
|
for name in valid_names:
|
|
with self.subTest(name=name):
|
|
issues = self.validator.validate_test_name(name)
|
|
self.assertEqual(
|
|
issues, [], f"Expected {name} to be valid, but got issues: {issues}"
|
|
)
|
|
|
|
def test_valid_test_names_with_datatypes(self) -> None:
|
|
"""Test valid test names with datatype suffixes"""
|
|
valid_names = [
|
|
"TestConvolutionFp32.Forward",
|
|
"TestBatchNormFp16.Backward",
|
|
"IntegrationConvolutionBfp16.Wrw",
|
|
"TestGpuConvolutionFp64.Performance",
|
|
"IntegrationGpuBatchNormFp32.Convergence",
|
|
]
|
|
|
|
for name in valid_names:
|
|
with self.subTest(name=name):
|
|
issues = self.validator.validate_test_name(name)
|
|
self.assertEqual(
|
|
issues, [], f"Expected {name} to be valid, but got issues: {issues}"
|
|
)
|
|
|
|
def test_valid_test_names_with_instance(self) -> None:
|
|
"""Test valid parameterized test names with instance"""
|
|
valid_names = [
|
|
"Temp/TestConvolution.Forward",
|
|
"Group/IntegrationGpuBatchNormFp32.Accuracy",
|
|
"Config/TestMyClass.Method",
|
|
]
|
|
|
|
for name in valid_names:
|
|
with self.subTest(name=name):
|
|
issues = self.validator.validate_test_name(name)
|
|
self.assertEqual(
|
|
issues, [], f"Expected {name} to be valid, but got issues: {issues}"
|
|
)
|
|
|
|
def test_valid_disabled_test_names(self) -> None:
|
|
"""Test valid disabled test names"""
|
|
valid_names = [
|
|
"TestMyClass.DISABLED_Something",
|
|
"IntegrationGpuConvolutionFp32.DISABLED_Forward",
|
|
"DISABLED_Medium2d/TestGpuConvRefShapesFp32.MatchesCpuRef/Basic3x3",
|
|
"DISABLED_Large2d/TestGpuConvRefShapesFp32.MatchesCpuRef/ResNet",
|
|
"DISABLED_Nhwc2dMedium/TestGpuConvRefShapesFp16.MatchesCpuRef/Stride2",
|
|
]
|
|
|
|
for name in valid_names:
|
|
with self.subTest(name=name):
|
|
issues = self.validator.validate_test_name(name)
|
|
self.assertEqual(
|
|
issues, [], f"Expected {name} to be valid, but got issues: {issues}"
|
|
)
|
|
|
|
def test_invalid_format(self) -> None:
|
|
"""Test names with invalid format"""
|
|
invalid_names = [
|
|
"test_my_class.test_something", # lowercase
|
|
"TestMyClass_Something", # underscore instead of dot
|
|
"TestMyClass.test_something", # lowercase test case
|
|
"testMyClass.Something", # lowercase suite
|
|
"Test-MyClass.Something", # hyphens
|
|
"Test My Class.Something", # spaces
|
|
]
|
|
|
|
for name in invalid_names:
|
|
with self.subTest(name=name):
|
|
issues = self.validator.validate_test_name(name)
|
|
self.assertTrue(len(issues) > 0, f"Expected format error for {name}")
|
|
|
|
def test_keywords_in_test_case(self) -> None:
|
|
"""Test that positional keywords are not allowed in test case names"""
|
|
invalid_names = [
|
|
"TestMyClass.TestGpuFunction", # Gpu in test case
|
|
"TestMyClass.TestFp32Precision", # Fp32 in test case
|
|
"TestMyClass.IntegrationTest", # Integration in test case
|
|
"TestMyClass.TestBfp16Type", # Bfp16 in test case
|
|
]
|
|
|
|
for name in invalid_names:
|
|
with self.subTest(name=name):
|
|
issues = self.validator.validate_test_name(name)
|
|
self.assertTrue(
|
|
any("should not contain keywords" in issue for issue in issues),
|
|
f"Expected keyword error in test case for {name}",
|
|
)
|
|
|
|
def test_invalid_suite_structure(self) -> None:
|
|
"""Test suite names that don't follow the required structure"""
|
|
invalid_names = [
|
|
"MyClass.Something", # Missing Test/Integration prefix
|
|
"GpuTestConvolution.Forward", # Gpu must come after Test/Integration
|
|
"TestConvolutionGpu.Forward", # Gpu must come before feature name
|
|
"ConvolutionTest.Forward", # Test must be at the beginning
|
|
"TestFp32Convolution.Forward", # Datatype must be at the end
|
|
]
|
|
|
|
for name in invalid_names:
|
|
with self.subTest(name=name):
|
|
issues = self.validator.validate_test_name(name)
|
|
self.assertTrue(len(issues) > 0, f"Expected structure error for {name}")
|
|
|
|
def test_keyword_misplacement(self) -> None:
|
|
"""Test keywords misplaced in the feature name"""
|
|
invalid_names = [
|
|
"TestConvolutionGpuPlannerFp32.Forward", # Gpu in middle
|
|
"TestConvolutionTestPlannerFp32.Forward", # Test in middle
|
|
"IntegrationConvolutionFp32Planner.Forward", # Fp32 in middle
|
|
]
|
|
|
|
for name in invalid_names:
|
|
with self.subTest(name=name):
|
|
issues = self.validator.validate_test_name(name)
|
|
self.assertTrue(
|
|
any("misplaced" in issue for issue in issues),
|
|
f"Expected misplacement error for {name}",
|
|
)
|
|
|
|
def test_keyword_capitalization(self) -> None:
|
|
"""Test incorrect keyword capitalization"""
|
|
invalid_names = [
|
|
"TestGPUConvolution.Forward", # GPU instead of Gpu
|
|
"TestConvolutionFP32.Forward", # FP32 instead of Fp32
|
|
"testConvolution.Forward", # test instead of Test
|
|
"INTEGRATIONConvolution.Forward", # INTEGRATION instead of Integration
|
|
]
|
|
|
|
for name in invalid_names:
|
|
with self.subTest(name=name):
|
|
issues = self.validator.validate_test_name(name)
|
|
self.assertTrue(
|
|
len(issues) > 0, f"Expected capitalization issues for {name}"
|
|
)
|
|
|
|
def test_keyword_duplicates(self) -> None:
|
|
"""Test duplicate keywords"""
|
|
invalid_names = [
|
|
"TestTestConvolution.Forward", # Duplicate Test
|
|
"TestGpuConvolutionGpu.Forward", # Duplicate Gpu
|
|
]
|
|
|
|
for name in invalid_names:
|
|
with self.subTest(name=name):
|
|
issues = self.validator.validate_test_name(name)
|
|
self.assertTrue(
|
|
len(issues) > 0, f"Expected issues for duplicate keywords in {name}"
|
|
)
|
|
|
|
def test_complex_valid_names(self) -> None:
|
|
"""Test more complex but valid test names"""
|
|
valid_names = [
|
|
"IntegrationGpuConvolutionPlannerNchwFp32.Forward",
|
|
"TestActivationKernelNchwFp32.Relu",
|
|
"TestExecutionPlanBuilderFp32.Optimization",
|
|
"IntegrationGraphFusion.MultipleOps",
|
|
"TestConvolutionHeuristicsFp32.Performance",
|
|
"TestConvolutionHeuristics.Accuracy",
|
|
]
|
|
|
|
for name in valid_names:
|
|
with self.subTest(name=name):
|
|
issues = self.validator.validate_test_name(name)
|
|
self.assertEqual(
|
|
issues, [], f"Expected {name} to be valid, but got issues: {issues}"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|