test_attr.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. # -*- coding: utf-8 -*-
  2. """CFC 测试失败逐例归因(纯只读)。
  3. 用途:把 surefire XML 的红例展开为逐例明细,并按「异常类型 + 是否含生产栈帧」预分桶,
  4. 避免跨轮归因边界漂移(B-26/B-28/B-32 教训)。
  5. 用法:
  6. python test_attr.py --reports <cfc-backend>/target/surefire-reports --out-json attr.json
  7. 输出:
  8. - cases[]:class / case / kind(F|E) / type / msg / prod_frames / env / bucket
  9. - buckets:{bucket: 例数}
  10. - classes:{class: {F, E, env, buckets}}
  11. 预分桶口径(写死,跨轮一致):
  12. env 环境/上下文加载失败(数据源连接类关键字)
  13. mockito Mockito 误用(InvalidUseOfMatchers / UnnecessaryStubbing / WantedButNotInvoked 等)
  14. npe_test NullPointer 且栈内无生产帧 → 测试装置(桩返回 null / 手工注入缺依赖)
  15. npe_prod NullPointer 且栈内含生产帧 → 需研发判定
  16. assert 断言失败(AssertionFailedError / ComparisonFailure)→ 需人工判「文案漂移 or 契约不一致」
  17. other 其余(异常类型非上述)
  18. """
  19. import argparse
  20. import glob
  21. import io
  22. import json
  23. import os
  24. import re
  25. import xml.etree.ElementTree as ET
  26. ENV_PAT = re.compile(
  27. r"Failed to load ApplicationContext|Communications link failure|Connection refused|"
  28. r"Could not create connection|Access denied for user|Unknown database|"
  29. r"CannotGetJdbcConnection|Unable to acquire JDBC Connection|"
  30. r"Failed to instantiate.*ApplicationContext",
  31. re.S,
  32. )
  33. MOCK_TYPES = {
  34. "MockitoException", "InvalidUseOfMatchersException", "WantedButNotInvoked",
  35. "ArgumentsAreDifferent", "UnnecessaryStubbingException", "MissingMethodInvocationException",
  36. "PotentialStubbingProblem", "MockitoAssertionError",
  37. }
  38. ASSERT_TYPES = {"AssertionFailedError", "ComparisonFailure", "AssertionError"}
  39. PROD_FRAME = re.compile(r"at (com\.etotem\.cfc\.[\w.]+)\.(\w+)\(")
  40. def read(p):
  41. with io.open(p, encoding="utf-8", errors="ignore") as f:
  42. return f.read()
  43. def simple(name):
  44. return (name or "").split(".")[-1]
  45. def classify(typ, body, msg, env):
  46. if env:
  47. return "env"
  48. if typ in MOCK_TYPES:
  49. return "mockito"
  50. if typ in ("NullPointerException",):
  51. prod = 0
  52. for line in body.split("\n"):
  53. line = line.strip()
  54. if not line.startswith("at "):
  55. continue
  56. m = PROD_FRAME.search(line)
  57. if m and not m.group(1).split(".")[-1].endswith(("Test", "Tests", "IT")):
  58. prod += 1
  59. return "npe_prod" if prod else "npe_test"
  60. if typ in ASSERT_TYPES:
  61. return "assert"
  62. return "other"
  63. def main():
  64. ap = argparse.ArgumentParser()
  65. ap.add_argument("--reports", required=True)
  66. ap.add_argument("--out-json", required=True)
  67. a = ap.parse_args()
  68. cases, classes = [], {}
  69. for p in sorted(glob.glob(os.path.join(a.reports, "TEST-*.xml"))):
  70. txt = read(p)
  71. try:
  72. root = ET.parse(p).getroot()
  73. except Exception:
  74. continue
  75. cls = simple(root.get("name"))
  76. for tc in root.iter("testcase"):
  77. nodes = list(tc.findall("failure")) + list(tc.findall("error"))
  78. if not nodes:
  79. continue
  80. n = nodes[0]
  81. body = n.text or ""
  82. typ = (n.get("type") or "").split(".")[-1]
  83. msg = (n.get("message") or "").strip() or (body.split("\n")[0] if body else "")
  84. frames = [l.strip() for l in body.split("\n") if l.strip().startswith("at ")]
  85. prod = 0
  86. for f in frames:
  87. m = PROD_FRAME.search(f)
  88. if m and not m.group(1).split(".")[-1].endswith(("Test", "Tests", "IT")):
  89. prod += 1
  90. first_frame = frames[0] if frames else ""
  91. # 环境红判定:类级 XML 命中环境关键字(整类上下文加载失败)
  92. env = bool(ENV_PAT.search(body))
  93. rec = {"class": cls, "case": tc.get("name"),
  94. "kind": "F" if n.tag == "failure" else "E", "type": typ,
  95. "msg": msg[:220], "prod_frames": prod, "first_frame": first_frame[:140],
  96. "env": env, "bucket": classify(typ, body, msg, env)}
  97. cases.append(rec)
  98. c = classes.setdefault(cls, {"F": 0, "E": 0, "env": False, "cases": 0})
  99. c["F" if rec["kind"] == "F" else "E"] += 1
  100. c["env"] = c["env"] or env
  101. c["cases"] += 1
  102. buckets = {}
  103. for c in cases:
  104. buckets[c["bucket"]] = buckets.get(c["bucket"], 0) + 1
  105. out = {"total_red": len(cases), "buckets": buckets, "classes": classes, "cases": cases}
  106. with io.open(a.out_json, "w", encoding="utf-8") as f:
  107. f.write(json.dumps(out, ensure_ascii=False, indent=1))
  108. print("red_cases=%d buckets=%s classes=%d" % (len(cases), buckets, len(classes)))
  109. if __name__ == "__main__":
  110. main()