[CI] Fail lint when a registered file's TestCase classes never run (#32735)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user