create TESTING

This commit is contained in:
2026-06-13 13:46:53 +07:00
parent 1a8bddb037
commit 695a942a5d
15 changed files with 831 additions and 14 deletions

33
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Test
on:
push:
paths:
- "src/**"
- "tests/**"
- "www/**"
- "CMakeLists.txt"
- "scripts/**"
pull_request:
paths:
- "src/**"
- "tests/**"
- "www/**"
- "CMakeLists.txt"
- "scripts/**"
jobs:
test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends build-essential cmake curl python3 python3-pip
- name: Run test suite
run: |
chmod +x scripts/run-tests.sh scripts/api-smoke.sh
./scripts/run-tests.sh

View File

@@ -62,3 +62,46 @@ target_include_directories(lidar_manager_web SYSTEM PRIVATE
target_compile_definitions(lidar_manager_web PRIVATE
_DEFAULT_SOURCE
)
option(BUILD_TESTING "Build unit and integration test helpers" ON)
if(BUILD_TESTING)
enable_testing()
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
set(LM_TEST_LIB_SOURCES
src/util/file_util.cpp
src/util/string_util.cpp
src/util/id_util.cpp
src/mission/mission_store.cpp
src/mission/mission_enqueue.cpp
src/validation/sensor_validator.cpp
)
add_executable(lidar_manager_tests
tests/test_mission_enqueue.cpp
tests/test_mission_store.cpp
tests/test_sensor_validator.cpp
${LM_TEST_LIB_SOURCES}
)
target_include_directories(lidar_manager_tests PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src"
)
target_include_directories(lidar_manager_tests SYSTEM PRIVATE
"${nlohmann_json_SOURCE_DIR}/single_include"
)
target_compile_definitions(lidar_manager_tests PRIVATE
TEST_FIXTURE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/data"
)
target_link_libraries(lidar_manager_tests PRIVATE GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(lidar_manager_tests WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
add_test(NAME unit COMMAND lidar_manager_tests)
endif()

View File

@@ -70,8 +70,36 @@ cat /proc/meminfo | head
```
```bash
# htop / stats từ ngoài (không cần vào shell)
sudo ./scripts/docker-htop.sh
sudo ./scripts/docker-stats.sh
```
## Test tự động
Chạy toàn bộ: unit C++ (GTest), API smoke (`curl`), pytest integration.
```bash
cd /home/robotics/RD/Test3
chmod +x scripts/run-tests.sh scripts/api-smoke.sh
./scripts/run-tests.sh
```
Chỉ unit test C++:
```bash
cmake -S . -B build -DBUILD_TESTING=ON
cmake --build build -j
ctest --test-dir build --output-on-failure
```
Chỉ API smoke (server đang chạy, dùng fixture `tests/fixtures/data/`):
```bash
./build/lidar_manager_web 18080 www tests/fixtures/data/state.json &
./scripts/api-smoke.sh http://127.0.0.1:18080
```
Fixture mission id mặc định: `testmission00001` (`tests/fixtures/data/missions.json`).
CI: GitHub Actions workflow `.github/workflows/test.yml`.

View File

@@ -0,0 +1 @@
---

View File

@@ -0,0 +1,3 @@
Start testing: Jun 13 13:41 +07
----------------------------------------------------------
End testing: Jun 13 13:41 +07

200
scripts/api-smoke.sh Executable file
View File

@@ -0,0 +1,200 @@
#!/usr/bin/env bash
# API smoke tests for lidar_manager_web — run against a live server instance.
set -euo pipefail
BASE="${1:-http://127.0.0.1:18080}"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MISSION_ID="${TEST_MISSION_ID:-testmission00001}"
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
PASS=0
FAIL=0
log_pass() { echo -e "${GREEN}PASS${NC} $*"; PASS=$((PASS + 1)); }
log_fail() { echo -e "${RED}FAIL${NC} $*"; FAIL=$((FAIL + 1)); }
json_field() {
python3 - "$1" "$2" <<'PY'
import json, sys
doc = json.loads(open(sys.argv[1]).read())
path = sys.argv[2].split(".")
cur = doc
for p in path:
cur = cur[p]
print(cur)
PY
}
http_code() {
curl -s -o "$1" -w '%{http_code}' "${@:2}"
}
assert_code() {
local name="$1" expect="$2" file="$3"
shift 3
local code
code="$(http_code "$file" "$@")"
if [[ "$code" == "$expect" ]]; then
log_pass "$name (HTTP $code)"
else
log_fail "$name — expected HTTP $expect, got $code"
[[ -f "$file" ]] && head -c 400 "$file" >&2 || true
echo >&2
fi
}
assert_json_true() {
local name="$1" file="$2" expr="$3"
if python3 - "$file" "$expr" <<'PY'
import json, sys
doc = json.loads(open(sys.argv[1]).read())
env = {"doc": doc, "any": any, "all": all, "len": len, "list": list, "isinstance": isinstance}
ok = eval(sys.argv[2], {"__builtins__": {}}, env)
sys.exit(0 if ok else 1)
PY
then
log_pass "$name"
else
log_fail "$name"
head -c 400 "$file" >&2 || true
echo >&2
fi
}
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
echo "API smoke tests → $BASE"
echo "Fixture mission id: $MISSION_ID"
echo
# --- Health & static ---
assert_code "GET /api/health" 200 "$TMP/health.json" -X GET "$BASE/api/health"
assert_json_true "health ok" "$TMP/health.json" 'doc.get("ok") is True'
assert_code "GET /" 200 "$TMP/index.html" -X GET "$BASE/"
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", []))'
# --- 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
curl -s -X POST "$BASE/api/mission_queue" \
-H 'Content-Type: application/json' \
-d "{\"mission_id\":\"$MISSION_ID\"}" -o "$TMP/qpost.json"
for _ in $(seq 1 30); do
curl -s "$BASE/api/mission_queue" -o "$TMP/runner_poll.json"
RUNNER_STATE="$(python3 -c "import json; print(json.load(open('$TMP/runner_poll.json')).get('runner',{}).get('state',''))")"
if [[ "$RUNNER_STATE" == "running" ]]; then
break
fi
sleep 0.2
done
if [[ "$RUNNER_STATE" == "running" ]]; then
assert_code "POST /api/mission_queue/pause" 200 "$TMP/pause.json" \
-X POST "$BASE/api/mission_queue/pause"
assert_json_true "runner paused" "$TMP/pause.json" 'doc.get("state") == "paused"'
assert_code "POST /api/mission_queue/continue" 200 "$TMP/cont.json" \
-X POST "$BASE/api/mission_queue/continue"
assert_json_true "runner not paused" "$TMP/cont.json" 'doc.get("state") != "paused"'
else
log_fail "runner never reached running (pause test skipped)"
fi
# --- LiDAR CRUD ---
assert_code "POST /api/lidars" 201 "$TMP/lidar.json" \
-X POST "$BASE/api/lidars" \
-H 'Content-Type: application/json' \
-d '{"name":"smoke-lidar","ip":"192.168.99.1","port":2112}'
LIDAR_ID="$(json_field "$TMP/lidar.json" id 2>/dev/null || echo "")"
if [[ -n "$LIDAR_ID" ]]; then
log_pass "lidar created id=$LIDAR_ID"
PASS=$((PASS + 1))
assert_code "PUT /api/lidars" 200 "$TMP/lidar_put.json" \
-X PUT "$BASE/api/lidars/$LIDAR_ID" \
-H 'Content-Type: application/json' \
-d '{"name":"smoke-lidar","ip":"192.168.99.2","port":2112}'
assert_code "DELETE /api/lidars" 204 "$TMP/lidar_del.txt" \
-X DELETE "$BASE/api/lidars/$LIDAR_ID"
else
log_fail "lidar create — no id in response"
fi
# --- IMU CRUD ---
assert_code "POST /api/imus" 201 "$TMP/imu.json" \
-X POST "$BASE/api/imus" \
-H 'Content-Type: application/json' \
-d '{"name":"smoke-imu","frame_id":"imu_smoke","topic":"/imu/smoke","source":"external"}'
IMU_ID="$(json_field "$TMP/imu.json" id 2>/dev/null || echo "")"
if [[ -n "$IMU_ID" ]]; then
log_pass "imu created id=$IMU_ID"
PASS=$((PASS + 1))
assert_code "DELETE /api/imus" 204 "$TMP/imu_del.txt" -X DELETE "$BASE/api/imus/$IMU_ID"
else
log_fail "imu create — no id in response"
fi
# --- Clear queue ---
curl -s -X DELETE "$BASE/api/mission_queue" -o /dev/null || true
curl -s -X DELETE "$BASE/api/v2.0.0/mission_queue" -o /dev/null || true
# --- MiR v2 enqueue (Cách C REST) ---
assert_code "POST /api/v2.0.0/mission_queue" 201 "$TMP/v2q.json" \
-X POST "$BASE/api/v2.0.0/mission_queue" \
-H 'Content-Type: application/json' \
-d "{\"mission_id\":\"$MISSION_ID\",\"priority\":2,\"robot_id\":\"default\"}"
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"
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"
assert_json_true "v2 status has state_text" "$TMP/v2status.json" '"state_text" in doc'
# --- Modbus trigger (Cách C) ---
assert_code "POST /api/triggers" 201 "$TMP/trig.json" \
-X POST "$BASE/api/triggers" \
-H 'Content-Type: application/json' \
-d "{\"name\":\"smoke-trigger\",\"coil_id\":1001,\"mission_id\":\"$MISSION_ID\"}"
TRIG_ID="$(json_field "$TMP/trig.json" id 2>/dev/null || echo "")"
if [[ -n "$TRIG_ID" ]]; then
log_pass "trigger created id=$TRIG_ID"
PASS=$((PASS + 1))
assert_code "POST modbus fire coil" 200 "$TMP/fire.json" \
-X POST "$BASE/api/modbus/coils/1001/trigger"
assert_code "DELETE /api/triggers" 204 "$TMP/trig_del.txt" \
-X DELETE "$BASE/api/triggers/$TRIG_ID"
else
log_fail "trigger create — no id"
fi
# --- Fleet schedule ---
assert_code "POST /api/fleet/schedules" 201 "$TMP/sched.json" \
-X POST "$BASE/api/fleet/schedules" \
-H 'Content-Type: application/json' \
-d "{\"name\":\"smoke-schedule\",\"mission_id\":\"$MISSION_ID\",\"start_mode\":\"asap\",\"priority\":1}"
SCHED_ID="$(json_field "$TMP/sched.json" id 2>/dev/null || echo "")"
if [[ -n "$SCHED_ID" ]]; then
log_pass "schedule created id=$SCHED_ID"
PASS=$((PASS + 1))
assert_code "DELETE /api/fleet/schedules" 204 "$TMP/sched_del.txt" \
-X DELETE "$BASE/api/fleet/schedules/$SCHED_ID"
else
log_fail "schedule create — no id"
fi
assert_code "GET /api/fleet/robots" 200 "$TMP/robots.json" -X GET "$BASE/api/fleet/robots"
assert_json_true "robots list" "$TMP/robots.json" 'isinstance(doc, list) and len(doc) >= 1'
echo
echo "Results: $PASS passed, $FAIL failed"
if [[ "$FAIL" -gt 0 ]]; then
exit 1
fi

71
scripts/run-tests.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Build unit tests, start lidar_manager_web on a temp data dir, run API smoke + pytest.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
PORT="${TEST_PORT:-18080}"
BASE="http://127.0.0.1:${PORT}"
BIN="${ROOT}/build/lidar_manager_web"
DATA_DIR="$(mktemp -d)"
SERVER_PID=""
cleanup() {
if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
fi
rm -rf "$DATA_DIR"
}
trap cleanup EXIT
echo "==> Configure & build (BUILD_TESTING=ON)"
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON
cmake --build build -j
echo "==> C++ unit tests (GTest)"
./build/lidar_manager_tests
echo "==> Prepare isolated data directory"
cp -a tests/fixtures/data/. "$DATA_DIR/"
mkdir -p "$DATA_DIR/models"
echo "==> Start server on port $PORT"
"$BIN" "$PORT" "$ROOT/www" "$DATA_DIR/state.json" >"$DATA_DIR/server.log" 2>&1 &
SERVER_PID=$!
for i in $(seq 1 30); do
if curl -sf "$BASE/api/health" >/dev/null 2>&1; then
break
fi
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
echo "Server exited early:" >&2
cat "$DATA_DIR/server.log" >&2 || true
exit 1
fi
sleep 0.2
done
if ! curl -sf "$BASE/api/health" >/dev/null 2>&1; then
echo "Server did not become ready on $BASE" >&2
cat "$DATA_DIR/server.log" >&2 || true
exit 1
fi
echo "==> API smoke tests"
chmod +x scripts/api-smoke.sh
./scripts/api-smoke.sh "$BASE"
if command -v python3 >/dev/null 2>&1; then
echo "==> Python integration tests (pytest)"
if ! python3 -c "import pytest" 2>/dev/null; then
python3 -m pip install --user -q -r tests/requirements.txt
fi
TEST_BASE_URL="$BASE" python3 -m pytest tests/test_api_integration.py -q
else
echo "==> Skipping pytest (python3 not found)"
fi
echo
echo "All tests passed."

View File

@@ -38,12 +38,18 @@ bool ApiServer::enqueueRequest(const nlohmann::json& request, httplib::Response&
nlohmann::json ApiServer::toMirQueueEntry(const nlohmann::json& entry) const
{
return nlohmann::json{{"id", entry.value("id", 0)},
{"mission_id", entry.value("mission_id", "")},
{"state", entry.value("status", "pending")},
{"message", entry.value("mission_name", "")},
{"priority", entry.value("priority", 0)},
{"robot_id", entry.value("robot_id", "default")}};
nlohmann::json out = nlohmann::json::object();
if (entry.contains("id"))
out["id"] = entry["id"];
out["mission_id"] = entry.value("mission_id", std::string(""));
out["state"] = entry.contains("status") ? entry["status"] : nlohmann::json("pending");
out["message"] = entry.value("mission_name", std::string(""));
if (entry.contains("priority") && entry["priority"].is_number())
out["priority"] = entry["priority"];
else
out["priority"] = 0;
out["robot_id"] = entry.value("robot_id", std::string("default"));
return out;
}
void ApiServer::registerMissionRoutes(httplib::Server& svr)
@@ -226,11 +232,15 @@ void ApiServer::registerMirV2Routes(httplib::Server& svr)
svr.Get("/api/v2.0.0/mission_queue", [this](const httplib::Request&, httplib::Response& res) {
HttpUtil::addCors(res);
nlohmann::json out = nlohmann::json::array();
for (const auto& item : mission_queue_.list())
const nlohmann::json queue = mission_queue_.list();
if (queue.is_array())
{
for (const auto& item : queue)
{
if (item.is_object())
out.push_back(toMirQueueEntry(item));
}
}
res.set_header("Content-Type", "application/json; charset=utf-8");
res.body = out.dump();
});
@@ -248,10 +258,17 @@ void ApiServer::registerMirV2Routes(httplib::Server& svr)
}
if (!payload.contains("source"))
payload["source"] = "rest_api_v2";
if (!enqueueRequest(payload, res, 201))
return;
nlohmann::json created = nlohmann::json::parse(res.body);
res.body = toMirQueueEntry(created).dump();
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);
if (!entry)
return HttpUtil::jsonError(res, 400, err);
HttpUtil::addCors(res);
res.status = 201;
res.set_header("Content-Type", "application/json; charset=utf-8");
res.body = toMirQueueEntry(*entry).dump();
});
svr.Delete("/api/v2.0.0/mission_queue", [this](const httplib::Request&, httplib::Response& res) {

39
tests/fixtures/data/missions.json vendored Normal file
View File

@@ -0,0 +1,39 @@
{
"dashboard": {
"widgets": []
},
"groups": [
"Missions"
],
"missions": [
{
"actions": [
{
"id": "a1",
"kind": "action",
"label": "Wait",
"params": {
"seconds": 3
},
"type": "wait"
}
],
"description": "Fixture mission for automated tests",
"group": "Missions",
"id": "testmission00001",
"name": "Test Wait",
"updated_at": "2026-06-13T00:00:00Z"
}
],
"robots": [
{
"id": "default",
"name": "Robot test",
"online": true,
"serial": "T-001"
}
],
"schedules": [],
"triggers": [],
"version": 1
}

5
tests/fixtures/data/state.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"active_layout_id": "",
"layouts": [],
"version": 3
}

2
tests/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
pytest>=7.0
requests>=2.28

View File

@@ -0,0 +1,145 @@
"""Integration tests for lidar_manager_web REST API."""
from __future__ import annotations
import os
import time
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
@pytest.fixture(scope="module")
def api():
session = requests.Session()
session.headers.update({"Content-Type": "application/json"})
deadline = time.time() + TIMEOUT
while time.time() < deadline:
try:
r = session.get(f"{BASE}/api/health", timeout=1)
if r.status_code == 200 and r.json().get("ok"):
break
except requests.RequestException:
pass
time.sleep(0.2)
else:
pytest.fail(f"Server not ready at {BASE}")
return session
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):
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
def test_mir_v2_enqueue_and_list(api):
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"},
timeout=TIMEOUT,
)
assert r.status_code == 201
body = r.json()
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())
def test_queue_pause_continue(api):
api.delete(f"{BASE}/api/mission_queue", timeout=TIMEOUT)
api.post(
f"{BASE}/api/mission_queue",
json={"mission_id": MISSION_ID},
timeout=TIMEOUT,
)
deadline = time.time() + 5
while time.time() < deadline:
runner = api.get(f"{BASE}/api/mission_queue", timeout=TIMEOUT).json().get("runner", {})
if runner.get("state") == "running":
break
time.sleep(0.2)
else:
pytest.skip("runner did not enter running state in time")
r = api.post(f"{BASE}/api/mission_queue/pause", timeout=TIMEOUT)
assert r.status_code == 200
assert r.json().get("state") == "paused"
r = api.post(f"{BASE}/api/mission_queue/continue", timeout=TIMEOUT)
assert r.status_code == 200
assert r.json().get("state") != "paused"
def test_modbus_trigger_flow(api):
trig = api.post(
f"{BASE}/api/triggers",
json={"name": "pytest-trigger", "coil_id": 1005, "mission_id": MISSION_ID},
timeout=TIMEOUT,
)
assert trig.status_code == 201
trig_id = trig.json()["id"]
before = len(api.get(f"{BASE}/api/mission_queue", timeout=TIMEOUT).json())
fire = api.post(f"{BASE}/api/modbus/coils/1005/trigger", timeout=TIMEOUT)
assert fire.status_code == 200
after = api.get(f"{BASE}/api/mission_queue", timeout=TIMEOUT).json()
assert len(after) >= before
deleted = api.delete(f"{BASE}/api/triggers/{trig_id}", timeout=TIMEOUT)
assert deleted.status_code == 204
def test_fleet_schedule_asap(api):
r = api.post(
f"{BASE}/api/fleet/schedules",
json={
"name": "pytest-schedule",
"mission_id": MISSION_ID,
"start_mode": "asap",
"priority": 0,
"robot_id": "default",
},
timeout=TIMEOUT,
)
assert r.status_code == 201
sched_id = r.json()["id"]
deleted = api.delete(f"{BASE}/api/fleet/schedules/{sched_id}", timeout=TIMEOUT)
assert deleted.status_code == 204
def test_lidar_crud(api):
created = api.post(
f"{BASE}/api/lidars",
json={"name": "pytest-lidar", "ip": "10.99.0.1", "port": 2112},
timeout=TIMEOUT,
)
assert created.status_code == 201
lidar_id = created.json()["id"]
updated = api.put(
f"{BASE}/api/lidars/{lidar_id}",
json={"name": "pytest-lidar", "ip": "10.99.0.2", "port": 2112},
timeout=TIMEOUT,
)
assert updated.status_code == 200
deleted = api.delete(f"{BASE}/api/lidars/{lidar_id}", timeout=TIMEOUT)
assert deleted.status_code == 204

View File

@@ -0,0 +1,71 @@
#include "mission/mission_enqueue.hpp"
#include "mission/mission_store.hpp"
#include <gtest/gtest.h>
#include <filesystem>
namespace {
std::filesystem::path fixtureMissionsPath()
{
return std::filesystem::path(TEST_FIXTURE_DIR) / "missions.json";
}
lm::MissionStore makeStore()
{
const auto dir = std::filesystem::temp_directory_path() / "lm_test_enqueue";
std::filesystem::create_directories(dir);
const auto path = dir / "missions.json";
std::filesystem::copy_file(fixtureMissionsPath(), path, std::filesystem::copy_options::overwrite_existing);
return lm::MissionStore(path);
}
} // namespace
TEST(MissionEnqueue, NormalizeParametersFromMirArray)
{
const nlohmann::json params = nlohmann::json::array({{{"id", "pos"}, {"value", "A1"}},
{{"key", "speed"}, {"value", 0.5}}});
const nlohmann::json out = lm::MissionEnqueue::normalizeParameters(params);
EXPECT_TRUE(out.is_object());
EXPECT_EQ(out["pos"], "A1");
EXPECT_EQ(out["speed"], 0.5);
}
TEST(MissionEnqueue, NormalizeParametersObjectPassthrough)
{
const nlohmann::json params = {{"x", 1}};
const nlohmann::json out = lm::MissionEnqueue::normalizeParameters(params);
EXPECT_EQ(out, params);
}
TEST(MissionEnqueue, BuildPayloadFromMissionId)
{
lm::MissionStore store = makeStore();
nlohmann::json payload;
std::string err;
const nlohmann::json request = {{"mission_id", "testmission00001"}, {"priority", 3}, {"robot_id", "default"}};
ASSERT_TRUE(lm::MissionEnqueue::buildPayload(store, request, payload, err)) << err;
EXPECT_EQ(payload["mission"]["id"], "testmission00001");
EXPECT_EQ(payload["priority"], 3);
EXPECT_EQ(payload["robot_id"], "default");
}
TEST(MissionEnqueue, BuildPayloadMissingMissionFails)
{
lm::MissionStore store = makeStore();
nlohmann::json payload;
std::string err;
const nlohmann::json request = {{"mission_id", "does_not_exist"}};
EXPECT_FALSE(lm::MissionEnqueue::buildPayload(store, request, payload, err));
EXPECT_FALSE(err.empty());
}
TEST(MissionEnqueue, BuildPayloadRequiresMissionOrId)
{
lm::MissionStore store = makeStore();
nlohmann::json payload;
std::string err;
EXPECT_FALSE(lm::MissionEnqueue::buildPayload(store, nlohmann::json::object(), payload, err));
}

View File

@@ -0,0 +1,96 @@
#include "mission/mission_store.hpp"
#include <gtest/gtest.h>
#include <filesystem>
namespace {
class MissionStoreTest : public ::testing::Test
{
protected:
void SetUp() override
{
dir_ = std::filesystem::temp_directory_path() / "lm_test_store";
std::filesystem::remove_all(dir_);
std::filesystem::create_directories(dir_);
store_path_ = dir_ / "missions.json";
std::filesystem::copy_file(std::filesystem::path(TEST_FIXTURE_DIR) / "missions.json",
store_path_,
std::filesystem::copy_options::overwrite_existing);
store_ = std::make_unique<lm::MissionStore>(store_path_);
}
std::filesystem::path dir_;
std::filesystem::path store_path_;
std::unique_ptr<lm::MissionStore> store_;
};
} // namespace
TEST_F(MissionStoreTest, FindMissionFromFixture)
{
const auto mission = store_->findMission("testmission00001");
ASSERT_TRUE(mission.has_value());
EXPECT_EQ((*mission)["name"], "Test Wait");
}
TEST_F(MissionStoreTest, AddTriggerValidCoil)
{
std::string err;
const auto trigger = store_->addTrigger(
{{"name", "PLC line 1"}, {"coil_id", 1001}, {"mission_id", "testmission00001"}}, err);
ASSERT_TRUE(trigger.has_value()) << err;
EXPECT_EQ((*trigger)["coil_id"], 1001);
}
TEST_F(MissionStoreTest, AddTriggerRejectsInvalidCoil)
{
std::string err;
const auto trigger =
store_->addTrigger({{"name", "bad"}, {"coil_id", 999}, {"mission_id", "testmission00001"}}, err);
EXPECT_FALSE(trigger.has_value());
EXPECT_FALSE(err.empty());
}
TEST_F(MissionStoreTest, AddTriggerRejectsDuplicateCoil)
{
std::string err;
ASSERT_TRUE(store_->addTrigger(
{{"name", "first"}, {"coil_id", 1002}, {"mission_id", "testmission00001"}}, err)
.has_value())
<< err;
const auto dup = store_->addTrigger(
{{"name", "second"}, {"coil_id", 1002}, {"mission_id", "testmission00001"}}, err);
EXPECT_FALSE(dup.has_value());
}
TEST_F(MissionStoreTest, DeleteTrigger)
{
std::string err;
const auto trigger = store_->addTrigger(
{{"name", "tmp"}, {"coil_id", 1003}, {"mission_id", "testmission00001"}}, err);
ASSERT_TRUE(trigger.has_value()) << err;
const std::string id = (*trigger)["id"].get<std::string>();
EXPECT_TRUE(store_->deleteTrigger(id, err)) << err;
EXPECT_FALSE(store_->findTriggerByCoil(1003).has_value());
}
TEST_F(MissionStoreTest, AddScheduleAsap)
{
std::string err;
const auto schedule = store_->addSchedule(
{{"name", "Morning run"}, {"mission_id", "testmission00001"}, {"priority", 5}, {"start_mode", "asap"}},
err);
ASSERT_TRUE(schedule.has_value()) << err;
EXPECT_EQ((*schedule)["priority"], 5);
EXPECT_EQ((*schedule)["start_mode"], "asap");
}
TEST_F(MissionStoreTest, AddScheduleUnknownMissionFails)
{
std::string err;
const auto schedule =
store_->addSchedule({{"name", "bad"}, {"mission_id", "missing"}, {"start_mode", "asap"}}, err);
EXPECT_FALSE(schedule.has_value());
}

View File

@@ -0,0 +1,63 @@
#include "validation/sensor_validator.hpp"
#include <gtest/gtest.h>
TEST(SensorValidator, LidarRequiresNameIpPort)
{
std::string err;
EXPECT_FALSE(lm::SensorValidator::validateLidarPayload(nlohmann::json::object(), err));
EXPECT_FALSE(err.empty());
err.clear();
EXPECT_TRUE(lm::SensorValidator::validateLidarPayload(
{{"name", "front"}, {"ip", "192.168.1.10"}, {"port", 2112}}, err))
<< err;
}
TEST(SensorValidator, LidarPortRange)
{
std::string err;
EXPECT_FALSE(lm::SensorValidator::validateLidarPayload(
{{"name", "front"}, {"ip", "192.168.1.10"}, {"port", 70000}}, err));
}
TEST(SensorValidator, ImuRequiresFrameAndTopic)
{
std::string err;
EXPECT_FALSE(lm::SensorValidator::validateImuPayload({{"name", "imu1"}}, err));
err.clear();
EXPECT_TRUE(lm::SensorValidator::validateImuPayload(
{{"name", "imu1"}, {"frame_id", "imu_link"}, {"topic", "/imu/data"}}, err))
<< err;
}
TEST(SensorValidator, ImuInvalidSource)
{
std::string err;
EXPECT_FALSE(lm::SensorValidator::validateImuPayload(
{{"name", "imu1"},
{"frame_id", "imu_link"},
{"topic", "/imu/data"},
{"source", "invalid_source"}},
err));
}
TEST(SensorValidator, LidarTripletDuplicateDetection)
{
const nlohmann::json state = {
{"lidars",
nlohmann::json::array({{{"id", "l1"}, {"name", "front"}, {"ip", "10.0.0.1"}, {"port", 2112}}})}};
EXPECT_TRUE(lm::SensorValidator::lidarTripletExists(state, "front", "10.0.0.1", 2112));
const std::string exclude = "l1";
EXPECT_FALSE(lm::SensorValidator::lidarTripletExists(state, "front", "10.0.0.1", 2112, &exclude));
}
TEST(SensorValidator, ImuFrameDuplicateDetection)
{
const nlohmann::json state = {
{"imus", nlohmann::json::array({{{"id", "i1"}, {"frame_id", "base_imu"}}})}};
EXPECT_TRUE(lm::SensorValidator::imuFrameExists(state, "base_imu"));
const std::string exclude = "i1";
EXPECT_FALSE(lm::SensorValidator::imuFrameExists(state, "base_imu", &exclude));
}