test: add go native production smoke

This commit is contained in:
lingniu
2026-07-02 01:34:28 +08:00
parent 4ea8d18972
commit 1477b997c9
3 changed files with 308 additions and 1 deletions

217
tools/go_native_prod_smoke.py Executable file
View File

@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""HTTP smoke checks for the Go native production vehicle ingest stack."""
from __future__ import annotations
import argparse
import datetime as dt
import json
import sys
import urllib.parse
import urllib.request
from dataclasses import asdict, dataclass
from typing import Any
SHANGHAI = dt.timezone(dt.timedelta(hours=8))
DEFAULT_BASE_URL = "http://115.29.187.205:20210"
@dataclass(frozen=True)
class CheckSpec:
name: str
path: str
params: dict[str, Any]
minimum: int
@dataclass(frozen=True)
class Check:
name: str
status: str
count: int
minimum: int
message: str
def shanghai_day_window(now: dt.datetime | None = None) -> tuple[str, str]:
now = now or dt.datetime.now(tz=SHANGHAI)
local = now.astimezone(SHANGHAI)
start = dt.datetime(local.year, local.month, local.day, tzinfo=SHANGHAI)
end = start + dt.timedelta(days=1)
return start.isoformat(timespec="seconds"), end.isoformat(timespec="seconds")
def api_url(base_url: str, path: str, params: dict[str, Any]) -> str:
return base_url.rstrip("/") + path + "?" + urllib.parse.urlencode(params)
def query_json(base_url: str, path: str, params: dict[str, Any], timeout: float) -> dict[str, Any]:
request = urllib.request.Request(
api_url(base_url, path, params),
headers={"Accept": "application/json"},
)
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
def check_total(name: str, payload: dict[str, Any], minimum: int) -> Check:
count = int(payload.get("total") or 0)
sample = sample_summary(payload.get("items") or [])
if count >= minimum:
return Check(name, "pass", count, minimum, f"count={count}; sample={sample}")
return Check(name, "fail", count, minimum, f"count={count}; expected >= {minimum}; sample={sample}")
def sample_summary(items: list[Any]) -> str:
if not items:
return "{}"
first = items[0]
if not isinstance(first, dict):
return json.dumps(first, ensure_ascii=False, sort_keys=True)
keep = {
key: first.get(key)
for key in (
"ts",
"stat_date",
"protocol",
"vehicle_key",
"vin",
"phone",
"message_id_hex",
"metric_key",
"metric_value",
"total_mileage_km",
"latest_total_mileage_km",
)
if first.get(key) not in (None, "")
}
return json.dumps(keep, ensure_ascii=False, sort_keys=True)
def overall_status(checks: list[Check]) -> str:
return "fail" if any(check.status == "fail" for check in checks) else "pass"
def build_check_specs(
*,
date_from: str,
date_to: str,
stat_date: str,
min_raw: int,
min_history: int,
min_stat: int,
) -> list[CheckSpec]:
raw_params = {"dateFrom": date_from, "dateTo": date_to, "limit": 1}
history_params = {"protocol": "JT808", "dateFrom": date_from, "dateTo": date_to, "limit": 1}
return [
CheckSpec("gb32960.raw", "/api/history/raw-frames", {**raw_params, "protocol": "GB32960"}, min_raw),
CheckSpec("jt808.raw", "/api/history/raw-frames", {**raw_params, "protocol": "JT808"}, min_raw),
CheckSpec("yutong_mqtt.raw", "/api/history/raw-frames", {**raw_params, "protocol": "YUTONG_MQTT"}, min_raw),
CheckSpec("jt808.locations", "/api/history/locations", history_params, min_history),
CheckSpec("jt808.mileage_points", "/api/history/mileage-points", history_params, min_history),
CheckSpec(
"gb32960.daily_mileage",
"/api/stats/daily-metrics",
{
"protocol": "GB32960",
"metricKey": "daily_mileage_km",
"dateFrom": stat_date,
"dateTo": stat_date,
"limit": 1,
},
min_stat,
),
CheckSpec(
"gb32960.daily_total_mileage",
"/api/stats/daily-metrics",
{
"protocol": "GB32960",
"metricKey": "daily_total_mileage_km",
"dateFrom": stat_date,
"dateTo": stat_date,
"limit": 1,
},
min_stat,
),
CheckSpec(
"jt808.daily_mileage",
"/api/stats/daily-metrics",
{
"protocol": "JT808",
"metricKey": "daily_mileage_km",
"dateFrom": stat_date,
"dateTo": stat_date,
"limit": 1,
},
min_stat,
),
CheckSpec(
"jt808.daily_total_mileage",
"/api/stats/daily-metrics",
{
"protocol": "JT808",
"metricKey": "daily_total_mileage_km",
"dateFrom": stat_date,
"dateTo": stat_date,
"limit": 1,
},
min_stat,
),
]
def run_checks(base_url: str, specs: list[CheckSpec], timeout: float) -> list[Check]:
checks: list[Check] = []
for spec in specs:
try:
payload = query_json(base_url, spec.path, spec.params, timeout)
checks.append(check_total(spec.name, payload, spec.minimum))
except Exception as exc: # pragma: no cover - exercised by live failures.
checks.append(Check(spec.name, "fail", 0, spec.minimum, f"request failed: {exc}"))
return checks
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-url", default=DEFAULT_BASE_URL)
parser.add_argument("--date", help="Shanghai local date, YYYY-MM-DD. Defaults to today.")
parser.add_argument("--timeout", type=float, default=5.0)
parser.add_argument("--min-raw", type=int, default=1)
parser.add_argument("--min-history", type=int, default=1)
parser.add_argument("--min-stat", type=int, default=1)
return parser.parse_args(argv)
def main(argv: list[str]) -> int:
args = parse_args(argv)
if args.date:
day = dt.date.fromisoformat(args.date)
start = dt.datetime(day.year, day.month, day.day, tzinfo=SHANGHAI)
date_from, date_to = shanghai_day_window(start)
stat_date = args.date
else:
date_from, date_to = shanghai_day_window()
stat_date = date_from[:10]
specs = build_check_specs(
date_from=date_from,
date_to=date_to,
stat_date=stat_date,
min_raw=args.min_raw,
min_history=args.min_history,
min_stat=args.min_stat,
)
checks = run_checks(args.base_url, specs, args.timeout)
status = overall_status(checks)
print(json.dumps({
"status": status,
"baseUrl": args.base_url,
"dateFrom": date_from,
"dateTo": date_to,
"checks": [asdict(check) for check in checks],
}, ensure_ascii=False, indent=2))
return 0 if status == "pass" else 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))

View File

@@ -0,0 +1,83 @@
import datetime as dt
import unittest
from tools import go_native_prod_smoke as smoke
class GoNativeProdSmokeTest(unittest.TestCase):
def test_shanghai_day_window_uses_plus_eight_bounds(self):
now = dt.datetime(2026, 7, 2, 1, 35, tzinfo=smoke.SHANGHAI)
date_from, date_to = smoke.shanghai_day_window(now)
self.assertEqual(date_from, "2026-07-02T00:00:00+08:00")
self.assertEqual(date_to, "2026-07-03T00:00:00+08:00")
def test_api_url_encodes_plus_eight_date_range(self):
url = smoke.api_url(
"http://example.test/base/",
"/api/history/raw-frames",
{
"protocol": "JT808",
"dateFrom": "2026-07-02T00:00:00+08:00",
"dateTo": "2026-07-03T00:00:00+08:00",
"limit": 1,
},
)
self.assertEqual(
url,
"http://example.test/base/api/history/raw-frames?"
"protocol=JT808&dateFrom=2026-07-02T00%3A00%3A00%2B08%3A00"
"&dateTo=2026-07-03T00%3A00%3A00%2B08%3A00&limit=1",
)
def test_total_check_passes_when_total_reaches_minimum(self):
check = smoke.check_total(
"jt808.raw",
{"total": 2, "items": [{"vehicle_key": "JT808:013307811170"}]},
minimum=1,
)
self.assertEqual(check.status, "pass")
self.assertEqual(check.count, 2)
self.assertIn("JT808:013307811170", check.message)
def test_total_check_fails_when_total_is_below_minimum(self):
check = smoke.check_total("gb32960.raw", {"total": 0, "items": []}, minimum=1)
self.assertEqual(check.status, "fail")
self.assertEqual(check.count, 0)
self.assertIn("expected >= 1", check.message)
def test_overall_status_fails_if_any_check_fails(self):
checks = [
smoke.Check("a", "pass", 1, 1, "ok"),
smoke.Check("b", "fail", 0, 1, "bad"),
]
self.assertEqual(smoke.overall_status(checks), "fail")
def test_build_checks_cover_three_raw_protocols_and_two_daily_metric_protocols(self):
specs = smoke.build_check_specs(
date_from="2026-07-02T00:00:00+08:00",
date_to="2026-07-03T00:00:00+08:00",
stat_date="2026-07-02",
min_raw=1,
min_history=1,
min_stat=1,
)
names = [spec.name for spec in specs]
self.assertIn("gb32960.raw", names)
self.assertIn("jt808.raw", names)
self.assertIn("yutong_mqtt.raw", names)
self.assertIn("jt808.locations", names)
self.assertIn("jt808.mileage_points", names)
self.assertIn("gb32960.daily_mileage", names)
self.assertIn("jt808.daily_total_mileage", names)
if __name__ == "__main__":
unittest.main()