"""
复刻旧线 access_control 的 4 个策略,输入变更文件列表,输出应跑的测试文件。
策略:
1. TestFileStrategy: 改了 test/**/test_*.py → 返回该文件
2. CoreTestStrategy: 改了 torch_npu/ 下非 test/非 docs 文件 → 跑 test/npu/ 全量
3. DirectoryMapping: 改了 torch_npu/<module>/ → 映射到对应 test/<module>/
4. OpStrategy: 改了 *KernelNpu*.cpp → 从文件名解析算子名查找匹配的测试文件
输出格式:
所有路径为相对路径,去掉 test/ 前缀和 .py 后缀:
test/npu/test_add.py → npu/test_add
test/distributed/test_ddp.py → distributed/test_ddp
与 detect_changed_tests.sh 的直接检测格式一致,
可直接传给 run_npu_test_shard.py --test-files。
用法:
echo "torch_npu/nn/functional.py" | python classify_changed_tests.py
"""
import fnmatch
import os
import re
import sys
from pathlib import Path
from typing import List
def normalize_test_path(path: str) -> str:
"""test/npu/test_add.py → npu/test_add"""
p = path
if p.startswith("test/"):
p = p[5:]
if p.endswith(".py"):
p = p[:-3]
return p
def strategy_test_file(modify_file: str, repo_root: str) -> List[str]:
"""
如果变更路径在 test/ 下且文件名匹配 test_*.py,直接返回该文件 (去掉前缀后缀)。
"""
f = Path(modify_file)
if f.parts and f.parts[0] == "test" and re.match(r"test_(.+)\.py", f.name):
full = Path(repo_root) / modify_file
if full.exists():
return [normalize_test_path(modify_file)]
return []
def strategy_core(modify_file: str, repo_root: str) -> List[str]:
"""
变更的第一级目录不是 test 也不是 docs → 跑 test/npu/ 下全部 test_*.py。
"""
parts = Path(modify_file).parts
if not parts:
return []
top = parts[0]
if top not in ("test", "docs", ".github", ".gitcode"):
npu_dir = Path(repo_root) / "test" / "npu"
if npu_dir.is_dir():
return [normalize_test_path(str(p.relative_to(repo_root))) for p in npu_dir.rglob("test_*.py")]
return []
DIRECTORY_MAPPING = {
"contrib": "test/contrib",
"cpp_extension": "test/cpp_extensions",
"distributed": "test/distributed",
"fx": "test/test_fx.py",
"optim": "test/optim",
"profiler": "test/profiler",
"onnx": "test/onnx",
"utils": "test/test_utils.py",
"testing": "test/test_testing.py",
"jit": "test/test_jit.py",
"rpc": "test/distributed/rpc",
"meta": "test/test_fake_tensor.py",
}
def _get_module_name(modify_file: str) -> str:
"""从 torch_npu 变更文件中提取二级模块名 (参照 DirectoryMappingStrategy.get_module_name)。"""
parts = Path(modify_file).parts
if len(parts) >= 2 and parts[0] == "torch_npu":
module_name = parts[1]
if module_name == "csrc" and len(parts) >= 3:
module_name = parts[2]
for p in parts:
if p == "rpc":
module_name = "rpc"
if module_name == "utils" and len(parts) >= 3 and parts[2] == "cpp_extension.py":
module_name = "cpp_extension"
return module_name
return ""
def strategy_directory_mapping(modify_file: str, repo_root: str) -> List[str]:
"""
torch_npu/<module>/ 变更 → 找到映射的测试文件/目录 → 返回里面的 test_*.py。
"""
if modify_file.split("/")[0] != "torch_npu":
return []
module_name = _get_module_name(modify_file)
file_stem = Path(modify_file).stem
mapped_paths = []
for name in (module_name, file_stem):
if name in DIRECTORY_MAPPING:
mapped_paths.append(DIRECTORY_MAPPING[name])
result = []
for mapped in mapped_paths:
full = Path(repo_root) / mapped
if full.is_file():
result.append(normalize_test_path(mapped))
elif full.is_dir():
result.extend(
normalize_test_path(str(p.relative_to(repo_root)))
for p in sorted(full.rglob("test_*.py"))
)
return sorted(set(result))
def strategy_op(modify_file: str, repo_root: str) -> List[str]:
"""
文件名包含 KernelNpu → 从文件名提取关键字 → 用 glob 查找匹配的测试文件。
例: BinaryCrossEntropyWithLogitsBackwardKernelNpu.cpp
→ Binary, Cross, Entropy, With, Logits, Backward
→ *binary*cross*entropy*with*logits*backward*
"""
filename = Path(modify_file).name
if "KernelNpu" not in filename:
return []
feature = filename.split("KernelNpu")[0]
words = re.findall(r"[A-Z][^A-Z]*", feature)
if not words:
return []
pattern = "*" + "*".join(w.lower() for w in words) + "*"
test_dir = Path(repo_root) / "test"
matches = []
for f in test_dir.rglob("test_*.py"):
rel = str(f.relative_to(repo_root))
if fnmatch.fnmatch(rel, pattern):
matches.append(normalize_test_path(rel))
return sorted(set(matches))
def is_source_file(path: str) -> bool:
"""判断是否需要重新编译 torch_npu。只有 C++/构建脚本/CMake 变更才需要。"""
return (
path.endswith((".cpp", ".h", ".cu"))
or "CMakeLists.txt" in path
or path.startswith("ci/build.sh")
or path in ("setup.py", "setup.cfg", "pyproject.toml")
)
def main():
changed_files = [line.strip() for line in sys.stdin if line.strip()]
repo_root = os.getcwd()
all_test_files = set()
need_rebuild = False
strategies = [
("test_file", strategy_test_file),
("core", strategy_core),
("directory_mapping", strategy_directory_mapping),
("op", strategy_op),
]
for modify_file in changed_files:
for strategy_name, func in strategies:
try:
matched = func(modify_file, repo_root)
for f in matched:
all_test_files.add(f)
print(f" [{strategy_name}] {modify_file} → {f}", file=sys.stderr)
except Exception:
pass
if is_source_file(modify_file):
need_rebuild = True
has_test_changes = len(all_test_files) > 0
test_files_str = ",".join(sorted(all_test_files))
print(f"has_test_changes={'true' if has_test_changes else 'false'}")
print(f"test_files={test_files_str}")
print(f"need_rebuild={'true' if need_rebuild else 'false'}")
print(f"strategy_count={len(all_test_files)}")
if __name__ == "__main__":
main()