"""比较 libmcpp 实现、API 文档和 mdb_interface 定义并生成报告。
功能:从 libmcpp 生产源码自动发现资源协作接口,检查接口、属性、方法、方法签名
以及可静态识别的属性读写权限;同时检查 API 文档是否由当前实现和 MDB 元数据生成。
输入:mdb_interface 仓库路径和待检查的 API 文档。
输出:docs/api/mdb_interface_comparison_report.md。
退出码:普通模式在文档漂移、接口差异、未校验项或扫描警告存在时返回 1;
--check 模式只检查已提交报告是否需要重新生成,报告过期、无法读取或扫描存在警告时返回 1。
执行:在 libmcpp 根目录运行 ``python scripts/compare_mdb_api_docs.py
--mdb-root ../mdb_interface``。
"""
from __future__ import annotations
import argparse
import difflib
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from generate_mdb_api_docs import DEFAULT_OUTPUT, partition_interfaces, render_document
from mdb_api_common import (
Interface,
load_implementation_interfaces,
load_mdb_interfaces,
markdown_text,
)
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_REPORT = ROOT / "docs" / "api" / "mdb_interface_comparison_report.md"
@dataclass
class Difference:
"""单个实现差异。"""
interface: str
subject: str
expected: str
actual: str
@dataclass
class Unchecked:
"""无法由源码静态确认的一致性检查项。"""
interface: str
subject: str
reason: str
@dataclass
class MemberMatch:
"""实现成员与 MDB 成员的一组名称匹配。"""
source_name: str
expected_name: str
@property
def name_mismatch(self) -> bool:
return self.source_name != self.expected_name
def _git_revision(repository: Path) -> str:
try:
result = subprocess.run(
["git", "-C", str(repository), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
)
except (OSError, subprocess.CalledProcessError):
return "unknown"
return result.stdout.strip()
def _display_types(parameters: list) -> str:
values = [parameter.type_name for parameter in parameters]
return ", ".join(values) if values else "NA"
def _match_members(
source_mapping: dict, expected_mapping: dict
) -> tuple[list[MemberMatch], list[str], list[str]]:
"""统一匹配属性和方法;优先精确匹配,再做无歧义的 casefold 匹配。"""
source_names = set(source_mapping)
expected_names = set(expected_mapping)
matches = [MemberMatch(name, name) for name in sorted(source_names & expected_names)]
source_remaining = source_names - expected_names
expected_remaining = expected_names - source_names
matched_source: set[str] = set()
matched_expected: set[str] = set()
for expected_name in sorted(expected_remaining):
candidates = [
source_name
for source_name in source_remaining - matched_source
if source_name.casefold() == expected_name.casefold()
]
if len(candidates) != 1:
continue
source_name = candidates[0]
matches.append(MemberMatch(source_name, expected_name))
matched_source.add(source_name)
matched_expected.add(expected_name)
return (
matches,
sorted(source_remaining - matched_source),
sorted(expected_remaining - matched_expected),
)
def _property_pairs(
source: Interface, expected: Interface, differences: list[Difference]
) -> list[MemberMatch]:
matches, source_only, expected_only = _match_members(
source.properties, expected.properties
)
for match in matches:
if match.name_mismatch:
differences.append(
Difference(
source.name,
"属性名称",
f"`{match.expected_name}`",
f"`{match.source_name}`",
)
)
for name in expected_only:
differences.append(
Difference(
source.name,
f"缺少属性 `{name}`",
expected.properties[name].type_name,
"不存在",
)
)
for name in source_only:
differences.append(
Difference(
source.name,
f"多余属性 `{name}`",
"不存在",
source.properties[name].type_name,
)
)
return matches
def _compare_properties(
source: Interface,
expected: Interface,
differences: list[Difference],
unchecked: list[Unchecked],
) -> None:
for match in _property_pairs(source, expected, differences):
actual_property = source.properties[match.source_name]
expected_property = expected.properties[match.expected_name]
actual_type = actual_property.type_name
expected_type = expected_property.type_name
if actual_type == "unknown" or expected_type == "unknown":
reason = (
"MDB 属性缺少可静态比较的 baseType"
if expected_type == "unknown"
else f"MDB 期望 `{expected_type}`,源码类型无法识别"
)
unchecked.append(
Unchecked(source.name, f"属性 `{match.expected_name}` 类型", reason)
)
elif actual_type != expected_type:
differences.append(
Difference(
source.name,
f"属性 `{match.expected_name}` 类型",
expected_type,
actual_type,
)
)
if actual_property.access == "NA":
unchecked.append(
Unchecked(
source.name,
f"属性 `{match.expected_name}` 读写",
f"MDB 期望 `{expected_property.access}`,源码包含无法静态解析的只读设置",
)
)
elif actual_property.access != expected_property.access:
differences.append(
Difference(
source.name,
f"属性 `{match.expected_name}` 读写",
expected_property.access,
actual_property.access,
)
)
def _method_pairs(
source: Interface, expected: Interface, differences: list[Difference]
) -> list[MemberMatch]:
matches, source_only, expected_only = _match_members(source.methods, expected.methods)
for match in matches:
if match.name_mismatch:
differences.append(
Difference(
source.name,
"方法名称",
f"`{match.expected_name}`",
f"`{match.source_name}`",
)
)
for name in expected_only:
differences.append(Difference(source.name, f"缺少方法 `{name}`", "存在", "不存在"))
for name in source_only:
differences.append(Difference(source.name, f"多余方法 `{name}`", "不存在", "存在"))
return matches
def _compare_parameter_types(
interface_name: str,
method_name: str,
direction: str,
actual_parameters: list,
expected_parameters: list,
differences: list[Difference],
unchecked: list[Unchecked],
) -> None:
"""比较方法参数类型;无法识别的类型单独列为未校验。"""
subject = f"方法 `{method_name}` {direction}类型及顺序"
if len(actual_parameters) != len(expected_parameters):
differences.append(
Difference(
interface_name,
subject,
_display_types(expected_parameters),
_display_types(actual_parameters),
)
)
return
has_known_mismatch = False
pending_unchecked = []
for index, (actual, expected) in enumerate(
zip(actual_parameters, expected_parameters), start=1
):
if expected.type_name == "unknown":
reference = f" `{expected.reference}`" if expected.reference else ""
pending_unchecked.append(
Unchecked(
interface_name,
f"方法 `{method_name}` {direction}第 {index} 个参数类型",
f"MDB 参考类型{reference}缺少 baseType,无法静态比较",
)
)
elif actual.type_name == "unknown":
pending_unchecked.append(
Unchecked(
interface_name,
f"方法 `{method_name}` {direction}第 {index} 个参数类型",
f"MDB 期望 `{expected.type_name}`,源码类型无法识别",
)
)
elif actual.type_name != expected.type_name:
has_known_mismatch = True
if has_known_mismatch:
differences.append(
Difference(
interface_name,
subject,
_display_types(expected_parameters),
_display_types(actual_parameters),
)
)
else:
unchecked.extend(pending_unchecked)
def _compare_methods(
source: Interface,
expected: Interface,
differences: list[Difference],
unchecked: list[Unchecked],
) -> None:
for match in _method_pairs(source, expected, differences):
source_method = source.methods[match.source_name]
expected_method = expected.methods[match.expected_name]
_compare_parameter_types(
source.name,
match.expected_name,
"入参",
source_method.inputs,
expected_method.inputs,
differences,
unchecked,
)
_compare_parameter_types(
source.name,
match.expected_name,
"返回值",
source_method.outputs,
expected_method.outputs,
differences,
unchecked,
)
def compare_implementation(
implementation: dict[str, Interface],
mdb: dict[str, Interface],
) -> tuple[list[Difference], list[Unchecked]]:
"""比较 libmcpp 已实现接口和 MDB 定义。"""
differences: list[Difference] = []
unchecked: list[Unchecked] = []
for name, source in implementation.items():
expected = mdb.get(name)
if expected is None:
differences.append(Difference(name, "接口定义", "MDB 中存在", "MDB 中不存在"))
continue
_compare_properties(source, expected, differences, unchecked)
_compare_methods(source, expected, differences, unchecked)
return differences, unchecked
def _document_diff(actual: str, expected: str, document_path: Path) -> list[str]:
return list(
difflib.unified_diff(
actual.splitlines(),
expected.splitlines(),
fromfile=str(document_path),
tofile="由当前 libmcpp 实现和 MDB 元数据重新生成的文档",
lineterm="",
)
)
def _read_document(document_path: Path) -> tuple[str | None, str]:
"""读取待比较文档,并返回内容或明确的失败原因。"""
if not document_path.is_file():
return None, "文档文件不存在"
try:
return document_path.read_text(encoding="utf-8"), ""
except (OSError, UnicodeDecodeError) as error:
return None, f"文档读取失败:{error}"
def _read_report(report_path: Path) -> tuple[str | None, str]:
"""读取已提交的比较报告,并返回内容或明确的失败原因。"""
if not report_path.is_file():
return None, "报告文件不存在"
try:
return report_path.read_text(encoding="utf-8"), ""
except (OSError, UnicodeDecodeError) as error:
return None, f"报告读取失败:{error}"
def _report_diff(actual: str, expected: str, report_path: Path) -> list[str]:
return list(
difflib.unified_diff(
actual.splitlines(),
expected.splitlines(),
fromfile=str(report_path),
tofile="由当前 libmcpp 实现、API 文档和 MDB 定义重新生成的报告",
lineterm="",
)
)
def _marked(value: str, mismatch: bool, unchecked: bool = False) -> str:
text = markdown_text(value)
if mismatch:
return f"{text} ❌"
if unchecked:
return f"{text} ⚠️"
return text
def _parameter_comparison(
actual_parameters: list, expected_parameters: list
) -> tuple[bool, bool]:
"""返回(确定不一致、无法静态确认)。"""
if len(actual_parameters) != len(expected_parameters):
return True, False
unknown = False
for actual, expected in zip(actual_parameters, expected_parameters):
if actual.type_name == "unknown" or expected.type_name == "unknown":
unknown = True
elif actual.type_name != expected.type_name:
return True, False
return False, unknown
def _member_name(match: MemberMatch) -> str:
if match.name_mismatch:
return f"{markdown_text(match.expected_name)} / {_marked(match.source_name, True)}"
return markdown_text(match.source_name)
def _render_property_comparison(
source: Interface, expected: Interface | None
) -> list[str]:
expected_properties = expected.properties if expected is not None else {}
matches, source_only, expected_only = _match_members(
source.properties, expected_properties
)
if not matches and not source_only and not expected_only:
return []
lines = [
"#### 属性",
"",
"| 属性名(定义 / 实现) | 定义类型 | 定义读写 | 实现类型 | 实现读写 |",
"| ---------------------- | -------- | -------- | -------- | -------- |",
]
for match in matches:
actual = source.properties[match.source_name]
definition = expected_properties[match.expected_name]
type_unchecked = actual.type_name == "unknown" or definition.type_name == "unknown"
type_mismatch = not type_unchecked and actual.type_name != definition.type_name
access_unchecked = actual.access == "NA"
access_mismatch = not access_unchecked and actual.access != definition.access
lines.append(
f"| {_member_name(match)} | {markdown_text(definition.type_name)} | "
f"{markdown_text(definition.access)} | "
f"{_marked(actual.type_name, type_mismatch, type_unchecked)} | "
f"{_marked(actual.access, access_mismatch, access_unchecked)} |"
)
for name in expected_only:
definition = expected_properties[name]
lines.append(
f"| {markdown_text(name)} / 不存在 ❌ | {markdown_text(definition.type_name)} | "
f"{markdown_text(definition.access)} | 不存在 ❌ | 不存在 ❌ |"
)
for name in source_only:
actual = source.properties[name]
lines.append(
f"| 不存在 / {markdown_text(name)} ❌ | NA | NA | "
f"{_marked(actual.type_name, True)} | {_marked(actual.access, True)} |"
)
return lines
def _render_method_comparison(
source: Interface, expected: Interface | None
) -> list[str]:
expected_methods = expected.methods if expected is not None else {}
matches, source_only, expected_only = _match_members(source.methods, expected_methods)
if not matches and not source_only and not expected_only:
return []
lines = [
"#### 方法",
"",
"| 方法名(定义 / 实现) | 定义入参 | 实现入参 | 定义返回值 | 实现返回值 |",
"| ---------------------- | -------- | -------- | ---------- | ---------- |",
]
for match in matches:
actual = source.methods[match.source_name]
definition = expected_methods[match.expected_name]
input_mismatch, input_unchecked = _parameter_comparison(
actual.inputs, definition.inputs
)
output_mismatch, output_unchecked = _parameter_comparison(
actual.outputs, definition.outputs
)
lines.append(
f"| {_member_name(match)} | {markdown_text(_display_types(definition.inputs))} | "
f"{_marked(_display_types(actual.inputs), input_mismatch, input_unchecked)} | "
f"{markdown_text(_display_types(definition.outputs))} | "
f"{_marked(_display_types(actual.outputs), output_mismatch, output_unchecked)} |"
)
for name in expected_only:
definition = expected_methods[name]
lines.append(
f"| {markdown_text(name)} / 不存在 ❌ | "
f"{markdown_text(_display_types(definition.inputs))} | 不存在 ❌ | "
f"{markdown_text(_display_types(definition.outputs))} | 不存在 ❌ |"
)
for name in source_only:
actual = source.methods[name]
lines.append(
f"| 不存在 / {markdown_text(name)} ❌ | NA | "
f"{_marked(_display_types(actual.inputs), True)} | NA | "
f"{_marked(_display_types(actual.outputs), True)} |"
)
return lines
def _render_report(
mdb_revision: str,
implementation: dict[str, Interface],
comparable_implementation: dict[str, Interface],
missing_mdb: list[str],
document_matches: bool,
document_diff: list[str],
differences: list[Difference],
unchecked: list[Unchecked],
scan_warnings: list[str],
mdb: dict[str, Interface],
) -> str:
document_status = "是" if document_matches else "否 ❌"
properties = [
prop
for interface in comparable_implementation.values()
for prop in interface.properties.values()
]
access_checked = sum(prop.access != "NA" for prop in properties)
difference_interfaces = {item.interface for item in differences if item.interface}
unchecked_interfaces = {item.interface for item in unchecked if item.interface}
issues_by_interface = difference_interfaces | unchecked_interfaces
lines = [
"# libmcpp API 与 mdb_interface 一致性比较报告",
"",
"<!-- 本文件由 scripts/compare_mdb_api_docs.py 生成,请勿手工修改。 -->",
"",
"## 比较基线",
"",
f"- mdb_interface 版本:`{mdb_revision}`",
"- libmcpp 实现发现:生产源码中通过字面量 `MC_INTERFACE` 声明的 "
"`bmc.kepler.*` 和 `bmc.dev.*` 接口",
"- API 文档主体:libmcpp 实际暴露的接口、属性、方法、类型和读写权限",
"- MDB 用途:判定标准接口并补充说明、取值范围和复杂类型定义",
"- 比较范围:以 libmcpp 生产源码实际实现的接口为边界;mdb_interface 是跨组件全局定义仓,"
"未被 libmcpp 引用的 MDB 接口不判定为实现缺失",
"- 方法签名规则:比较入参/返回值类型及顺序,不以参数名称作为一致性条件",
"",
"## 整体情况",
"",
"| libmcpp 实现接口 | 标准接口(MDB 已定义) | 与定义一致 |",
"| ----------------- | ---------------------- | ---------- |",
]
for name in sorted(implementation):
standard = name in mdb
if not standard or name in difference_interfaces:
consistency = "否 ❌"
elif name in unchecked_interfaces:
consistency = "未确认 ⚠️"
else:
consistency = "是"
lines.append(
f"| `{markdown_text(name)}` | {'是' if standard else '否'} | "
f"{consistency} |"
)
lines.extend(
[
"",
"### 汇总",
"",
"| 检查项 | 结果 |",
"| ------ | ---- |",
f"| 已发现的 libmcpp 接口数量 | {len(implementation)} |",
f"| 标准接口数量 | {len(comparable_implementation)} |",
f"| 非标准接口数量 | {len(missing_mdb)} |",
f"| 实现侧属性读写静态校验覆盖 | {access_checked}/{len(properties)} |",
f"| 扫描警告数量 | {len(scan_warnings)} |",
f"| 未校验项数量 | {len(unchecked)} |",
f"| API 文档是否最新 | {document_status} |",
f"| 接口差异数量 | {len(differences)} |",
]
)
if not document_matches:
lines.extend(
[
"",
"## API 文档漂移",
"",
"API 文档需要重新生成。以下为差异(最多展示前 200 行):",
"",
"```diff",
*document_diff[:200],
"```",
]
)
if scan_warnings:
lines.extend(["", "## 扫描警告", ""])
lines.extend(f"- {markdown_text(warning)}" for warning in scan_warnings)
if unchecked:
lines.extend(
[
"",
"## 未校验项",
"",
"以下项目无法由源码静态确认,不计为确定差异,但会导致检查返回非零退出码:",
"",
"| 接口 | 检查项 | 原因 |",
"| ---- | ------ | ---- |",
]
)
for item in unchecked:
lines.append(
f"| `{markdown_text(item.interface)}` | {markdown_text(item.subject)} | "
f"{markdown_text(item.reason)} |"
)
lines.extend(["", "## 接口差异", ""])
if not differences and not unchecked:
lines.append("未发现接口、属性、方法、方法签名或读写权限差异。")
else:
lines.append("实现侧不一致或不存在的内容以 ❌ 标记;无法静态确认的内容以 ⚠️ 标记。")
for name in sorted(issues_by_interface):
source = implementation.get(name)
if source is None:
continue
expected = mdb.get(name)
lines.extend(["", f"### {name}", ""])
if expected is None:
lines.append("该接口已由 libmcpp 实现,但 mdb_interface 中不存在标准定义。 ❌")
lines.extend(
[
"",
"该接口的实现成员不单独计为差异,具体能力请查看 API 文档。",
]
)
continue
property_section = _render_property_comparison(source, expected)
if property_section:
lines.extend(["", *property_section])
method_section = _render_method_comparison(source, expected)
if method_section:
lines.extend(["", *method_section])
lines.extend(
[
"",
"## 结论",
"",
"API 文档应始终反映 libmcpp 的实际接口能力;接口实现与 mdb_interface 的差异应在本报告中呈现。"
"扫描警告和未校验项必须清零,接口差异应评估影响后修改实现或标准定义。",
"",
]
)
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--mdb-root", required=True, type=Path, help="mdb_interface 仓库根目录")
parser.add_argument("--document", type=Path, default=DEFAULT_OUTPUT, help="待比较的 API Markdown 文档")
parser.add_argument("--report", type=Path, default=DEFAULT_REPORT, help="比较报告输出文件")
parser.add_argument("--check", action="store_true", help="只检查现有比较报告是否需要重新生成")
args = parser.parse_args()
mdb_root = args.mdb_root.resolve()
scan_warnings: list[str] = []
implementation = load_implementation_interfaces(ROOT, scan_warnings)
try:
mdb = load_mdb_interfaces(mdb_root, scan_warnings)
except FileNotFoundError as error:
print(f"无法加载 MDB 定义:{error}", file=sys.stderr)
print(
"请通过 --mdb-root 指定 mdb_interface 仓库根目录。",
file=sys.stderr,
)
return 1
mdb_revision = _git_revision(mdb_root)
comparable_implementation, missing_mdb = partition_interfaces(implementation, mdb)
expected_document = render_document(implementation, mdb)
document_path = args.document.resolve()
actual_document, document_error = _read_document(document_path)
if actual_document is None:
print(f"无法读取 API 文档 {document_path}:{document_error}", file=sys.stderr)
print(
"请先运行 generate_mdb_api_docs.py 生成文档,或通过 --document 指定现有文档。",
file=sys.stderr,
)
return 1
document_matches = actual_document == expected_document
document_diff = _document_diff(actual_document, expected_document, document_path)
differences, unchecked = compare_implementation(implementation, mdb)
report_content = _render_report(
mdb_revision,
implementation,
comparable_implementation,
missing_mdb,
document_matches,
document_diff,
differences,
unchecked,
scan_warnings,
mdb,
)
report_path = args.report.resolve()
if args.check:
actual_report, read_error = _read_report(report_path)
if actual_report is None:
print(f"比较报告需要重新生成: {report_path}({read_error})")
return 1
if actual_report != report_content:
print(f"比较报告需要重新生成: {report_path}")
print("\n".join(_report_diff(actual_report, report_content, report_path)[:200]))
return 1
if scan_warnings:
print(f"比较报告内容是最新的,但源码或 MDB 扫描存在 {len(scan_warnings)} 项警告")
return 1
print(f"比较报告已是最新: {report_path}")
return 0
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(report_content, encoding="utf-8", newline="\n")
print(f"已生成比较报告: {report_path}")
print(f"API 文档: {'已是最新' if document_matches else '需要重新生成'}")
if differences or unchecked or scan_warnings:
print(
"libmcpp 实现与 MDB: "
f"发现 {len(differences)} 项差异、{len(unchecked)} 项未校验、"
f"{len(scan_warnings)} 项扫描警告"
)
else:
print("libmcpp 实现与 MDB: 通过")
return 0 if document_matches and not differences and not unchecked and not scan_warnings else 1
if __name__ == "__main__":
raise SystemExit(main())