TDengine/.github/scripts/generate_case_md.py
2025-05-26 18:09:36 +08:00

153 lines
No EOL
6.1 KiB
Python

import os
import re
import ast
import sys
import yaml
def generate_md_files_from_nav(mkdocs_file, doc_dir):
"""
根据 mkdocs.yml 中的 nav 配置生成 case_list_docs 目录下的 .md 文件
:param mkdocs_file: mkdocs.yml 文件路径
:param doc_dir: case_list_docs 目录路径
"""
os.makedirs(doc_dir, exist_ok=True)
with open(mkdocs_file, "r", encoding="utf-8") as f:
content = re.sub(r'!ENV\s*\[[^\]]+\]', 'main', f.read())
mkdocs_config = yaml.safe_load(content)
nav = mkdocs_config.get("nav", [])
case_list_docs = None
for item in nav:
if isinstance(item, dict) and "Case list docs" in item:
case_list_docs = item["Case list docs"]
break
if not case_list_docs:
print("Error: 'Case list docs' not found in nav configuration.")
return
def process_nav(nav_item, base_path=""):
if isinstance(nav_item, dict):
for key, value in nav_item.items():
if isinstance(value, str):
md_file_path = os.path.join(doc_dir, value.replace("case_list_docs/", ""))
os.makedirs(os.path.dirname(md_file_path), exist_ok=True)
if not os.path.exists(md_file_path):
with open(md_file_path, "w", encoding="utf-8") as f:
f.write(f"# {key}\n")
print(f"Created: {md_file_path}")
elif isinstance(value, list):
process_nav(value, base_path=os.path.join(base_path, key))
elif isinstance(nav_item, list):
for sub_item in nav_item:
process_nav(sub_item, base_path)
process_nav(case_list_docs)
def process_markdown_files(doc_dir, content, catalog):
catalog_set = set(catalog.split(','))
for item in catalog_set:
item = item.replace(":", "/")
md_file = os.path.join(doc_dir, f"{item}.md")
if not os.path.exists(md_file):
print(f"Warning: File {md_file} does not exist")
continue
with open(md_file, "a", encoding="utf-8") as f:
f.write(f"{content}\n")
print(f"Added content to {md_file}")
def get_md_file(base_dir, search_term):
normalized_search_term = search_term.replace(":", "/")
matched_file = os.path.join(base_dir, f"{normalized_search_term}.md")
return matched_file
def extract_functions_with_docstring(file_path):
"""提取 Python 文件中的以 test_ 开头的函数、其 docstring、所属类名和文件名"""
with open(file_path, "r", encoding="utf-8") as f:
tree = ast.parse(f.read())
add_parent_references(tree) # 确保为 AST 节点添加父节点引用
functions = []
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.name.startswith("test_") and ast.get_docstring(node):
class_name = None
parent = node
while parent:
parent = getattr(parent, 'parent', None)
if isinstance(parent, ast.ClassDef):
class_name = parent.name
break
functions.append((node.name, ast.get_docstring(node), class_name, file_path))
return functions
# Add parent references to AST nodes
def add_parent_references(tree):
for node in ast.walk(tree):
for child in ast.iter_child_nodes(node):
child.parent = node
def parse_catalog(module_name, docstring):
catalog_pattern = re.compile(
r"Catalog:\s*((?:- .+?\n?)+)(?:\n\s*\n|(?=\n(?:Since:|Labels:|Jira:|History:)))",
re.DOTALL
)
match = catalog_pattern.search(docstring)
catalogs = []
if match:
# 提取 Catalog 块并按行分割
catalog_block = match.group(1)
catalogs = [line.strip("- ").strip() for line in catalog_block.splitlines() if line.strip()]
if module_name not in catalogs:
catalogs.append(module_name)
return catalogs if catalogs else ["Uncategorized"]
def process_file(doc_dir, file_path):
with open(file_path, "r", encoding="utf-8") as f:
tree = ast.parse(f.read())
add_parent_references(tree)
functions = extract_functions_with_docstring(file_path)
for function_name, docstring, class_name, file_name in functions:
# 提取 file_name 中 cases 后的字段,去掉 .py 后缀
relative_path = file_name.split("test/cases/")[1].replace(".py", "").replace("/", ".")
content = f"::: {relative_path}.{class_name}.{function_name}" if class_name else f"{relative_path}.{function_name}"
# 解析 Catalog
module_path = file_path.replace("/", ":")
if module_path.startswith("test:cases:"):
module_path = module_path[len("test:cases:"):]
module_path = ":".join(module_path.split(":")[:-1])
# 去掉每一级目录中的前两位数字和 '-'
module_path = ":".join([re.sub(r"^\d{2}-", "", part) for part in module_path.split(":")])
catalog = parse_catalog(module_path, docstring)
print(f"function: {function_name} in class: {class_name} in file: {file_name} with catalog: {catalog}, relative path: {relative_path}")
process_markdown_files(doc_dir, content, ",".join(catalog))
if __name__ == "__main__":
if len(sys.argv) != 4:
print("Usage: python generate_function_docs.py <mkdocs yaml file> <doc_dir> <case_list>")
sys.exit(1)
mkdocs_file = sys.argv[1]
doc_dir = sys.argv[2]
case_list = sys.argv[3]
generate_md_files_from_nav(mkdocs_file, doc_dir)
if case_list:
files = [file.strip() for file in case_list.split(",") if file.strip()]
else:
source_dir = "test/cases"
files = []
for root, dirs, file_names in os.walk(source_dir):
dirs[:] = sorted([d for d in dirs if re.match(r"^\d{2}-", d)])
file_names = sorted(file_names)
files.extend(os.path.join(root, file) for file in file_names if file.endswith(".py"))
for file_path in files:
print(f"Processing file: {file_path}")
process_file(doc_dir, file_path)