[CI] Fail lint when a registered file's TestCase classes never run (#32735)

This commit is contained in:
Liangsheng Yin
2026-07-29 00:45:01 -07:00
committed by GitHub
parent f05c92fb6d
commit cce5873513
2 changed files with 68 additions and 3 deletions
+52
View File
@@ -21,6 +21,7 @@ Reuses ut_parse_one_file() from ci_register.py (AST-based parsing)
to match the same logic used by run_suite.py's collect_tests(). to match the same logic used by run_suite.py's collect_tests().
""" """
import ast
import glob import glob
import importlib.util import importlib.util
import os import os
@@ -39,6 +40,37 @@ _MODERN_SHAPE = re.compile(r"^(.+)-test-(.+)$")
_LEGACY_CUDA_PREFIXES = ("nightly", "stress", "weekly") _LEGACY_CUDA_PREFIXES = ("nightly", "stress", "weekly")
def _defines_testcase(tree: ast.AST) -> bool:
"""True if the file defines unittest classes, statically or via type()."""
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
if any("TestCase" in ast.unparse(b) for b in node.bases):
return True
elif isinstance(node, ast.Call):
if (
isinstance(node.func, ast.Name)
and node.func.id == "type"
and len(node.args) >= 2
and isinstance(node.args[1], ast.Tuple)
and any("TestCase" in ast.unparse(e) for e in node.args[1].elts)
):
return True
return False
def _main_runs_tests(tree: ast.Module) -> bool:
for stmt in tree.body:
if not (
isinstance(stmt, ast.If)
and ast.unparse(stmt.test).replace("'", '"') == '__name__ == "__main__"'
):
continue
body = ast.unparse(ast.Module(body=stmt.body, type_ignores=[]))
if "unittest.main" in body or "pytest.main" in body:
return True
return False
def main() -> int: def main() -> int:
# Import ci_register directly to avoid pulling in all of sglang # Import ci_register directly to avoid pulling in all of sglang
spec = importlib.util.spec_from_file_location( spec = importlib.util.spec_from_file_location(
@@ -61,6 +93,7 @@ def main() -> int:
missing = [] missing = []
legacy_shape = [] # (file, suite, stage, runner_config) -- has a -test- split legacy_shape = [] # (file, suite, stage, runner_config) -- has a -test- split
non_dispatchable = [] # (file, suite) -- legacy CUDA suite no workflow invokes non_dispatchable = [] # (file, suite) -- legacy CUDA suite no workflow invokes
dead_tests = [] # (file) -- TestCase classes that `python3 file.py` never runs
for f in files: for f in files:
try: try:
registries, _has_main_entry = ci_register.ut_parse_one_file(f) registries, _has_main_entry = ci_register.ut_parse_one_file(f)
@@ -70,6 +103,12 @@ def main() -> int:
if len(registries) == 0: if len(registries) == 0:
missing.append(f) missing.append(f)
continue continue
# TestCase classes are dead unless __main__ runs them (CI does
# `python3 file.py`); the ERROR text below explains the fix.
with open(f, "r", encoding="utf-8") as fh:
tree = ast.parse(fh.read(), filename=f)
if _defines_testcase(tree) and not _main_runs_tests(tree):
dead_tests.append(f)
for r in registries: for r in registries:
# Pure legacy form on a CUDA registry: suite set, stage/runner unset. # Pure legacy form on a CUDA registry: suite set, stage/runner unset.
if not ( if not (
@@ -127,6 +166,19 @@ def main() -> int:
) )
print() print()
exit_code = 1 exit_code = 1
if dead_tests:
print(
"ERROR: Test file(s) define TestCase classes that CI never runs: "
"the registered file is executed as `python3 file.py`, but its "
'`if __name__ == "__main__"` block does not call unittest.main() '
"or pytest.main(), so the classes are silently skipped while the "
"file reports success. Make __main__ run the tests (put any CLI "
"entry point behind an explicit flag):\n"
)
for f in dead_tests:
print(f" {f}")
print()
exit_code = 1
return exit_code return exit_code
@@ -1,7 +1,7 @@
"""Large-scale benchmark + fuzz correctness tests for UnifiedRadixCache. """Large-scale benchmark + fuzz correctness tests for UnifiedRadixCache.
Usage (standalone): Usage (standalone):
bench: python3 test/registered/unit/mem_cache/test_unified_radix_cache_bench.py --num-seqs 5000 --verify --components mamba legacy-mamba swa legacy-swa bench: python3 test/registered/unit/mem_cache/test_unified_radix_cache_bench.py --bench --num-seqs 5000 --verify --components mamba legacy-mamba swa legacy-swa
CI Test: python -m pytest test/registered/unit/mem_cache/test_unified_radix_cache_bench.py -v -s CI Test: python -m pytest test/registered/unit/mem_cache/test_unified_radix_cache_bench.py -v -s
""" """
@@ -10,6 +10,7 @@ import gc
import logging import logging
import random import random
import statistics import statistics
import sys
import time import time
import unittest import unittest
from array import array from array import array
@@ -239,7 +240,7 @@ def create_bench_cache(
_rid = [0] _rid = [0]
def make_req(): def make_req():
from sglang.srt.managers.schedule_batch import Req from sglang.srt.managers.schedule_batch import Req, ReqKvInfo
from sglang.srt.sampling.sampling_params import SamplingParams from sglang.srt.sampling.sampling_params import SamplingParams
req = Req( req = Req(
@@ -250,6 +251,8 @@ def create_bench_cache(
) )
_rid[0] += 1 _rid[0] += 1
req_to_token_pool.alloc([req]) req_to_token_pool.alloc([req])
# fabricated reqs bypass alloc_for_extend, the normal creator of req.kv
req.kv = ReqKvInfo(kv_allocated_len=0, swa_evicted_seqlen=0)
return req return req
return tree, allocator, req_to_token_pool, make_req return tree, allocator, req_to_token_pool, make_req
@@ -821,7 +824,8 @@ _TREE_CONFIGS = {
"legacy-swa": ((ComponentType.FULL, ComponentType.SWA), SWARadixCache), "legacy-swa": ((ComponentType.FULL, ComponentType.SWA), SWARadixCache),
} }
if __name__ == "__main__":
def _run_bench_cli():
parser = argparse.ArgumentParser(description="UnifiedRadixCache benchmark") parser = argparse.ArgumentParser(description="UnifiedRadixCache benchmark")
parser.add_argument("--num-seqs", type=int, default=5000) parser.add_argument("--num-seqs", type=int, default=5000)
parser.add_argument("--chunk-len", type=int, default=256) parser.add_argument("--chunk-len", type=int, default=256)
@@ -857,3 +861,12 @@ if __name__ == "__main__":
tree_cls=tree_cls, tree_cls=tree_cls,
page_size=args.page_size, page_size=args.page_size,
) )
if __name__ == "__main__":
# CI runs `python3 file.py`; it must execute the TestBench_* classes
if "--bench" in sys.argv:
sys.argv.remove("--bench")
_run_bench_cli()
else:
unittest.main()