From c05b1d5f5cc6d69824f5e9911ff4a73243f6e1a9 Mon Sep 17 00:00:00 2001 From: HiepLM Date: Sat, 13 Jun 2026 14:04:56 +0700 Subject: [PATCH] =?UTF-8?q?Clean=20and=20Test=20l=C3=A0n=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/mission_queue.json | 163 +++++++++++++++++- data/models/ea89e39c835c0557.json | 2 +- data/state.json | 2 +- scripts/api-smoke.sh | 36 +++- scripts/docker-test.sh | 41 +++++ src/mission/mission_scheduler.cpp | 1 - src/mission/modbus_trigger_service.cpp | 1 - src/mission/modbus_trigger_service.hpp | 1 - src/server/api_mission_routes.cpp | 21 ++- src/server/api_server.hpp | 9 +- ...pi_integration.cpython-38-pytest-8.3.5.pyc | Bin 9563 -> 10203 bytes tests/test_api_integration.py | 44 +++-- 12 files changed, 281 insertions(+), 40 deletions(-) create mode 100755 scripts/docker-test.sh diff --git a/data/mission_queue.json b/data/mission_queue.json index 2385525..35dd188 100644 --- a/data/mission_queue.json +++ b/data/mission_queue.json @@ -1,11 +1,168 @@ { - "queue": [], + "queue": [ + { + "created_at": "2026-06-13T07:03:00Z", + "finished_at": "2026-06-13T07:03:01Z", + "id": "c636ad0d89937cd2", + "log": [ + { + "level": "info", + "message": "Wait 1000ms", + "ts": "2026-06-13T07:03:00Z" + } + ], + "mission": { + "actions": [ + { + "id": "a1", + "kind": "action", + "label": "Wait", + "params": { + "seconds": 1 + }, + "type": "wait" + } + ], + "description": "", + "group": "Missions", + "id": "5ae9dbcb0722dffb", + "name": "Test run", + "updated_at": "2026-06-13T04:44:03Z" + }, + "mission_group": "Missions", + "mission_id": "5ae9dbcb0722dffb", + "mission_name": "Test run", + "parameters": {}, + "priority": 0, + "robot_id": "default", + "source": "ui", + "started_at": "2026-06-13T07:03:00Z", + "status": "completed" + }, + { + "created_at": "2026-06-13T07:03:01Z", + "finished_at": "2026-06-13T07:03:02Z", + "id": "06048341b549f0ac", + "log": [ + { + "level": "info", + "message": "Wait 1000ms", + "ts": "2026-06-13T07:03:01Z" + } + ], + "mission": { + "actions": [ + { + "id": "a1", + "kind": "action", + "label": "Wait", + "params": { + "seconds": 1 + }, + "type": "wait" + } + ], + "description": "", + "group": "Missions", + "id": "5ae9dbcb0722dffb", + "name": "Test run", + "updated_at": "2026-06-13T04:44:03Z" + }, + "mission_group": "Missions", + "mission_id": "5ae9dbcb0722dffb", + "mission_name": "Test run", + "parameters": {}, + "priority": 0, + "robot_id": "default", + "source": "ui", + "started_at": "2026-06-13T07:03:01Z", + "status": "completed" + }, + { + "created_at": "2026-06-13T07:03:01Z", + "finished_at": "2026-06-13T07:03:03Z", + "id": "887245afd51df357", + "log": [ + { + "level": "info", + "message": "Wait 1000ms", + "ts": "2026-06-13T07:03:02Z" + } + ], + "mission": { + "actions": [ + { + "id": "a1", + "kind": "action", + "label": "Wait", + "params": { + "seconds": 1 + }, + "type": "wait" + } + ], + "description": "", + "group": "Missions", + "id": "5ae9dbcb0722dffb", + "name": "Test run", + "updated_at": "2026-06-13T04:44:03Z" + }, + "mission_group": "Missions", + "mission_id": "5ae9dbcb0722dffb", + "mission_name": "Test run", + "parameters": {}, + "priority": 0, + "robot_id": "default", + "source": "modbus:1005", + "started_at": "2026-06-13T07:03:02Z", + "status": "completed" + }, + { + "created_at": "2026-06-13T07:03:01Z", + "finished_at": "2026-06-13T07:03:04Z", + "id": "4365bd4d8beedfd2", + "log": [ + { + "level": "info", + "message": "Wait 1000ms", + "ts": "2026-06-13T07:03:03Z" + } + ], + "mission": { + "actions": [ + { + "id": "a1", + "kind": "action", + "label": "Wait", + "params": { + "seconds": 1 + }, + "type": "wait" + } + ], + "description": "", + "group": "Missions", + "id": "5ae9dbcb0722dffb", + "name": "Test run", + "updated_at": "2026-06-13T04:44:03Z" + }, + "mission_group": "Missions", + "mission_id": "5ae9dbcb0722dffb", + "mission_name": "Test run", + "parameters": {}, + "priority": 0, + "robot_id": "default", + "source": "fleet:pytest-schedule", + "started_at": "2026-06-13T07:03:03Z", + "status": "completed" + } + ], "runner": { "current_action": null, "current_queue_id": null, - "message": "", + "message": "Hoàn thành: Test run", "paused": false, "state": "idle", - "updated_at": "2026-06-13T06:56:22Z" + "updated_at": "2026-06-13T07:03:04Z" } } \ No newline at end of file diff --git a/data/models/ea89e39c835c0557.json b/data/models/ea89e39c835c0557.json index 62be4af..f2b5b72 100644 --- a/data/models/ea89e39c835c0557.json +++ b/data/models/ea89e39c835c0557.json @@ -193,5 +193,5 @@ } ], "name": "T800", - "updated_at": "2026-05-29T10:11:49Z" + "updated_at": "2026-06-13T07:03:01Z" } diff --git a/data/state.json b/data/state.json index 97ef6d3..c235b58 100644 --- a/data/state.json +++ b/data/state.json @@ -17,7 +17,7 @@ "lidar_count": 2, "model": "diff", "name": "T800", - "updated_at": "2026-05-29T10:11:49Z" + "updated_at": "2026-06-13T07:03:01Z" } ], "version": 3 diff --git a/scripts/api-smoke.sh b/scripts/api-smoke.sh index d317aa2..bff3b99 100755 --- a/scripts/api-smoke.sh +++ b/scripts/api-smoke.sh @@ -4,7 +4,7 @@ set -euo pipefail BASE="${1:-http://127.0.0.1:18080}" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -MISSION_ID="${TEST_MISSION_ID:-testmission00001}" +MISSION_ID="${TEST_MISSION_ID:-}" RED='\033[0;31m' GREEN='\033[0;32m' @@ -67,7 +67,6 @@ TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT echo "API smoke tests → $BASE" -echo "Fixture mission id: $MISSION_ID" echo # --- Health & static --- @@ -80,8 +79,30 @@ assert_code "GET /missions.js" 200 "$TMP/missions.js" -X GET "$BASE/missions.js" assert_code "GET /api/state" 200 "$TMP/state.json" -X GET "$BASE/api/state" assert_code "GET /api/missions" 200 "$TMP/missions.json" -X GET "$BASE/api/missions" -assert_json_true "missions fixture present" "$TMP/missions.json" \ - 'any(m.get("id") == "'"$MISSION_ID"'" for m in doc.get("missions", []))' + +if [[ -z "$MISSION_ID" ]]; then + MISSION_ID="$(python3 -c " +import json +doc = json.load(open('$TMP/missions.json')) +missions = doc.get('missions', []) +preferred = 'testmission00001' +ids = [m.get('id') for m in missions if isinstance(m, dict)] +if preferred in ids: + print(preferred) +elif ids: + print(ids[0]) +")" +fi + +if [[ -z "$MISSION_ID" ]]; then + log_fail "no mission available for queue tests" + exit 1 +fi + +echo "Mission id: $MISSION_ID" +echo + +assert_json_true "missions available" "$TMP/missions.json" 'len(doc.get("missions", [])) >= 1' # --- Queue pause/continue (chạy sớm, trước các test enqueue khác) --- curl -s -X DELETE "$BASE/api/mission_queue" -o /dev/null || true @@ -153,6 +174,13 @@ assert_json_true "v2 queue entry mission_id" "$TMP/v2q.json" \ 'doc.get("mission_id") == "'"$MISSION_ID"'"' assert_code "GET /api/v2.0.0/mission_queue" 200 "$TMP/v2list.json" -X GET "$BASE/api/v2.0.0/mission_queue" +for _ in $(seq 1 15); do + curl -s "$BASE/api/v2.0.0/mission_queue" -o "$TMP/v2list.json" + if python3 -c "import json; d=json.load(open('$TMP/v2list.json')); exit(0 if isinstance(d,list) and len(d)>=1 else 1)" 2>/dev/null; then + break + fi + sleep 0.2 +done assert_json_true "v2 queue non-empty" "$TMP/v2list.json" 'isinstance(doc, list) and len(doc) >= 1' assert_code "GET /api/v2.0.0/status" 200 "$TMP/v2status.json" -X GET "$BASE/api/v2.0.0/status" diff --git a/scripts/docker-test.sh b/scripts/docker-test.sh new file mode 100755 index 0000000..d902599 --- /dev/null +++ b/scripts/docker-test.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Build image, start container, chạy smoke + pytest trên port 8080. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" +# shellcheck source=docker-lib.sh +source "$ROOT/scripts/docker-lib.sh" + +docker_cmd + +BASE="${TEST_BASE_URL:-http://127.0.0.1:8080}" + +echo "==> Docker compose up --build -d" +"${DOCKER[@]}" compose up --build -d + +echo "==> Đợi server sẵn sàng ($BASE)" +for i in $(seq 1 40); do + if curl -sf "$BASE/api/health" >/dev/null 2>&1; then + break + fi + sleep 0.5 +done + +if ! curl -sf "$BASE/api/health" >/dev/null 2>&1; then + echo "Container không phản hồi tại $BASE" >&2 + "${DOCKER[@]}" logs --tail 30 lidar-manager-limited 2>&1 || true + exit 1 +fi + +echo "==> Unit + integration tests (local build)" +./scripts/run-tests.sh + +echo "==> API smoke (container $BASE)" +./scripts/api-smoke.sh "$BASE" + +echo "==> pytest (container $BASE)" +TEST_BASE_URL="$BASE" python3 -m pytest tests/test_api_integration.py -q + +echo +echo "Docker + tests OK." diff --git a/src/mission/mission_scheduler.cpp b/src/mission/mission_scheduler.cpp index 8473e2d..24cd9ac 100644 --- a/src/mission/mission_scheduler.cpp +++ b/src/mission/mission_scheduler.cpp @@ -3,7 +3,6 @@ #include "util/id_util.hpp" #include -#include namespace lm { diff --git a/src/mission/modbus_trigger_service.cpp b/src/mission/modbus_trigger_service.cpp index 5c1e6f6..0c0ca57 100644 --- a/src/mission/modbus_trigger_service.cpp +++ b/src/mission/modbus_trigger_service.cpp @@ -67,7 +67,6 @@ bool ModbusTriggerService::writeCoil(int coil_id, bool value, std::string& err) std::lock_guard lock(mu_); const bool prev = coils_.count(coil_id) ? coils_.at(coil_id) : false; coils_[coil_id] = value; - prev_coils_[coil_id] = value; if (!prev && value) { const auto trigger = store_.findTriggerByCoil(coil_id); diff --git a/src/mission/modbus_trigger_service.hpp b/src/mission/modbus_trigger_service.hpp index 5ffd7bf..feca0bf 100644 --- a/src/mission/modbus_trigger_service.hpp +++ b/src/mission/modbus_trigger_service.hpp @@ -35,7 +35,6 @@ private: mutable std::mutex mu_; std::unordered_map coils_; - std::unordered_map prev_coils_; std::atomic stop_{false}; std::thread tcp_thread_; diff --git a/src/server/api_mission_routes.cpp b/src/server/api_mission_routes.cpp index a83fff1..da1ebda 100644 --- a/src/server/api_mission_routes.cpp +++ b/src/server/api_mission_routes.cpp @@ -5,16 +5,18 @@ namespace lm { -bool ApiServer::enqueueRequest(const nlohmann::json& request, httplib::Response& res, int status_code) +std::optional ApiServer::enqueueMission(const nlohmann::json& request, std::string& err) { nlohmann::json payload; - std::string err; if (!MissionEnqueue::buildPayload(mission_store_, request, payload, err)) - { - HttpUtil::jsonError(res, 400, err); - return false; - } - const auto entry = mission_queue_.enqueue(payload, err); + return std::nullopt; + return mission_queue_.enqueue(payload, err); +} + +bool ApiServer::enqueueRequest(const nlohmann::json& request, httplib::Response& res, int status_code) +{ + std::string err; + const auto entry = enqueueMission(request, err); if (!entry) { HttpUtil::jsonError(res, 400, err); @@ -249,11 +251,8 @@ void ApiServer::registerMirV2Routes(httplib::Server& svr) } if (!payload.contains("source")) payload["source"] = "rest_api_v2"; - nlohmann::json built; std::string err; - if (!MissionEnqueue::buildPayload(mission_store_, payload, built, err)) - return HttpUtil::jsonError(res, 400, err); - const auto entry = mission_queue_.enqueue(built, err); + const auto entry = enqueueMission(payload, err); if (!entry) return HttpUtil::jsonError(res, 400, err); HttpUtil::addCors(res); diff --git a/src/server/api_server.hpp b/src/server/api_server.hpp index ca98cd8..ea8babc 100644 --- a/src/server/api_server.hpp +++ b/src/server/api_server.hpp @@ -14,10 +14,10 @@ class ApiServer { public: ApiServer(StateRepository& repo, - MissionQueue& mission_queue, - MissionStore& mission_store, - ModbusTriggerService& modbus, - MissionScheduler& scheduler); + MissionQueue& mission_queue, + MissionStore& mission_store, + ModbusTriggerService& modbus, + MissionScheduler& scheduler); void registerRoutes(httplib::Server& svr); @@ -29,6 +29,7 @@ private: MissionScheduler& scheduler_; bool enqueueRequest(const nlohmann::json& request, httplib::Response& res, int status_code = 201); + std::optional enqueueMission(const nlohmann::json& request, std::string& err); nlohmann::json toMirQueueEntry(const nlohmann::json& entry) const; void registerMissionRoutes(httplib::Server& svr); void registerMirV2Routes(httplib::Server& svr); diff --git a/tests/__pycache__/test_api_integration.cpython-38-pytest-8.3.5.pyc b/tests/__pycache__/test_api_integration.cpython-38-pytest-8.3.5.pyc index 0a8fa53d73eee09dbe51646e4c46bd19dadde62a..3912ca1d9d73a2616392092ceeeaefd5ba461d39 100644 GIT binary patch literal 10203 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 aYdA&)ew$^wx;yRIPTrYz7M(d~_J08KZr&CE diff --git a/tests/test_api_integration.py b/tests/test_api_integration.py index c42bf0f..07d47b5 100644 --- a/tests/test_api_integration.py +++ b/tests/test_api_integration.py @@ -9,8 +9,21 @@ import pytest import requests BASE = os.environ.get("TEST_BASE_URL", "http://127.0.0.1:18080") -MISSION_ID = os.environ.get("TEST_MISSION_ID", "testmission00001") TIMEOUT = 10 +PREFERRED_MISSION_ID = "testmission00001" + + +def resolve_mission_id(api: requests.Session) -> str: + env_id = os.environ.get("TEST_MISSION_ID", "").strip() + if env_id: + return env_id + missions = api.get(f"{BASE}/api/missions", timeout=TIMEOUT).json().get("missions", []) + ids = [m["id"] for m in missions if isinstance(m, dict) and m.get("id")] + if PREFERRED_MISSION_ID in ids: + return PREFERRED_MISSION_ID + if not ids: + pytest.fail("no missions available") + return ids[0] @pytest.fixture(scope="module") @@ -31,41 +44,46 @@ def api(): return session +@pytest.fixture(scope="module") +def mission_id(api): + return resolve_mission_id(api) + + def test_health(api): r = api.get(f"{BASE}/api/health", timeout=TIMEOUT) assert r.status_code == 200 assert r.json()["ok"] is True -def test_missions_fixture(api): +def test_missions_available(api, mission_id): r = api.get(f"{BASE}/api/missions", timeout=TIMEOUT) assert r.status_code == 200 ids = {m["id"] for m in r.json().get("missions", [])} - assert MISSION_ID in ids + assert mission_id in ids -def test_mir_v2_enqueue_and_list(api): +def test_mir_v2_enqueue_and_list(api, mission_id): api.delete(f"{BASE}/api/mission_queue", timeout=TIMEOUT) r = api.post( f"{BASE}/api/v2.0.0/mission_queue", - json={"mission_id": MISSION_ID, "priority": 3, "robot_id": "default"}, + json={"mission_id": mission_id, "priority": 3, "robot_id": "default"}, timeout=TIMEOUT, ) assert r.status_code == 201 body = r.json() - assert body["mission_id"] == MISSION_ID + assert body["mission_id"] == mission_id assert body["priority"] == 3 listed = api.get(f"{BASE}/api/v2.0.0/mission_queue", timeout=TIMEOUT) assert listed.status_code == 200 - assert any(item.get("mission_id") == MISSION_ID for item in listed.json()) + assert any(item.get("mission_id") == mission_id for item in listed.json()) -def test_queue_pause_continue(api): +def test_queue_pause_continue(api, mission_id): api.delete(f"{BASE}/api/mission_queue", timeout=TIMEOUT) api.post( f"{BASE}/api/mission_queue", - json={"mission_id": MISSION_ID}, + json={"mission_id": mission_id}, timeout=TIMEOUT, ) deadline = time.time() + 5 @@ -86,10 +104,10 @@ def test_queue_pause_continue(api): assert r.json().get("state") != "paused" -def test_modbus_trigger_flow(api): +def test_modbus_trigger_flow(api, mission_id): trig = api.post( f"{BASE}/api/triggers", - json={"name": "pytest-trigger", "coil_id": 1005, "mission_id": MISSION_ID}, + json={"name": "pytest-trigger", "coil_id": 1005, "mission_id": mission_id}, timeout=TIMEOUT, ) assert trig.status_code == 201 @@ -106,12 +124,12 @@ def test_modbus_trigger_flow(api): assert deleted.status_code == 204 -def test_fleet_schedule_asap(api): +def test_fleet_schedule_asap(api, mission_id): r = api.post( f"{BASE}/api/fleet/schedules", json={ "name": "pytest-schedule", - "mission_id": MISSION_ID, + "mission_id": mission_id, "start_mode": "asap", "priority": 0, "robot_id": "default",