create TESTING
This commit is contained in:
33
.github/workflows/test.yml
vendored
Normal file
33
.github/workflows/test.yml
vendored
Normal 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
|
||||||
@@ -62,3 +62,46 @@ target_include_directories(lidar_manager_web SYSTEM PRIVATE
|
|||||||
target_compile_definitions(lidar_manager_web PRIVATE
|
target_compile_definitions(lidar_manager_web PRIVATE
|
||||||
_DEFAULT_SOURCE
|
_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()
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -70,8 +70,36 @@ cat /proc/meminfo | head
|
|||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# htop / stats từ ngoài (không cần vào shell)
|
|
||||||
sudo ./scripts/docker-htop.sh
|
sudo ./scripts/docker-htop.sh
|
||||||
sudo ./scripts/docker-stats.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`.
|
||||||
|
|
||||||
|
|||||||
1
Testing/Temporary/CTestCostData.txt
Normal file
1
Testing/Temporary/CTestCostData.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
---
|
||||||
3
Testing/Temporary/LastTest.log
Normal file
3
Testing/Temporary/LastTest.log
Normal 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
200
scripts/api-smoke.sh
Executable 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
71
scripts/run-tests.sh
Executable 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."
|
||||||
@@ -38,12 +38,18 @@ bool ApiServer::enqueueRequest(const nlohmann::json& request, httplib::Response&
|
|||||||
|
|
||||||
nlohmann::json ApiServer::toMirQueueEntry(const nlohmann::json& entry) const
|
nlohmann::json ApiServer::toMirQueueEntry(const nlohmann::json& entry) const
|
||||||
{
|
{
|
||||||
return nlohmann::json{{"id", entry.value("id", 0)},
|
nlohmann::json out = nlohmann::json::object();
|
||||||
{"mission_id", entry.value("mission_id", "")},
|
if (entry.contains("id"))
|
||||||
{"state", entry.value("status", "pending")},
|
out["id"] = entry["id"];
|
||||||
{"message", entry.value("mission_name", "")},
|
out["mission_id"] = entry.value("mission_id", std::string(""));
|
||||||
{"priority", entry.value("priority", 0)},
|
out["state"] = entry.contains("status") ? entry["status"] : nlohmann::json("pending");
|
||||||
{"robot_id", entry.value("robot_id", "default")}};
|
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)
|
void ApiServer::registerMissionRoutes(httplib::Server& svr)
|
||||||
@@ -226,10 +232,14 @@ void ApiServer::registerMirV2Routes(httplib::Server& svr)
|
|||||||
svr.Get("/api/v2.0.0/mission_queue", [this](const httplib::Request&, httplib::Response& res) {
|
svr.Get("/api/v2.0.0/mission_queue", [this](const httplib::Request&, httplib::Response& res) {
|
||||||
HttpUtil::addCors(res);
|
HttpUtil::addCors(res);
|
||||||
nlohmann::json out = nlohmann::json::array();
|
nlohmann::json out = nlohmann::json::array();
|
||||||
for (const auto& item : mission_queue_.list())
|
const nlohmann::json queue = mission_queue_.list();
|
||||||
|
if (queue.is_array())
|
||||||
{
|
{
|
||||||
if (item.is_object())
|
for (const auto& item : queue)
|
||||||
out.push_back(toMirQueueEntry(item));
|
{
|
||||||
|
if (item.is_object())
|
||||||
|
out.push_back(toMirQueueEntry(item));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
res.set_header("Content-Type", "application/json; charset=utf-8");
|
res.set_header("Content-Type", "application/json; charset=utf-8");
|
||||||
res.body = out.dump();
|
res.body = out.dump();
|
||||||
@@ -248,10 +258,17 @@ void ApiServer::registerMirV2Routes(httplib::Server& svr)
|
|||||||
}
|
}
|
||||||
if (!payload.contains("source"))
|
if (!payload.contains("source"))
|
||||||
payload["source"] = "rest_api_v2";
|
payload["source"] = "rest_api_v2";
|
||||||
if (!enqueueRequest(payload, res, 201))
|
nlohmann::json built;
|
||||||
return;
|
std::string err;
|
||||||
nlohmann::json created = nlohmann::json::parse(res.body);
|
if (!MissionEnqueue::buildPayload(mission_store_, payload, built, err))
|
||||||
res.body = toMirQueueEntry(created).dump();
|
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) {
|
svr.Delete("/api/v2.0.0/mission_queue", [this](const httplib::Request&, httplib::Response& res) {
|
||||||
|
|||||||
39
tests/fixtures/data/missions.json
vendored
Normal file
39
tests/fixtures/data/missions.json
vendored
Normal 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
5
tests/fixtures/data/state.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"active_layout_id": "",
|
||||||
|
"layouts": [],
|
||||||
|
"version": 3
|
||||||
|
}
|
||||||
2
tests/requirements.txt
Normal file
2
tests/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pytest>=7.0
|
||||||
|
requests>=2.28
|
||||||
145
tests/test_api_integration.py
Normal file
145
tests/test_api_integration.py
Normal 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
|
||||||
71
tests/test_mission_enqueue.cpp
Normal file
71
tests/test_mission_enqueue.cpp
Normal 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));
|
||||||
|
}
|
||||||
96
tests/test_mission_store.cpp
Normal file
96
tests/test_mission_store.cpp
Normal 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());
|
||||||
|
}
|
||||||
63
tests/test_sensor_validator.cpp
Normal file
63
tests/test_sensor_validator.cpp
Normal 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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user