Clean and Test làn 3
Some checks failed
Test / test (push) Has been cancelled

This commit is contained in:
2026-06-13 14:04:56 +07:00
parent fbc0c11be2
commit c05b1d5f5c
12 changed files with 281 additions and 40 deletions

View File

@@ -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"
}
}

View File

@@ -193,5 +193,5 @@
}
],
"name": "T800",
"updated_at": "2026-05-29T10:11:49Z"
"updated_at": "2026-06-13T07:03:01Z"
}

View File

@@ -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

View File

@@ -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"

41
scripts/docker-test.sh Executable file
View File

@@ -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."

View File

@@ -3,7 +3,6 @@
#include "util/id_util.hpp"
#include <chrono>
#include <ctime>
namespace lm {

View File

@@ -67,7 +67,6 @@ bool ModbusTriggerService::writeCoil(int coil_id, bool value, std::string& err)
std::lock_guard<std::mutex> 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);

View File

@@ -35,7 +35,6 @@ private:
mutable std::mutex mu_;
std::unordered_map<int, bool> coils_;
std::unordered_map<int, bool> prev_coils_;
std::atomic<bool> stop_{false};
std::thread tcp_thread_;

View File

@@ -5,16 +5,18 @@
namespace lm {
bool ApiServer::enqueueRequest(const nlohmann::json& request, httplib::Response& res, int status_code)
std::optional<nlohmann::json> 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);

View File

@@ -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<nlohmann::json> 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);

View File

@@ -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",