From 4b5d89bcac346ab4e8d21eb3ba164822b3a0bc41 Mon Sep 17 00:00:00 2001
From: Tomoya Fujita <tomoya.fujita825@gmail.com>
Date: Sun, 22 Dec 2024 13:35:16 -0800
Subject: [PATCH] add system test to github workflow for all distributions.
 (#32)

* add system test to github workflow for all distributions.

Signed-off-by: Tomoya.Fujita <tomoya.fujita825@gmail.com>

* test yaml configuration path fix.

Signed-off-by: Tomoya.Fujita <tomoya.fujita825@gmail.com>

---------

Signed-off-by: Tomoya.Fujita <tomoya.fujita825@gmail.com>
---
 README.md                     |  5 ++--
 scripts/build-verification.sh | 18 +++++++++++++-
 test/launch/test.launch.py    |  7 ------
 test/src/test.cpp             | 13 +++++++---
 test/test.py                  | 47 ++++++++++++++++++++++++++++-------
 5 files changed, 67 insertions(+), 23 deletions(-)

diff --git a/README.md b/README.md
index 93c6712..2627d1c 100644
--- a/README.md
+++ b/README.md
@@ -162,7 +162,7 @@ to install local colcon workspace,
 # cd <colcon_workspace>/src
 # git clone https://github.com/fujitatomoya/ros2_persist_parameter_server
 # cd <colcon_workspace>
-# colcon build --symlink-install
+# colcon build --symlink-install --packages-select parameter_server ros2_persistent_parameter_server_test
 # source install/local_setup.bash
 ```
 
@@ -237,8 +237,7 @@ make sure to add the path of `launch` package to the PATH environment.
 ```bash
 # mkdir -p /tmp/test
 # cp <colcon_workspace>/src/ros2_persist_parameter/server/param/parameter_server.yaml /tmp/test
-# cd <colcon_workspace>/src/ros2_persist_parameter/test
-# ./test.py
+# ./<colcon_workspace>/src/ros2_persist_parameter/test/test.py
 ```
 
 All of the test is listed with result as following
diff --git a/scripts/build-verification.sh b/scripts/build-verification.sh
index 93ec38a..e731e04 100755
--- a/scripts/build-verification.sh
+++ b/scripts/build-verification.sh
@@ -49,10 +49,25 @@ function build_parameter_server () {
     echo "[${FUNCNAME[0]}]: build ROS 2 parameter server."
     source /opt/ros/${ROS_DISTRO}/setup.bash
     cd ${COLCON_WORKSPACE}
-    # TODO: test project should be integrated with `colcon test`.
     colcon build --symlink-install --packages-select parameter_server ros2_persistent_parameter_server_test
 }
 
+function test_parameter_server () {
+    trap exit_trap ERR
+    echo "[${FUNCNAME[0]}]: test ROS 2 parameter server."
+    source /opt/ros/${ROS_DISTRO}/setup.bash
+    cd ${COLCON_WORKSPACE}
+
+    # TODO(@fujitatomoya): currently unit tests are missing for parameter server with `colcon test`.
+
+    # source the parameter server local packages
+    source ./install/local_setup.bash
+    # setup and execute the system test
+    mkdir /tmp/test
+    cp ./src/ros2_persist_parameter_server/server/param/parameter_server.yaml /tmp/test
+    ./src/ros2_persist_parameter_server/test/test.py
+}
+
 ########
 # Main #
 ########
@@ -70,5 +85,6 @@ trap exit_trap ERR
 install_prerequisites
 setup_build_colcon_env
 build_parameter_server
+test_parameter_server
 
 exit 0
diff --git a/test/launch/test.launch.py b/test/launch/test.launch.py
index 6bc0cdf..403aca3 100644
--- a/test/launch/test.launch.py
+++ b/test/launch/test.launch.py
@@ -16,19 +16,12 @@
 
 from launch import LaunchDescription
 from launch.substitutions import EnvironmentVariable
-import launch_ros.actions
 import launch
-import os
-import sys 
-import pathlib
 
 def generate_launch_description():
     return LaunchDescription([
         launch.actions.ExecuteProcess(
             cmd = ['ros2', 'run', 'parameter_server', 'server', '--file-path', '/tmp/test/parameter_server.yaml'],
             respawn=True
-        ),
-        launch.actions.ExecuteProcess(
-            cmd = ['ros2', 'run', 'ros2_persistent_parameter_server_test', 'client']
         )
     ])
diff --git a/test/src/test.cpp b/test/src/test.cpp
index 5aafa94..1bd6e50 100644
--- a/test/src/test.cpp
+++ b/test/src/test.cpp
@@ -118,17 +118,23 @@ class TestPersistParameter
     }
 
     // Get all test results.
-    inline void print_result() const
+    inline int  print_result() const
     {
+      int ret = EXIT_SUCCESS;
       RCLCPP_INFO(this->get_logger(), "****************************************************"
                                         "***********************");
       RCLCPP_INFO(this->get_logger(), "*********************************Test Result*********"
                                         "**********************");
       for(const auto & res : result_map_) {
         RCLCPP_INFO(this->get_logger(), "%-60s : %16s", res.first.c_str(), res.second?"PASS":"NOT PASS");
+
+        // if any tests are not passed, return EXIT_FAILURE.
+        if (res.second == false) {
+          ret = EXIT_FAILURE;
+        }
       }
 
-      return;
+      return ret;
     }
 
     static inline rclcpp::Logger get_logger()
@@ -223,7 +229,8 @@ int main(int argc, char ** argv)
     RCLCPP_ERROR(test_client->get_logger(), "unexpectedly failed: %s", e.what());
   }
 
-  test_client->print_result();
+  // if any tests are not passed, return EXIT_FAILURE.
+  ret_code = test_client->print_result();
   rclcpp::shutdown();
 
   return ret_code;
diff --git a/test/test.py b/test/test.py
index 0e09ac6..b45205f 100755
--- a/test/test.py
+++ b/test/test.py
@@ -1,17 +1,20 @@
 #!/usr/bin/python3
 
-"""A script to start `Server && Client` throught launch file and responsible for killing the Server"""
+"""A script to start `Server && Client` through launch file and responsible for killing the Server"""
 from threading import Thread
-import time 
-import os 
-import sys
-import signal 
-import shutil
+
+import os
 import psutil
+import shutil
+import signal
+import subprocess
+import sys
+import time
 
 signal.signal(signal.SIGINT, signal.SIG_DFL)
 sleep_time = 3
-launchCmd = 'ros2 launch ros2_persistent_parameter_server_test test.launch.py'
+launchServerCmd = ['ros2', 'launch', 'ros2_persistent_parameter_server_test', 'test.launch.py']
+launchClientCmd = ['ros2', 'run', 'ros2_persistent_parameter_server_test', 'client']
 
 if shutil.which('ros2') is None:
     print("source <colcon_ws>/install/setup.bash...then retry.")
@@ -33,8 +36,34 @@ def kill_server():
         print("parameter server cannot be killed")
         return
     time.sleep(5)
-    print("Press CTRL-C to shutdown...")
+    #print("Press CTRL-C to shutdown...")
+
+# Start Server process with respawn enabled, this process stays running
+server_process = subprocess.Popen(launchServerCmd, preexec_fn=os.setsid)
+print(f"Parameter Server Process started with PID: {server_process.pid}")
 
+# Start test client process
+client_process = subprocess.Popen(launchClientCmd)
+print(f"Parameter Client Process started with PID: {client_process.pid}")
+
+# Start killer thread to respawn the parameter server
 t = Thread(target = kill_server, args = ())
 t.start()
-os.system(launchCmd)
+
+# Wait until the client process finishes
+return_code = client_process.wait()
+
+# Cleanup the process and thread
+t.join()
+os.killpg(os.getpgid(server_process.pid), signal.SIGTERM)
+
+print("\nTest process finished.")
+print(f"Return Code: {return_code}")
+
+# Check if the client process completed successfully
+if return_code == 0:
+    print("The process completed successfully.")
+    sys.exit(0)
+else:
+    print("The process failed.")
+    sys.exit(1)