Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automated gtest formatting checks #3370

Open
wants to merge 27 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,19 @@ pipeline {
buildHipClangJobAndReboot(setup_cmd: "", build_cmd: "", execute_cmd: execute_cmd, needs_gpu:false, needs_reboot:false)
}
}
stage('Check GTest Format') {
agent { label rocmnode("nogpu") }
when {
changeset "**/test/gtest/**"
}
steps {
script {
checkout scm
sh 'cd ./test/utils && python3 gtest_formating_checks.py'
}
}
}

stage('HipNoGPU Debug Build Test') {
when {
beforeAgent true
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ To build and run a single test, use the following code:
cmake --build . --config Release --target test_tensor
./bin/test_tensor
```
Check gtests formats

```shell
cd ./test/utils && python3 gtest_formating_checks.py
```


## Formatting the code

Expand Down
116 changes: 116 additions & 0 deletions test/utils/gtest_formating_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import os
import re
import logging

logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

FOLDER_PATH = "../../test/gtest"

# Ignore list: Add test names or file paths you want to exclude
IGNORE_LIST = {
"CPU_MIOpenDriverRegressionBigTensorTest_FP32",
"../../test/gtest/reduce_custom_fp32.cpp",
"../../test/gtest/binary_tensor_ops.cpp",
"../../test/gtest/layout_transpose.cpp",
"../../test/gtest/graphapi_conv_bias_res_add_activ_fwd.cpp",
"../../test/gtest/unary_tensor_ops.cpp",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it excluded? It's not an UNINSTANTIATED_PARAMETERIZED_TEST - it is perfectly instantiated.
I guess the problem is in the parsing, since you have to run preprocessor prior to get proper code, but preproceesor will eliminate all INSTANTIATE_TEST_SUITE_P macros.

I would suggest keeping "hard to parse" cases separated from "UNINSTANTIATED_PARAMETERIZED_TEST" cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests have formatting names that aren't to standard for gtest folder - some are in the macro section, there are regexes like ##SRC_TYPE## in the tests name:

We can either accommodate them or modify them with the standard in different PR:

ERROR: The following issues were found:
ERROR:   ../../test/gtest/graphapi_operation_rng.cpp: INSTANTIATE_TEST_SUITE_P references non-existent TESTSUITE_NAME 'GraphApiOperationRng'.
ERROR:   ../../test/gtest/graphapi_operation_rng.cpp: Invalid TEST_TYPE 'InvalidAtLeastSeeds' in INSTANTIATE_TEST_SUITE_P.
ERROR:   ../../test/gtest/graphapi_conv_bias_res_add_activ_fwd.cpp: Invalid TESTSUITE_NAME 'GPU_ConvBiasResAddActivation_##dir##_##type' in TEST_P.
ERROR:   ../../test/gtest/graphapi_conv_bias_res_add_activ_fwd.cpp: Test 'GPU_ConvBiasResAddActivation_##dir##_##type.Test' does not have a matching INSTANTIATE_TEST_SUITE_P or GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST.
ERROR:   ../../test/gtest/binary_tensor_ops.cpp: Invalid TESTSUITE_NAME 'GPU_binaryTensorOps_cast_##SRC_TYPE##_##TEST_TYPE' in TEST_P.
ERROR:   ../../test/gtest/binary_tensor_ops.cpp: Test 'GPU_binaryTensorOps_cast_##SRC_TYPE##_##TEST_TYPE.\
           X_CONCAT_FIRST_SECOND_(__VA_ARGS__, TestTensorCast' does not have a matching INSTANTIATE_TEST_SUITE_P or GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST.
ERROR:   ../../test/gtest/binary_tensor_ops.cpp: Invalid TESTSUITE_NAME 'GPU_binaryTensorOps_copy_##TEST_TYPE' in TEST_P.
ERROR:   ../../test/gtest/binary_tensor_ops.cpp: Test 'GPU_binaryTensorOps_copy_##TEST_TYPE.TestTensorCopy' does not have a matching INSTANTIATE_TEST_SUITE_P or GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST.
ERROR:   ../../test/gtest/unary_tensor_ops.cpp: Invalid TESTSUITE_NAME 'GPU_unaryTensorOps_##TEST_TYPE' in TEST_P.
ERROR:   ../../test/gtest/unary_tensor_ops.cpp: Test 'GPU_unaryTensorOps_##TEST_TYPE.TestTensorSet' does not have a matching INSTANTIATE_TEST_SUITE_P or GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST.
ERROR:   ../../test/gtest/layout_transpose.cpp: Invalid TESTSUITE_NAME 'GPU_LayoutTransposeTest_2D_##sol##_##naming_type' in TEST_P.
ERROR:   ../../test/gtest/layout_transpose.cpp: Test 'GPU_LayoutTransposeTest_2D_##sol##_##naming_type.\
           LayoutTransposeTest_2D_##sol##_##type##_P' does not have a matching INSTANTIATE_TEST_SUITE_P or GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST.
ERROR:   ../../test/gtest/layout_transpose.cpp: Invalid TESTSUITE_NAME 'GPU_LayoutTransposeTest_3D_##sol##_##naming_type' in TEST_P.
ERROR:   ../../test/gtest/layout_transpose.cpp: Test 'GPU_LayoutTransposeTest_3D_##sol##_##naming_type.\
           LayoutTransposeTest_3D_##sol##_##type##_P' does not have a matching INSTANTIATE_TEST_SUITE_P or GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST.

"../../test/gtest/graphapi_operation_rng.cpp"
}

# Valid enums and Regex for validation
VALID_HW_TYPES = {"CPU", "GPU"}
VALID_DATATYPES = {"FP8", "FP16", "FP32", "FP64", "BFP16", "BFP8", "I64", "I32", "I16", "I8", "NONE"}
TESTSUITE_REGEX = re.compile(
r"^(CPU|GPU)_[A-Za-z0-9]+(?:_[A-Za-z0-9]+)*_(" + "|".join(VALID_DATATYPES) + r")$"
)
Comment on lines +21 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't it duplicate #3218 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one extends on it + CI integration that runs upon adding new gtest

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need separate python scripts?
Seems like we might be able to unify these.

TEST_P_REGEX = re.compile(r"\bTEST_P\(\s*([^,]+),\s*([^)]+)\)")
INSTANTIATE_TEST_REGEX = re.compile(r"\bINSTANTIATE_TEST_SUITE_P\(\s*([^,]+),\s*([^,]+),")
ALLOW_UNINSTANTIATED_REGEX = re.compile(r"\bGTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST\(\s*([^)]+)\)")
TEST_TYPE_REGEX = re.compile(r"^(Smoke|Full|Perf|Unit)([A-Za-z0-9]*)?$")


def analyze_tests(folder_path):
errors = []

for root, _, files in os.walk(folder_path):
for file in files:
if file.endswith(".cpp"):
file_path = os.path.join(root, file)

if file_path in IGNORE_LIST:
logging.info(f"Skipping ignored file: {file_path}")
continue

with open(file_path, "r") as f:
lines = f.readlines()

# Store all content in a single string for regex matching
content = "".join(lines)

# Extract TEST_P, INSTANTIATE_TEST_SUITE_P, and GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST
test_p_matches = [(m.start(), m.groups()) for m in TEST_P_REGEX.finditer(content)]
instantiate_matches = [(m.start(), m.groups()) for m in INSTANTIATE_TEST_REGEX.finditer(content)]
allow_uninstantiated_matches = [(m.start(), m.group(1)) for m in ALLOW_UNINSTANTIATED_REGEX.finditer(content)]

# Map line numbers to errors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line numbers, nice! They aren't quite right though. In all the instances I've seen, they are too small by 1. I also injected an error on line 1 and it printed as line 0, which suggests a counting or indexing issue.

The image also shows another problem I found--if a line is an exact copy of a previous line, one of them is ignored. In this example, line 86 was copied and pasted at line 91 and line 86 was missed. I've also seen cases when 91 was the missing one. You'll probably need to do something along the lines of generating a hash and use that for the key for each line instead of the text itself.

If a comma is missed, the regex consumes everything until the next comma is hit--including CR's. See the error prints for line 88 (printed as line 87). This needs to be fixed in this PR.

This also demonstrates other checks we should add in the future:

  • TEST: matches our name formatting rules only
  • TEST_F: matches name format and has a matching TestFixtureName
  • When a duplicate TEST_P occurs, it should be detected and an error thrown.
  • Multiple INSTANTIATE_TEST_SUITE_P are allowed for a given TestFixtureName. The INSTANTIATION_NAME (first macro argument) must be unique. Check for this.
  • Multiple TEST_F are allowed per TestFixtureName. The TestName must be unique. Check for this.

image

def get_line_number(position):
return sum(1 for i, line in enumerate(lines) if position >= len("".join(lines[:i + 1])))

test_p_suites = {suite: (info, get_line_number(pos)) for pos, (suite, info) in test_p_matches}
instantiated_suites = {suite: (test_type, get_line_number(pos)) for pos, (test_type, suite) in instantiate_matches}
allowed_uninstantiated_suites = {(suite, get_line_number(pos)) for pos, suite in allow_uninstantiated_matches}

# Validate TEST_P suites
for suite, (info, line) in test_p_suites.items():
if suite in IGNORE_LIST:
logging.info(f"Skipping ignored test suite: {suite}")
continue

if not TESTSUITE_REGEX.match(suite):
errors.append(f"{file_path}:{line}: Invalid TESTSUITE_NAME '{suite}' in TEST_P.")

if suite not in instantiated_suites and suite not in [s[0] for s in allowed_uninstantiated_suites]:
errors.append(
f"{file_path}:{line}: Test '{suite}.{info}' does not have a matching "
f"INSTANTIATE_TEST_SUITE_P or GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST."
)

# Validate instantiated suites
for suite, (test_type, line) in instantiated_suites.items():
normalized_test_type = test_type.replace("\\", "").strip()

if suite in IGNORE_LIST:
logging.info(f"Skipping ignored instantiated suite: {suite}")
continue

if suite not in test_p_suites:
errors.append(f"{file_path}:{line}: INSTANTIATE_TEST_SUITE_P references non-existent TESTSUITE_NAME '{suite}'.")
if not TEST_TYPE_REGEX.match(normalized_test_type):
errors.append(f"{file_path}:{line}: Invalid TEST_TYPE '{test_type}' in INSTANTIATE_TEST_SUITE_P.")

# Validate allowed uninstantiated suites
for suite, line in allowed_uninstantiated_suites:
if suite in IGNORE_LIST:
logging.info(f"Skipping ignored allowed uninstantiated suite: {suite}")
continue

if suite not in test_p_suites:
errors.append(f"{file_path}:{line}: GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST references non-existent TESTSUITE_NAME '{suite}'.")

return errors


def main():
errors = analyze_tests(FOLDER_PATH)

if errors:
logging.error("The following issues were found:")
for error in errors:
logging.error(f" {error}")
raise ValueError("Validation failed. See the errors above.")
else:
logging.info("All tests meet the criteria.")


if __name__ == "__main__":
main()
Loading