This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -193,5 +193,5 @@
|
||||
}
|
||||
],
|
||||
"name": "T800",
|
||||
"updated_at": "2026-05-29T10:11:49Z"
|
||||
"updated_at": "2026-06-13T07:03:01Z"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
41
scripts/docker-test.sh
Executable 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."
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "util/id_util.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
|
||||
namespace lm {
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Binary file not shown.
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user