From cce5873513fe4a9059cd0f0bab416e1c64db4c31 Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Wed, 29 Jul 2026 00:45:01 -0700 Subject: [PATCH] [CI] Fail lint when a registered file's TestCase classes never run (#32735) --- scripts/ci/check_registered_tests.py | 52 +++++++++++++++++++ .../test_unified_radix_cache_bench.py | 19 +++++-- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/scripts/ci/check_registered_tests.py b/scripts/ci/check_registered_tests.py index d9f80121c..963fc74f3 100755 --- a/scripts/ci/check_registered_tests.py +++ b/scripts/ci/check_registered_tests.py @@ -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(). """ +import ast import glob import importlib.util import os @@ -39,6 +40,37 @@ _MODERN_SHAPE = re.compile(r"^(.+)-test-(.+)$") _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: # Import ci_register directly to avoid pulling in all of sglang spec = importlib.util.spec_from_file_location( @@ -61,6 +93,7 @@ def main() -> int: missing = [] legacy_shape = [] # (file, suite, stage, runner_config) -- has a -test- split 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: try: registries, _has_main_entry = ci_register.ut_parse_one_file(f) @@ -70,6 +103,12 @@ def main() -> int: if len(registries) == 0: missing.append(f) 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: # Pure legacy form on a CUDA registry: suite set, stage/runner unset. if not ( @@ -127,6 +166,19 @@ def main() -> int: ) print() 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 diff --git a/test/registered/unit/mem_cache/test_unified_radix_cache_bench.py b/test/registered/unit/mem_cache/test_unified_radix_cache_bench.py index d8aadd662..40584284d 100644 --- a/test/registered/unit/mem_cache/test_unified_radix_cache_bench.py +++ b/test/registered/unit/mem_cache/test_unified_radix_cache_bench.py @@ -1,7 +1,7 @@ """Large-scale benchmark + fuzz correctness tests for UnifiedRadixCache. 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 """ @@ -10,6 +10,7 @@ import gc import logging import random import statistics +import sys import time import unittest from array import array @@ -239,7 +240,7 @@ def create_bench_cache( _rid = [0] 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 req = Req( @@ -250,6 +251,8 @@ def create_bench_cache( ) _rid[0] += 1 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 tree, allocator, req_to_token_pool, make_req @@ -821,7 +824,8 @@ _TREE_CONFIGS = { "legacy-swa": ((ComponentType.FULL, ComponentType.SWA), SWARadixCache), } -if __name__ == "__main__": + +def _run_bench_cli(): parser = argparse.ArgumentParser(description="UnifiedRadixCache benchmark") parser.add_argument("--num-seqs", type=int, default=5000) parser.add_argument("--chunk-len", type=int, default=256) @@ -857,3 +861,12 @@ if __name__ == "__main__": tree_cls=tree_cls, 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()