test: include realtime in production smoke

This commit is contained in:
lingniu
2026-07-02 01:36:14 +08:00
parent 1477b997c9
commit 2214abe9c5
3 changed files with 141 additions and 4 deletions

View File

@@ -142,7 +142,7 @@ curl -sS 'http://115.29.187.205:20210/api/stats/daily-metrics?dateFrom=2026-07-0
python3 tools/go_native_prod_smoke.py --date 2026-07-02 --timeout 8 python3 tools/go_native_prod_smoke.py --date 2026-07-02 --timeout 8
``` ```
该脚本通过 `20210` HTTP API 验证 GB32960、JT808、宇通 MQTT 的 RAW 查询JT808 的位置/里程点查询,以及 GB32960/JT808 的 `daily_mileage_km``daily_total_mileage_km` 统计。任一检查不达标时退出码为非 0。 该脚本通过 `20210` HTTP API 验证 GB32960、JT808、宇通 MQTT 的 RAW 查询JT808 的位置/里程点查询GB32960/JT808 的 `daily_mileage_km``daily_total_mileage_km` 统计,以及三类协议的 Redis realtime snapshot、online、protocol 查询。任一检查不达标时退出码为非 0。
## 部署命令 ## 部署命令
@@ -188,3 +188,4 @@ journalctl -u lingniu-go-realtime-api --since '5 minutes ago' --no-pager
- `realtime-api` 正在监听 `20210` - `realtime-api` 正在监听 `20210`
- GB32960、JT808、YUTONG_MQTT 的东八区 RAW 查询均可命中生产数据。 - GB32960、JT808、YUTONG_MQTT 的东八区 RAW 查询均可命中生产数据。
- `vehicle_daily_metric` 可查到 `daily_mileage_km``daily_total_mileage_km` - `vehicle_daily_metric` 可查到 `daily_mileage_km``daily_total_mileage_km`
- Redis realtime 可查到 GB32960、JT808、YUTONG_MQTT 的在线状态和协议快照。

View File

@@ -23,6 +23,7 @@ class CheckSpec:
path: str path: str
params: dict[str, Any] params: dict[str, Any]
minimum: int minimum: int
kind: str = "total"
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -43,7 +44,10 @@ def shanghai_day_window(now: dt.datetime | None = None) -> tuple[str, str]:
def api_url(base_url: str, path: str, params: dict[str, Any]) -> str: def api_url(base_url: str, path: str, params: dict[str, Any]) -> str:
return base_url.rstrip("/") + path + "?" + urllib.parse.urlencode(params) url = base_url.rstrip("/") + path
if not params:
return url
return url + "?" + urllib.parse.urlencode(params)
def query_json(base_url: str, path: str, params: dict[str, Any], timeout: float) -> dict[str, Any]: def query_json(base_url: str, path: str, params: dict[str, Any], timeout: float) -> dict[str, Any]:
@@ -63,6 +67,31 @@ def check_total(name: str, payload: dict[str, Any], minimum: int) -> Check:
return Check(name, "fail", count, minimum, f"count={count}; expected >= {minimum}; sample={sample}") return Check(name, "fail", count, minimum, f"count={count}; expected >= {minimum}; sample={sample}")
def check_realtime(name: str, payload: dict[str, Any]) -> Check:
if payload.get("online") is False:
return Check(name, "fail", 0, 1, "online=false")
identifier = payload.get("vin") or payload.get("vehicle_key") or ""
protocols = payload.get("protocols") or []
protocol = payload.get("protocol") or ""
fields = payload.get("fields") or {}
message = json.dumps(
{
key: value
for key, value in {
"identifier": identifier,
"protocol": protocol,
"protocols": protocols,
"field_count": len(fields) if isinstance(fields, dict) else 0,
"online": payload.get("online"),
}.items()
if value not in (None, "", [])
},
ensure_ascii=False,
sort_keys=True,
)
return Check(name, "pass", 1, 1, message)
def sample_summary(items: list[Any]) -> str: def sample_summary(items: list[Any]) -> str:
if not items: if not items:
return "{}" return "{}"
@@ -161,17 +190,65 @@ def build_check_specs(
] ]
def vehicle_identifier(item: dict[str, Any]) -> str:
for key in ("vin", "vehicle_key"):
value = str(item.get(key) or "").strip()
if value and value.lower() != "unknown":
return value
return ""
def build_realtime_specs(payloads: dict[str, dict[str, Any]]) -> list[CheckSpec]:
configs = [
("gb32960", "gb32960.raw", "GB32960"),
("jt808", "jt808.raw", "JT808"),
("yutong_mqtt", "yutong_mqtt.raw", "YUTONG_MQTT"),
]
specs: list[CheckSpec] = []
for prefix, source_name, protocol in configs:
items = payloads.get(source_name, {}).get("items") or []
if not items or not isinstance(items[0], dict):
continue
identifier = vehicle_identifier(items[0])
if not identifier:
continue
encoded = urllib.parse.quote(identifier, safe="")
base_path = "/api/realtime/vehicles/" + encoded
specs.extend([
CheckSpec(prefix + ".realtime", base_path, {}, 1, "realtime"),
CheckSpec(prefix + ".realtime_online", base_path + "/online", {}, 1, "realtime"),
CheckSpec(prefix + ".realtime_protocol", base_path + "/protocols/" + protocol, {}, 1, "realtime"),
])
return specs
def run_checks(base_url: str, specs: list[CheckSpec], timeout: float) -> list[Check]: def run_checks(base_url: str, specs: list[CheckSpec], timeout: float) -> list[Check]:
checks: list[Check] = [] checks: list[Check] = []
for spec in specs: for spec in specs:
try: try:
payload = query_json(base_url, spec.path, spec.params, timeout) payload = query_json(base_url, spec.path, spec.params, timeout)
if spec.kind == "realtime":
checks.append(check_realtime(spec.name, payload))
else:
checks.append(check_total(spec.name, payload, spec.minimum)) checks.append(check_total(spec.name, payload, spec.minimum))
except Exception as exc: # pragma: no cover - exercised by live failures. except Exception as exc: # pragma: no cover - exercised by live failures.
checks.append(Check(spec.name, "fail", 0, spec.minimum, f"request failed: {exc}")) checks.append(Check(spec.name, "fail", 0, spec.minimum, f"request failed: {exc}"))
return checks return checks
def query_payloads(base_url: str, specs: list[CheckSpec], timeout: float) -> tuple[list[Check], dict[str, dict[str, Any]]]:
checks: list[Check] = []
payloads: dict[str, dict[str, Any]] = {}
for spec in specs:
try:
payload = query_json(base_url, spec.path, spec.params, timeout)
payloads[spec.name] = payload
checks.append(check_total(spec.name, payload, spec.minimum))
except Exception as exc:
checks.append(Check(spec.name, "fail", 0, spec.minimum, f"request failed: {exc}"))
return checks, payloads
def parse_args(argv: list[str]) -> argparse.Namespace: def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-url", default=DEFAULT_BASE_URL) parser.add_argument("--base-url", default=DEFAULT_BASE_URL)
@@ -201,7 +278,8 @@ def main(argv: list[str]) -> int:
min_history=args.min_history, min_history=args.min_history,
min_stat=args.min_stat, min_stat=args.min_stat,
) )
checks = run_checks(args.base_url, specs, args.timeout) checks, payloads = query_payloads(args.base_url, specs, args.timeout)
checks.extend(run_checks(args.base_url, build_realtime_specs(payloads), args.timeout))
status = overall_status(checks) status = overall_status(checks)
print(json.dumps({ print(json.dumps({
"status": status, "status": status,

View File

@@ -78,6 +78,64 @@ class GoNativeProdSmokeTest(unittest.TestCase):
self.assertIn("gb32960.daily_mileage", names) self.assertIn("gb32960.daily_mileage", names)
self.assertIn("jt808.daily_total_mileage", names) self.assertIn("jt808.daily_total_mileage", names)
def test_vehicle_identifier_prefers_vin_over_vehicle_key(self):
identifier = smoke.vehicle_identifier({
"vin": "LB9A32A20R0LS1343",
"vehicle_key": "GB32960:ignored",
})
self.assertEqual(identifier, "LB9A32A20R0LS1343")
def test_vehicle_identifier_falls_back_to_vehicle_key(self):
identifier = smoke.vehicle_identifier({
"vin": "",
"vehicle_key": "JT808:013307811170",
})
self.assertEqual(identifier, "JT808:013307811170")
def test_realtime_specs_are_built_from_raw_samples_with_vin(self):
payloads = {
"gb32960.raw": {"items": [{"vin": "LB9A32A20R0LS1343", "vehicle_key": "LB9A32A20R0LS1343"}]},
"jt808.raw": {"items": [{"vin": "", "vehicle_key": "JT808:013307811170"}]},
"yutong_mqtt.raw": {"items": [{"vin": "LMRKH9AC6R1004108"}]},
}
specs = smoke.build_realtime_specs(payloads)
self.assertEqual(
[(spec.name, spec.path) for spec in specs],
[
("gb32960.realtime", "/api/realtime/vehicles/LB9A32A20R0LS1343"),
("gb32960.realtime_online", "/api/realtime/vehicles/LB9A32A20R0LS1343/online"),
("gb32960.realtime_protocol", "/api/realtime/vehicles/LB9A32A20R0LS1343/protocols/GB32960"),
("jt808.realtime", "/api/realtime/vehicles/JT808%3A013307811170"),
("jt808.realtime_online", "/api/realtime/vehicles/JT808%3A013307811170/online"),
("jt808.realtime_protocol", "/api/realtime/vehicles/JT808%3A013307811170/protocols/JT808"),
("yutong_mqtt.realtime", "/api/realtime/vehicles/LMRKH9AC6R1004108"),
("yutong_mqtt.realtime_online", "/api/realtime/vehicles/LMRKH9AC6R1004108/online"),
("yutong_mqtt.realtime_protocol", "/api/realtime/vehicles/LMRKH9AC6R1004108/protocols/YUTONG_MQTT"),
],
)
def test_realtime_check_requires_online_true_when_field_exists(self):
check = smoke.check_realtime(
"gb32960.realtime_online",
{"vin": "LB9A32A20R0LS1343", "online": True, "protocols": ["GB32960"]},
)
self.assertEqual(check.status, "pass")
self.assertEqual(check.count, 1)
def test_realtime_check_fails_when_online_false(self):
check = smoke.check_realtime(
"gb32960.realtime_online",
{"vin": "LB9A32A20R0LS1343", "online": False},
)
self.assertEqual(check.status, "fail")
self.assertIn("online=false", check.message)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()