Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/sanity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,10 @@ jobs:
with:
go-version-file: gimble-ros/go.mod

- name: Install integration test dependencies
run: |
sudo apt-get update
sudo apt-get install -y jq

- name: Run integration tests
run: ./tests/integration.sh
21 changes: 13 additions & 8 deletions gimble-ros/internal/ros/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"time"
)

var graphCommandTimeout = 5 * time.Second

type TopicConnection struct {
Name string `json:"name"`
Publishers []string `json:"publishers"`
Expand All @@ -33,36 +35,33 @@ func CollectSystemGraph(ctx context.Context, runner Runner) (SystemGraph, error)
runner = ExecRunner{}
}

tctx, cancel := WithTimeout(ctx, 5*time.Second)
defer cancel()

graph := SystemGraph{Raw: map[string]string{}}

nodeList, err := runner.Run(tctx, "ros2", "node", "list")
nodeList, err := runGraphCommand(ctx, runner, "ros2", "node", "list")
if err != nil {
return graph, err
}
nodes := NonEmptyLines(nodeList)
for _, node := range nodes {
info, ierr := runner.Run(tctx, "ros2", "node", "info", node)
info, ierr := runGraphCommand(ctx, runner, "ros2", "node", "info", node)
if ierr != nil {
continue
}
graph.Raw["node_info:"+node] = info
graph.Nodes = append(graph.Nodes, ParseNodeDetails(node, info))
}

topicList, _ := runner.Run(tctx, "ros2", "topic", "list")
topicList, _ := runGraphCommand(ctx, runner, "ros2", "topic", "list")
for _, topic := range NonEmptyLines(topicList) {
info, ierr := runner.Run(tctx, "ros2", "topic", "info", "-v", topic)
info, ierr := runGraphCommand(ctx, runner, "ros2", "topic", "info", "-v", topic)
if ierr != nil {
continue
}
graph.Raw["topic_info:"+topic] = info
graph.Topics = append(graph.Topics, ParseTopicInfo(topic, info))
}

svcList, _ := runner.Run(tctx, "ros2", "service", "list")
svcList, _ := runGraphCommand(ctx, runner, "ros2", "service", "list")
graph.Services = NonEmptyLines(svcList)
sort.Strings(graph.Services)

Expand Down Expand Up @@ -152,3 +151,9 @@ func NonEmptyLines(in string) []string {
}
return out
}

func runGraphCommand(ctx context.Context, runner Runner, name string, args ...string) (string, error) {
tctx, cancel := WithTimeout(ctx, graphCommandTimeout)
defer cancel()
return runner.Run(tctx, name, args...)
}
74 changes: 74 additions & 0 deletions gimble-ros/internal/ros/graph_timeout_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package ros

import (
"context"
"fmt"
"strings"
"testing"
"time"
)

type delayedGraphRunner struct {
delay time.Duration
output map[string]string
}

func (r delayedGraphRunner) Run(ctx context.Context, name string, args ...string) (string, error) {
select {
case <-time.After(r.delay):
case <-ctx.Done():
return "", ctx.Err()
}
key := strings.Join(append([]string{name}, args...), " ")
if out, ok := r.output[key]; ok {
return out, nil
}
return "", fmt.Errorf("unexpected command: %s", key)
}

func TestCollectSystemGraph_UsesPerCommandTimeout(t *testing.T) {
old := graphCommandTimeout
graphCommandTimeout = 20 * time.Millisecond
t.Cleanup(func() { graphCommandTimeout = old })

runner := delayedGraphRunner{
delay: 12 * time.Millisecond,
output: map[string]string{
"ros2 node list": "/talker\n/listener",
"ros2 node info /talker": `Publishers:
* /chatter [std_msgs/msg/String]
Subscribers:
* /parameter_events [rcl_interfaces/msg/ParameterEvent]
Service Servers:
* /talker/get_parameters`,
"ros2 node info /listener": `Publishers:
* /parameter_events [rcl_interfaces/msg/ParameterEvent]
Subscribers:
* /chatter [std_msgs/msg/String]
Service Servers:
* /listener/get_parameters`,
"ros2 topic list": "/chatter",
"ros2 topic info -v /chatter": `Publisher count:
Publishers:
Node name: /talker
Subscription count:
Subscriptions:
Node name: /listener`,
"ros2 service list": "/talker/get_parameters\n/listener/get_parameters",
},
}

graph, err := CollectSystemGraph(context.Background(), runner)
if err != nil {
t.Fatalf("CollectSystemGraph returned error: %v", err)
}
if len(graph.Nodes) != 2 {
t.Fatalf("expected 2 nodes, got %d", len(graph.Nodes))
}
if len(graph.Topics) != 1 {
t.Fatalf("expected 1 topic, got %d", len(graph.Topics))
}
if len(graph.Services) != 2 {
t.Fatalf("expected 2 services, got %d", len(graph.Services))
}
}
30 changes: 29 additions & 1 deletion tests/integration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,37 @@ cleanup() {
docker stop "$CONTAINER_NAME" 2>/dev/null || true
docker rm "$CONTAINER_NAME" 2>/dev/null || true
}

print_debug_info() {
echo "==> Debug: container status"
docker ps -a --filter "name=$CONTAINER_NAME" || true
echo "==> Debug: container logs"
docker logs "$CONTAINER_NAME" 2>/dev/null || true
}
on_error() {
echo "ERROR: integration script failed at line $1"
print_debug_info
}
trap 'on_error $LINENO' ERR
trap cleanup EXIT

cd "$REPO_ROOT"

echo "==> Ensuring no stale container with same name exists..."
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true

echo "==> Building ROS2 demo image..."
docker build -t "$IMAGE_NAME" -f gimble-ros/docker/Dockerfile .

echo "==> Starting ROS2 container..."
docker run -d --name "$CONTAINER_NAME" "$IMAGE_NAME"

if [[ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || echo false)" != "true" ]]; then
echo "ERROR: ROS2 container is not running"
print_debug_info
exit 1
fi

echo "==> Waiting for ROS2 nodes to start..."
for i in {1..30}; do
if docker exec "$CONTAINER_NAME" ros2 node list 2>/dev/null | grep -q "/talker"; then
Expand All @@ -32,6 +53,7 @@ for i in {1..30}; do
if [[ $i -eq 30 ]]; then
echo "ERROR: /talker node did not appear within 30s"
docker exec "$CONTAINER_NAME" ros2 node list 2>/dev/null || true
print_debug_info
exit 1
fi
sleep 1
Expand All @@ -43,6 +65,12 @@ make build
echo "==> Running gim graph ros2 --container $CONTAINER_NAME..."
GRAPH_JSON=$(./gimble-ros/gim graph ros2 --container "$CONTAINER_NAME")

if ! echo "$GRAPH_JSON" | jq -e . >/dev/null; then
echo "ERROR: gim graph did not return valid JSON"
echo "$GRAPH_JSON"
exit 1
fi

echo "==> Verifying graph output..."
NODE_COUNT=$(echo "$GRAPH_JSON" | jq -r '.nodes | length')
TOPIC_COUNT=$(echo "$GRAPH_JSON" | jq -r '.topics | length')
Expand All @@ -62,7 +90,7 @@ fi
echo " Found $NODE_COUNT node(s), $TOPIC_COUNT topic(s)"

echo "==> Running gim analyze ros2 --container $CONTAINER_NAME..."
ANALYZE_OUT=$(./gimble-ros/gim analyze ros2 --container "$CONTAINER_NAME" 2>&1)
ANALYZE_OUT=$(GIMBLE_AI_API_KEY= ./gimble-ros/gim analyze ros2 --container "$CONTAINER_NAME" 2>&1)

if [[ -z "$ANALYZE_OUT" ]]; then
echo "ERROR: gim analyze produced no output"
Expand Down
Loading