diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh index fc55bd84..7b3ba41f 100755 --- a/.devcontainer/postCreate.sh +++ b/.devcontainer/postCreate.sh @@ -1,4 +1,9 @@ cd /workspaces/bsn &&\ rosdep install --from-paths src --ignore-src -r -y &&\ + rm -rf build &&\ + rm -rf devel &&\ catkin_make echo "source /workspaces/bsn/devel/setup.sh" >> /root/.bashrc + source ~/.bashrc + source /opt/ros/melodic/setup.bash + diff --git a/.gitignore b/.gitignore index 3881cf91..892666f6 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,8 @@ json/ # Ignore generated docs *.dox *.wikidoc - +html +latex # eclipse stuff .project .cproject @@ -64,4 +65,6 @@ test.py json venv TODO -bsn_remote_control \ No newline at end of file +bsn_remote_control + +coverage/ \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev index 2bc98893..7466f63b 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,10 +1,21 @@ -FROM ros:melodic-ros-core-bionic - -RUN apt update &&\ - apt install -y python-rosdep build-essential git python-pip python3-pip python-virtualenv python-rospkg ros-melodic-rosmon &&\ - sudo rosdep init && rosdep update &&\ - pip3 install pandas - -COPY .devcontainer/additional_bashrc.sh /root/.additional_bashrc - -RUN cat "/root/.additional_bashrc" >> /root/.bashrc && rm /root/.additional_bashrc \ No newline at end of file +FROM ros:melodic-ros-core-bionic + +RUN apt update &&\ + apt install -y python-rosdep build-essential git python-pip python3-pip python-virtualenv python-rospkg ros-melodic-rosmon &&\ + sudo rosdep init && rosdep update &&\ + pip3 install pandas &&\ + pip3 install behave &&\ + sudo apt-get install python3-rosdep python3-rosinstall-generator python3-vcstool &&\ + sudo apt-get install python3-pybind11 &&\ + sudo apt-get install python-pybind11 + + +ENV DISPLAY=:0 +ENV QT_X11_NO_MITSHM=1 + +COPY .devcontainer/additional_bashrc.sh /root/.additional_bashrc + +RUN cat "/root/.additional_bashrc" >> /root/.bashrc && rm /root/.additional_bashrc + + + diff --git a/bsn.launch b/bsn.launch index 985aeb1f..ec98689c 100644 --- a/bsn.launch +++ b/bsn.launch @@ -1,25 +1,25 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/coverage.sh b/coverage.sh new file mode 100755 index 00000000..3f93a944 --- /dev/null +++ b/coverage.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# --- Configurações --- +OUTPUT_DIR="coverage" +# Agora os arquivos intermediários ficarão DENTRO da pasta de output +INFO_FILE="$OUTPUT_DIR/coverage.info" +INFO_FILE_CLEAN="$OUTPUT_DIR/coverage_cleaned.info" + +# Verifica se estamos na raiz do workspace +if [ ! -f "devel/setup.bash" ]; then + echo "❌ Erro: Execute este script da raiz do seu workspace catkin." + exit 1 +fi + +# Carrega o ambiente +source devel/setup.bash + +# Cria a pasta de destino imediatamente para guardar a bagunça lá +mkdir -p "$OUTPUT_DIR" + +echo "=========================================" +echo "🧹 1. Zerando contadores anteriores..." +echo "=========================================" +lcov --directory . --zerocounters --quiet + +echo "=========================================" +echo "🚀 2. Rodando os testes..." +echo "=========================================" + +if [ $# -eq 0 ]; then + echo "⚠️ Nenhum comando passado. Rodando 'catkin_make run_tests'..." + catkin_make run_tests +else + echo "Executando: $@" + "$@" +fi + +# Captura erro do teste mas não para o script +TEST_EXIT_CODE=$? +if [ $TEST_EXIT_CODE -ne 0 ]; then + echo "⚠️ Os testes falharam. Gerando relatório para diagnóstico..." +fi + +echo "=========================================" +echo "📸 3. Capturando dados (Salvando em $OUTPUT_DIR)..." +echo "=========================================" +# Gera o arquivo bruto JÁ dentro da pasta output +lcov --directory . --capture --output-file "$INFO_FILE" --rc lcov_branch_coverage=1 + +echo "=========================================" +echo "🗑️ 4. Filtrando arquivos..." +echo "=========================================" +lcov --remove "$INFO_FILE" \ + '/usr/*' \ + '/opt/*' \ + '*/test/*' \ + '*/tests/*' \ + '*/build/*' \ + '*/devel/*' \ + '*/CMakeCCompilerId.c' \ + '*/CMakeCXXCompilerId.cpp' \ + --output-file "$INFO_FILE_CLEAN" \ + --rc lcov_branch_coverage=1 + +echo "=========================================" +echo "📊 5. Gerando HTML..." +echo "=========================================" +genhtml "$INFO_FILE_CLEAN" --output-directory "$OUTPUT_DIR" --rc lcov_branch_coverage=1 --legend + +# --- LIMPEZA FINAL --- +# Remove os arquivos .info para deixar apenas o site HTML. +# Se quiser guardar os dados brutos para usar no Codecov/Sonar, comente as linhas abaixo. +echo "✨ Limpando arquivos temporários..." +rm "$INFO_FILE" +rm "$INFO_FILE_CLEAN" + +echo "" +echo "✅ Concluído!" +echo "📄 Abra: $(pwd)/$OUTPUT_DIR/index.html" \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 66dd650a..92e1c35c 120000 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1 +1,2 @@ -/opt/ros/melodic/share/catkin/cmake/toplevel.cmake \ No newline at end of file +CMAKE_MINIMUM_REQUIRED(VERSION 2.8.3) +include(/opt/ros/melodic/share/catkin/cmake/toplevel.cmake) diff --git a/src/libbsn/src/configuration/SensorConfiguration.cpp b/src/libbsn/src/configuration/SensorConfiguration.cpp index 5c73995e..d7d6f87a 100644 --- a/src/libbsn/src/configuration/SensorConfiguration.cpp +++ b/src/libbsn/src/configuration/SensorConfiguration.cpp @@ -159,15 +159,17 @@ namespace bsn { } bool SensorConfiguration::isLowRisk(double val) { - return lowRisk.in_range(val); + return lowPercentage.in_range(val); } bool SensorConfiguration::isMediumRisk(double val) { - return (mediumRisk[0].in_range(val) || mediumRisk[1].in_range(val)); + // return (mediumRisk[0].in_range(val) || mediumRisk[1].in_range(val)); + return midPercentage.in_range(val); } bool SensorConfiguration::isHighRisk(double val) { - return (highRisk[0].in_range(val) || highRisk[1].in_range(val)); + // return (highRisk[0].in_range(val) || highRisk[1].in_range(val)); + return highPercentage.in_range(val); } void SensorConfiguration::setHighRisk(const array h) { diff --git a/src/libbsn/test/integration/IntegrationTest.cpp b/src/libbsn/test/integration/IntegrationTest.cpp index 5b338f5b..63f52a84 100644 --- a/src/libbsn/test/integration/IntegrationTest.cpp +++ b/src/libbsn/test/integration/IntegrationTest.cpp @@ -1,134 +1,140 @@ -// #include -// #include - -// #include "libbsn/range/Range.hpp" -// #include "libbsn/generator/Markov.hpp" -// #include "libbsn/filters/MovingAverage.hpp" -// #include "libbsn/configuration/SensorConfiguration.hpp" - -// using namespace std; -// using namespace bsn::range; -// using namespace bsn::filters; -// using namespace bsn::configuration; -// using namespace bsn::generator; - -// class IntegrationTest : public testing::Test { -// protected: -// MovingAverage avg1; -// MovingAverage avg2; -// MovingAverage avg4; - -// Markov mar1; -// Markov mar2; -// Markov mar4; - -// Range l; -// Range m1; -// Range m2; -// Range h1; -// Range h2; - -// array a1; -// array a2; - -// SensorConfiguration s; -// array transitions; -// array states; - -// IntegrationTest() : avg1(), avg2(), avg4(), mar1(), mar2(), mar4(), -// l(), m1(), m2(), h1(), h2(), a1(), -// a2(), s(), transitions(), states() {} - -// virtual void SetUp() { -// transitions = {{ -// 0,100,0,0,0, -// 0,0,100,0,0, -// 0,0,0,100,0, -// 0,0,0,0,100, -// 100,0,0,0,0}}; - -// states = {{Range(30.0, 33.0), Range(33.1, 36.4), Range(36.5, 37.5), Range(37.6, 39.0), Range(39.1, 42.0)}}; - -// h1.setLowerBound(30.0); -// h1.setUpperBound(33.0); - -// m1.setLowerBound(33.1); -// m1.setUpperBound(36.4); - -// l.setLowerBound(36.5); -// l.setUpperBound(37.5); - -// m2.setLowerBound(37.6); -// m2.setUpperBound(39.0); - -// h2.setLowerBound(39.1); -// h2.setUpperBound(42.0); - -// a1 = {{m1, m2}}; -// a2 = {{h1, h2}}; - -// avg1.setRange(1); -// avg2.setRange(2); -// avg4.setRange(4); - -// mar1.transitions = transitions; -// mar1.states = states; -// mar1.currentState = 1; - -// mar2.transitions = transitions; -// mar2.states = states; -// mar2.currentState = 2; - -// mar4.transitions = transitions; -// mar4.states = states; -// mar4.currentState = 4; - -// s.setId(1); -// s.setLowRisk(l); -// s.setMediumRisk(a1); -// s.setHighRisk(a2); -// s.setLowPercentage(Range(0, 0.2)); -// s.setMidPercentage(Range(0.21, 0.65)); -// s.setHighPercentage(Range(0.66, 1.0)); -// } -// }; - -// TEST_F(IntegrationTest, IntegrationLow) { -// ASSERT_EQ(mar2.currentState, 2); - -// avg2.insert(mar2.calculate_state(), "thermometer"); -// avg2.insert(mar2.calculate_state(), "thermometer"); -// double data = avg2.getValue("thermometer "); - -// ASSERT_LE(data, 37.5); -// ASSERT_LE(36.5, data); - -// ASSERT_LE(s.evaluateNumber(data), 0.2); -// ASSERT_LE(0.0, s.evaluateNumber(data)); -// } - -// TEST_F(IntegrationTest, IntegrationHigh) { -// ASSERT_EQ(mar4.currentState, 4); -// avg4.insert(mar4.calculate_state(), "thermometer"); -// avg4.insert(mar4.calculate_state(), "thermometer"); -// double data = avg4.getValue("thermometer "); - -// ASSERT_LE(data, 42); -// ASSERT_LE(39.1, data); - -// ASSERT_LE(s.evaluateNumber(data), 1.0); -// ASSERT_LE(0.66, s.evaluateNumber(data)); -// } - -// TEST_F(IntegrationTest, IntegrationMedium) { -// ASSERT_EQ(mar1.currentState, 1); -// avg1.insert(mar1.calculate_state(), "thermometer"); -// avg1.insert(mar1.calculate_state(), "thermometer"); -// double data = avg1.getValue("thermometer "); - -// ASSERT_LE(data, 36.4); -// ASSERT_LE(33.1, data); - -// ASSERT_LE(s.evaluateNumber(data), 0.65); -// ASSERT_LE(0.21, s.evaluateNumber(data)); -// } \ No newline at end of file +/* +#include +#include + +#include "libbsn/range/Range.hpp" +#include "libbsn/generator/Markov.hpp" +#include "libbsn/filters/MovingAverage.hpp" +#include "libbsn/configuration/SensorConfiguration.hpp" + +using namespace std; +using namespace bsn::range; +using namespace bsn::filters; +using namespace bsn::configuration; +using namespace bsn::generator; + +class IntegrationTest : public testing::Test +{ +protected: + MovingAverage avg1; + MovingAverage avg2; + MovingAverage avg4; + + Markov mar1; + Markov mar2; + Markov mar4; + + Range l; + Range m1; + Range m2; + Range h1; + Range h2; + + array a1; + array a2; + + SensorConfiguration s; + array transitions; + array states; + + IntegrationTest() : avg1(), avg2(), avg4(), mar1(), mar2(), mar4(), + l(), m1(), m2(), h1(), h2(), a1(), + a2(), s(), transitions(), states() {} + + virtual void SetUp() + { + transitions = {{0, 100, 0, 0, 0, + 0, 0, 100, 0, 0, + 0, 0, 0, 100, 0, + 0, 0, 0, 0, 100, + 100, 0, 0, 0, 0}}; + + states = {{Range(30.0, 33.0), Range(33.1, 36.4), Range(36.5, 37.5), Range(37.6, 39.0), Range(39.1, 42.0)}}; + + h1.setLowerBound(30.0); + h1.setUpperBound(33.0); + + m1.setLowerBound(33.1); + m1.setUpperBound(36.4); + + l.setLowerBound(36.5); + l.setUpperBound(37.5); + + m2.setLowerBound(37.6); + m2.setUpperBound(39.0); + + h2.setLowerBound(39.1); + h2.setUpperBound(42.0); + + a1 = {{m1, m2}}; + a2 = {{h1, h2}}; + + avg1.setRange(1); + avg2.setRange(2); + avg4.setRange(4); + + mar1.transitions = transitions; + mar1.states = states; + mar1.currentState = 1; + + mar2.transitions = transitions; + mar2.states = states; + mar2.currentState = 2; + + mar4.transitions = transitions; + mar4.states = states; + mar4.currentState = 4; + + s.setId(1); + s.setLowRisk(l); + s.setMediumRisk(a1); + s.setHighRisk(a2); + s.setLowPercentage(Range(0, 0.2)); + s.setMidPercentage(Range(0.21, 0.65)); + s.setHighPercentage(Range(0.66, 1.0)); + } +}; + +TEST_F(IntegrationTest, IntegrationLow) +{ + ASSERT_EQ(mar2.currentState, 2); + + avg2.insert(mar2.calculate_state(), "thermometer"); + avg2.insert(mar2.calculate_state(), "thermometer"); + double data = avg2.getValue("thermometer"); + + ASSERT_LE(data, 37.5); + ASSERT_LE(36.5, data); + + ASSERT_LE(s.evaluateNumber(data), 0.2); + ASSERT_LE(0.0, s.evaluateNumber(data)); +} + +TEST_F(IntegrationTest, IntegrationHigh) +{ + ASSERT_EQ(mar4.currentState, 4); + avg4.insert(mar4.calculate_state(), "thermometer"); + avg4.insert(mar4.calculate_state(), "thermometer"); + double data = avg4.getValue("thermometer"); + + ASSERT_LE(data, 42); + ASSERT_LE(39.1, data); + + ASSERT_LE(s.evaluateNumber(data), 1.0); + ASSERT_LE(0.66, s.evaluateNumber(data)); +} + +TEST_F(IntegrationTest, IntegrationMedium) +{ + ASSERT_EQ(mar1.currentState, 1); + avg1.insert(mar1.calculate_state(), "thermometer"); + avg1.insert(mar1.calculate_state(), "thermometer"); + double data = avg1.getValue("thermometer"); + + ASSERT_LE(data, 36.4); + ASSERT_LE(33.1, data); + + ASSERT_LE(s.evaluateNumber(data), 0.65); + ASSERT_LE(0.21, s.evaluateNumber(data)); +} +*/ \ No newline at end of file diff --git a/src/libbsn/test/main.cpp b/src/libbsn/test/main.cpp index 23f41c29..8608288d 100644 --- a/src/libbsn/test/main.cpp +++ b/src/libbsn/test/main.cpp @@ -1,6 +1,7 @@ #include -int main(int argc, char **argv) { +int main(int argc, char **argv) +{ testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } \ No newline at end of file diff --git a/src/sa-bsn/configurations/knowledge_repository/test_data_access.launch b/src/sa-bsn/configurations/knowledge_repository/test_data_access.launch new file mode 100644 index 00000000..0131707a --- /dev/null +++ b/src/sa-bsn/configurations/knowledge_repository/test_data_access.launch @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/sa-bsn/configurations/logging_infrastructure/test_logger.launch b/src/sa-bsn/configurations/logging_infrastructure/test_logger.launch new file mode 100644 index 00000000..56fcd67a --- /dev/null +++ b/src/sa-bsn/configurations/logging_infrastructure/test_logger.launch @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/sa-bsn/configurations/system_manager/test_strategy_enactor.launch b/src/sa-bsn/configurations/system_manager/test_strategy_enactor.launch new file mode 100644 index 00000000..5e9a5f69 --- /dev/null +++ b/src/sa-bsn/configurations/system_manager/test_strategy_enactor.launch @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/configurations/system_manager/test_strategy_manager.launch b/src/sa-bsn/configurations/system_manager/test_strategy_manager.launch new file mode 100644 index 00000000..26749ae8 --- /dev/null +++ b/src/sa-bsn/configurations/system_manager/test_strategy_manager.launch @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/configurations/target_system/real_sensor_g3t1_1.launch b/src/sa-bsn/configurations/target_system/real_sensor_g3t1_1.launch index a8db95ab..7053b383 100644 --- a/src/sa-bsn/configurations/target_system/real_sensor_g3t1_1.launch +++ b/src/sa-bsn/configurations/target_system/real_sensor_g3t1_1.launch @@ -1,6 +1,6 @@ - + diff --git a/src/sa-bsn/configurations/target_system/real_sensor_g3t1_2.launch b/src/sa-bsn/configurations/target_system/real_sensor_g3t1_2.launch index 6e6e1404..9f858849 100644 --- a/src/sa-bsn/configurations/target_system/real_sensor_g3t1_2.launch +++ b/src/sa-bsn/configurations/target_system/real_sensor_g3t1_2.launch @@ -1,6 +1,6 @@ - + diff --git a/src/sa-bsn/configurations/target_system/real_sensor_g3t1_3.launch b/src/sa-bsn/configurations/target_system/real_sensor_g3t1_3.launch index df9c3c70..9785cda3 100644 --- a/src/sa-bsn/configurations/target_system/real_sensor_g3t1_3.launch +++ b/src/sa-bsn/configurations/target_system/real_sensor_g3t1_3.launch @@ -1,6 +1,6 @@ - + diff --git a/src/sa-bsn/configurations/target_system/real_sensor_g3t1_4.launch b/src/sa-bsn/configurations/target_system/real_sensor_g3t1_4.launch new file mode 100644 index 00000000..b92bf245 --- /dev/null +++ b/src/sa-bsn/configurations/target_system/real_sensor_g3t1_4.launch @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sa-bsn/configurations/target_system/real_sensor_g3t1_5.launch b/src/sa-bsn/configurations/target_system/real_sensor_g3t1_5.launch new file mode 100644 index 00000000..c0ab560d --- /dev/null +++ b/src/sa-bsn/configurations/target_system/real_sensor_g3t1_5.launch @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sa-bsn/configurations/target_system/real_sensor_g3t1_6.launch b/src/sa-bsn/configurations/target_system/real_sensor_g3t1_6.launch new file mode 100644 index 00000000..bd9e6ac4 --- /dev/null +++ b/src/sa-bsn/configurations/target_system/real_sensor_g3t1_6.launch @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sa-bsn/configurations/target_system/test_effector.launch b/src/sa-bsn/configurations/target_system/test_effector.launch new file mode 100644 index 00000000..befabe6a --- /dev/null +++ b/src/sa-bsn/configurations/target_system/test_effector.launch @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/sa-bsn/configurations/target_system/test_g3t1_1.launch b/src/sa-bsn/configurations/target_system/test_g3t1_1.launch new file mode 100644 index 00000000..8f8b3600 --- /dev/null +++ b/src/sa-bsn/configurations/target_system/test_g3t1_1.launch @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/configurations/target_system/test_g3t1_2.launch b/src/sa-bsn/configurations/target_system/test_g3t1_2.launch new file mode 100644 index 00000000..a5c9ace2 --- /dev/null +++ b/src/sa-bsn/configurations/target_system/test_g3t1_2.launch @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/configurations/target_system/test_g3t1_3.launch b/src/sa-bsn/configurations/target_system/test_g3t1_3.launch new file mode 100644 index 00000000..e5d3708c --- /dev/null +++ b/src/sa-bsn/configurations/target_system/test_g3t1_3.launch @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/configurations/target_system/test_g3t1_4.launch b/src/sa-bsn/configurations/target_system/test_g3t1_4.launch new file mode 100644 index 00000000..73c3a024 --- /dev/null +++ b/src/sa-bsn/configurations/target_system/test_g3t1_4.launch @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sa-bsn/configurations/target_system/test_g3t1_5.launch b/src/sa-bsn/configurations/target_system/test_g3t1_5.launch new file mode 100644 index 00000000..1d05f036 --- /dev/null +++ b/src/sa-bsn/configurations/target_system/test_g3t1_5.launch @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sa-bsn/configurations/target_system/test_g3t1_6.launch b/src/sa-bsn/configurations/target_system/test_g3t1_6.launch new file mode 100644 index 00000000..4904ae25 --- /dev/null +++ b/src/sa-bsn/configurations/target_system/test_g3t1_6.launch @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sa-bsn/configurations/target_system/test_g4t1.launch b/src/sa-bsn/configurations/target_system/test_g4t1.launch new file mode 100644 index 00000000..2d1d3b11 --- /dev/null +++ b/src/sa-bsn/configurations/target_system/test_g4t1.launch @@ -0,0 +1,5 @@ + + + + + diff --git a/src/sa-bsn/configurations/target_system/test_patient.launch b/src/sa-bsn/configurations/target_system/test_patient.launch new file mode 100644 index 00000000..7bf78629 --- /dev/null +++ b/src/sa-bsn/configurations/target_system/test_patient.launch @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sa-bsn/configurations/target_system/test_probe.launch b/src/sa-bsn/configurations/target_system/test_probe.launch new file mode 100644 index 00000000..b8c2148b --- /dev/null +++ b/src/sa-bsn/configurations/target_system/test_probe.launch @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/sa-bsn/environment/patient/CMakeLists.txt b/src/sa-bsn/environment/patient/CMakeLists.txt index 835c7802..fb6309cf 100644 --- a/src/sa-bsn/environment/patient/CMakeLists.txt +++ b/src/sa-bsn/environment/patient/CMakeLists.txt @@ -29,4 +29,7 @@ FILE(GLOB ${PROJECT_NAME}-src "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") SET(patient-src "${CMAKE_CURRENT_SOURCE_DIR}/src/PatientModule.cpp") ADD_EXECUTABLE (patient "${CMAKE_CURRENT_SOURCE_DIR}/apps/patient.cpp" ${${PROJECT_NAME}-src} ${patient-src}) TARGET_LINK_LIBRARIES (patient ${catkin_LIBRARIES} ${LIBRARIES}) -ADD_DEPENDENCIES(patient services_generate_messages_cpp) \ No newline at end of file +ADD_DEPENDENCIES(patient services_generate_messages_cpp) + +########################################################################### +# Add tests diff --git a/src/sa-bsn/target_system/components/component/CMakeLists.txt b/src/sa-bsn/target_system/components/component/CMakeLists.txt index 28426f77..f11bf21c 100644 --- a/src/sa-bsn/target_system/components/component/CMakeLists.txt +++ b/src/sa-bsn/target_system/components/component/CMakeLists.txt @@ -1,16 +1,24 @@ -CMAKE_MINIMUM_REQUIRED (VERSION 2.8.3) +CMAKE_MINIMUM_REQUIRED(VERSION 2.8.3) PROJECT(component) add_compile_options(-std=c++11) ## Find catkin and any catkin packages -FIND_PACKAGE(catkin REQUIRED COMPONENTS roscpp std_msgs genmsg messages archlib lepton libbsn) +FIND_PACKAGE(catkin REQUIRED COMPONENTS + roscpp + std_msgs + genmsg + messages + archlib + lepton + libbsn +) # Export catkin package. CATKIN_PACKAGE( - INCLUDE_DIRS include - LIBRARIES ${PROJECT_NAME} - CATKIN_DEPENDS messages message_runtime archlib lepton libbsn + INCLUDE_DIRS include + LIBRARIES ${PROJECT_NAME} + CATKIN_DEPENDS messages message_runtime archlib lepton libbsn ) ########################################################################### @@ -24,43 +32,99 @@ INCLUDE_DIRECTORIES(include) # Build this project. FILE(GLOB ${PROJECT_NAME}-src "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") +# Add executables for G3T1 series (g3t1_1, g3t1_2, etc.) SET(g3t1_1-src "${CMAKE_CURRENT_SOURCE_DIR}/src/g3t1_1/G3T1_1.cpp") -ADD_EXECUTABLE (g3t1_1 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g3t1_1.cpp" ${${PROJECT_NAME}-src} ${g3t1_1-src}) -TARGET_LINK_LIBRARIES (g3t1_1 ${catkin_LIBRARIES} ${LIBRARIES}) +ADD_EXECUTABLE(g3t1_1 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g3t1_1.cpp" ${${PROJECT_NAME}-src} ${g3t1_1-src}) +TARGET_LINK_LIBRARIES(g3t1_1 ${catkin_LIBRARIES} ${LIBRARIES}) ADD_DEPENDENCIES(g3t1_1 messages_generate_messages_cpp) SET(g3t1_2-src "${CMAKE_CURRENT_SOURCE_DIR}/src/g3t1_2/G3T1_2.cpp") -ADD_EXECUTABLE (g3t1_2 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g3t1_2.cpp" ${${PROJECT_NAME}-src} ${g3t1_2-src}) -TARGET_LINK_LIBRARIES (g3t1_2 ${catkin_LIBRARIES} ${LIBRARIES}) +ADD_EXECUTABLE(g3t1_2 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g3t1_2.cpp" ${${PROJECT_NAME}-src} ${g3t1_2-src}) +TARGET_LINK_LIBRARIES(g3t1_2 ${catkin_LIBRARIES} ${LIBRARIES}) ADD_DEPENDENCIES(g3t1_2 messages_generate_messages_cpp) SET(g3t1_3-src "${CMAKE_CURRENT_SOURCE_DIR}/src/g3t1_3/G3T1_3.cpp") -ADD_EXECUTABLE (g3t1_3 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g3t1_3.cpp" ${${PROJECT_NAME}-src} ${g3t1_3-src}) -TARGET_LINK_LIBRARIES (g3t1_3 ${catkin_LIBRARIES} ${LIBRARIES}) +ADD_EXECUTABLE(g3t1_3 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g3t1_3.cpp" ${${PROJECT_NAME}-src} ${g3t1_3-src}) +TARGET_LINK_LIBRARIES(g3t1_3 ${catkin_LIBRARIES} ${LIBRARIES}) ADD_DEPENDENCIES(g3t1_3 messages_generate_messages_cpp) SET(g3t1_4-src "${CMAKE_CURRENT_SOURCE_DIR}/src/g3t1_4/G3T1_4.cpp") -ADD_EXECUTABLE (g3t1_4 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g3t1_4.cpp" ${${PROJECT_NAME}-src} ${g3t1_4-src}) -TARGET_LINK_LIBRARIES (g3t1_4 ${catkin_LIBRARIES} ${LIBRARIES}) +ADD_EXECUTABLE(g3t1_4 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g3t1_4.cpp" ${${PROJECT_NAME}-src} ${g3t1_4-src}) +TARGET_LINK_LIBRARIES(g3t1_4 ${catkin_LIBRARIES} ${LIBRARIES}) ADD_DEPENDENCIES(g3t1_4 messages_generate_messages_cpp) SET(g3t1_5-src "${CMAKE_CURRENT_SOURCE_DIR}/src/g3t1_5/G3T1_5.cpp") -ADD_EXECUTABLE (g3t1_5 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g3t1_5.cpp" ${${PROJECT_NAME}-src} ${g3t1_5-src}) -TARGET_LINK_LIBRARIES (g3t1_5 ${catkin_LIBRARIES} ${LIBRARIES}) +ADD_EXECUTABLE(g3t1_5 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g3t1_5.cpp" ${${PROJECT_NAME}-src} ${g3t1_5-src}) +TARGET_LINK_LIBRARIES(g3t1_5 ${catkin_LIBRARIES} ${LIBRARIES}) ADD_DEPENDENCIES(g3t1_5 messages_generate_messages_cpp) SET(g3t1_6-src "${CMAKE_CURRENT_SOURCE_DIR}/src/g3t1_6/G3T1_6.cpp") -ADD_EXECUTABLE (g3t1_6 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g3t1_6.cpp" ${${PROJECT_NAME}-src} ${g3t1_6-src}) -TARGET_LINK_LIBRARIES (g3t1_6 ${catkin_LIBRARIES} ${LIBRARIES}) +ADD_EXECUTABLE(g3t1_6 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g3t1_6.cpp" ${${PROJECT_NAME}-src} ${g3t1_6-src}) +TARGET_LINK_LIBRARIES(g3t1_6 ${catkin_LIBRARIES} ${LIBRARIES}) ADD_DEPENDENCIES(g3t1_6 messages_generate_messages_cpp) SET(g4t1-src "${CMAKE_CURRENT_SOURCE_DIR}/src/g4t1/G4T1.cpp") -ADD_EXECUTABLE (g4t1 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g4t1.cpp" ${${PROJECT_NAME}-src} ${g4t1-src}) -TARGET_LINK_LIBRARIES (g4t1 ${catkin_LIBRARIES} ${LIBRARIES}) +ADD_EXECUTABLE(g4t1 "${CMAKE_CURRENT_SOURCE_DIR}/apps/g4t1.cpp" ${${PROJECT_NAME}-src} ${g4t1-src}) +TARGET_LINK_LIBRARIES(g4t1 ${catkin_LIBRARIES} ${LIBRARIES}) ADD_DEPENDENCIES(g4t1 messages_generate_messages_cpp) +SET(patient_data_service-test "${CMAKE_CURRENT_SOURCE_DIR}/test/patient_data_service.cpp") +add_executable(patient_data_service "${CMAKE_CURRENT_SOURCE_DIR}/test/patient_data_service.cpp" ${${PROJECT_NAME}-test} ${patient_data_service-test}) +target_link_libraries(patient_data_service + ${catkin_LIBRARIES} + ${LIBRARIES} +) +ADD_DEPENDENCIES(patient_data_service messages_generate_messages_cpp) + +# Adicionar flags de cobertura SOMENTE se estivermos em modo Debug +if(CMAKE_BUILD_TYPE MATCHES Debug) + message(STATUS "Build de Debug detectado: Habilitando Cobertura de Código (GCOV)") + + # -O0: Desativa otimizações (essencial para que a linha do código bata com a execução) + # -fprofile-arcs: Instrumenta o código para contar execuções + # -ftest-coverage: Gera dados para o GCOV + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage -O0") + + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fprofile-arcs -ftest-coverage") +endif() + +if (CATKIN_ENABLE_TESTING) + find_package(rostest REQUIRED) + find_package(ros_pytest REQUIRED) + add_rostest(test/test_bdd/test_target_system.launch) + add_rostest(test/test_bdd/test_health_status.launch) + add_rostest(test/test_bdd/test_data_persistance.launch) + add_rostest(test/test_bdd/test_knowledge_repo.launch) + add_rostest(test/test_bdd/test_managing_system.launch) + add_rostest(test/test_bdd/test_injector.launch) + add_rostest(test/test_bdd/test_check_bsn.launch) + + # unit test + add_rostest(test/unit/test_G3T1_1.launch) + add_rostest(test/unit/test_G3T1_2.launch) + add_rostest(test/unit/test_G3T1_3.launch) + add_rostest(test/unit/test_G3T1_4.launch) + add_rostest(test/unit/test_G3T1_5.launch) + add_rostest(test/unit/test_G3T1_6.launch) + + add_rostest(test/unit/test_G3T1_1_connected.launch) + add_rostest(test/unit/test_G3T1_2_connected.launch) + add_rostest(test/unit/test_G3T1_3_connected.launch) + add_rostest(test/unit/test_G3T1_4_connected.launch) + add_rostest(test/unit/test_G3T1_5_connected.launch) + add_rostest(test/unit/test_G3T1_6_connected.launch) + + add_rostest(test/unit/test_G4T1.launch) +endif() + + +#if(CATKIN_ENABLE_TESTING) +# catkin_add_nosetests(test/integration/test_my_cpp_node.py) +# catkin_add_nosetests(test/bdd/behave_config.py) +#endif() + ########################################################################### -# Install this project. +# Install this project (if needed) #INSTALL(TARGETS ${PROJECT_NAME} # ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} # LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} @@ -68,3 +132,4 @@ ADD_DEPENDENCIES(g4t1 messages_generate_messages_cpp) #INSTALL(DIRECTORY include/${PROJECT_NAME}/ # DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}) + diff --git a/src/sa-bsn/target_system/components/component/include/component/g4t1/G4T1.hpp b/src/sa-bsn/target_system/components/component/include/component/g4t1/G4T1.hpp index 9c66a744..4f417cc9 100644 --- a/src/sa-bsn/target_system/components/component/include/component/g4t1/G4T1.hpp +++ b/src/sa-bsn/target_system/components/component/include/component/g4t1/G4T1.hpp @@ -12,7 +12,7 @@ #include "libbsn/processor/Processor.hpp" #include "libbsn/utils/utils.hpp" -#include "component/CentralHub.hpp" +#include "component/CentralHub.hpp" #include "archlib/target_system/Component.hpp" #include "archlib/AdaptationCommand.h" @@ -20,54 +20,55 @@ #include "messages/SensorData.h" #include "messages/TargetSystemData.h" -class G4T1 : public CentralHub { - - public: - G4T1(int &argc, char **argv, const std::string &name); - virtual ~G4T1(); - - private: - G4T1(const G4T1 & /*obj*/); - G4T1 &operator=(const G4T1 & /*obj*/); - - std::string makePacket(); - std::vector getPatientStatus(); - int32_t getSensorId(std::string type); - - public: - virtual void setUp(); - virtual void tearDown(); - - virtual void collect(const messages::SensorData::ConstPtr& sensor_data); - virtual void process(); - virtual void transfer(); - - private: - double patient_status; - - double abps_risk; - double abpd_risk; - double oxi_risk; - double ecg_risk; - double trm_risk; - double glc_risk; - - double abps_batt; - double abpd_batt; - double oxi_batt; - double ecg_batt; - double trm_batt; - double glc_batt; - - double abps_raw; - double abpd_raw; - double oxi_raw; - double ecg_raw; - double trm_raw; - double glc_raw; - - ros::Publisher pub; - bool lost_packt; +class G4T1 : public CentralHub +{ + +public: + G4T1(int &argc, char **argv, const std::string &name); + virtual ~G4T1(); + +private: + G4T1(const G4T1 & /*obj*/); + G4T1 &operator=(const G4T1 & /*obj*/); + + std::string makePacket(); + std::vector getPatientStatus(); + int32_t getSensorId(std::string type); + +public: + virtual void setUp(); + virtual void tearDown(); + + virtual void collect(const messages::SensorData::ConstPtr &sensor_data); + virtual void process(); + virtual void transfer(); + + // private: + double patient_status; + + double abps_risk; + double abpd_risk; + double oxi_risk; + double ecg_risk; + double trm_risk; + double glc_risk; + + double abps_batt; + double abpd_batt; + double oxi_batt; + double ecg_batt; + double trm_batt; + double glc_batt; + + double abps_raw; + double abpd_raw; + double oxi_raw; + double ecg_raw; + double trm_raw; + double glc_raw; + + ros::Publisher pub; + bool lost_packt; }; -#endif \ No newline at end of file +#endif \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/package.xml b/src/sa-bsn/target_system/components/component/package.xml index b43cfee7..43b1843d 100644 --- a/src/sa-bsn/target_system/components/component/package.xml +++ b/src/sa-bsn/target_system/components/component/package.xml @@ -18,15 +18,22 @@ archlib lepton libbsn + python3 + pybind11 - + python3 std_msgs messages message_runtime archlib lepton libbsn - + pybind11 + + ros_pytest + python-pytest + rosunit + rostest diff --git a/src/sa-bsn/target_system/components/component/src/bindings/example.cpp b/src/sa-bsn/target_system/components/component/src/bindings/example.cpp new file mode 100644 index 00000000..54232880 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/src/bindings/example.cpp @@ -0,0 +1,10 @@ +#include + +namespace py = pybind11; + +PYBIND11_MODULE(example, m) +{ + m.doc() = "Example Python module using Pybind11"; // Module docstring + m.def("add", [](int a, int b) + { return a + b; }, "A function that adds two numbers"); +} diff --git a/src/sa-bsn/target_system/components/component/src/g3t1_1/G3T1_1.cpp b/src/sa-bsn/target_system/components/component/src/g3t1_1/G3T1_1.cpp index ac3198d9..46589b11 100644 --- a/src/sa-bsn/target_system/components/component/src/g3t1_1/G3T1_1.cpp +++ b/src/sa-bsn/target_system/components/component/src/g3t1_1/G3T1_1.cpp @@ -9,27 +9,27 @@ using namespace bsn::range; using namespace bsn::generator; using namespace bsn::configuration; -G3T1_1::G3T1_1(int &argc, char **argv, const std::string &name) : - Sensor(argc, argv, name, "oximeter", true, 1, bsn::resource::Battery("oxi_batt", 100, 100, 1), false), - markov(), - dataGenerator(), - filter(1), - sensorConfig(), - collected_risk() {} +G3T1_1::G3T1_1(int &argc, char **argv, const std::string &name) : Sensor(argc, argv, name, "oximeter", true, 1, bsn::resource::Battery("oxi_batt", 100, 100, 1), false), + markov(), + dataGenerator(), + filter(1), + sensorConfig(), + collected_risk() {} G3T1_1::~G3T1_1() {} -void G3T1_1::setUp() { +void G3T1_1::setUp() +{ Component::setUp(); std::string s; - std::array ranges; + std::array ranges; handle.getParam("start", shouldStart); - + { // Configure markov chain - std::vector lrs,mrs0,hrs0,mrs1,hrs1; + std::vector lrs, mrs0, hrs0, mrs1, hrs1; handle.getParam("oxigenation_LowRisk", s); lrs = bsn::utils::split(s, ','); @@ -51,16 +51,16 @@ void G3T1_1::setUp() { { // Configure sensor configuration Range low_range = ranges[2]; - - std::array midRanges; + + std::array midRanges; midRanges[0] = ranges[1]; midRanges[1] = ranges[3]; - - std::array highRanges; + + std::array highRanges; highRanges[0] = ranges[0]; highRanges[1] = ranges[4]; - std::array percentages; + std::array percentages; handle.getParam("lowrisk", s); std::vector low_p = bsn::utils::split(s, ','); @@ -76,47 +76,56 @@ void G3T1_1::setUp() { sensorConfig = SensorConfiguration(0, low_range, midRanges, highRanges, percentages); } - - { //Check for instant recharge parameter + + { // Check for instant recharge parameter handle.getParam("instant_recharge", instant_recharge); } } -void G3T1_1::tearDown() { +void G3T1_1::tearDown() +{ Component::tearDown(); } -double G3T1_1::collect() { +double G3T1_1::collect() +{ double m_data = 0; std::string res; - if(connected_sensor) { + if (connected_sensor) + { ros::ServiceClient client = handle.serviceClient("spo2"); std_srvs::SetBool srv; srv.request.data = true; - if (client.call(srv)) { + if (client.call(srv)) + { res = srv.response.message; m_data = std::stof(res); ROS_INFO("new data collected: [%s]", std::to_string(m_data).c_str()); - } else { + } + else + { ROS_INFO("error collecting data"); } - } else{ + } + else + { ros::ServiceClient client = handle.serviceClient("getPatientData"); services::PatientData srv; srv.request.vitalSign = "oxigenation"; - if (client.call(srv)) { + if (client.call(srv)) + { m_data = srv.response.data; ROS_INFO("new data collected: [%s]", std::to_string(m_data).c_str()); - } else { + } + else + { ROS_INFO("error collecting data"); } } - - battery.consume(BATT_UNIT); cost += BATT_UNIT; @@ -125,24 +134,28 @@ double G3T1_1::collect() { return m_data; } -double G3T1_1::process(const double &m_data) { +double G3T1_1::process(const double &m_data) +{ double filtered_data; - + filter.insert(m_data); filtered_data = filter.getValue(); - battery.consume(BATT_UNIT*filter.getRange()); - cost += BATT_UNIT*filter.getRange(); + battery.consume(BATT_UNIT * filter.getRange()); + cost += BATT_UNIT * filter.getRange(); ROS_INFO("filtered data: [%s]", std::to_string(filtered_data).c_str()); return filtered_data; } -void G3T1_1::transfer(const double &m_data) { +void G3T1_1::transfer(const double &m_data) +{ double risk; risk = sensorConfig.evaluateNumber(m_data); - if (risk < 0 || risk > 100) throw std::domain_error("risk data out of boundaries"); - if (label(risk) != label(collected_risk)) throw std::domain_error("sensor accuracy fail"); + if (risk < 0 || risk > 100) + throw std::domain_error("risk data out of boundaries"); + if (label(risk) != label(collected_risk)) + throw std::domain_error("sensor accuracy fail"); ros::NodeHandle handle; data_pub = handle.advertise("oximeter_data", 10); @@ -159,15 +172,23 @@ void G3T1_1::transfer(const double &m_data) { ROS_INFO("risk calculated and transferred: [%.2f%%]", risk); } -std::string G3T1_1::label(double &risk) { +std::string G3T1_1::label(double &risk) +{ std::string ans; - if(sensorConfig.isLowRisk(risk)){ + if (sensorConfig.isLowRisk(risk)) + { ans = "low"; - } else if (sensorConfig.isMediumRisk(risk)) { + } + else if (sensorConfig.isMediumRisk(risk)) + { ans = "moderate"; - } else if (sensorConfig.isHighRisk(risk)) { + } + else if (sensorConfig.isHighRisk(risk)) + { ans = "high"; - } else { + } + else + { ans = "unknown"; } diff --git a/src/sa-bsn/target_system/components/component/test/asserts.py b/src/sa-bsn/target_system/components/component/test/asserts.py new file mode 100644 index 00000000..06018a11 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/asserts.py @@ -0,0 +1,172 @@ +import subprocess +from parsers import get_rosnode_info +import threading +import time +import rosnode + +TIMEOUT_SECONDS = 5 + +class Command(object): + """A class to execute a command and enforce a timeout.""" + def __init__(self, cmd): + self.cmd = cmd + self.process = None + self.stdout = None + self.stderr = None + self.returncode = None + + def run(self, timeout=10): + """Run the command, waiting for a maximum of 'timeout' seconds.""" + + # Function executed in a separate thread + def target(): + self.process = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # communicate() blocks until the process finishes + self.stdout, self.stderr = self.process.communicate() + self.returncode = self.process.returncode + + thread = threading.Thread(target=target) + thread.start() + + thread.join(timeout) + + if thread.is_alive(): + # If the thread is still alive, the timeout expired. + try: + # Terminate the process gently, then forcefully kill if needed + self.process.terminate() + time.sleep(0.1) + if thread.is_alive(): + os.kill(self.process.pid, 9) # Force kill + except OSError: + pass # Process already terminated + + # Raise the equivalent of the TimeoutExpired exception + raise Exception("TimeoutExpired") + + # If we reach here, the thread finished naturally. + return self.stdout, self.stderr, self.returncode + +def kill_node(node_name): + result = subprocess.Popen(['rosnode', 'kill', node_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = result.communicate() + assert not bool_node_is_active(node_name), "{} is active".format(node_name) + +def is_node_receiving_multiple_topics(node_name, expected_topics): + """ + Check if a node is receiving data from multiple topics. + + Parameters: + - node_name (str): The name of the ROS node to check. + - expected_topics (list): A list of topics that the node is expected to receive data from. + + Returns: + - bool: True if the node is receiving data from all the expected topics, False otherwise. + - missing_topics (list): List of topics that are missing if the node is not subscribed to all topics. + """ + try: + cmd = ['rosnode', 'info', node_name] + command = Command(cmd) + stdout, stderr, returncode = command.run(timeout=TIMEOUT_SECONDS) + + node_info = get_rosnode_info(returncode, stdout, stderr) + + subscriptions = [sub['topic'] for sub in node_info.get('subscriptions', [])] + + inbound_connections = [conn['topic'] for conn in node_info.get('connections', []) if 'inbound' in conn['direction']] + + missing_topics = [topic for topic in expected_topics if topic not in subscriptions and topic not in inbound_connections] + print('node {}, inbound_connections {} subscriptions: {}'.format(node_name, inbound_connections, subscriptions)) + print('missing_topics: {}'.format(missing_topics)) + + if not missing_topics: + return True, [] + else: + return False, missing_topics + + except Exception: + raise AssertionError("Timeout: Failed to check if node {} is receiving data.".format(node_name)) + +def is_node_publishing_to_topics(node_name, expected_topics): + """ + Check if a node is publishing to multiple expected topics. + + Parameters: + - node_name (str): The name of the ROS node to check. + - expected_topics (list): A list of topics that the node is expected to publish to. + + Returns: + - dict: A dictionary with the expected topics as keys and a tuple as the value. + The tuple contains (is_publishing (bool), missing_topics (list)). + """ + try: + node_data = subprocess.Popen(['rosnode', 'info', node_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = node_data.communicate() + + node_info = get_rosnode_info(node_data.returncode, stdout, stderr) + + publications = [pub['topic'] for pub in node_info.get('publications', [])] + + outbound_connections = [conn['topic'] for conn in node_info.get('connections', []) if 'outbound' in conn['direction']] + + missing_topics = [topic for topic in expected_topics if topic not in publications and topic not in outbound_connections] + print('node {}, outbound_connections {} publications: {}'.format(node_name, outbound_connections, publications)) + print('missing_topics: {}'.format(missing_topics)) + if not missing_topics: + print('node {} is publishing to all expected topics.'.format(node_name)) + return True, [] + else: + return False, missing_topics + + except Exception as e: + print("Error occurred while checking node {}: {}".format(node_name, str(e))) + raise AssertionError("Timeout: Failed to check if node {} is publishing to topics {}".format(node_name, expected_topics)) + +def assert_node_is_online(node_name): + rosnode_list = rosnode.get_node_names() + assert node_name in rosnode_list, "{} is not online".format(node_name) + +def node_is_active(node_names): + if isinstance(node_names, str): + node_names = [node_names] + + result = subprocess.Popen(['rosnode', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = result.communicate() + node_list = stdout.decode('utf-8').splitlines() + for node_name in node_names: + assert node_name in node_list, "{} is not online. Make sure give the system more time to start up.".format(node_name) + +def bool_node_is_active(node_names): + if isinstance(node_names, str): + node_names = [node_names] + + result = subprocess.Popen(['rosnode', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = result.communicate() + node_list = stdout.decode('utf-8').splitlines() + for node_name in node_names: + if node_name in node_list: + return True + + return False + +def check_time_performance(sensor_data, target_system_data, key, value, evaluate): + time_threshold=250000 + # Iterate over both lists and check for matching values and time condition + for i, sensor_risk in enumerate(sensor_data[key][evaluate]): + for j, target_risk in enumerate(target_system_data[value]): + print('SENSOR RISK of {}: {} TARGET RISK: {}'.format(key, sensor_risk, target_risk)) + if sensor_risk == target_risk: + # Parse time strings into floats + sensor_time = float(sensor_data[key]['%time'][i]) / 1e3 + target_time = float(target_system_data['%time'][j]) / 1e3 + + # Round and compare times + #rounded_sensor_time = round(sensor_time, -5) / 1e6 + #rounded_target_time = round(target_time, -5) / 1e6 + time_diff = sensor_time - target_time + print("diff: {}".format(time_diff)) + if time_diff < time_threshold: + return True + print('TIME DIFFERENCE in {}: {} us'.format(key, time_diff)) + + return False diff --git a/src/sa-bsn/target_system/components/component/test/bdd/behave_config.py b/src/sa-bsn/target_system/components/component/test/bdd/behave_config.py new file mode 100644 index 00000000..5c4753e4 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/bdd/behave_config.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python2 + +import sys +from behave.__main__ import main as behave_main + +if __name__ == '__main__': + sys.exit(behave_main("features")) diff --git a/src/sa-bsn/target_system/components/component/test/bdd/features/component.feature b/src/sa-bsn/target_system/components/component/test/bdd/features/component.feature new file mode 100644 index 00000000..1e135490 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/bdd/features/component.feature @@ -0,0 +1,6 @@ +Feature: ROS Component Testing + + Scenario: Verify message publishing + Given ROS is running + When I publish a message to "/test_topic" + Then the message should be received diff --git a/src/sa-bsn/target_system/components/component/test/bdd/steps/test_steps.py b/src/sa-bsn/target_system/components/component/test/bdd/steps/test_steps.py new file mode 100644 index 00000000..4b26f4de --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/bdd/steps/test_steps.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python2 + +from behave import given, when, then +import rospy +from std_msgs.msg import String +import subprocess +import time +received_message = None + +def message_callback(msg): + global received_message + received_message = msg.data + +@given('ROS is running') +def step_impl(context): + """ Ensure ROS is initialized. """ + if not rospy.core.is_initialized(): + rospy.init_node('bdd_test_node', anonymous=True) + +@when('I publish a message to "/test_topic"') +def step_impl(context): + """ Publish a message to a ROS topic. """ + global received_message + received_message = None # Reset message + + pub = rospy.Publisher('/test_topic', String, queue_size=10) + sub = rospy.Subscriber('/test_topic', String, message_callback) + + rospy.sleep(1) # Wait for subscriber connection + + test_msg = String() + test_msg.data = "Hello from Behave!" + pub.publish(test_msg) + + rospy.sleep(2) # Wait for message to be received + +@then('the message should be received') +def step_impl(context): + """ Check if the message was received. """ + assert received_message == "Hello from Behave!", "Message not received!" diff --git a/src/sa-bsn/target_system/components/component/test/conftest.py b/src/sa-bsn/target_system/components/component/test/conftest.py new file mode 100644 index 00000000..613428bb --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/conftest.py @@ -0,0 +1,102 @@ +from pytest_bdd import given, parsers, when, then +import rospy +import rosnode +import pytest +from asserts import is_node_receiving_multiple_topics, assert_node_is_online, is_node_publishing_to_topics +from parsers import capture_topic_data, process_real_time_topics + +FULL_SYSTEM = ['/collector', '/param_adapter', + '/g3t1_1', '/g3t1_2', '/g3t1_3', + '/g3t1_4', '/g3t1_5', '/g3t1_6', + '/g4t1'] + +@pytest.fixture(scope='module') +def context(): + """ + Fixture to hold context data for BDD tests. + """ + return {} + +# Shared step definitions + +@given("the ROS environment is on") +def ros_environment_is_on(): + rospy.sleep(1) # Give ROS some time to initialize + nodes = rosnode.get_node_names() + + assert len(nodes) > 0, "ROS environment is not running or no nodes are active." + +@given(parsers.parse("the {node_name} node is online")) +def node_is_online(context, node_name): + context['is_node_online'] = True + assert_node_is_online(node_name) + +@when(parsers.parse("I check if topics {topic} are outbound to {node}")) +def check_topic_outbound_to_node(context, topic, node): + if ',' in topic: + topic_list = topic.split(',') + else: + topic_list = [topic] + is_receiving, missing_topics = is_node_receiving_multiple_topics(node, topic_list) + context['is_node_online'] = missing_topics == [] + print('is_receiving: {}, for topic {} and node {}'.format(is_receiving, topic, node)) + print('missing_topics: {}'.format(missing_topics)) + assert is_receiving, "{} is missing data from these topics: {}".format(node, missing_topics) + +@when(parsers.parse("I check if topics {topic} are inbound from {node}")) +def check_topic_inbound_from_node(context, topic, node): + if ',' in topic: + topic_list = topic.split(',') + else: + topic_list = [topic] + is_receiving, missing_topics = is_node_publishing_to_topics(node, topic_list) + context['is_node_online'] = missing_topics == [] + assert is_receiving, "{} is missing data from these topics: {}".format(node, missing_topics) + +@when(parsers.parse("I check if topics {topic} are inbound and {topic_outbound} are outbound to {node}")) +def check_topic_inbound_and_outbound(context, topic, topic_outbound, node): + rospy.sleep(3) # Small delay to ensure topics are being published/subscribed + print('check') + if ',' in topic: + topic_list = topic.split(',') + else: + topic_list = [topic] + is_receiving, missing_topics = is_node_publishing_to_topics(node, topic_list) + + if ',' in topic_outbound: + topic_outbound_list = topic_outbound.split(',') + else: + topic_outbound_list = [topic_outbound] + is_publishing, missing_outbound_topics = is_node_receiving_multiple_topics(node, topic_outbound_list) + + context['is_node_online'] = missing_topics == [] and missing_outbound_topics == [] + assert is_receiving, "{} is missing data from these topics: {}".format(node, missing_topics) + assert is_publishing, "{} is missing data from these topics: {}".format(node, missing_topics) + +@then(parsers.parse("{node_name} node is connected appropriately")) +def node_connected_appropriately(context, node_name): + assert context['is_node_online'], "/{} node is not connected appropriately".format(node_name) + +@when('I listen to thermometer') +def listen_to_thermometer(context): + # Aguarda thread de erro terminar se existir + print('Listening to thermometer...') + if 'error_publisher_thread' in context: + print('error?') + context['error_publisher_thread'].join(timeout=3.0) + + context['sensor_data'] = {} + context['found_high_risk'] = [] + context['target_system_data'] = {} + context['non_sensor'] = {} + topics = [ + '/thermometer_data', + '/collect_energy_status', + '/persist', + '/log_energy_status', + '/TargetSystemData' + ] + print('Topics to listen: {}'.format(topics)) + + process_real_time_topics(context, capture_topic_data, topics) + print('finished listening to thermometer.') diff --git a/src/sa-bsn/target_system/components/component/test/features/patient_test/patient.launch b/src/sa-bsn/target_system/components/component/test/features/patient_test/patient.launch new file mode 100644 index 00000000..53780ede --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/features/patient_test/patient.launch @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sa-bsn/target_system/components/component/test/features/persistance_system.launch b/src/sa-bsn/target_system/components/component/test/features/persistance_system.launch new file mode 100644 index 00000000..a7a2bebe --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/features/persistance_system.launch @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/sa-bsn/target_system/components/component/test/features/sensor_execution.launch b/src/sa-bsn/target_system/components/component/test/features/sensor_execution.launch new file mode 100644 index 00000000..d5de5596 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/features/sensor_execution.launch @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/sa-bsn/target_system/components/component/test/features/sensor_execution_test.cpp b/src/sa-bsn/target_system/components/component/test/features/sensor_execution_test.cpp new file mode 100644 index 00000000..5a8f9bb0 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/features/sensor_execution_test.cpp @@ -0,0 +1,80 @@ +#include +#include +#include +#include +#include +#include + +ros::NodeHandle *nh; +bool emergency_detected = false; +ros::Time start_time; + +// Callback function for emergency detection +void targetSystemCallback(const messages::TargetSystemData::ConstPtr &msg) +{ + if (msg->patient_status > 65.0) // High-risk threshold + { + emergency_detected = true; + } +} + +// Mock service callback to simulate patient data collection +bool mockPatientDataService(services::PatientData::Request &req, services::PatientData::Response &res) +{ + if (req.vitalSign == "temperature") + { + res.data = 41.0; // Simulated high-risk temperature + return true; + } + return false; +} + +// Test case: Full data flow - G3T1_3 collects patient data, G4T1 detects emergency +TEST(SensorExecutionTest, EmergencyDetectionWithFullDataFlow) +{ + // Set up subscribers for emergency detection + ros::Subscriber sub = nh->subscribe("TargetSystemData", 10, targetSystemCallback); + + // Sleep to ensure nodes have stabilized + ros::Duration(1.0).sleep(); // Wait 1 second for stability + + // Create a service client for getPatientData + ros::ServiceClient client = nh->serviceClient("getPatientData"); + services::PatientData srv; + srv.request.vitalSign = "temperature"; + + // Call the service before checking emergency detection + if (client.call(srv)) + { + ROS_INFO("Service response: %.2f", srv.response.data); + } + else + { + FAIL() << "Failed to call getPatientData service!"; + } + + // Ensure the service is available before attempting to call it + bool service_available = ros::service::waitForService("getPatientData", ros::Duration(5.0)); + ASSERT_TRUE(service_available) << "The service getPatientData is not available."; + + // Simulate the data flow by triggering the callback and checking emergency detection + ros::Time timeout = ros::Time::now() + ros::Duration(1.0); + while (ros::Time::now() < timeout) + { + ros::spinOnce(); + if (emergency_detected) + break; + } + + // Verify that emergency was detected in time + ASSERT_TRUE(emergency_detected) << "G4T1 did not detect emergency in time!"; +} + +int main(int argc, char **argv) +{ + ros::init(argc, argv, "sensor_execution_test"); + ros::NodeHandle nh_local; + nh = &nh_local; + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/sa-bsn/target_system/components/component/test/features/test_Sensor.cpp b/src/sa-bsn/target_system/components/component/test/features/test_Sensor.cpp new file mode 100644 index 00000000..c141283d --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/features/test_Sensor.cpp @@ -0,0 +1,361 @@ +/* +#include +#include "component/g3t1_3/G3T1_3.hpp" +#include "ros/ros.h" +#include + +// Mock ROS NodeHandle +class MockNodeHandle : public ros::NodeHandle +{ +public: + bool getParam(const std::string ¶m_name, std::string ¶m_value) + { + if (param_name == "test_param") + { + param_value = "mock_value"; + return true; + } + if (param_name == "start") + { + param_value = "true"; + return true; + } + if (param_name == "temperature_LowRisk") + { + param_value = "36.5,37.5"; + return true; + } + if (param_name == "temperature_MidRisk0" || param_name == "temperature_MidRisk1") + { + param_value = "37.5,38.0"; + return true; + } + if (param_name == "temperature_HighRisk0" || param_name == "temperature_HighRisk1") + { + param_value = "38.0,39.0"; + return true; + } + if (param_name == "lowrisk") + { + param_value = "0,50"; + return true; + } + if (param_name == "midrisk") + { + param_value = "50,80"; + return true; + } + if (param_name == "highrisk") + { + param_value = "80,100"; + return true; + } + if (param_name == "instant_recharge") + { + param_value = "true"; + return true; + } + return false; + } +}; + +// Test Fixture +class G3T1_3Fixture : public ::testing::Test +{ +protected: + int argc = 0; + char **argv = nullptr; + G3T1_3 *sensor; + + G3T1_3Fixture() : argc(0), argv(nullptr) + { + ros::NodeHandle *mock_handle = new MockNodeHandle(); + sensor = new G3T1_3(argc, argv, "test_sensor"); + sensor->handle = *mock_handle; // Inject the mocked handle + } + + ~G3T1_3Fixture() + { + delete sensor; + } + + void SetUp() override + { + sensor->setUp(); + } + + void TearDown() override + { + sensor->tearDown(); + } +}; + +// Test: setUp and tearDown +TEST_F(G3T1_3Fixture, TestSetUpAndTearDown) +{ + EXPECT_NO_THROW(sensor->setUp()); + EXPECT_NO_THROW(sensor->tearDown()); +} + +// Test: collect +TEST_F(G3T1_3Fixture, TestCollect) +{ + double data = 0; + EXPECT_NO_THROW(data = sensor->collect()); + EXPECT_GE(data, 0); // Data should be non-negative +} + +// Test: process +TEST_F(G3T1_3Fixture, TestProcess) +{ + double raw_data = 37.0; // Simulated raw data + double filtered_data = 0; + EXPECT_NO_THROW(filtered_data = sensor->process(raw_data)); + EXPECT_GT(filtered_data, 0); // Processed data should be greater than 0 +} + +// Test: transfer +TEST_F(G3T1_3Fixture, TestTransfer) +{ + double valid_data = 37.5; // Simulated valid data + EXPECT_NO_THROW(sensor->transfer(valid_data)); + + // Test transfer with invalid data + double invalid_data = -1.0; // Out of bounds risk + EXPECT_THROW(sensor->transfer(invalid_data), std::domain_error); +} + +// Test: Label function (indirectly tested in collect and transfer) +/*TEST_F(G3T1_3Fixture, TestLabel) +{ + double risk_low = 45.0; + double risk_mid = 60.0; + double risk_high = 90.0; + std::string label; + + EXPECT_NO_THROW(label = sensor->label(risk_low)); + EXPECT_EQ(label, "low"); + + EXPECT_NO_THROW(label = sensor->label(risk_mid)); + EXPECT_EQ(label, "moderate"); + + EXPECT_NO_THROW(label = sensor->label(risk_high)); + EXPECT_EQ(label, "high"); +} + +TEST_F(G3T1_3Fixture, TestCollectWithLabel) +{ + double data = sensor->collect(); + ASSERT_NO_THROW(sensor->transfer(data)); // Indirectly validates `label` +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + ros::init(argc, argv, "test_g3t1_3"); + // ros::NodeHandle nh; + return RUN_ALL_TESTS(); +} +#include +#include "component/g3t1_3/G3T1_3.hpp" +#include "ros/ros.h" +#include "std_srvs/SetBool.h" +#include "services/PatientData.h" + +// Mock service callback for "temp" +bool mockTempCallback(std_srvs::SetBool::Request &req, std_srvs::SetBool::Response &res) +{ + res.success = true; + res.message = "37.0"; // Simulate temperature data + return true; +} + +// Mock service callback for "getPatientData" +bool mockPatientDataCallback(services::PatientData::Request &req, services::PatientData::Response &res) +{ + ROS_INFO_STREAM("mockPatientDataCallback called with vitalSign: " << req.vitalSign); + + if (req.vitalSign == "temperature") + { + res.data = 37.0; + } + else if (req.vitalSign == "heartRate") + { + res.data = 75.0; + } + else + { + res.data = -1.0; + } + + ROS_INFO_STREAM("mockPatientDataCallback returning data: " << res.data); + return true; +} + +// Test Fixture +class G3T1_3Fixture : public ::testing::Test +{ +protected: + int argc = 0; + char **argv = nullptr; + G3T1_3 *sensor; + ros::AsyncSpinner *spinner; + + G3T1_3Fixture() + { + sensor = new G3T1_3(argc, argv, "test_sensor"); + spinner = new ros::AsyncSpinner(1); // Use 1 thread for spinning + spinner->start(); + } + + ~G3T1_3Fixture() + { + spinner->stop(); + delete spinner; + delete sensor; + } + + void SetUp() override + { + ros::param::set("start", true); + ros::NodeHandle nh; + + ROS_INFO("Advertising mock services..."); + nh.advertiseService("temp", mockTempCallback); + nh.advertiseService("getPatientData", mockPatientDataCallback); + ROS_INFO("Mock services advertised successfully."); + + sensor->setUp(); + } + void TearDown() override + { + sensor->tearDown(); + } +}; + +// Test: setUp and tearDown +TEST_F(G3T1_3Fixture, TestSetUpAndTearDown) +{ + EXPECT_NO_THROW(sensor->setUp()); + EXPECT_NO_THROW(sensor->tearDown()); +} +TEST_F(G3T1_3Fixture, TestGetPatientData) +{ + ros::NodeHandle nh; + ros::ServiceClient client = nh.serviceClient("getPatientData"); + services::PatientData srv; + + // Ensure service is available before calling + ASSERT_TRUE(client.waitForExistence(ros::Duration(5.0))) << "Service 'getPatientData' not available"; + + srv.request.vitalSign = "temperature"; + ASSERT_TRUE(client.call(srv)) << "Service call failed for 'temperature'"; + EXPECT_EQ(srv.response.data, 37.0); + + srv.request.vitalSign = "heartRate"; + ASSERT_TRUE(client.call(srv)) << "Service call failed for 'heartRate'"; + EXPECT_EQ(srv.response.data, 75.0); + + srv.request.vitalSign = "unknown"; + ASSERT_TRUE(client.call(srv)) << "Service call failed for 'unknown'"; + EXPECT_EQ(srv.response.data, -1.0); +} +// Test: collect +TEST_F(G3T1_3Fixture, TestCollect) +{ + double data = 0; + EXPECT_NO_THROW(data = sensor->collect()); + EXPECT_GE(data, 0); // Data should be non-negative +} + +// Test: process +TEST_F(G3T1_3Fixture, TestProcess) +{ + double raw_data = 37.0; // Simulated raw data + double filtered_data = 0; + EXPECT_NO_THROW(filtered_data = sensor->process(raw_data)); + EXPECT_GT(filtered_data, 0); // Processed data should be greater than 0 +} + +// Test: transfer +TEST_F(G3T1_3Fixture, TestTransfer) +{ + double valid_data = 37.5; // Simulated valid data + EXPECT_NO_THROW(sensor->transfer(valid_data)); + + double invalid_data = -1.0; // Out of bounds risk + EXPECT_THROW(sensor->transfer(invalid_data), std::domain_error); +} + +// Main +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + ros::init(argc, argv, "test_g3t1_3"); + + ros::AsyncSpinner spinner(1); // Start a spinner for ROS callbacks + spinner.start(); + + int result = RUN_ALL_TESTS(); + + ros::shutdown(); // Ensure ROS is properly shut down + return result; +} + +#include +#include "ros/ros.h" +#include "ros/master.h" +#include +#include +TEST(SimpleROS, InitAndSpin) +{ + ros::NodeHandle nh; + + for (int i = 0; i < 10; ++i) + { // Limit retries to 10 attempts + if (!ros::master::check()) + { + FAIL() << "ROS master is not available. Please start roscore."; + return; + } + if (!ros::ok()) + { + FAIL() << "ROS shutdown detected."; + return; + } + ros::spinOnce(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } +} + +TEST(FailingTest, AlwaysFails) +{ + ASSERT_TRUE(false) << "This test is designed to fail."; +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + ros::init(argc, argv, "test_ros_minimal"); + + if (!ros::master::check()) + { + std::cerr << "ROS master is not running. Please start roscore." << std::endl; + return EXIT_FAILURE; + } + + return RUN_ALL_TESTS(); +} +*/ +#include + +TEST(ComponentTest, ExampleTest) +{ + EXPECT_EQ(1, 1); +} + +int main(int argc, char **argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/sa-bsn/target_system/components/component/test/integration/test_my_cpp_node.py b/src/sa-bsn/target_system/components/component/test/integration/test_my_cpp_node.py new file mode 100644 index 00000000..3d877c30 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/integration/test_my_cpp_node.py @@ -0,0 +1,12 @@ +#!/usr/bin/python3 + +import unittest + +class TestComponent(unittest.TestCase): + def test_example(self): + self.assertEqual(1, 1) + def test2_example(self): + self.assertEqual(3,3) + +if __name__ == '__main__': + unittest.main() diff --git a/src/sa-bsn/target_system/components/component/test/parsers.py b/src/sa-bsn/target_system/components/component/test/parsers.py new file mode 100644 index 00000000..19940f37 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/parsers.py @@ -0,0 +1,280 @@ +from concurrent.futures import ThreadPoolExecutor, as_completed +import re +import subprocess +import time +# from utils.constants import NON_SENSOR_TOPICS + +NON_SENSOR_TOPICS = [ + '/collect_energy_status', + '/persist', + '/log_energy_status', + '/TargetSystemData' +] + +def format_debug_data(data): + formatted_output = [] + + for topic, details in data.items(): + formatted_output.append("\n{0}\nTOPIC: {1}\n{0}".format('='*40, topic)) + + headers = list(details.keys()) + rows = zip(*details.values()) # Transpose to get row-wise data + + # Add headers + formatted_output.append(" | ".join(headers)) + formatted_output.append("-" * len(formatted_output[-1])) # Add separator + + # Add rows + for row in rows: + formatted_output.append(" | ".join(str(value) for value in row)) + + return "\n".join(formatted_output) + + +def format_entity(raw_string): + # Check if any words in the string start with an uppercase letter + words = raw_string.split() + if any(word[0].isupper() for word in words): + # If there are uppercase letters, format in camel case + formatted_string = ''.join(word.capitalize() for word in words) + else: + # Otherwise, format in snake case + formatted_string = '_'.join(word.lower() for word in words) + + # Add a forward slash at the beginning + return '/{0}'.format(formatted_string) + +def capture_topic_data(topic): + + if topic in NON_SENSOR_TOPICS: + parsed_data = parse_topic_data(topic, line_limit=10) + + return topic, parsed_data, False, None + parsed_data = parse_topic_data(topic, line_limit=10) + + if parsed_data is None: + print("Alerta: Falha ao analisar dados para o topico '{}'. parse_topic_data retornou None.".format(topic)) + return topic, {}, False, None + + high_risk_detected = any( + float(value) > 10 for value in parsed_data['risk'] # Check each value in each list + ) + risk_key = "{0}_risk".format(topic) # Append '_risk' to the data type + return topic, parsed_data, high_risk_detected, risk_key + +def get_rostopic_sensor_data(returncode, stdout, stderr): + if returncode != 0: + raise Exception("Error getting topic data: {0}".format(stderr.decode('utf-8'))) + + # Decode the output from bytes to string + output = stdout.decode('utf-8') + + # Parse the output using regex + data = {} + + # Match key-value pairs + header_seq = re.search(r'seq: (\d+)', output) + header_stamp_secs = re.search(r'secs: (\d+)', output) + header_stamp_nsecs = re.search(r'nsecs: (\d+)', output) + data_type = re.search(r'type: "(.*?)"', output) + data_value = re.search(r'data: ([\d\.]+)', output) + risk_value = re.search(r'risk: ([\d\.]+)', output) + batt_value = re.search(r'batt: ([\d\.]+)', output) + + # Fill the parsed data dictionary + if header_seq: + data['seq'] = int(header_seq.group(1)) + if header_stamp_secs: + data['stamp_secs'] = int(header_stamp_secs.group(1)) + if header_stamp_nsecs: + data['stamp_nsecs'] = int(header_stamp_nsecs.group(1)) + if data_type: + data['type'] = data_type.group(1) + if data_value: + data['data'] = float(data_value.group(1)) + if risk_value: + data['risk'] = float(risk_value.group(1)) + if batt_value: + data['batt'] = float(batt_value.group(1)) + + return data + +def get_rosnode_info(returncode, stdout, stderr): + if returncode != 0: + raise Exception("Error getting node info: {0}".format(stderr.decode('utf-8'))) + + # Decode the output from bytes to string + output = stdout.decode('utf-8') + lines = output.splitlines() + if lines and lines[-1].startswith("cannot contact"): + return "unreachable" + # Initialize dictionaries for storing parsed data + node_info = { + "publications": [], + "subscriptions": [], + "services": [], + "connections": [] + } + + # Helper function to parse lines with topic and type + def parse_topic_lines(start_index): + topics = [] + i = start_index + while i < len(lines) and lines[i].startswith(' * '): + line = lines[i].strip().split(' [') + topic = line[0][2:].strip() # Remove leading '* ' + type_ = line[1][:-1].strip() if len(line) > 1 else "unknown type" + topics.append({"topic": topic, "type": type_}) + i += 1 + return topics, i + + # Parse sections + i = 0 + while i < len(lines): + line = lines[i].strip() + + if line.startswith('Publications:'): + # Parse publications starting from the next line + node_info["publications"], i = parse_topic_lines(i + 1) + + elif line.startswith('Subscriptions:'): + # Parse subscriptions starting from the next line + node_info["subscriptions"], i = parse_topic_lines(i + 1) + + elif line.startswith('Services:'): + # Parse services starting from the next line + i += 1 + while i < len(lines) and lines[i].startswith(' * '): + service = lines[i].strip()[2:] # Remove leading '* ' + node_info["services"].append(service) + i += 1 + + elif line.startswith('Connections:'): + # Parse connections starting from the next line + i += 1 + while i < len(lines) and lines[i].startswith(' * topic:'): + connection = {} + connection["topic"] = lines[i].split(': ')[1].strip() + i += 1 + connection["to"] = lines[i].split(': ')[1].strip() + i += 1 + connection["direction"] = lines[i].split(': ')[1].strip() + i += 1 + connection["transport"] = lines[i].split(': ')[1].strip() + + # Move to the next line to check for the next connection + i += 1 + # Add the connection to the list + node_info["connections"].append(connection) + + else: + i += 1 + return node_info + + + +import threading +import Queue as queue # Python 2 uses Queue instead of queue + + +def enqueue_output(out, output_queue): + """ + Continuously reads lines from the process output and puts them into a queue. + This function is intended to be run in a separate thread. + """ + for line in iter(out.readline, ''): + output_queue.put(line.strip()) + out.close() + +def parse_topic_data(topic, line_limit=10): + """ + Capture CSV data from a ROS topic using Popen and organize it into a dictionary + with headers dynamically set from the first line. Stops reading once line_limit + is reached or after the timeout if no data is received. + """ + process = subprocess.Popen( + ['rostopic', 'echo', '-p', '--offset', topic], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + output_queue = queue.Queue() + thread = threading.Thread(target=enqueue_output, args=(process.stdout, output_queue)) + thread.daemon = True + thread.start() + + parsed_data = None + headers = None + start_time = time.time() + + try: + for i in range(line_limit + 1): + # Check if we've exceeded the timeout + + try: + # Try to read a line from the queue with a small timeout + line = output_queue.get(timeout=13) + + # First line contains headers + if i == 0: + headers = [header.replace("field.", "").strip() for header in line.split(",")] + parsed_data = {header: [] for header in headers} + continue + + # Process data lines if headers are set + if headers and parsed_data is not None: + columns = line.split(",") + if len(columns) == len(headers): + for header, value in zip(headers, columns): + parsed_data[header].append(value.strip()) + except queue.Empty: + # No new data was found in the queue, continue until timeout + process.terminate() # Ensure subprocess terminates + process.wait() + return parsed_data + except Exception as e: + print("An error occurred: {0}".format(e)) + finally: + # check if process is still running + if process.poll() is None: + process.terminate() # Ensure subprocess terminates + process.wait() # Ensure cleanup + if parsed_data is not None: + parsed_data = {key: tuple(values) for key, values in parsed_data.items()} + + return parsed_data if parsed_data is not None else {} + + + +def process_real_time_topics(context, capture_topic_data, topics): + """ + Process topics concurrently and organize results into context. + + Args: + context: An object containing the table and attributes to store results. + capture_topic_data: A function to capture and process topic data. + format_entity: A function to format topic names. + """ + with ThreadPoolExecutor() as executor: + # Map futures to rows for tracking + future_to_topic = { + executor.submit(capture_topic_data, topic): topic + for topic in topics + } + + for future in as_completed(future_to_topic): + row = future_to_topic[future] + try: + topic, parsed_data, is_high_risk, risk_key = future.result() + if topic == '/TargetSystemData': + context['target_system_data'] = parsed_data + elif topic in NON_SENSOR_TOPICS: + print('Non sensor topic data captured for {}: {}'.format(topic, parsed_data)) + print(topic) + print(parsed_data) + context['non_sensor'][topic] = parsed_data + else: + context['sensor_data'][topic] = parsed_data + + except Exception as e: + print("Error processing topic for row {0}: {1}".format(row, e)) diff --git a/src/sa-bsn/target_system/components/component/test/patient.launch b/src/sa-bsn/target_system/components/component/test/patient.launch new file mode 100644 index 00000000..7bf78629 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/patient.launch @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sa-bsn/target_system/components/component/test/patient_data_service.cpp b/src/sa-bsn/target_system/components/component/test/patient_data_service.cpp new file mode 100644 index 00000000..c58c5ac5 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/patient_data_service.cpp @@ -0,0 +1,52 @@ +#include +#include // Replace with the correct header if necessary +#include // For generating random temperature values + +// Random number generators +std::random_device rd; +std::mt19937 gen(rd()); +std::uniform_real_distribution normal_dist(36.5, 37.5); // Normal range +std::uniform_real_distribution high_dist(39.0, 41.0); // High-risk range +std::bernoulli_distribution state_change(0.6); // 60% chance of switching states + +// Global state to track normal vs. high-risk +bool is_high_risk = false; + +// Function to generate temperature based on current state +double generateTemperature() +{ + // Occasionally switch between normal and high-risk states + if (state_change(gen)) + { + is_high_risk = !is_high_risk; + ROS_INFO("Patient state changed: Now %s", is_high_risk ? "HIGH RISK" : "NORMAL"); + } + + // Generate temperature based on the current state + return is_high_risk ? high_dist(gen) : normal_dist(gen); +} + +// Callback function for the 'getPatientData' service +bool mockPatientDataService(services::PatientData::Request &req, services::PatientData::Response &res) +{ + if (req.vitalSign == "temperature") + { + res.data = generateTemperature(); // Generate dynamic temperature + ROS_INFO("Returning simulated temperature: %.2f", res.data); + return true; + } + return false; // If the vital sign is not recognized +} + +int main(int argc, char **argv) +{ + ros::init(argc, argv, "patient_data_service"); + ros::NodeHandle nh; + + // Advertise the service + ros::ServiceServer service = nh.advertiseService("getPatientData", mockPatientDataService); + ROS_INFO("Patient data service ready to provide dynamic data..."); + + ros::spin(); // Keep the node running + return 0; +} diff --git a/src/sa-bsn/target_system/components/component/test/patient_data_service.launch b/src/sa-bsn/target_system/components/component/test/patient_data_service.launch new file mode 100644 index 00000000..d6d310e3 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/patient_data_service.launch @@ -0,0 +1,4 @@ + + + + diff --git a/src/sa-bsn/target_system/components/component/test/patient_with_low_data.launch b/src/sa-bsn/target_system/components/component/test/patient_with_low_data.launch new file mode 100644 index 00000000..eb4531a9 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/patient_with_low_data.launch @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/features/BSN-P03.feature b/src/sa-bsn/target_system/components/component/test/test_bdd/features/BSN-P03.feature new file mode 100644 index 00000000..08ff1f03 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/features/BSN-P03.feature @@ -0,0 +1,14 @@ +Feature: BSN-P03: Whenever the patients' health status is on high risk and an emergency has been detected it implies that is less or equal 250 (ms) + + Scenario: Successful Sensor Execution + Given that nodes thermometer and central hub are online + When I listen to thermometer + And thermometer sends data with high risk + Then Central hub will detect an emergency in less than 250 ms + + Scenario: Overloaded sensor data + Given that nodes thermometer and central hub are online + When I listen to thermometer + And thermometer sends low-risk data with high frequency + But thermometer sends data with high risk + Then Central Hub will experience delayed emergency detection \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/features/check_bsn.feature b/src/sa-bsn/target_system/components/component/test/test_bdd/features/check_bsn.feature new file mode 100644 index 00000000..99c92711 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/features/check_bsn.feature @@ -0,0 +1,35 @@ +Feature: Check for bsn features + + # @full_system + Scenario: BSN-P09 If data has been sent by the sensor node, the BodyHub is able to process it as low, moderate or high risk vital sign data. + # Scenario: sucessful process + Given that all sensors and central hub nodes are online + When I listen to sensors data + Then sensors will process the risks + And Central hub will process the risk + + # @full_system + Scenario: BSN-P08 If data has been sent by the sensor node, the BodyHub is able to process it + # Scenario: sucessful process + Given that all sensors and central hub nodes are online + When I listen to sensors data + Then Sensors will process the data + And Central hub will receive data from sensors + + # @inactive_central_hub + Scenario: BSN-P09 - Sad Path: central hub is inactive + # Scenario: central hub is inactive (Sad Path) + Given that all sensors are and online Central hub is inactive + And Central hub is inactive + When I listen to ecg and thermometer data + Then sensors will process the risks + But Central hub will not process the risk + + # @inactive_central_hub + Scenario: BSN-P08 - Sad Path: central hub is inactive + # Scenario: central hub is inactive (Sad Path) + Given that all sensors are and online Central hub is inactive + When I listen to ecg and thermometer data + Then Sensors will process the data + # not process data + But Central hub will not process the data diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/features/data_persistance.feature b/src/sa-bsn/target_system/components/component/test/test_bdd/features/data_persistance.feature new file mode 100644 index 00000000..d440a496 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/features/data_persistance.feature @@ -0,0 +1,14 @@ +Feature: Data Persistence (BSN-P08) - Whether the sensor node has collected some data, eventually the bodyhub will persist it. + + Scenario: Data Persisted Successfully (Happy Path) + Given that persistence system is online + When I listen to thermometer + And I send data to collector + Then the data will be in persist topic + + Scenario: Data Not Persisted (Sad Path) + Given that persistence system is online + When I listen to thermometer + And I send data to collector + But a database error prevents persistence + Then the system must log a persistence failure diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/features/health_status.feature b/src/sa-bsn/target_system/components/component/test/test_bdd/features/health_status.feature new file mode 100644 index 00000000..ea2c91d6 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/features/health_status.feature @@ -0,0 +1,12 @@ +Feature: Patient Health Status (BSN-P10) - Whether the bodyhub has processed some data, it eventually will detect a new patient health status. + + Scenario: Successful Health Status (Happy Path) + Given that nodes thermometer and central hub are online + When I listen to thermometer + Then g4t1 will detect new patient health status + + Scenario: Failure to Detect Health Status (Sad Path) + Given that nodes thermometer and central hub are online + When an internal processing error occurs in g4t1 + And I listen to thermometer + Then Central hub will fail to detect the new patient health status \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/features/injector.feature b/src/sa-bsn/target_system/components/component/test/test_bdd/features/injector.feature new file mode 100644 index 00000000..a92dca30 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/features/injector.feature @@ -0,0 +1,6 @@ +Feature: ensure simulation components communicate properly + + Scenario: Ensure the data is being injected into nodes /logger, /g3t1_6, /g3t1_5, /g3t1_4, /g3t1_3, /g3t1_2, /g3t1_1 + Given the /injector node is online + When I check if topics /uncertainty_/g3t1_1,/uncertainty_/g3t1_2,/uncertainty_/g3t1_3,/uncertainty_/g3t1_4,/uncertainty_/g3t1_5,/uncertainty_/g3t1_6,/log_uncertainty are inbound from /injector + Then /injector node is connected appropriately diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/features/knowledge_repo.feature b/src/sa-bsn/target_system/components/component/test/test_bdd/features/knowledge_repo.feature new file mode 100644 index 00000000..3cf1acd3 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/features/knowledge_repo.feature @@ -0,0 +1,7 @@ +Feature: Ensure knowledge repository components are communicating correctly + + Scenario: Check if /data_access communicates with /g4t1, /logger + Given the /data_access node is online + When I check if topics /persist are inbound from /logger + And I check if topics /TargetSystemData are inbound from /g4t1 + Then /data_access node is connected appropriately \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/features/managing_system.feature b/src/sa-bsn/target_system/components/component/test/test_bdd/features/managing_system.feature new file mode 100644 index 00000000..2b452fce --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/features/managing_system.feature @@ -0,0 +1,16 @@ +Feature: Ensure managing system and logger components are communicating correctly + + Scenario: Check if /enactor communicates with /reli_engine and /logger + Given the /enactor node is online + When I check if topics /strategy are inbound and /exception are outbound to /reli_engine + And I check if topics /event are inbound and /log_adapt are outbound to /logger + Then /enactor node is connected appropriately + + Scenario: Check if /logger communicates with /collector, /param_adapter, /enactor, /data_access, /injector + Given the /logger node is online + When I check if topics /log_uncertainty are inbound from /injector + And I check if topics /log_adapt are inbound and /event are outbound to /enactor + And I check if topics /log_event,/log_status,/log_energy_status are inbound from /collector + And I check if topics /reconfigure are outbound to /param_adapter + And I check if topics /persist are outbound to /data_access + Then /logger node is connected appropriately diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/features/target_system.feature b/src/sa-bsn/target_system/components/component/test/test_bdd/features/target_system.feature new file mode 100644 index 00000000..57036173 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/features/target_system.feature @@ -0,0 +1,34 @@ +Feature: Ensure that central hub receives data from body sensors and publishes to data access node + + Scenario: Check if sensors are publishing data to /collector and to /g4t1 + Given the ROS environment is on + When I check if the sensors are publishing data + And I check if respective topics have data + Then /g4t1 should receive data + + Scenario: Check if the central hub (/g4t1) is publishing to /data_access + Given the ROS environment is on + When the /g4t1 node is publishing data + And respective topics have data + Then the /data_access node should receive it + + Scenario: Check if /param_adapter node communicate with the g3t1_n, g4t1 and /logger nodes + Given the /param_adapter node is online + When I check if topics /reconfigure are inbound from /logger + And I check if topics /reconfigure_/g3t1_1 are outbound to /g3t1_1 + And I check if topics /reconfigure_/g3t1_2 are outbound to /g3t1_2 + And I check if topics /reconfigure_/g3t1_3 are outbound to /g3t1_3 + And I check if topics /reconfigure_/g3t1_4 are outbound to /g3t1_4 + And I check if topics /reconfigure_/g3t1_5 are outbound to /g3t1_5 + And I check if topics /reconfigure_/g3t1_6 are outbound to /g3t1_6 + And I check if topics /reconfigure_/g4t1 are outbound to /g4t1 + Then /param_adapter node is connected appropriately + + Scenario: Verify if rosservices in /Patient node transmit data + Given the /Patient node is online + When I call rosservice /getPatientData with oxigenation and None + And I call rosservice /getPatientData with heart_rate and None + And I call rosservice /getPatientData with abps and None + And I call rosservice /getPatientData with abpd and None + And I call rosservice /getPatientData with glucose and None + Then response should not be null diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_check_bsn.launch b/src/sa-bsn/target_system/components/component/test/test_bdd/test_check_bsn.launch new file mode 100644 index 00000000..dd6de2ad --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_check_bsn.launch @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_check_bsn.py b/src/sa-bsn/target_system/components/component/test/test_bdd/test_check_bsn.py new file mode 100644 index 00000000..0289e305 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_check_bsn.py @@ -0,0 +1,187 @@ +import ros_pytest +from pytest_bdd import scenarios, given, when, then, parsers +from conftest import FULL_SYSTEM +import rospy +import rosnode +from parsers import parse_topic_data, format_entity, process_real_time_topics, capture_topic_data, format_debug_data +from asserts import node_is_active, bool_node_is_active + +scenarios("./features/check_bsn.feature") + +def count_and_get_matching_elements_with_time(sensor_data, target_system_data, key, value, evaluate): + matching_count = 0 + matched_data = [] + + # Iterate over both lists and check for matching values and time condition + for i, sensor_risk in enumerate(sensor_data[key][evaluate]): + for j, target_risk in enumerate(target_system_data[value]): + print('SENSOR RISK of {}: {} TARGET RISK: {}'.format(key, sensor_risk, target_risk)) + if sensor_risk == target_risk: + # Parse time strings into floats + sensor_time = float(sensor_data[key]['%time'][i]) + target_time = float(target_system_data['%time'][j]) + + # Round and compare times + rounded_sensor_time = round(sensor_time, -5) / 1e6 + rounded_target_time = round(target_time, -5) / 1e6 + + print('TIME DIFFERENCE in {}: {} - {}'.format(key, rounded_sensor_time, rounded_target_time)) + + if abs(rounded_sensor_time - rounded_target_time) < 2000: + matching_count += 1 + matched_data.append({ + 'sensor_risk': sensor_risk, + 'sensor_time': sensor_time, + 'target_risk': target_risk, + 'target_time': target_time + }) + + return matching_count, matched_data + + +@given(parsers.parse('the {topic_name} topic is online')) +def step_given_topic_is_online(context, topic_name): + # Check if /TargetSystemData topic is active + cmd = ['rosnode', 'info', node_name] + command = Command(cmd) + stdout, stderr, returncode = command.run(timeout=TIMEOUT_SECONDS) + + node_info = get_rosnode_info(returncode, stdout, stderr) + topic_name = format_entity(topic_name) + result = subprocess.run(['rostopic', 'list', topic_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + topic_list = result.stdout.decode('utf-8').splitlines() + assert topic_name in topic_list, "{} is not online".format(topic_name) + +@given('that all sensors and central hub nodes are online') +def step_given_full_system_nodes_online(context): + node_is_active(FULL_SYSTEM) + +@given('that all sensors are and online Central hub is inactive') +def step_given_full_system_nodes_online(context): + node_is_active(FULL_SYSTEM[:-1]) + if '/g4t1' in rosnode.get_node_names(): + rosnode.kill_nodes(['/g4t1']) + rospy.sleep(.2) + + +@given(parsers.parse('{node_name} is inactive')) +def step_given_node_is_inactive(context, node_name): + is_active = bool_node_is_active(node_name) + assert not is_active, "{} is active".format(node_name) + +@when('I listen to sensors data') +def step_when_check_sensors_publishing_data(context): + + context['sensor_data'] = {} + context['found_high_risk'] = [] + context['target_system_data'] = {} + + topics = [ + "/thermometer_data", + "/ecg_data", + "/oximeter_data", + "/abps_data", + "/abpd_data", + "/glucosemeter_data", + "/TargetSystemData" + ] + + process_real_time_topics(context, capture_topic_data, topics) + +@when('I listen to ecg and thermometer data') +def step_when_check_sensors_publishing_data(context): + + context['sensor_data'] = {} + context['found_high_risk'] = [] + context['target_system_data'] = {} + + topics = [ + "/thermometer_data", + "/ecg_data", + "/TargetSystemData" + ] + + process_real_time_topics(context, capture_topic_data, topics) + + +@then('sensors will process the risks') +def step_then_check_high_risk(context): + assert any(context['sensor_data'].values()), "No risk data found in sensor topics." + print('Sensor data: {}'.format(format_debug_data(context['sensor_data']))) + + for topic, data in context['sensor_data'].items(): + assert 'risk' in data and data['risk'], "No risk data detected in topic {}".format(topic) + +@then("Central hub will process the risk") +def step_then_check_target_system_receives_risk(context): + #print(f'TargetSystemData is receiving the risk data from sensors: {context.target_system_data}') + risk_key_mapping = { + '/thermometer_data': 'trm_risk', + '/ecg_data': 'ecg_risk', + '/oximeter_data': 'oxi_risk', + '/abps_data': 'abps_risk', + '/abpd_data': 'abpd_risk', + '/glucosemeter_data': 'glc_risk', + } + + target_system_data = context['target_system_data'] + + sensor_data = context['sensor_data'] + + for key, value in risk_key_mapping.items(): + + print("Target:", key, "Sensor:", value) + print("Target risks: {} Sensor risks: {} and patient status: {}".format(sensor_data[key]['risk'], target_system_data[value], target_system_data['patient_status'])) + count, matched = count_and_get_matching_elements_with_time(sensor_data, target_system_data, key, value, 'risk') + assert target_system_data['patient_status'], 'patient_status is not being provided' + assert len(target_system_data['patient_status']) >= count, "Patient status is not being updated in TargetSystemData." + assert count > 0, "Topics {} and {} do not have matching risk data.".format(key, value) + assert all((x.replace('.', '', 1).isdigit() and 0 <= float(x) <= 100) + for x in target_system_data['patient_status']), 'patient status is not processing valid risk.' + +@then("Central hub will not process the risk") +def step_then_check_target_system_does_not_receive_risk(context): + target_system_data = context['target_system_data'] + assert not target_system_data, "Patient status is unexpectedly updated in TargetSystemData." + # Check that no risks are present in the target system data + if target_system_data: + for key in ['trm_risk', 'ecg_risk', 'oxi_risk', 'abps_risk', 'abpd_risk', 'glc_risk']: + # Assert that the target system data for risks is empty or doesn't contain any values + assert not target_system_data[key], "Expected no data for {}, but found: {}".format(key, target_system_data[key]) + +@then("Central hub will not process the data") +def step_then_check_target_system_does_not_receive_risk(context): + target_system_data = context['target_system_data'] + + assert not target_system_data, "Patient status is unexpectedly updated in TargetSystemData." + # Check that no risks are present in the target system data + if target_system_data: + for key in ['trm_data', 'ecg_data', 'oxi_data', 'abps_data', 'abpd_data', 'glc_data']: + # Assert that the target system data for risks is empty or doesn't contain any values + assert not target_system_data[key], "Expected no data for {}, but found: {}".format(key, target_system_data[key]) + +@then('Sensors will process the data') +def step_check_if_sensors_process_data(context): + assert any(context['sensor_data'].values()), "No risk data found in sensor topics." + + for topic, data in context['sensor_data'].items(): + assert 'data' in data and data['data'], "No risk data detected in topic {}".format(topic) +@then('Central hub will receive data from sensors') +def step_check_if_TargetSystem_process_data(context): + data_key_mapping = { + '/thermometer_data': 'trm_data', + '/ecg_data': 'ecg_data', + '/oximeter_data': 'oxi_data', + '/abps_data': 'abps_data', + '/abpd_data': 'abpd_data', + '/glucosemeter_data': 'glc_data', + } + + target_data= context['target_system_data'] + sensor_data = context['sensor_data'] + + for key, value in data_key_mapping.items(): + + + count, matched = count_and_get_matching_elements_with_time(sensor_data, target_data, key, value, 'data') + assert count > 0, "Topics {} and {} do not have matching risk data.".format(key, value) \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_data_persistance.launch b/src/sa-bsn/target_system/components/component/test/test_bdd/test_data_persistance.launch new file mode 100644 index 00000000..d0fb9ad4 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_data_persistance.launch @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_data_persistance.py b/src/sa-bsn/target_system/components/component/test/test_bdd/test_data_persistance.py new file mode 100644 index 00000000..a6dbc881 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_data_persistance.py @@ -0,0 +1,46 @@ +import ros_pytest +from pytest_bdd import scenarios, given, when, then, parsers +from test_sensor import SharedSensorTests +import rospy +import rosnode +from asserts import is_node_receiving_multiple_topics, node_is_active, is_node_publishing_to_topics,check_time_performance +from parsers import process_real_time_topics, parse_topic_data, capture_topic_data + +scenarios("./features/data_persistance.feature") + +PERSISTENCE_NODES = [ + "/g4t1", + "/collector", + "/param_adapter", + "/g3t1_3", + "/data_access", + "/logger" +] + +@given('that persistence system is online') +def node_is_online(): + node_is_active(PERSISTENCE_NODES) + +@when('I send data to collector') +def step_when_send_data_to_collector(context): + """Simulate sending data to the collector.""" + assert '/g3t1_3' in context['non_sensor']['/collect_energy_status']['source'], 'No data detected in /collect_energy_status.' + +@then('the data will be in persist topic') +def step_then_data_persisted(context): + """Simulate data persistence.""" + assert 'Status' in context['non_sensor']['/persist']['type'], 'Data was not sent to collector, so it cannot be persisted.' + +@when('a database error prevents persistence') +def step_when_database_error_occurs(context): + """Simulate a database error preventing persistence.""" + rosnode.kill_nodes('/logger') + rospy.sleep(2) + +@then('the system must log a persistence failure') +def step_then_system_logs_failure(context): + """Ensure the system logs a persistence failure.""" + energyStatus = parse_topic_data('/log_energy_status') + assert all(val == '' for val in energyStatus['target']) + persist_topic = parse_topic_data('/persist') + assert 'fail' in persist_topic['content'] diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_health_status.launch b/src/sa-bsn/target_system/components/component/test/test_bdd/test_health_status.launch new file mode 100644 index 00000000..82a7d6d5 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_health_status.launch @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_health_status.py b/src/sa-bsn/target_system/components/component/test/test_bdd/test_health_status.py new file mode 100644 index 00000000..64730614 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_health_status.py @@ -0,0 +1,79 @@ +import ros_pytest +from pytest_bdd import scenarios, given, when, then, parsers +from test_sensor import SharedSensorTests +import rospy +import rosnode +from asserts import is_node_receiving_multiple_topics, assert_node_is_online, is_node_publishing_to_topics,check_time_performance +from parsers import process_real_time_topics, parse_topic_data, capture_topic_data + +# def capture_topic_data(topic): +# parsed_data = parse_topic_data(topic, line_limit=10) or {} +# # accept common risk key names, default to empty list +# risk_list = parsed_data.get('risk') or parsed_data.get('risk_levels') or parsed_data.get('risk_values') or [] +# high_risk_detected = False +# print('risk_list: {}'.format(risk_list)) +# for value in risk_list: +# try: +# if float(value) > 10: +# high_risk_detected = True +# break +# except (TypeError, ValueError): +# continue +# risk_key = next((k for k in parsed_data.keys() if 'risk' in k), 'risk') +# return topic, parsed_data, high_risk_detected, risk_key + + +# scenarios("./features/health_status.feature") +scenarios("./features/BSN-P03.feature") + +# @given('that nodes thermometer and central hub are online') +# def thermometer_and_central_hub_are_online(): +# nodes = rosnode.get_node_names() +# thermometer_node = '/g3t1_3' +# central_hub_node = '/g4t1' +# assert thermometer_node in nodes, "{} is not online.".format(thermometer_node) +# assert central_hub_node in nodes, "{} is not online.".format(central_hub_node) + +@then('g4t1 will detect new patient health status') +def g4t1_detects_health_status(context): + assert len(set(context['target_system_data']['patient_status'])) > 1, "status has not changed. Patient Status: {}".format(context['target_system_data']['patient_status']) + +@when('an internal processing error occurs in g4t1') +def step_when_internal_error_occurs(context): + context['internal_error'] = True + +@then('Central hub will fail to detect the new patient health status') +def step_then_g4t1_fails_to_detect_status(context): + assert context['internal_error'], "No internal error detected" + +@given('that nodes thermometer and central hub are online') +def step_given_reduced_system_nodes_online(context): + assert_node_is_online('/g3t1_3') # Thermometer node + assert_node_is_online('/g4t1') # Central hub node + +@when('thermometer sends data with high risk') +def step_when_high_risk_data_sent(context): + assert_node_is_online('/patient_data_service') + +@then('Central hub will detect an emergency in less than 250 ms') +def step_then_g4t1_detects_emergency(context): + print('teste_final') + print(context['sensor_data']) + performance_check = check_time_performance(context['sensor_data'], context['target_system_data'], + '/thermometer_data','trm_data', 'data') + + assert performance_check, "Central hub failed to detect an emergency in less than 250 ms" + +@when(parsers.parse('{node_name} sends low-risk data with high frequency')) +def step_when_overloaded_data_sent(context, node_name): + topic = '/{}_data'.format(node_name) + _, parsed_data, high_risk_detected, risk_key = capture_topic_data(topic) + context['overloaded'] = True + print('high_risk_detected: {}'.format(high_risk_detected)) + print('risk_key: {}'.format(risk_key)) + context['high_risk_detected'] = high_risk_detected + assert context['overloaded'], "Sensor data overload did not occur" + +@then('Central Hub will experience delayed emergency detection') +def step_then_g4t1_might_delay_detection(context): + assert context['overloaded'] and context['high_risk_detected'], "Delayed detection scenario not met" diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_injector.launch b/src/sa-bsn/target_system/components/component/test/test_bdd/test_injector.launch new file mode 100644 index 00000000..2ae09869 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_injector.launch @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_injector.py b/src/sa-bsn/target_system/components/component/test/test_bdd/test_injector.py new file mode 100644 index 00000000..cdabd04e --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_injector.py @@ -0,0 +1,7 @@ +import ros_pytest +from pytest_bdd import scenarios, given, when, then, parsers +from test_sensor import SharedSensorTests +import rospy +import rosnode + +scenarios("./features/injector.feature") diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_knowledge_repo.launch b/src/sa-bsn/target_system/components/component/test/test_bdd/test_knowledge_repo.launch new file mode 100644 index 00000000..8453dc1d --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_knowledge_repo.launch @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_knowledge_repo.py b/src/sa-bsn/target_system/components/component/test/test_bdd/test_knowledge_repo.py new file mode 100644 index 00000000..214210da --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_knowledge_repo.py @@ -0,0 +1,9 @@ +import ros_pytest +from pytest_bdd import scenarios, given, when, then, parsers +from test_sensor import SharedSensorTests +import rospy +import rosnode +from asserts import is_node_receiving_multiple_topics, assert_node_is_online, is_node_publishing_to_topics +from parsers import process_real_time_topics, parse_topic_data, capture_topic_data + +scenarios("./features/knowledge_repo.feature") diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_managing_system.launch b/src/sa-bsn/target_system/components/component/test/test_bdd/test_managing_system.launch new file mode 100644 index 00000000..62cc9391 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_managing_system.launch @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_managing_system.py b/src/sa-bsn/target_system/components/component/test/test_bdd/test_managing_system.py new file mode 100644 index 00000000..c628e38d --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_managing_system.py @@ -0,0 +1,4 @@ +import ros_pytest +from pytest_bdd import scenarios + +scenarios("./features/managing_system.feature") diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_target_system.launch b/src/sa-bsn/target_system/components/component/test/test_bdd/test_target_system.launch new file mode 100644 index 00000000..766d5234 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_target_system.launch @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/test_bdd/test_target_system.py b/src/sa-bsn/target_system/components/component/test/test_bdd/test_target_system.py new file mode 100644 index 00000000..63e48bc6 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_bdd/test_target_system.py @@ -0,0 +1,82 @@ +import ros_pytest +from pytest_bdd import scenarios, given, when, then, parsers +from test_sensor import SharedSensorTests +import rospy +import rosnode +from asserts import is_node_receiving_multiple_topics, assert_node_is_online, is_node_publishing_to_topics + +scenarios("./features/target_system.feature") + +topics = ['/oximeter_data', '/ecg_data', '/thermometer_data','/abps_data', '/abpd_data', '/glucosemeter_data'] +sensors = ['/g3t1_1', '/g3t1_2', '/g3t1_3', '/g3t1_4', '/g3t1_5', '/g3t1_6'] +sensor_topic = { + '/g3t1_1': ['/oximeter_data'], + '/g3t1_2': ['/ecg_data'], + '/g3t1_3': ['/thermometer_data'], + '/g3t1_4': ['/abps_data'], + '/g3t1_5': ['/abpd_data'], + '/g3t1_6': ['/glucosemeter_data'], +} + +@when("I check if the sensors are publishing data") +def sensors_are_publishing_data(): + for sensor, topics in sensor_topic.items(): + SharedSensorTests.assert_sensors_are_publishing_data(sensor, topics) + + +@when("I check if respective topics have data") +def sensor_topics_have_data(): + for topic in topics: + SharedSensorTests.assert_topic_has_data(topic) + +@then("/g4t1 should receive data") +def g4t1_should_receive_data(): + node_name = '/g4t1' + is_receiving, missing_topics = is_node_receiving_multiple_topics(node_name, topics) + + assert is_receiving, "{} is missing data from these topics: {}".format(node_name, missing_topics) + +@when("the /g4t1 node is publishing data") +def g4t1_is_publishing_data(): + SharedSensorTests.assert_sensors_are_publishing_data('/g4t1', ['/TargetSystemData']) + +@when("respective topics have data") +def target_system_topics_have_data(): + SharedSensorTests.assert_topic_has_data('/TargetSystemData') + +@then("the /data_access node should receive it") +def data_access_should_receive_data(): + node_name = '/data_access' + is_receiving, missing_topics = is_node_receiving_multiple_topics(node_name, ['/TargetSystemData']) + + assert is_receiving, "{} is missing data from these topics: {}".format(node_name, missing_topics) + +patient_response = { + 'oxigenation': None, + 'heart_rate': None, + 'abps': None, + 'abpd': None, + 'glucose': None, +} + +@given("the /Patient node is online") +def patient_node_is_online(): + assert_node_is_online('/patient') + +from services.srv import PatientData + +@when(parsers.parse("I call rosservice /getPatientData with {sensor_type} and None")) +def call_get_patient_data_service(sensor_type): + rospy.wait_for_service('/getPatientData') + try: + get_patient_data = rospy.ServiceProxy('/getPatientData', PatientData) + response = get_patient_data(sensor_type) + patient_response[sensor_type] = response.data + assert response.data != '', "No data received from /getPatientData service" + except rospy.ServiceException as e: + pytest.fail("Service call to /getPatientData failed: %s" % str(e)) + +@then("response should not be null") +def response_should_not_be_null(): + for response in patient_response.values(): + assert response is not None, "/getPatientData service returned null response" diff --git a/src/sa-bsn/target_system/components/component/test/test_g4t1.cpp b/src/sa-bsn/target_system/components/component/test/test_g4t1.cpp new file mode 100644 index 00000000..83236afb --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_g4t1.cpp @@ -0,0 +1,118 @@ +#include +#include +#include +#include "component/g4t1/G4T1.hpp" + +// Helper function to create a SensorData message +auto createSensorData = [](const std::string &type, double risk, double batt, double data) +{ + messages::SensorData sensor_data; + sensor_data.type = type; + sensor_data.risk = risk; + sensor_data.batt = batt; + sensor_data.data = data; + return sensor_data; +}; + +class MockPublisher +{ +public: + messages::TargetSystemData last_msg; + + void publish(const messages::TargetSystemData &msg) { last_msg = msg; } +}; + +class TestableG4T1 : public G4T1 +{ +public: + MockPublisher mock_pub; + + TestableG4T1(int &argc, char **argv, const std::string &name) + : G4T1(argc, argv, name) {} + + void transfer() override + { + messages::TargetSystemData msg; + // Copy data to msg as in your previous implementation... + mock_pub.publish(msg); + } +}; + +// Test fixture +class G4T1Fixture : public ::testing::Test +{ +protected: + int argc = 0; + char **argv = nullptr; + TestableG4T1 *g4t1; + + G4T1Fixture() + { + g4t1 = new TestableG4T1(argc, argv, "test_g4t1"); + g4t1->setUp(); + } + + ~G4T1Fixture() + { + g4t1->tearDown(); + delete g4t1; + } +}; + +// Grant the test fixture access to private members of G4T1 +/* +FRIEND_TEST(G4T1Fixture, TestCollect); + +TEST_F(G4T1Fixture, TestCollect) +{ + auto sensor_data = createSensorData("thermometer", 15.0, 90.0, 37.5); + messages::SensorData::Ptr sensor_ptr(new messages::SensorData(sensor_data)); + + g4t1->collect(sensor_ptr); + + // Access private members directly (because of FRIEND_TEST) + EXPECT_EQ(g4t1->trm_batt, 90.0) << "Battery level mismatch for thermometer"; + EXPECT_EQ(g4t1->trm_raw, 37.5) << "Raw data mismatch for thermometer"; +} + +TEST_F(G4T1Fixture, TestTransfer) +{ + // Simulate setting data + g4t1->trm_batt = 90.0; + g4t1->trm_risk = 15.0; + g4t1->trm_raw = 37.5; + + g4t1->transfer(); + + const auto &msg = g4t1->mock_pub.last_msg; + + EXPECT_EQ(msg.trm_batt, 90.0) << "Battery level mismatch in transfer"; + EXPECT_EQ(msg.trm_risk, 15.0) << "Risk level mismatch in transfer"; + EXPECT_EQ(msg.trm_data, 37.5) << "Raw data mismatch in transfer"; +} + +TEST_F(G4T1Fixture, TestBufferOverflow) +{ + auto sensor_data = createSensorData("thermometer", 15.0, 90.0, 37.5); + messages::SensorData::Ptr sensor_ptr(new messages::SensorData(sensor_data)); + + // Fill the buffer beyond max size + for (int i = 0; i < g4t1->max_size + 1; ++i) + { + g4t1->collect(sensor_ptr); + } + + EXPECT_TRUE(g4t1->lost_packt) << "Buffer overflow not detected"; +} +*/ +TEST_F(G4T1Fixture, TestHelloWorld) +{ + EXPECT_TRUE(true); +} +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + ros::init(argc, argv, "test_g4t1"); + ros::NodeHandle nh; + return RUN_ALL_TESTS(); +} diff --git a/src/sa-bsn/target_system/components/component/test/test_sensor.py b/src/sa-bsn/target_system/components/component/test/test_sensor.py new file mode 100644 index 00000000..685c06ca --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/test_sensor.py @@ -0,0 +1,413 @@ +import pytest +import rospy +import rostopic +import ros_pytest +from std_msgs.msg import String, Float64 +from asserts import is_node_publishing_to_topics, Command, TIMEOUT_SECONDS +from parsers import get_rostopic_sensor_data, get_rosnode_info +from messages.msg import SensorData +from archlib.msg import Uncertainty, Status +import subprocess +import threading +from services.srv import PatientData, PatientDataResponse, PatientDataRequest +import rosnode +import rosservice +import time +import math + +SENSORS = ['/g3t1_1', '/g3t1_2', '/g3t1_3', '/g3t1_4', '/g3t1_5', '/g3t1_6'] +low_risk_value_dict = { + 'oxigenation': 90.0, + 'heart_rate': 90.0, + 'temperature': 37.0, + 'abps': 100.0, + 'abpd': 70.0, + 'glucose': 90.0 +} +high_risk_value_dict = { + 'oxigenation': 50.0, + 'heart_rate': 120.0, + 'temperature': 45.0, + 'abps': 250.0, + 'abpd': 95.0, + 'glucose': 200.0 +} +mid_risk_value_dict = { + 'oxigenation': 60, + 'heart_rate': 100.0, + 'temperature': 40.0, + 'abps': 130.0, + 'abpd': 85.0, + 'glucose': 100.0 +} +out_of_range_value_dict = { + 'oxigenation': 101, + 'heart_rate': 301, + 'temperature': 51.0, + 'abps': 301.0, + 'abpd': 305.0, + 'glucose': 301.0 +} +low_risk_threshold_dict = 20 +high_risk_threshold_dict = 66 + +class SharedSensorTests: + """Shared test methods for sensor testing""" + + @staticmethod + def assert_sensors_are_publishing_data(node, topic_name): + """Assert that sensors are actively publishing data""" + is_publishing, missing_topics = is_node_publishing_to_topics(node, topic_name) + assert is_publishing, "{} is missing data from these topics: {}".format(node, missing_topics) + + @staticmethod + def assert_topic_has_data(sensor_topic_name, timeout=5.0): + """Assert that a topic is publishing data within a timeout period""" + try: + sensor_data = {} + cmd = ['rostopic', 'echo', sensor_topic_name, '-n', '1'] + topic_executor = Command(cmd) + stdout_bytes, stderr_bytes, returncode = topic_executor.run(timeout=TIMEOUT_SECONDS) + sensor_data[sensor_topic_name] = get_rostopic_sensor_data(returncode, stdout_bytes, stderr_bytes) + assert sensor_data[sensor_topic_name]['data'], "No data published on topic {}".format(sensor_topic_name) + except Exception as e: + if str(e) == "TimeoutExpired": + raise AssertionError("Timeout: No data published on topic {}".format(sensor_topic_name)) + else: + raise + + @classmethod + def setup_class(cls): + """Initialize ROS node for testing""" + cls.received_messages = [] + cls.serviceCount = [] + cls.message_lock = threading.Lock() + + rospy.init_node('node_test') + + service_name = "getPatientData" + cls.patient_service_server = rospy.Service( + service_name, + PatientData, + cls.mock_patient_data_service_callback + ) + rospy.sleep(0.1) + rospy.loginfo("Servico '{}' mockado iniciado para teste.".format(service_name)) + + def setup_method(self): + """Setup for each test method""" + self.received_messages = [] + self.subscriber = rospy.Subscriber( + self.topic, + SensorData, + self.message_callback + ) + time.sleep(0.1) + + def teardown_method(self): + """Cleanup after each test""" + self.subscriber.unregister() + self.received_messages = [] + + if self.patient_service_server is not None: + self.patient_service_server.shutdown("Testes concluidos.") + + rospy.loginfo("Servico de mock desligado.") + + @staticmethod + def mock_patient_data_service_callback(req): + """ + Funcao callback que simula a logica do servidor. + Recebe a requisicao (PatientData) e retorna uma Resposta de Low Risk (PatientDataResponse). + """ + rospy.loginfo("Servico 'getPatientData' chamado no teste: {}".format(req.vitalSign)) + + res = PatientDataResponse() + res.data = low_risk_value_dict[req.vitalSign] + return res + + @staticmethod + def mock_patient_data_service_with_high_risk_callback(req): + """ + Funcao callback que simula a logica do servidor. + Recebe a requisicao (PatientData) e retorna uma Resposta de High Risk (PatientDataResponse). + """ + rospy.loginfo("Servico 'getPatientData' (High risk) chamado no teste: {}".format(req.vitalSign)) + + res = PatientDataResponse() + res.data = high_risk_value_dict[req.vitalSign] + return res + + @staticmethod + def mock_patient_data_service_with_mid_risk_callback(req): + """ + Funcao callback que simula a logica do servidor. + Recebe a requisicao (PatientData) e retorna uma Resposta de Mid Risk (PatientDataResponse). + """ + rospy.loginfo("Servico 'getPatientData' (Mid risk) chamado no teste: {}".format(req.vitalSign)) + + res = PatientDataResponse() + res.data = mid_risk_value_dict[req.vitalSign] + return res + + @staticmethod + def mock_patient_data_service_with_out_of_range_callback(req): + """ + Funcao callback que simula a logica do servidor. + Recebe a requisicao (PatientData) e retorna uma Resposta de Out of Range (PatientDataResponse). + """ + rospy.loginfo("Servico 'getPatientData' (Out of range) chamado no teste: {}".format(req.vitalSign)) + + res = PatientDataResponse() + res.data = out_of_range_value_dict[req.vitalSign] + return res + + @staticmethod + def mock_patient_data_service_with_unknown_callback(req): + """ + Funcao callback que simula a logica do servidor. + Recebe a requisicao (PatientData) e retorna uma Resposta de Unknown (PatientDataResponse). + """ + rospy.loginfo("Servico 'getPatientData' (Unknown) chamado no teste: {}".format(req.vitalSign)) + + res = PatientDataResponse() + res.data = -1 + return res + + def message_callback(self, msg): + """Callback for receiving messages from the sensor""" + print("Mensagem recebida no topico {}: data={}".format(self.topic, msg.data)) + with self.message_lock: + self.received_messages.append(msg) + + def wait_for_message(self, timeout=2.0): + """Wait for a message to be received""" + start_time = time.time() + while time.time() - start_time < timeout: + with self.message_lock: + if self.received_messages: + return self.received_messages[-1] + time.sleep(0.01) + return None + + def wait_for_status(self, status_messages, status_lock, timeout=2.0, matcher=None): + """Wait for a status message to be received""" + start_time = time.time() + while time.time() - start_time < timeout: + with status_lock: + if status_messages: + if matcher is None: + return status_messages[-1] + for msg in status_messages: + if matcher(msg): + return msg + time.sleep(0.01) + return None + + def test_transfer_with_low_risk_data(self): + """Test transfer with low risk data""" + test_data = low_risk_value_dict[self.vital_sign] + received_msg = self.wait_for_message() + print("Received message: {}".format(received_msg.risk)) + assert received_msg is not None + assert received_msg.risk < low_risk_threshold_dict + assert received_msg.data == test_data + + @pytest.fixture + def mock_high_risk_service(self): + """Fixture to setup high risk service mock""" + if self.patient_service_server is not None: + self.patient_service_server.shutdown("Reconfigurando para high risk.") + rospy.sleep(0.1) + service_name = "getPatientData" + self.patient_service_server = rospy.Service( + service_name, + PatientData, + self.mock_patient_data_service_with_high_risk_callback + ) + rospy.wait_for_service(service_name) + rospy.sleep(1) + + self.subscriber = rospy.Subscriber( + self.topic, + SensorData, + self.message_callback + ) + print("High risk service mock setup complete.") + time.sleep(1) + yield + + def test_transfer_with_high_risk_data(self, mock_high_risk_service): + """Test transfer with high risk data""" + test_data = high_risk_value_dict[self.vital_sign] + received_msg = self.wait_for_message() + + assert received_msg is not None + assert received_msg.risk > high_risk_threshold_dict + assert received_msg.data == test_data + + @pytest.fixture + def mock_mid_risk_service(self): + """Fixture to setup mid risk service mock""" + if self.patient_service_server is not None: + self.patient_service_server.shutdown("Reconfigurando para mid risk.") + rospy.sleep(0.1) + service_name = "getPatientData" + self.patient_service_server = rospy.Service( + service_name, + PatientData, + self.mock_patient_data_service_with_mid_risk_callback + ) + rospy.wait_for_service(service_name) + rospy.sleep(1) + + self.subscriber = rospy.Subscriber( + self.topic, + SensorData, + self.message_callback + ) + + print("Mid risk service mock setup complete.") + time.sleep(1) + yield + + def test_transfer_with_mid_risk_data(self, mock_mid_risk_service): + """Test transfer with mid risk data""" + test_data = mid_risk_value_dict[self.vital_sign] + received_msg = self.wait_for_message() + print("Received message: {}, {}".format(received_msg.risk, test_data)) + assert received_msg is not None + assert received_msg.risk > low_risk_threshold_dict and received_msg.risk < high_risk_threshold_dict + assert received_msg.data == test_data + + @pytest.fixture + def mock_out_of_range_service(self): + """Fixture to setup out of range service mock""" + if self.patient_service_server is not None: + self.patient_service_server.shutdown("Reconfigurando para out of range.") + rospy.sleep(0.1) + service_name = "getPatientData" + self.patient_service_server = rospy.Service( + service_name, + PatientData, + self.mock_patient_data_service_with_out_of_range_callback + ) + rospy.wait_for_service(service_name) + rospy.sleep(1) + + self.subscriber = rospy.Subscriber( + self.topic, + SensorData, + self.message_callback + ) + print("Out of range service mock setup complete.") + time.sleep(1) + yield + + def test_transfer_with_out_of_range_data(self, mock_out_of_range_service): + """Test transfer with out of range data""" + received_msg = self.wait_for_message() + assert received_msg is None + + @pytest.fixture + def mock_unknown_service(self): + """Fixture to setup unknown service mock""" + if self.patient_service_server is not None: + self.patient_service_server.shutdown("Reconfigurando para unknown.") + rospy.sleep(0.1) + service_name = "getPatientData" + self.patient_service_server = rospy.Service( + service_name, + PatientData, + self.mock_patient_data_service_with_unknown_callback + ) + rospy.wait_for_service(service_name) + rospy.sleep(1) + + self.subscriber = rospy.Subscriber( + self.topic, + SensorData, + self.message_callback + ) + print("Out of range service mock setup complete.") + time.sleep(1) + yield + + def test_transfer_with_unknown_data(self, mock_unknown_service): + """Test transfer with unknown data (out of range)""" + received_msg = self.wait_for_message() + assert received_msg is None or math.isnan(received_msg.risk) or received_msg.risk == -1, "Expected risk to be NaN or -1 for unknown data, got {}".format(received_msg.risk) + + def test_transfer_with_accuracy_fail(self, mock_mid_risk_service): + """Test transfer with accuracy fail (label mismatch)""" + if self.vital_sign != 'oxigenation': + pytest.skip("Accuracy fail test only applies to g3t1_1") + + status_messages = [] + status_lock = threading.Lock() + + def status_callback(msg): + with status_lock: + status_messages.append(msg) + + status_subscriber = rospy.Subscriber( + "collect_status", + Status, + status_callback + ) + + received_msg = self.wait_for_message() + assert received_msg is not None + + uncertainty_topics = ["uncertainty_g3t1_1", "uncertainty_/g3t1_1"] + uncertainty_pubs = [ + rospy.Publisher(topic, Uncertainty, queue_size=10) + for topic in uncertainty_topics + ] + time.sleep(0.2) + + uncertainty_msg = Uncertainty() + uncertainty_msg.source = "test" + uncertainty_msg.target = "g3t1_1" + uncertainty_msg.content = "noise_factor=0.3" + + for _ in range(3): + for publisher in uncertainty_pubs: + publisher.publish(uncertainty_msg) + time.sleep(0.2) + + status_msg = self.wait_for_status( + status_messages, + status_lock, + timeout=6.0, + matcher=lambda msg: msg.source.endswith("g3t1_1") and msg.content == "fail" + ) + status_subscriber.unregister() + + assert status_msg is not None + assert status_msg.source.endswith("g3t1_1") + assert status_msg.content == "fail" + + + def test_battery_consumption(self, mock_mid_risk_service): + """Test that battery level decreases after operations""" + received_msg1 = self.wait_for_message() + assert received_msg1 is not None + initial_battery = received_msg1.batt + + # Wait for next message + time.sleep(0.5) + del self.received_messages[:] + received_msg2 = self.wait_for_message() + + if received_msg2 is not None: + # Battery should decrease or stay same (if instant recharge) + assert received_msg2.batt <= initial_battery + + # def test_service_integration(self): + # """Test that sensor properly integrates with patient data service""" + # received_msg = self.wait_for_message(timeout=3.0) + + # assert received_msg is not None + # # Data should come from the mocked service + # assert received_msg.data == low_risk_value_dict[self.vital_sign] diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_1.launch b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_1.launch new file mode 100644 index 00000000..31be54ec --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_1.launch @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_1.py b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_1.py new file mode 100644 index 00000000..5b5ae487 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_1.py @@ -0,0 +1,8 @@ +import pytest +from test_sensor import SharedSensorTests + +class TestG3T1_1(SharedSensorTests): + """Test suite for G3T1_1 sensor""" + topic = 'oximeter_data' + vital_sign = 'oxigenation' + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_1_connected.launch b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_1_connected.launch new file mode 100644 index 00000000..7e82e9fe --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_1_connected.launch @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_1_connected.py b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_1_connected.py new file mode 100644 index 00000000..bc33c5c5 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_1_connected.py @@ -0,0 +1,83 @@ +import threading +import time + +import pytest +import rospy +from std_srvs.srv import SetBool, SetBoolResponse + +from messages.msg import SensorData +from test_sensor import low_risk_value_dict + + +class TestG3T1_1ConnectedSensor(object): + """Tests for G3T1_1 when connected to a real sensor (spo2 service).""" + + topic = "oximeter_data" + + @classmethod + def setup_class(cls): + cls.received_messages = [] + cls.message_lock = threading.Lock() + cls.service_lock = threading.Lock() + cls.spo2_service = None + cls.test_value = low_risk_value_dict["oxigenation"] + + if not rospy.core.is_initialized(): + rospy.init_node("node_test_g3t1_1_connected", anonymous=True) + + cls._start_spo2_service() + rospy.sleep(0.2) + + @classmethod + def _start_spo2_service(cls): + with cls.service_lock: + if cls.spo2_service is not None: + return + cls.spo2_service = rospy.Service("spo2", SetBool, cls._spo2_callback) + + @classmethod + def _stop_spo2_service(cls): + with cls.service_lock: + if cls.spo2_service is not None: + cls.spo2_service.shutdown("spo2 service stopped for test") + cls.spo2_service = None + + @classmethod + def _spo2_callback(cls, _req): + return SetBoolResponse(success=True, message=str(cls.test_value)) + + def setup_method(self): + self.received_messages = [] + self.subscriber = rospy.Subscriber(self.topic, SensorData, self._message_callback) + time.sleep(0.2) + + def teardown_method(self): + self.subscriber.unregister() + self.received_messages = [] + self._start_spo2_service() + time.sleep(0.1) + + def _message_callback(self, msg): + with self.message_lock: + self.received_messages.append(msg) + + def _wait_for_message(self, timeout=3.0): + start_time = time.time() + while time.time() - start_time < timeout: + with self.message_lock: + if self.received_messages: + return self.received_messages[-1] + time.sleep(0.01) + return None + + def test_collect_uses_spo2_service_value(self): + message = self._wait_for_message() + assert message is not None + assert message.data == self.test_value + + def test_collect_handles_spo2_service_failure(self): + self._stop_spo2_service() + with self.message_lock: + del self.received_messages[:] + message = self._wait_for_message() + assert message is not None diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_2.launch b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_2.launch new file mode 100644 index 00000000..2d43e645 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_2.launch @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_2.py b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_2.py new file mode 100644 index 00000000..c3c630c0 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_2.py @@ -0,0 +1,8 @@ +import pytest +from test_sensor import SharedSensorTests + +class TestG3T1_2(SharedSensorTests): + """Test suite for G3T1_2 sensor""" + topic = 'ecg_data' + vital_sign = 'heart_rate' + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_2_connected.launch b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_2_connected.launch new file mode 100644 index 00000000..9eeabc2b --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_2_connected.launch @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_2_connected.py b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_2_connected.py new file mode 100644 index 00000000..18ace14a --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_2_connected.py @@ -0,0 +1,78 @@ +import threading +import time + +import pytest +import rospy +from std_srvs.srv import SetBool, SetBoolResponse + +from messages.msg import SensorData +from test_sensor import low_risk_value_dict + + +class TestG3T1_2ConnectedSensor(object): + """Tests for G3T1_2 when connected to a real sensor (spo2 service).""" + + topic = "ecg_data" + + @classmethod + def setup_class(cls): + cls.received_messages = [] + cls.message_lock = threading.Lock() + cls.service_lock = threading.Lock() + cls.spo2_service = None + cls.test_value = low_risk_value_dict["oxigenation"] + + if not rospy.core.is_initialized(): + rospy.init_node("node_test_g3t1_2_connected", anonymous=True) + + cls._start_spo2_service() + rospy.sleep(0.2) + + @classmethod + def _start_spo2_service(cls): + with cls.service_lock: + if cls.spo2_service is not None: + return + cls.spo2_service = rospy.Service("spo2", SetBool, cls._spo2_callback) + + @classmethod + def _stop_spo2_service(cls): + with cls.service_lock: + if cls.spo2_service is not None: + cls.spo2_service.shutdown("spo2 service stopped for test") + cls.spo2_service = None + + @classmethod + def _spo2_callback(cls, _req): + return SetBoolResponse(success=True, message=str(cls.test_value)) + + def setup_method(self): + self.received_messages = [] + self.subscriber = rospy.Subscriber(self.topic, SensorData, self._message_callback) + time.sleep(0.2) + + def teardown_method(self): + self.subscriber.unregister() + self.received_messages = [] + self._start_spo2_service() + time.sleep(0.1) + + def _message_callback(self, msg): + with self.message_lock: + self.received_messages.append(msg) + + def _wait_for_message(self, timeout=3.0): + start_time = time.time() + while time.time() - start_time < timeout: + with self.message_lock: + if self.received_messages: + return self.received_messages[-1] + time.sleep(0.01) + return None + + def test_collect_handles_spo2_service_failure(self): + self._stop_spo2_service() + with self.message_lock: + del self.received_messages[:] + message = self._wait_for_message() + assert message is not None diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_3.launch b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_3.launch new file mode 100644 index 00000000..09c84efb --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_3.launch @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_3.py b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_3.py new file mode 100644 index 00000000..50a158ff --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_3.py @@ -0,0 +1,8 @@ +import pytest +from test_sensor import SharedSensorTests + +class TestG3T1_3(SharedSensorTests): + """Test suite for G3T1_3 sensor""" + topic = 'thermometer_data' + vital_sign = 'temperature' + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_3_connected.launch b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_3_connected.launch new file mode 100644 index 00000000..df2dd6fd --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_3_connected.launch @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_3_connected.py b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_3_connected.py new file mode 100644 index 00000000..a74234a1 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_3_connected.py @@ -0,0 +1,78 @@ +import threading +import time + +import pytest +import rospy +from std_srvs.srv import SetBool, SetBoolResponse + +from messages.msg import SensorData +from test_sensor import low_risk_value_dict + + +class TestG3T1_3ConnectedSensor(object): + """Tests for G3T1_3 when connected to a real sensor (spo2 service).""" + + topic = "thermometer_data" + + @classmethod + def setup_class(cls): + cls.received_messages = [] + cls.message_lock = threading.Lock() + cls.service_lock = threading.Lock() + cls.spo2_service = None + cls.test_value = low_risk_value_dict["oxigenation"] + + if not rospy.core.is_initialized(): + rospy.init_node("node_test_g3t1_3_connected", anonymous=True) + + cls._start_spo2_service() + rospy.sleep(0.2) + + @classmethod + def _start_spo2_service(cls): + with cls.service_lock: + if cls.spo2_service is not None: + return + cls.spo2_service = rospy.Service("spo2", SetBool, cls._spo2_callback) + + @classmethod + def _stop_spo2_service(cls): + with cls.service_lock: + if cls.spo2_service is not None: + cls.spo2_service.shutdown("spo2 service stopped for test") + cls.spo2_service = None + + @classmethod + def _spo2_callback(cls, _req): + return SetBoolResponse(success=True, message=str(cls.test_value)) + + def setup_method(self): + self.received_messages = [] + self.subscriber = rospy.Subscriber(self.topic, SensorData, self._message_callback) + time.sleep(0.2) + + def teardown_method(self): + self.subscriber.unregister() + self.received_messages = [] + self._start_spo2_service() + time.sleep(0.1) + + def _message_callback(self, msg): + with self.message_lock: + self.received_messages.append(msg) + + def _wait_for_message(self, timeout=3.0): + start_time = time.time() + while time.time() - start_time < timeout: + with self.message_lock: + if self.received_messages: + return self.received_messages[-1] + time.sleep(0.01) + return None + + def test_collect_handles_spo2_service_failure(self): + self._stop_spo2_service() + with self.message_lock: + del self.received_messages[:] + message = self._wait_for_message() + assert message is not None diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_4.launch b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_4.launch new file mode 100644 index 00000000..9f54eac2 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_4.launch @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_4.py b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_4.py new file mode 100644 index 00000000..e9a61ab8 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_4.py @@ -0,0 +1,8 @@ +import pytest +from test_sensor import SharedSensorTests + +class TestG3T1_4(SharedSensorTests): + """Test suite for G3T1_4 sensor""" + topic = 'abps_data' + vital_sign = 'abps' + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_4_connected.launch b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_4_connected.launch new file mode 100644 index 00000000..66ddc392 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_4_connected.launch @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_4_connected.py b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_4_connected.py new file mode 100644 index 00000000..1b40d1b0 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_4_connected.py @@ -0,0 +1,78 @@ +import threading +import time + +import pytest +import rospy +from std_srvs.srv import SetBool, SetBoolResponse + +from messages.msg import SensorData +from test_sensor import low_risk_value_dict + + +class TestG3T1_4ConnectedSensor(object): + """Tests for G3T1_4 when connected to a real sensor (spo2 service).""" + + topic = "abps_data" + + @classmethod + def setup_class(cls): + cls.received_messages = [] + cls.message_lock = threading.Lock() + cls.service_lock = threading.Lock() + cls.spo2_service = None + cls.test_value = low_risk_value_dict["oxigenation"] + + if not rospy.core.is_initialized(): + rospy.init_node("node_test_g3t1_4_connected", anonymous=True) + + cls._start_spo2_service() + rospy.sleep(0.2) + + @classmethod + def _start_spo2_service(cls): + with cls.service_lock: + if cls.spo2_service is not None: + return + cls.spo2_service = rospy.Service("spo2", SetBool, cls._spo2_callback) + + @classmethod + def _stop_spo2_service(cls): + with cls.service_lock: + if cls.spo2_service is not None: + cls.spo2_service.shutdown("spo2 service stopped for test") + cls.spo2_service = None + + @classmethod + def _spo2_callback(cls, _req): + return SetBoolResponse(success=True, message=str(cls.test_value)) + + def setup_method(self): + self.received_messages = [] + self.subscriber = rospy.Subscriber(self.topic, SensorData, self._message_callback) + time.sleep(0.2) + + def teardown_method(self): + self.subscriber.unregister() + self.received_messages = [] + self._start_spo2_service() + time.sleep(0.1) + + def _message_callback(self, msg): + with self.message_lock: + self.received_messages.append(msg) + + def _wait_for_message(self, timeout=3.0): + start_time = time.time() + while time.time() - start_time < timeout: + with self.message_lock: + if self.received_messages: + return self.received_messages[-1] + time.sleep(0.01) + return None + + def test_collect_handles_spo2_service_failure(self): + self._stop_spo2_service() + with self.message_lock: + del self.received_messages[:] + message = self._wait_for_message() + assert message is not None diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_5.launch b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_5.launch new file mode 100644 index 00000000..d9e92246 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_5.launch @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_5.py b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_5.py new file mode 100644 index 00000000..42a97948 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_5.py @@ -0,0 +1,8 @@ +import pytest +from test_sensor import SharedSensorTests + +class TestG3T1_5(SharedSensorTests): + """Test suite for G3T1_5 sensor""" + topic = 'abpd_data' + vital_sign = 'abpd' + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_5_connected.launch b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_5_connected.launch new file mode 100644 index 00000000..11589b8a --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_5_connected.launch @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_5_connected.py b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_5_connected.py new file mode 100644 index 00000000..ea34fdb4 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_5_connected.py @@ -0,0 +1,78 @@ +import threading +import time + +import pytest +import rospy +from std_srvs.srv import SetBool, SetBoolResponse + +from messages.msg import SensorData +from test_sensor import low_risk_value_dict + + +class TestG3T1_5ConnectedSensor(object): + """Tests for G3T1_5 when connected to a real sensor (spo2 service).""" + + topic = "abpd_data" + + @classmethod + def setup_class(cls): + cls.received_messages = [] + cls.message_lock = threading.Lock() + cls.service_lock = threading.Lock() + cls.spo2_service = None + cls.test_value = low_risk_value_dict["oxigenation"] + + if not rospy.core.is_initialized(): + rospy.init_node("node_test_G3T1_5_connected", anonymous=True) + + cls._start_spo2_service() + rospy.sleep(0.2) + + @classmethod + def _start_spo2_service(cls): + with cls.service_lock: + if cls.spo2_service is not None: + return + cls.spo2_service = rospy.Service("spo2", SetBool, cls._spo2_callback) + + @classmethod + def _stop_spo2_service(cls): + with cls.service_lock: + if cls.spo2_service is not None: + cls.spo2_service.shutdown("spo2 service stopped for test") + cls.spo2_service = None + + @classmethod + def _spo2_callback(cls, _req): + return SetBoolResponse(success=True, message=str(cls.test_value)) + + def setup_method(self): + self.received_messages = [] + self.subscriber = rospy.Subscriber(self.topic, SensorData, self._message_callback) + time.sleep(0.2) + + def teardown_method(self): + self.subscriber.unregister() + self.received_messages = [] + self._start_spo2_service() + time.sleep(0.1) + + def _message_callback(self, msg): + with self.message_lock: + self.received_messages.append(msg) + + def _wait_for_message(self, timeout=3.0): + start_time = time.time() + while time.time() - start_time < timeout: + with self.message_lock: + if self.received_messages: + return self.received_messages[-1] + time.sleep(0.01) + return None + + def test_collect_handles_spo2_service_failure(self): + self._stop_spo2_service() + with self.message_lock: + del self.received_messages[:] + message = self._wait_for_message() + assert message is not None diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_6.launch b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_6.launch new file mode 100644 index 00000000..a7f9f76b --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_6.launch @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_6.py b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_6.py new file mode 100644 index 00000000..44dcab55 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_6.py @@ -0,0 +1,8 @@ +import pytest +from test_sensor import SharedSensorTests + +class TestG3T1_6(SharedSensorTests): + """Test suite for G3T1_6 sensor""" + topic = 'glucosemeter_data' + vital_sign = 'glucose' + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_6_connected.launch b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_6_connected.launch new file mode 100644 index 00000000..da55bc70 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_6_connected.launch @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_6_connected.py b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_6_connected.py new file mode 100644 index 00000000..cf562e86 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G3T1_6_connected.py @@ -0,0 +1,78 @@ +import threading +import time + +import pytest +import rospy +from std_srvs.srv import SetBool, SetBoolResponse + +from messages.msg import SensorData +from test_sensor import low_risk_value_dict + + +class TestG3T1_6ConnectedSensor(object): + """Tests for G3T1_6 when connected to a real sensor (spo2 service).""" + + topic = "glucosemeter_data" + + @classmethod + def setup_class(cls): + cls.received_messages = [] + cls.message_lock = threading.Lock() + cls.service_lock = threading.Lock() + cls.spo2_service = None + cls.test_value = low_risk_value_dict["oxigenation"] + + if not rospy.core.is_initialized(): + rospy.init_node("node_test_g3t1_6_connected", anonymous=True) + + cls._start_spo2_service() + rospy.sleep(0.2) + + @classmethod + def _start_spo2_service(cls): + with cls.service_lock: + if cls.spo2_service is not None: + return + cls.spo2_service = rospy.Service("spo2", SetBool, cls._spo2_callback) + + @classmethod + def _stop_spo2_service(cls): + with cls.service_lock: + if cls.spo2_service is not None: + cls.spo2_service.shutdown("spo2 service stopped for test") + cls.spo2_service = None + + @classmethod + def _spo2_callback(cls, _req): + return SetBoolResponse(success=True, message=str(cls.test_value)) + + def setup_method(self): + self.received_messages = [] + self.subscriber = rospy.Subscriber(self.topic, SensorData, self._message_callback) + time.sleep(0.2) + + def teardown_method(self): + self.subscriber.unregister() + self.received_messages = [] + self._start_spo2_service() + time.sleep(0.1) + + def _message_callback(self, msg): + with self.message_lock: + self.received_messages.append(msg) + + def _wait_for_message(self, timeout=3.0): + start_time = time.time() + while time.time() - start_time < timeout: + with self.message_lock: + if self.received_messages: + return self.received_messages[-1] + time.sleep(0.01) + return None + + def test_collect_handles_spo2_service_failure(self): + self._stop_spo2_service() + with self.message_lock: + del self.received_messages[:] + message = self._wait_for_message() + assert message is not None diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G4T1.launch b/src/sa-bsn/target_system/components/component/test/unit/test_G4T1.launch new file mode 100644 index 00000000..1136f837 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G4T1.launch @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/sa-bsn/target_system/components/component/test/unit/test_G4T1.py b/src/sa-bsn/target_system/components/component/test/unit/test_G4T1.py new file mode 100644 index 00000000..8e8966f6 --- /dev/null +++ b/src/sa-bsn/target_system/components/component/test/unit/test_G4T1.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- +import pytest +import rospy +import time +import threading +from messages.msg import SensorData, TargetSystemData +from asserts import Command, TIMEOUT_SECONDS + +class TestG4T1: + """Test suite for G4T1 Central Hub component""" + + @classmethod + def setup_class(cls): + """Initialize ROS node for testing""" + cls.received_messages = [] + cls.message_lock = threading.Lock() + rospy.init_node('test_g4t1_node', anonymous=True) + + def setup_method(self): + """Setup for each test method""" + # Clear old messages with lock + with self.message_lock: + self.received_messages = [] + + # Subscribe to TargetSystemData topic + self.subscriber = rospy.Subscriber( + '/TargetSystemData', + TargetSystemData, + self.message_callback + ) + + # Publisher for sensor data + self.sensors_pub = { + 'glucosemeter': rospy.Publisher( + '/glucosemeter_data', + SensorData, + queue_size=10 + ), + 'thermometer': rospy.Publisher( + '/thermometer_data', + SensorData, + queue_size=10 + ), + 'ecg': rospy.Publisher( + '/ecg_data', + SensorData, + queue_size=10 + ), + 'oximeter': rospy.Publisher( + '/oximeter_data', + SensorData, + queue_size=10 + ), + 'abps': rospy.Publisher( + '/abps_data', + SensorData, + queue_size=10 + ), + 'abpd': rospy.Publisher( + '/abpd_data', + SensorData, + queue_size=10 + ), + } + self.sensor_pub = rospy.Publisher( + '/SensorData', + SensorData, + queue_size=10 + ) + + time.sleep(0.5) # Wait for connections to establish + + def teardown_method(self): + """Cleanup after each test""" + self.subscriber.unregister() + self.sensor_pub.unregister() + + # Clear with lock to ensure thread-safety + with self.message_lock: + self.received_messages = [] + + def message_callback(self, msg): + """Callback for receiving TargetSystemData messages""" + with self.message_lock: + self.received_messages.append(msg) + + def wait_for_message(self, timeout=3.0, clear_old=True): + """Wait for a message to be received + + Args: + timeout: Maximum time to wait for message + clear_old: If True, clears old messages before waiting (default: True) + """ + if clear_old: + with self.message_lock: + self.received_messages = [] + time.sleep(0.2) # Small delay to allow new messages to arrive + + start_time = time.time() + while time.time() - start_time < timeout: + with self.message_lock: + if self.received_messages: + return self.received_messages[-1] + time.sleep(0.01) + return None + + def publish_sensor_data(self, sensor_type, risk_value, data_value, batt_value=100.0): + """Helper to publish sensor data""" + msg = SensorData() + msg.type = sensor_type + msg.risk = risk_value + msg.data = data_value + msg.batt = batt_value + self.sensors_pub[sensor_type].publish(msg) + time.sleep(0.1) + + # Test collect() method + def test_collect_thermometer_data(self): + """Test collecting thermometer sensor data""" + self.publish_sensor_data('thermometer', 15.0, 36.5, 95.0) + + rospy.sleep(0.5) # Allow time for processing + received_msg = self.wait_for_message() + + assert received_msg is not None + assert received_msg.trm_risk == 15.0 + assert received_msg.trm_data == 36.5 + assert received_msg.trm_batt == 95.0 + + # Test transfer() with different risk scenarios + def test_transfer_very_low_risk_patient(self): + """Test transfer with very low risk patient (<=20)""" + sensors = ['thermometer', 'ecg', 'oximeter', 'abps', 'abpd', 'glucosemeter'] + for sensor in sensors: + self.publish_sensor_data(sensor, 10.0, 100.0, 95.0) + + rospy.sleep(1.5) + received_msg = self.wait_for_message(clear_old=False) + assert received_msg is not None + # 10.0 * 0.833 = 8.33 + assert received_msg.patient_status <= 20.0, \ + "patient_status expected <=20, received: {}".format(received_msg.patient_status) + + def test_transfer_low_risk_patient(self): + """Test transfer with low risk patient (20-40)""" + sensors = ['thermometer', 'ecg', 'oximeter', 'abps', 'abpd', 'glucosemeter'] + for sensor in sensors: + self.publish_sensor_data(sensor, 35.0, 100.0, 90.0) # 35 * 0.833 = 29.16 + + rospy.sleep(1.5) + received_msg = self.wait_for_message(clear_old=False) + assert received_msg is not None + assert 20.0 < received_msg.patient_status <= 40.0, \ + "patient_status expected 20-40, received: {}".format(received_msg.patient_status) + + def test_transfer_moderate_risk_patient(self): + """Test transfer with moderate risk patient (40-60)""" + sensors = ['thermometer', 'ecg', 'oximeter', 'abps', 'abpd', 'glucosemeter'] + + # 55.0 * 0.833 = 45.8 (dentro de 40-60) + for sensor in sensors: + self.publish_sensor_data(sensor, 55.0, 100.0, 85.0) + + rospy.sleep(1.5) + received_msg = self.wait_for_message(clear_old=False) + assert received_msg is not None, "No message received" + + # Verify all sensors were processed + assert received_msg.trm_risk == 55.0 + assert received_msg.ecg_risk == 55.0 + assert received_msg.oxi_risk == 55.0 + assert received_msg.abps_risk == 55.0 + assert received_msg.abpd_risk == 55.0 + assert received_msg.glc_risk == 55.0 + + # Verify patient_status is in moderate range + assert 40.0 < received_msg.patient_status <= 60.0, \ + "patient_status expected 40-60, received: {}".format(received_msg.patient_status) + + def test_transfer_critical_risk_patient(self): + """Test transfer with critical risk patient (60-80)""" + sensors = ['thermometer', 'ecg', 'oximeter', 'abps', 'abpd', 'glucosemeter'] + for sensor in sensors: + self.publish_sensor_data(sensor, 85.0, 100.0, 80.0) # 85 * 0.833 = 70.8 + + rospy.sleep(1.5) + received_msg = self.wait_for_message(clear_old=False) + assert received_msg is not None + assert 60.0 < received_msg.patient_status <= 80.0, \ + "patient_status expected 60-80, received: {}".format(received_msg.patient_status) + + def test_transfer_very_critical_risk_patient(self): + """Test transfer with very critical risk patient (>80)""" + sensors = ['thermometer', 'ecg', 'oximeter', 'abps', 'abpd', 'glucosemeter'] + for sensor in sensors: + self.publish_sensor_data(sensor, 100.0, 100.0, 75.0) # 100 * 0.833 = 83.3 + + rospy.sleep(1.5) + received_msg = self.wait_for_message(clear_old=False) + assert received_msg is not None + assert received_msg.patient_status > 80.0, \ + "patient_status expected >80, received: {}".format(received_msg.patient_status) + + def test_transfer_mixed_risk_levels(self): + """Test transfer with different risk levels across sensors""" + self.publish_sensor_data('thermometer', 10.0, 36.8, 95.0) + self.publish_sensor_data('ecg', 50.0, 110.0, 90.0) + self.publish_sensor_data('oximeter', 80.0, 88.0, 85.0) + self.publish_sensor_data('abps', 15.0, 115.0, 80.0) + self.publish_sensor_data('abpd', 45.0, 95.0, 75.0) + self.publish_sensor_data('glucosemeter', 30.0, 120.0, 70.0) + + rospy.sleep(1.5) + received_msg = self.wait_for_message(clear_old=False) + assert received_msg is not None + assert 0 <= received_msg.patient_status <= 100.0 + + def test_transfer_all_sensor_data_fields(self): + """Test that all sensor data fields are transferred correctly""" + self.publish_sensor_data('thermometer', 15.0, 37.0, 95.0) + self.publish_sensor_data('ecg', 20.0, 75.0, 90.0) + self.publish_sensor_data('oximeter', 18.0, 96.0, 85.0) + self.publish_sensor_data('abps', 22.0, 120.0, 80.0) + self.publish_sensor_data('abpd', 19.0, 80.0, 75.0) + self.publish_sensor_data('glucosemeter', 25.0, 100.0, 70.0) + + rospy.sleep(1.5) + received_msg = self.wait_for_message(clear_old=False) + assert received_msg is not None, "No message received" + + # Check risk values + assert received_msg.trm_risk == 15.0, "trm_risk: expected 15.0, received {}".format(received_msg.trm_risk) + assert received_msg.ecg_risk == 20.0, "ecg_risk: expected 20.0, received {}".format(received_msg.ecg_risk) + assert received_msg.oxi_risk == 18.0, "oxi_risk: expected 18.0, received {}".format(received_msg.oxi_risk) + assert received_msg.abps_risk == 22.0, "abps_risk: expected 22.0, received {}".format(received_msg.abps_risk) + assert received_msg.abpd_risk == 19.0, "abpd_risk: expected 19.0, received {}".format(received_msg.abpd_risk) + assert received_msg.glc_risk == 25.0, "glc_risk: expected 25.0, received {}".format(received_msg.glc_risk) + + # Check raw data values + assert received_msg.trm_data == 37.0 + assert received_msg.ecg_data == 75.0 + assert received_msg.oxi_data == 96.0 + assert received_msg.abps_data == 120.0 + assert received_msg.abpd_data == 80.0 + assert received_msg.glc_data == 100.0 + + # Check battery values + assert received_msg.trm_batt == 95.0 + assert received_msg.ecg_batt == 90.0 + assert received_msg.oxi_batt == 85.0 + assert received_msg.abps_batt == 80.0 + assert received_msg.abpd_batt == 75.0 + assert received_msg.glc_batt == 70.0 + + # Test boundary conditions + def test_collect_zero_risk_data(self): + """Test collecting data with zero risk""" + self.publish_sensor_data('thermometer', 0.0, 36.5, 100.0) + time.sleep(0.5) + + received_msg = self.wait_for_message() + assert received_msg is not None, "No message received" + assert received_msg.trm_risk == 0.0, "Expected 0.0, received {}".format(received_msg.trm_risk) + + def test_collect_maximum_risk_data(self): + """Test collecting data with maximum risk (100)""" + self.publish_sensor_data('ecg', 100.0, 150.0, 50.0) + received_msg = self.wait_for_message() + assert received_msg is not None + assert received_msg.ecg_risk == 100.0 + + def test_collect_low_battery_data(self): + """Test collecting data with low battery level""" + self.publish_sensor_data('oximeter', 20.0, 95.0, 10.0) + time.sleep(0.5) + + received_msg = self.wait_for_message() + assert received_msg is not None, "No message received" + assert received_msg.oxi_batt == 10.0, "Expected 10.0, received {}".format(received_msg.oxi_batt) + + # Test sequential data collection + def test_sequential_sensor_updates(self): + """Test that sensor data is updated sequentially""" + # First update + self.publish_sensor_data('thermometer', 10.0, 36.5, 95.0) + msg1 = self.wait_for_message() + assert msg1.trm_risk == 10.0 + + # Second update with different value + self.received_messages = [] + self.publish_sensor_data('thermometer', 30.0, 38.0, 90.0) + msg2 = self.wait_for_message() + assert msg2.trm_risk == 30.0 + assert msg2.trm_data == 38.0 \ No newline at end of file diff --git a/src/sa-bsn/target_system/effectors/param_adapter/CMakeLists.txt b/src/sa-bsn/target_system/effectors/param_adapter/CMakeLists.txt index da7aec2e..ba00a611 100644 --- a/src/sa-bsn/target_system/effectors/param_adapter/CMakeLists.txt +++ b/src/sa-bsn/target_system/effectors/param_adapter/CMakeLists.txt @@ -5,7 +5,16 @@ add_compile_options(-std=c++11) ########################################################################### ## Find catkin and any catkin packages -FIND_PACKAGE(catkin REQUIRED COMPONENTS roscpp std_msgs genmsg messages archlib) +FIND_PACKAGE(catkin REQUIRED COMPONENTS + roscpp + std_msgs + genmsg + messages + archlib + rospy + std_srvs + rostest + ) ########################################################################### # Export catkin package. @@ -29,4 +38,6 @@ FILE(GLOB ${PROJECT_NAME}-src "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") SET(param_adapter-src "${CMAKE_CURRENT_SOURCE_DIR}/src/ParamAdapter.cpp") ADD_EXECUTABLE (param_adapter "${CMAKE_CURRENT_SOURCE_DIR}/apps/param_adapter.cpp" ${${PROJECT_NAME}-src} ${param_adapter-src}) TARGET_LINK_LIBRARIES (param_adapter ${catkin_LIBRARIES} ${LIBRARIES}) -ADD_DEPENDENCIES(param_adapter services_generate_messages_cpp) \ No newline at end of file +ADD_DEPENDENCIES(param_adapter services_generate_messages_cpp) + +###########################################################################