Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration Tests Workflow #472

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d4df093
Git Ignored the node_modules folder in website
FireBoyAJ24 May 23, 2024
3094d85
Sending ROS-WEB inputs into the ROS Network
FireBoyAJ24 Jun 1, 2024
10f805c
Web tests except global path is working
FireBoyAJ24 Jul 9, 2024
bf537aa
Added Global Path tests
FireBoyAJ24 Jul 9, 2024
e83ae8e
Fixed an error in the global path success process
FireBoyAJ24 Jul 9, 2024
f300739
Merge branch 'main' into user/Jay/WEB_Int_test
FireBoyAJ24 Jul 9, 2024
460f6ed
Formatted the file
FireBoyAJ24 Jul 9, 2024
50a3f7d
Merge branch 'user/Jay/WEB_Int_test' of https://github.com/UBCSailbot…
FireBoyAJ24 Jul 9, 2024
ee634ac
Removed an unused package
FireBoyAJ24 Jul 9, 2024
1442c14
Fixed type issues
FireBoyAJ24 Jul 9, 2024
fc48f28
Added AIS Ships test data
FireBoyAJ24 Jul 10, 2024
3e2eba5
Added gps test files
FireBoyAJ24 Jul 10, 2024
e3c31be
Removed unneccesary functions and work
FireBoyAJ24 Jul 10, 2024
4a2be2d
Removed unused packages
FireBoyAJ24 Jul 10, 2024
71885d3
Added path and ros yaml files
FireBoyAJ24 Jul 10, 2024
e773c69
Cleaned up the markdown
FireBoyAJ24 Jul 10, 2024
1cdd2e2
Removed the unneccesary readme
FireBoyAJ24 Jul 10, 2024
a628812
Changed the POST URL to the correct one
FireBoyAJ24 Jul 10, 2024
61d2075
Added a web fail variable to fail when web publishing fails
FireBoyAJ24 Jul 10, 2024
cc94393
Linting of the file
FireBoyAJ24 Jul 10, 2024
4bade2f
Clarified a error message
FireBoyAJ24 Jul 16, 2024
4231711
Adding type ignore to timeout_cb
FireBoyAJ24 Aug 8, 2024
73cf43f
Update to the type ignore for timout_cb
FireBoyAJ24 Aug 8, 2024
e8dbe52
Merge remote-tracking branch 'origin' into user/Jay/WEB_Int_test
FireBoyAJ24 Jan 11, 2025
33574cf
Add an exception when no data is recieved from Database
FireBoyAJ24 Jan 11, 2025
c361a80
Created a new integration-test workflow
FireBoyAJ24 Jan 16, 2025
c7a6f6e
Run setup and build script before running tests
FireBoyAJ24 Jan 16, 2025
68ed558
Set the terminal to integration test directory
FireBoyAJ24 Jan 16, 2025
10cdcf6
Added source install/setup
FireBoyAJ24 Jan 16, 2025
2401e20
Add runing virtual iridium
FireBoyAJ24 Jan 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
20 changes: 20 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Integration-Tests

on:
push:
pull_request:
workflow_dispatch:

jobs:
integration-test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout workspace
uses: actions/checkout@v4

- name: Run integration tests
uses: ./.github/actions/
with:
script: './scripts/run-integration-tests'
run-website: 'true'
2 changes: 2 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ jobs:
uses: ./.github/actions/
with:
script: './scripts/clang-tidy'


1 change: 1 addition & 0 deletions scripts/build/.built_by
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
colcon
Empty file added scripts/build/COLCON_IGNORE
Empty file.
1 change: 1 addition & 0 deletions scripts/install/.colcon_install_layout
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
merged
Empty file added scripts/install/COLCON_IGNORE
Empty file.
Empty file added scripts/log/COLCON_IGNORE
Empty file.
2 changes: 2 additions & 0 deletions scripts/log/build_2025-01-11_09-55-00/logger_all.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[0.673s] DEBUG:colcon:Command line arguments: ['/usr/bin/colcon', 'build', '--packages-ignore', 'virtual_iridium', '--merge-install', '--symlink-install', '--cmake-args', '-DCMAKE_BUILD_TYPE=Debug', '-DSTATIC_ANALYSIS=OFF', '-DUNIT_TEST=ON', '--no-warn-unused-cli']
[0.673s] DEBUG:colcon:Parsed command line arguments: Namespace(log_base=None, log_level=None, verb_name='build', build_base='build', install_base='install', merge_install=True, symlink_install=True, test_result_base=None, continue_on_error=False, executor='parallel', parallel_workers=8, event_handlers=None, ignore_user_meta=False, metas=['./colcon.meta'], base_paths=['.'], packages_ignore=['virtual_iridium'], packages_ignore_regex=None, paths=None, packages_up_to=None, packages_up_to_regex=None, packages_above=None, packages_above_and_dependencies=None, packages_above_depth=None, packages_select_by_dep=None, packages_skip_by_dep=None, packages_skip_up_to=None, packages_select_build_failed=False, packages_skip_build_finished=False, packages_select_test_failures=False, packages_skip_test_passed=False, packages_select=None, packages_skip=None, packages_select_regex=None, packages_skip_regex=None, packages_start=None, packages_end=None, cmake_args=['-DCMAKE_BUILD_TYPE=Debug', '-DSTATIC_ANALYSIS=OFF', '-DUNIT_TEST=ON', '--no-warn-unused-cli'], cmake_target=None, cmake_target_skip_unavailable=False, cmake_clean_cache=False, cmake_clean_first=False, cmake_force_configure=False, ament_cmake_args=None, catkin_cmake_args=None, catkin_skip_building_tests=False, verb_parser=<colcon_defaults.argument_parser.defaults.DefaultArgumentsDecorator object at 0x7fed30eff820>, verb_extension=<colcon_core.verb.build.BuildVerb object at 0x7fed30efe800>, main=<bound method BuildVerb.main of <colcon_core.verb.build.BuildVerb object at 0x7fed30efe800>>)
1 change: 1 addition & 0 deletions scripts/log/latest
1 change: 1 addition & 0 deletions scripts/log/latest_build
25 changes: 25 additions & 0 deletions scripts/run-integration-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash
set -e

if [[ $LOCAL_RUN != "true" ]]; then
# give user permissions, required for GitHub Actions
sudo chown -R $(whoami):$(whoami) $ROS_WORKSPACE

source /opt/ros/${ROS_DISTRO}/setup.bash
./scripts/setup.sh
./scripts/build.sh
./scripts/run_virtual_iridium.sh
source $ROS_WORKSPACE/install/setup.bash
fi

echo "Integration tests started"
cd $ROS_WORKSPACE/src/integration_tests

for FILE in $ROS_WORKSPACE/src/integration_tests/testplans/*; do
if [ -f $FILE ]; then
echo "Running $FILE test plan"
ros2 run integration_tests run --ros-args -p testplan:=$FILE
fi
done

echo "Integration tests finished"
168 changes: 153 additions & 15 deletions src/integration_tests/integration_tests/integration_test_node.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import builtins
import functools
import json
import os
import signal
import subprocess
import sys
import time
import traceback
import urllib.request
from dataclasses import dataclass, field
from typing import Any, Optional, Tuple, Type, TypeVar, Union
from typing import Any, Optional, Tuple, Type, Union

import rclpy
import rclpy.node
import std_msgs.msg
import custom_interfaces.msg # type: ignore
import rclpy # type: ignore
import rclpy.node # type: ignore
import std_msgs.msg # type: ignore
import yaml
from rclpy.impl.rcutils_logger import RcutilsLogger
from rclpy.impl.rcutils_logger import RcutilsLogger # type: ignore
from rclpy.node import MsgType, Node

import custom_interfaces.msg

MIN_SETUP_DELAY_S = 1 # Minimum 1 second delay between setting up the test and sending inputs
DEFAULT_TIMEOUT_SEC = 3 # Number of seconds that the test has to run

Expand All @@ -30,9 +31,13 @@
NON_ROS_PACKAGES = ["virtual_iridium", "website"]

# TODO: TestMsgType needs to encompass/inherit whatever type we use for HTTP messages
HTTP_MSG_PLACEHOLDER_TYPE = TypeVar("HTTP_MSG_PLACEHOLDER_TYPE")
HTTP_MSG_PLACEHOLDER_TYPE = builtins.dict[str, Any]
TestMsgType = Union[rclpy.node.MsgType, HTTP_MSG_PLACEHOLDER_TYPE]

GLOBAL_PATH_API_NAME = "globalpath"
POST_URL = "http://localhost:8081/global-path"
PULL_URL = "http://localhost:3005/api/"


def main(args=None):
rclpy.init(args=args)
Expand Down Expand Up @@ -284,7 +289,7 @@ def get_ros_dtype(dtype: str) -> Tuple[Union[builtins.type, MsgType], Type[MsgTy
return (
getattr(custom_interfaces.msg, dtype)(),
getattr(custom_interfaces.msg, dtype),
)
) # type: ignore
except AttributeError:
raise TypeError(f"INVALID TYPE: {dtype}")

Expand Down Expand Up @@ -355,6 +360,24 @@ def parse_ros_data(data: dict) -> Tuple[Union[None, rclpy.node.MsgType], rclpy.n
return msg, msg_type


def parse_http_data(
data: dict,
) -> Tuple[Union[None, HTTP_MSG_PLACEHOLDER_TYPE], HTTP_MSG_PLACEHOLDER_TYPE]:
"""Parses

Args:
data (dict): Stores the expected HTTP output

Returns:
Tuple[Union[None, HTTP_MSG_PLACEHOLDER_TYPE], HTTP_MSG_PLACEHOLDER_TYPE]: _description_
"""

msg_type = data["dtype"]
data.pop("dtype")

return data, msg_type


@dataclass
class IOEntry:
"""Represents IO data"""
Expand Down Expand Up @@ -396,7 +419,7 @@ def __set_inputs(self, inputs: list[dict]):
inputs (list[dict]): list of inputs

Raises:
NotImplementedError: If an input of type HTTP is given. It's on the TODO list :)
NotImplementedError: GlobalPath messages are not yet supported
KeyError: If an invalid input type is given. Valid input types are ROS and HTTP.
"""
for input_dict in inputs:
Expand All @@ -408,7 +431,17 @@ def __set_inputs(self, inputs: list[dict]):
self.__ros_inputs.append(new_input)

elif input_dict["type"] == "HTTP":
raise NotImplementedError("HTTP support is a WIP")
data = input_dict["data"]
if GLOBAL_PATH_API_NAME == input_dict["name"]:
# This is a GlobalPath message
msg, msg_type = parse_http_data(data) # type: ignore
new_input = IOEntry(name=input_dict["name"], msg_type=msg_type, msg=msg)
else:
msg, msg_type = parse_ros_data(data) # type: ignore
new_input = IOEntry(name=input_dict["name"], msg_type=msg_type, msg=msg)

self.__http_inputs.append(new_input)

else:
raise KeyError(f"Invalid input type: {input_dict['type']}")

Expand All @@ -431,7 +464,12 @@ def __set_expected_outputs(self, outputs: list[dict]):
self.__ros_e_outputs.append(new_output)

elif output["type"] == "HTTP":
raise NotImplementedError("HTTP support is a WIP")
data = output["data"]

msg, msg_type = parse_http_data(data) # type: ignore
new_output = IOEntry(name=output["name"], msg_type=msg_type, msg=msg)
self.__http_e_outputs.append(new_output)

else:
raise KeyError(f"Invalid output type: {output['type']}")

Expand Down Expand Up @@ -585,6 +623,9 @@ def __init__(self) -> None:

testplan_file = self.get_parameter("testplan").get_parameter_value().string_value

self.__http_outputs: list[dict[str, Any]] = []
self.__web_fail = 0

try:
self.__test_inst = IntegrationTestSequence(testplan_file)
try:
Expand Down Expand Up @@ -615,13 +656,32 @@ def __init__(self) -> None:
)
self.__ros_subs.append(sub)

# TODO: HTTP
self.__http_inputs: list[ROSInputEntry] = []
self.__global_path_pub = False
for http_input in self.__test_inst.http_inputs():
if http_input.name != GLOBAL_PATH_API_NAME:
pub = self.create_publisher(
msg_type=http_input.msg_type,
topic=http_input.name,
qos_profile=10, # change
)
self.__http_inputs.append(ROSInputEntry(pub=pub, msg=http_input.msg))
else:
# This is a GlobalPath message
self.__global_path_pub = self.pub_global_path(http_input.msg)

if self.__global_path_pub:
self.get_logger().info("Published GlobalPath to database")
else:
self.__web_fail = 1

# IMPORTANT: MAKE SURE EXPECTED OUTPUTS ARE SETUP BEFORE SENDING INPUTS
time.sleep(MIN_SETUP_DELAY_S)
self.drive_inputs()

self.timeout = self.create_timer(self.__test_inst.timeout_sec(), self.__timeout_cb)
self.timeout = self.create_timer(
self.__test_inst.timeout_sec(), self.__timeout_cb() # type: ignore
)
except Exception as e:
# At this point, the test instance has successfully started all package processes.
# This except block is a failsafe to kill the processes in case anything crashes
Expand Down Expand Up @@ -651,14 +711,91 @@ def __pub_ros(self) -> None:
pub.publish(msg)
self.get_logger().info(f'Published to topic: "{pub.topic}", with msg: "{msg}"')

def __pub_http(self) -> None:
"""Publish to all registered ROS input topics that interact with HTTP endpoints"""
for web_input in self.__http_inputs:
pub = web_input.pub
msg = web_input.msg
pub.publish(msg)
self.get_logger().info(f'Published to topic: "{pub.topic}", with msg: "{msg}"')

def pub_global_path(self, msg: Union[dict[str, Any], None]) -> bool:
"""Publish to the GlobalPath HTTP endpoint

Args:
msg (HTTP_MSG_PLACEHOLDER_TYPE): Message to publish
"""
data = json.dumps(msg).encode("utf8")

try:
urllib.request.urlopen(
POST_URL,
data,
)
return True
except urllib.error.URLError as e:
self.get_logger().error(
f"Failed to publish Global path to the remote transceiver: {e}"
)
return False
except urllib.error.HTTPError as e:
self.get_logger().error(
f"Failed to publish Global path to the remote transceiver: {e}"
)
return False

def http_evaluate(self, http_outputs: list[dict[str, Any]]) -> int:

num_fail = 0

for count, http_e_output in enumerate(self.__test_inst.http_expected_outputs()):
topic = http_e_output.name
data = http_outputs[count]

if data != http_e_output.msg:
self._logger.error(
f"HTTP output from {topic} does not match expected output: {data}"
)
num_fail += 1

return num_fail

def get_http_outputs(self) -> int:

num_fail = 0

for e_http_output in self.__test_inst.http_expected_outputs():
topic = e_http_output.name
try:
contents = urllib.request.urlopen(PULL_URL + topic)
except urllib.error.HTTPError as e:
self._logger.error(f"HTTPError: {e}")
return False

data_dict = json.load(contents)
try:
data = (data_dict["data"])[0]
except IndexError:
self._logger.error(f"No data found for {topic}")
return False

data.pop(
"timestamp"
) # Remove timestamp from data as it is not relevant for comparison
self.__http_outputs.append(data)

num_fail = self.http_evaluate(self.__http_outputs)

return num_fail

def __timeout_cb(self) -> None:
"""Callback for when the test times out. Stops all test processes, evaluates correctness,
and exits
"""
self.__test_inst.finish() # Stop tests

num_fail, num_warn = self.__monitor.evaluate(self.get_logger())

num_fail += self.__web_fail + self.get_http_outputs()
if num_warn > 0:
self.get_logger().warn(
(
Expand All @@ -680,6 +817,7 @@ def drive_inputs(self) -> None:
"""Drive all registered test inputs"""
self.__pub_ros()
# TODO: add HTTP
self.__pub_http()


if __name__ == "__main__":
Expand Down
Loading
Loading