Skip to content

Commit 8e60ebb

Browse files
committed
[docker] add TREL automated integration test suite in DinD
Adds automated integration testing for the Thread Radio Encapsulation Link (TREL) feature in the Docker-in-Docker (DinD) test environment. Specifically: - Refactors tests/scripts/spinel_proxy.sh to support EXP_RCP_PORT for concurrent container simulation relays on shared bridge links. - Modifies tests/scripts/test_dind_dns_sd.sh to sequentially orchestrate dind_dns_sd.exp and dind_trel.exp expect suites, optimizing CI build layers caching while extending cleanup traps to drop PTY and bridges. - Implements tests/scripts/expect/dind_trel.exp to orchestrate PTY socat tunnels, configure datasets pre-boot, wait for child attachment, assert mDNS peer discovery via glob matches, parse unicast routable route targets, and verify TREL keep-alive inbound packet counters.
1 parent 2323cd1 commit 8e60ebb

5 files changed

Lines changed: 311 additions & 23 deletions

File tree

tests/scripts/expect/_common.exp

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,22 @@ proc create_socat {id} {
130130
return [list $pty1 $pty2]
131131
}
132132

133+
proc create_socat_tcp {id port} {
134+
global socat_pids
135+
spawn socat -d -d pty,raw,echo=0 tcp-listen:$port,reuseaddr,bind=0.0.0.0
136+
lappend socat_pids [exp_pid]
137+
set pty ""
138+
expect {
139+
-re {PTY is (\S+)} {
140+
set pty $expect_out(1,string)
141+
}
142+
timeout {
143+
fail "Error: Timed out starting socat for id $id on port $port"
144+
}
145+
}
146+
return $pty
147+
}
148+
133149
proc start_otbr_docker {name sim_app sim_id pty1 pty2} {
134150
exec $sim_app $sim_id <$pty1 >$pty1 &
135151
exec docker run -d \
@@ -159,8 +175,15 @@ proc dispose_node {id} {
159175
}
160176
proc dispose_socat {} {
161177
global socat_pid
178+
global socat_pids
162179
if { [info exists socat_pid] } {
163-
exec kill $socat_pid
180+
catch { exec kill $socat_pid }
181+
}
182+
if { [info exists socat_pids] } {
183+
foreach pid $socat_pids {
184+
catch { exec kill $pid }
185+
}
186+
set socat_pids {}
164187
}
165188
}
166189
proc dispose_all {} {

tests/scripts/expect/dind_dns_sd.exp

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,6 @@ proc start_otbr_docker_dind {name sim_app sim_id pty} {
6060
sleep 5
6161
}
6262

63-
proc create_socat_tcp {id port} {
64-
global socat_pid
65-
spawn socat -d -d pty,raw,echo=0 tcp-listen:$port,reuseaddr,bind=0.0.0.0
66-
set socat_pid [exp_pid]
67-
set pty ""
68-
expect {
69-
-re {PTY is (\S+)} {
70-
set pty $expect_out(1,string)
71-
}
72-
timeout {
73-
send_user "Error: Timed out starting socat\n"; exit 1
74-
}
75-
}
76-
return $pty
77-
}
7863

7964
set pty1 [create_socat_tcp 1 9000]
8065
set container "otbr-test-container"

tests/scripts/expect/dind_trel.exp

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
#!/usr/bin/expect -f
2+
#
3+
# Copyright (c) 2026, The OpenThread Authors.
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
# 1. Redistributions of source code must retain the above copyright
9+
# notice, this list of conditions and the following disclaimer.
10+
# 2. Redistributions in binary form must reproduce the above copyright
11+
# notice, this list of conditions and the following disclaimer in the
12+
# documentation and/or other materials provided with the distribution.
13+
# 3. Neither the name of the copyright holder nor the
14+
# names of its contributors may be used to endorse or promote products
15+
# derived from this software without specific prior written permission.
16+
#
17+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
# POSSIBILITY OF SUCH DAMAGE.
28+
#
29+
30+
proc fail {message} {
31+
send_user "Error: $message\n"
32+
exit 1
33+
}
34+
35+
source "tests/scripts/expect/_common.exp"
36+
37+
38+
39+
# Dynamic helper to start the OTBR docker container and map the spinel relay
40+
proc start_otbr_docker_trel {name sim_app sim_id pty rcp_port} {
41+
exec docker run -d \
42+
--name $name \
43+
--network infrastructure-link \
44+
--cap-add=NET_ADMIN \
45+
--privileged \
46+
--add-host=host.docker.internal:host-gateway \
47+
--sysctl net.ipv6.conf.all.disable_ipv6=0 \
48+
--sysctl net.ipv4.conf.all.forwarding=1 \
49+
--sysctl net.ipv4.conf.default.forwarding=1 \
50+
--sysctl net.ipv6.conf.all.forwarding=1 \
51+
--sysctl net.ipv6.conf.default.forwarding=1 \
52+
-v $::env(EXP_REPO_ROOT)/tests/scripts/spinel_proxy.sh:/usr/bin/spinel_proxy.sh \
53+
-e OT_RCP_DEVICE=spinel+hdlc+forkpty:///usr/bin/spinel_proxy.sh \
54+
-e OT_INFRA_IF=eth0 \
55+
-e OT_THREAD_IF=wpan0 \
56+
-e EXP_RCP_PORT=$rcp_port \
57+
$::env(EXP_OTBR_DOCKER_IMAGE)
58+
sleep 2
59+
60+
# Start the simulated RCP binary on the host
61+
exec $sim_app $sim_id <$pty >$pty &
62+
sleep 5
63+
}
64+
65+
# Start relays and containers
66+
set pty1 [create_socat_tcp 1 9000]
67+
set pty2 [create_socat_tcp 2 9001]
68+
69+
set container1 "otbr-test-container-1"
70+
set container2 "otbr-test-container-2"
71+
72+
set dataset "0e080000000000010000000300001435060004001fffe002087d61eb42cdc48d6a0708fd0d07fca1b9f0500510ba088fc2bd6c3b3897f7a10f58263ff3030f4f70656e5468726561642d353234660102524f04109dc023ccd447b12b50997ef68020f19e0c0402a0f7f8"
73+
74+
send_user -- "--- Starting container 1 ---\n"
75+
start_otbr_docker_trel $container1 $::env(EXP_OT_RCP_PATH) 2 $pty1 9000
76+
77+
send_user -- "--- Starting container 2 ---\n"
78+
start_otbr_docker_trel $container2 $::env(EXP_OT_RCP_PATH) 3 $pty2 9001
79+
80+
# Ensure both containers have initialized correctly and otbr-agent socket is ready
81+
foreach c [list $container1 $container2] {
82+
set socket_ready false
83+
for {set i 0} {$i < 10} {incr i} {
84+
if {![catch {exec docker exec -i $c ot-ctl state}]} {
85+
set socket_ready true
86+
break
87+
}
88+
sleep 2
89+
}
90+
if {!$socket_ready} {
91+
send_user "Error: ot-ctl failed to communicate with otbr-agent on $c\n"; exit 1
92+
}
93+
}
94+
95+
# Stop Thread and down interface on both containers initially to prevent default auto-start
96+
foreach c [list $container1 $container2] {
97+
exec docker exec -i $c ot-ctl thread stop
98+
exec docker exec -i $c ot-ctl ifconfig down
99+
}
100+
101+
# 1. Configure Active Dataset on both containers BEFORE starting Thread
102+
send_user -- "--- Configuring Active Dataset on both containers ---\n"
103+
exec docker exec -i $container1 ot-ctl dataset set active $dataset
104+
exec docker exec -i $container2 ot-ctl dataset set active $dataset
105+
106+
# Start Thread on Container 1 to form the network
107+
send_user -- "--- Starting Thread on Container 1 ---\n"
108+
exec docker exec -i $container1 ot-ctl ifconfig up
109+
exec docker exec -i $container1 ot-ctl thread start
110+
111+
# Wait for Container 1 to become leader
112+
set leader false
113+
for {set i 0} {$i < 20} {incr i} {
114+
spawn docker exec -i $container1 ot-ctl state
115+
set timeout 3
116+
expect {
117+
"leader" {
118+
set leader true
119+
break
120+
}
121+
timeout {}
122+
}
123+
catch {close}; catch {wait}
124+
sleep 2
125+
}
126+
if {!$leader} {
127+
send_user "Error: Container 1 did not become leader.\n"; exit 1
128+
}
129+
130+
# 2. Start Thread on Container 2 to join the network
131+
send_user -- "--- Starting Thread on Container 2 ---\n"
132+
exec docker exec -i $container2 ot-ctl ifconfig up
133+
exec docker exec -i $container2 ot-ctl thread start
134+
135+
# Wait for Container 2 to join successfully (router or child state)
136+
set joined false
137+
for {set i 0} {$i < 25} {incr i} {
138+
spawn docker exec -i $container2 ot-ctl state
139+
set timeout 3
140+
expect {
141+
"router" {
142+
set joined true
143+
break
144+
}
145+
"child" {
146+
set joined true
147+
break
148+
}
149+
timeout {}
150+
}
151+
catch {close}; catch {wait}
152+
sleep 2
153+
}
154+
if {!$joined} {
155+
send_user "Error: Container 2 failed to join the network.\n"; exit 1
156+
}
157+
158+
# 3. Fetch Extended MAC Addresses to verify TREL peer discovery later
159+
send_user -- "--- Fetching Extended MAC Addresses for TREL verification ---\n"
160+
set otbr1_extaddr [exec docker exec -i $container1 ot-ctl extaddr]
161+
set otbr1_extaddr [lindex [split $otbr1_extaddr "\n"] 0]
162+
set otbr1_extaddr [string trim $otbr1_extaddr]
163+
set otbr2_extaddr [exec docker exec -i $container2 ot-ctl extaddr]
164+
set otbr2_extaddr [lindex [split $otbr2_extaddr "\n"] 0]
165+
set otbr2_extaddr [string trim $otbr2_extaddr]
166+
167+
# Retrieve infrastructure global IP address of both containers to verify TREL SocketAddress mappings
168+
set format "\{\{range .NetworkSettings.Networks\}\}\{\{.GlobalIPv6Address\}\}\{\{end\}\}"
169+
set otbr1_infra_ip [exec docker inspect $container1 -f $format]
170+
set otbr1_infra_ip [string trim $otbr1_infra_ip]
171+
172+
set otbr2_infra_ip [exec docker inspect $container2 -f $format]
173+
set otbr2_infra_ip [string trim $otbr2_infra_ip]
174+
175+
send_user "Container 1 Infra IP: $otbr1_infra_ip\n"
176+
send_user "Container 2 Infra IP: $otbr2_infra_ip\n"
177+
178+
# Allow time for DNS-SD to perform TREL peer registration and browsing over local infrastructure bridge netif
179+
send_user "Waiting 15 seconds for TREL peer discovery to execute...\n"
180+
sleep 15
181+
182+
# 4. Verify peer discovery via 'trel peers' matching peer's Extended MAC address using glob wildcards
183+
send_user -- "--- Verifying TREL peer discovery on Container 1 ---\n"
184+
spawn docker exec -i $container1 ot-ctl trel peers
185+
set timeout 10
186+
expect {
187+
"*$otbr2_extaddr*" {
188+
send_user "Found Container 2 TREL peer (Ext MAC: $otbr2_extaddr) on Container 1!\n"
189+
}
190+
timeout {
191+
fail "Error: Container 2 TREL peer not discovered on Container 1 (Timeout)."
192+
}
193+
eof {
194+
fail "Error: Container 2 TREL peer not discovered on Container 1 (EOF)."
195+
}
196+
}
197+
catch {close}; catch {wait}
198+
199+
send_user -- "--- Verifying TREL peer discovery on Container 2 ---\n"
200+
spawn docker exec -i $container2 ot-ctl trel peers
201+
set timeout 10
202+
expect {
203+
"*$otbr1_extaddr*" {
204+
send_user "Found Container 1 TREL peer (Ext MAC: $otbr1_extaddr) on Container 2!\n"
205+
}
206+
timeout {
207+
fail "Error: Container 1 TREL peer not discovered on Container 2 (Timeout)."
208+
}
209+
eof {
210+
fail "Error: Container 1 TREL peer not discovered on Container 2 (EOF)."
211+
}
212+
}
213+
catch {close}; catch {wait}
214+
215+
# 5. Ping peer and verify UDP packet encapsulation counters
216+
set otbr2_addrs [exec docker exec -i $container2 ot-ctl ipaddr]
217+
set ping_target ""
218+
foreach addr [split $otbr2_addrs "\n"] {
219+
set addr [string trim $addr]
220+
if {[string match "fd*" $addr] && ![string match "*ff:fe00:*" $addr]} {
221+
set ping_target $addr
222+
break
223+
}
224+
}
225+
if {$ping_target == ""} {
226+
fail "Error: Could not locate a routable Thread OMR/ML-EID address for Container 2."
227+
}
228+
send_user "Pinging Container 2 Thread Routable Address: $ping_target\n"
229+
230+
spawn docker exec -i $container1 ot-ctl ping $ping_target
231+
set timeout 10
232+
expect {
233+
"16 bytes from" {
234+
send_user "Ping succeeded!\n"
235+
}
236+
timeout {
237+
fail "Error: Ping timed out."
238+
}
239+
eof {
240+
fail "Error: Ping failed (EOF)."
241+
}
242+
}
243+
catch {close}; catch {wait}
244+
245+
# Verify TREL link counters (either keep-alives or data packets must be present)
246+
spawn docker exec -i $container1 ot-ctl trel counters
247+
expect {
248+
-re {Inbound:\s+Packets\s+([1-9][0-9]*)} {
249+
send_user "TREL link traffic verified: Inbound $expect_out(1,string) packets received over TREL!\n"
250+
}
251+
timeout {
252+
fail "Error: TREL Inbound counter is zero (Timeout)."
253+
}
254+
eof {
255+
fail "Error: TREL Inbound counter is zero (EOF)."
256+
}
257+
}
258+
catch {close}; catch {wait}
259+
260+
# Clean up containers
261+
exec docker stop $container1
262+
exec docker rm $container1
263+
exec docker stop $container2
264+
exec docker rm $container2
265+
266+
send_user -- "--- TREL Integration Test PASSED successfully! ---\n"
267+
exit 0

tests/scripts/spinel_proxy.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
# POSSIBILITY OF SUCH DAMAGE.
2727
#
2828

29-
exec 3<>/dev/tcp/"${EXP_GATEWAY_IP:-host.docker.internal}"/9000
29+
exec 3<>/dev/tcp/"${EXP_GATEWAY_IP:-host.docker.internal}"/"${EXP_RCP_PORT:-9000}"
3030
stdbuf -i0 -o0 -e0 cat <&3 &
3131
stdbuf -i0 -o0 -e0 cat >&3
3232
kill $!

tests/scripts/test_dind_dns_sd.sh

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,20 @@ test_teardown()
7373
echo "Active and inactive containers:"
7474
docker ps -a || true
7575
echo "Container logs:"
76-
docker logs "${CONTAINER_NAME}" || true
77-
echo "Test client logs:"
78-
docker logs "test-client" || true
76+
for c in "${CONTAINER_NAME}" "otbr-test-container-1" "otbr-test-container-2" "test-client"; do
77+
docker logs "$c" || true
78+
done
79+
7980
echo "Agent logs:"
80-
docker exec -i "${CONTAINER_NAME}" cat /var/log/otbr-agent.log || true
81-
docker stop "${CONTAINER_NAME}" || true
81+
for c in "${CONTAINER_NAME}" "otbr-test-container-1" "otbr-test-container-2"; do
82+
docker exec -i "$c" cat /var/log/otbr-agent.log || true
83+
done
84+
85+
for c in "${CONTAINER_NAME}" "otbr-test-container-1" "otbr-test-container-2" "test-client"; do
86+
docker stop "$c" || true
87+
docker rm "$c" || true
88+
done
89+
8290
for c in $(docker network inspect "${INFRA_NET_NAME}" -f '{{range $id, $el := .Containers}}{{$id}} {{end}}' 2>/dev/null || true); do
8391
docker stop "$c" || true
8492
docker rm "$c" || true
@@ -87,6 +95,7 @@ test_teardown()
8795
docker network rm "${INFRA_NET_NAME}" || true
8896
pkill -f ot-rcp || true
8997
pkill -f ot-cli-ftd || true
98+
pkill -f socat || true
9099
rm -vf /tmp/otbr-pty1 /tmp/otbr-pty2 || true
91100
rm -vf "${REPO_ROOT}"/*.flash* || true
92101
}
@@ -147,7 +156,7 @@ test_run()
147156
{
148157
trap test_teardown EXIT
149158

150-
echo "--- Running integration expect script ---"
159+
echo "--- Running integration expect scripts ---"
151160
export EXP_OTBR_DOCKER_IMAGE="${OTBR_DOCKER_IMAGE}"
152161
export EXP_OTBR_AGENT_PATH="${REPO_ROOT}/build/src/agent/otbr-agent"
153162
export EXP_TUN_NAME="wpan0"
@@ -165,7 +174,11 @@ test_run()
165174
export EXP_OT_CLI_PATH="$ot_cli"
166175
export EXP_OT_RCP_PATH="$ot_rcp"
167176

177+
echo "--- Running DNS-SD integration test ---"
168178
expect -df "${SCRIPT_DIR}/expect/dind_dns_sd.exp"
179+
180+
echo "--- Running TREL integration test ---"
181+
expect -df "${SCRIPT_DIR}/expect/dind_trel.exp"
169182
}
170183

171184
main()

0 commit comments

Comments
 (0)