compare_final_batch.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. # -*- coding: utf-8 -*-
  2. """第 8 批接口双端对比(商品评价 / 客户端设置 / 订单导出 / 统计 / initialize)。
  3. PHP 侧用本地站点(php -S 127.0.0.1:8899,与 Java 共用 192.168.1.177 的 shop 库),
  4. 因此两端的 base_url 只有端口不同;对比前把 host 归一化,避免误报。
  5. 用法:python compare_final_batch.py [用例名...]
  6. 前置:
  7. 1. F:/shop/shop 启动 PHP 8.2 本地站点(8899);
  8. 2. python tools/seed_comment_golden.py 造评价数据;
  9. 3. Java 服务运行在 18080。
  10. """
  11. import json
  12. import re
  13. import sys
  14. import requests
  15. PHP_BASE = "http://127.0.0.1:8899/index.php?s="
  16. JAVA_BASE = "http://127.0.0.1:18080"
  17. TOKEN = "testsession123"
  18. SESSION = requests.Session()
  19. SESSION.trust_env = False
  20. CASES = [
  21. ("comment_list", "/store/goods.comment/list", "/store/goods.comment/list"),
  22. ("comment_list_score10", "/store/goods.comment/list&score=10", "/store/goods.comment/list?score=10"),
  23. ("comment_list_score30", "/store/goods.comment/list&score=30", "/store/goods.comment/list?score=30"),
  24. ("comment_list_status1", "/store/goods.comment/list&status=1", "/store/goods.comment/list?status=1"),
  25. ("comment_list_status0", "/store/goods.comment/list&status=0", "/store/goods.comment/list?status=0"),
  26. ("comment_list_goods", "/store/goods.comment/list&goodsName=%E7%99%BE%E9%A6%99%E6%9E%9C",
  27. "/store/goods.comment/list?goodsName=%E7%99%BE%E9%A6%99%E6%9E%9C"),
  28. ("comment_list_goods_no", "/store/goods.comment/list&goodsName=AI", "/store/goods.comment/list?goodsName=AI"),
  29. ("comment_list_order_no", "/store/goods.comment/list&orderNo=369583", "/store/goods.comment/list?orderNo=369583"),
  30. ("comment_list_user", "/store/goods.comment/list&userId=10001", "/store/goods.comment/list?userId=10001"),
  31. ("comment_list_user_missing", "/store/goods.comment/list&userId=99999", "/store/goods.comment/list?userId=99999"),
  32. ("comment_list_page2", "/store/goods.comment/list&page=2", "/store/goods.comment/list?page=2"),
  33. ("comment_detail_10001", "/store/goods.comment/detail&commentId=10001", "/store/goods.comment/detail?commentId=10001"),
  34. ("comment_detail_10002", "/store/goods.comment/detail&commentId=10002", "/store/goods.comment/detail?commentId=10002"),
  35. ("comment_detail_10003", "/store/goods.comment/detail&commentId=10003", "/store/goods.comment/detail?commentId=10003"),
  36. ("comment_detail_missing", "/store/goods.comment/detail&commentId=99999",
  37. "/store/goods.comment/detail?commentId=99999"),
  38. ("h5_detail", "/store/client.h5.setting/detail&key=basic", "/store/client.h5.setting/detail?key=basic"),
  39. ("h5_detail_unknown", "/store/client.h5.setting/detail&key=unknown",
  40. "/store/client.h5.setting/detail?key=unknown"),
  41. ("wxapp_detail", "/store/client.wxapp.setting/detail&key=basic", "/store/client.wxapp.setting/detail?key=basic"),
  42. ("wxapp_detail_unknown", "/store/client.wxapp.setting/detail&key=unknown",
  43. "/store/client.wxapp.setting/detail?key=unknown"),
  44. ("export_list", "/store/order.export/list", "/store/order.export/list"),
  45. ("statistics_data", "/store/statistics.data/data", "/store/statistics.data/data"),
  46. ("statistics_survey", "/store/statistics.data/survey", "/store/statistics.data/survey"),
  47. ("statistics_survey_range",
  48. "/store/statistics.data/survey&startDate=2026-10-01&endDate=2026-10-17",
  49. "/store/statistics.data/survey?startDate=2026-10-01&endDate=2026-10-17"),
  50. ("statistics_survey_empty_range",
  51. "/store/statistics.data/survey&startDate=2020-01-01&endDate=2020-01-31",
  52. "/store/statistics.data/survey?startDate=2020-01-01&endDate=2020-01-31"),
  53. ]
  54. HOSTS = ("127.0.0.1:8899", "127.0.0.1:18080", "localhost:8899", "localhost:18080")
  55. def normalize(obj):
  56. """把两端不同的 host:port 归一化,避免 base_url/下载地址误报"""
  57. if isinstance(obj, dict):
  58. return {k: normalize(v) for k, v in obj.items()}
  59. if isinstance(obj, list):
  60. return [normalize(v) for v in obj]
  61. if isinstance(obj, str):
  62. out = obj
  63. for host in HOSTS:
  64. out = out.replace(host, "{HOST}")
  65. return out
  66. return obj
  67. def diff(a, b, path="$", out=None, limit=25):
  68. out = out if out is not None else []
  69. if len(out) >= limit:
  70. return out
  71. if type(a) != type(b):
  72. if isinstance(a, (int, float)) and isinstance(b, (int, float)) \
  73. and not isinstance(a, bool) and not isinstance(b, bool):
  74. if float(a) != float(b):
  75. out.append(f"{path}: php={a!r} java={b!r}")
  76. return out
  77. out.append(f"{path}: 类型不同 php={type(a).__name__} java={type(b).__name__} (php={a!r} java={b!r})")
  78. return out
  79. if isinstance(a, dict):
  80. for k in a:
  81. if k not in b:
  82. out.append(f"{path}.{k}: Java 缺失 (php={a[k]!r})")
  83. else:
  84. diff(a[k], b[k], f"{path}.{k}", out, limit)
  85. for k in b:
  86. if k not in a:
  87. out.append(f"{path}.{k}: Java 多出 (java={b[k]!r})")
  88. return out
  89. if isinstance(a, list):
  90. if len(a) != len(b):
  91. out.append(f"{path}: 长度不同 php={len(a)} java={len(b)}")
  92. for i in range(min(len(a), len(b))):
  93. diff(a[i], b[i], f"{path}[{i}]", out, limit)
  94. return out
  95. if a != b:
  96. out.append(f"{path}: php={a!r} java={b!r}")
  97. return out
  98. def raw(path, java=False):
  99. base = JAVA_BASE if java else PHP_BASE
  100. url = base + path
  101. if java:
  102. url = JAVA_BASE + path
  103. return SESSION.get(url, headers={"Access-Token": TOKEN}, timeout=120)
  104. def main():
  105. only = set(sys.argv[1:])
  106. ok = fail = 0
  107. for name, php_path, java_path in CASES:
  108. if only and name not in only:
  109. continue
  110. try:
  111. php = SESSION.get(PHP_BASE + php_path, headers={"Access-Token": TOKEN}, timeout=120).json()
  112. except Exception as e:
  113. print(f"[skip] {name} 拉取 PHP 失败: {e}")
  114. continue
  115. try:
  116. java = SESSION.get(JAVA_BASE + java_path, headers={"Access-Token": TOKEN}, timeout=120).json()
  117. except Exception as e:
  118. print(f"[FAIL] {name} 拉取 Java 失败: {e}")
  119. fail += 1
  120. continue
  121. if php.get("status") != java.get("status"):
  122. print(f"[DIFF] {name} status php={php.get('status')} java={java.get('status')} "
  123. f"msg php={php.get('message')!r} java={java.get('message')!r}")
  124. fail += 1
  125. continue
  126. diffs = diff(normalize(php.get("data")), normalize(java.get("data")))
  127. if php.get("message") != java.get("message"):
  128. diffs.append(f"$.message: php={php.get('message')!r} java={java.get('message')!r}")
  129. if diffs:
  130. fail += 1
  131. print(f"[DIFF] {name} ({len(diffs)}+ 处)")
  132. for d in diffs:
  133. print(" ", d)
  134. else:
  135. ok += 1
  136. print(f"[PASS] {name}")
  137. # initialize 接口:PHP 与 Java 都应是 200 + 空响应体
  138. for path in ("/store/controller/initialize", "/store/statistics.data/initialize"):
  139. name = "initialize:" + path
  140. if only and name not in only:
  141. continue
  142. p = SESSION.get(PHP_BASE + path, headers={"Access-Token": TOKEN}, timeout=60)
  143. j = SESSION.get(JAVA_BASE + path, headers={"Access-Token": TOKEN}, timeout=60)
  144. if p.status_code == j.status_code == 200 and not p.text.strip() and not j.text.strip():
  145. ok += 1
  146. print(f"[PASS] {name}")
  147. else:
  148. fail += 1
  149. print(f"[DIFF] {name} php=({p.status_code},{p.text[:80]!r}) java=({j.status_code},{j.text[:80]!r})")
  150. print(f"\n通过 {ok} / 差异 {fail}")
  151. if __name__ == "__main__":
  152. main()