Skip to content

Commit 8814684

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 8814684

5 files changed

Lines changed: 311 additions & 23 deletions

File tree

tests/scripts/expect/_common.exp

Lines changed: 25 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,16 @@ 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+
unset socat_pid
182+
}
183+
if { [info exists socat_pids] } {
184+
foreach pid $socat_pids {
185+
catch { exec kill $pid }
186+
}
187+
set socat_pids {}
164188
}
165189
}
166190
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: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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+
fail "ot-ctl failed to communicate with otbr-agent on $c"
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+
}
120+
timeout {}
121+
}
122+
catch {close}; catch {wait}
123+
if {$leader} {
124+
break
125+
}
126+
sleep 2
127+
}
128+
if {!$leader} {
129+
fail "Container 1 did not become leader."
130+
}
131+
132+
# 2. Start Thread on Container 2 to join the network
133+
send_user -- "--- Starting Thread on Container 2 ---\n"
134+
exec docker exec -i $container2 ot-ctl ifconfig up
135+
exec docker exec -i $container2 ot-ctl thread start
136+
137+
# Wait for Container 2 to join successfully (router or child state)
138+
set joined false
139+
for {set i 0} {$i < 25} {incr i} {
140+
spawn docker exec -i $container2 ot-ctl state
141+
set timeout 3
142+
expect {
143+
"router" {
144+
set joined true
145+
}
146+
"child" {
147+
set joined true
148+
}
149+
timeout {}
150+
}
151+
catch {close}; catch {wait}
152+
if {$joined} {
153+
break
154+
}
155+
sleep 2
156+
}
157+
if {!$joined} {
158+
fail "Container 2 failed to join the network."
159+
}
160+
161+
# 3. Fetch Extended MAC Addresses to verify TREL peer discovery later
162+
send_user -- "--- Fetching Extended MAC Addresses for TREL verification ---\n"
163+
set otbr1_extaddr [exec docker exec -i $container1 ot-ctl extaddr]
164+
set otbr1_extaddr [lindex [split $otbr1_extaddr "\n"] 0]
165+
set otbr1_extaddr [string trim $otbr1_extaddr]
166+
set otbr2_extaddr [exec docker exec -i $container2 ot-ctl extaddr]
167+
set otbr2_extaddr [lindex [split $otbr2_extaddr "\n"] 0]
168+
set otbr2_extaddr [string trim $otbr2_extaddr]
169+
170+
# Retrieve infrastructure global IP address of both containers to verify TREL SocketAddress mappings
171+
set format "\{\{range .NetworkSettings.Networks\}\}\{\{.GlobalIPv6Address\}\}\{\{end\}\}"
172+
set otbr1_infra_ip [exec docker inspect $container1 -f $format]
173+
set otbr1_infra_ip [string trim $otbr1_infra_ip]
174+
175+
set otbr2_infra_ip [exec docker inspect $container2 -f $format]
176+
set otbr2_infra_ip [string trim $otbr2_infra_ip]
177+
178+
send_user "Container 1 Infra IP: $otbr1_infra_ip\n"
179+
send_user "Container 2 Infra IP: $otbr2_infra_ip\n"
180+
181+
# Allow time for DNS-SD to perform TREL peer registration and browsing over local infrastructure bridge netif
182+
send_user "Waiting 15 seconds for TREL peer discovery to execute...\n"
183+
sleep 15
184+
185+
# 4. Verify peer discovery via 'trel peers' matching peer's Extended MAC address using glob wildcards
186+
send_user -- "--- Verifying TREL peer discovery on Container 1 ---\n"
187+
spawn docker exec -i $container1 ot-ctl trel peers
188+
set timeout 10
189+
expect {
190+
"*$otbr2_extaddr*" {
191+
send_user "Found Container 2 TREL peer (Ext MAC: $otbr2_extaddr) on Container 1!\n"
192+
}
193+
timeout {
194+
fail "Error: Container 2 TREL peer not discovered on Container 1 (Timeout)."
195+
}
196+
eof {
197+
fail "Error: Container 2 TREL peer not discovered on Container 1 (EOF)."
198+
}
199+
}
200+
catch {close}; catch {wait}
201+
202+
send_user -- "--- Verifying TREL peer discovery on Container 2 ---\n"
203+
spawn docker exec -i $container2 ot-ctl trel peers
204+
set timeout 10
205+
expect {
206+
"*$otbr1_extaddr*" {
207+
send_user "Found Container 1 TREL peer (Ext MAC: $otbr1_extaddr) on Container 2!\n"
208+
}
209+
timeout {
210+
fail "Error: Container 1 TREL peer not discovered on Container 2 (Timeout)."
211+
}
212+
eof {
213+
fail "Error: Container 1 TREL peer not discovered on Container 2 (EOF)."
214+
}
215+
}
216+
catch {close}; catch {wait}
217+
218+
# 5. Ping peer and verify UDP packet encapsulation counters
219+
set otbr2_addrs [exec docker exec -i $container2 ot-ctl ipaddr]
220+
set ping_target ""
221+
foreach addr [split $otbr2_addrs "\n"] {
222+
set addr [string trim $addr]
223+
if {[string match "fd*" $addr] && ![string match "*ff:fe00:*" $addr]} {
224+
set ping_target $addr
225+
break
226+
}
227+
}
228+
if {$ping_target == ""} {
229+
fail "Error: Could not locate a routable Thread OMR/ML-EID address for Container 2."
230+
}
231+
send_user "Pinging Container 2 Thread Routable Address: $ping_target\n"
232+
233+
spawn docker exec -i $container1 ot-ctl ping $ping_target
234+
set timeout 10
235+
expect {
236+
"16 bytes from" {
237+
send_user "Ping succeeded!\n"
238+
}
239+
timeout {
240+
fail "Error: Ping timed out."
241+
}
242+
eof {
243+
fail "Error: Ping failed (EOF)."
244+
}
245+
}
246+
catch {close}; catch {wait}
247+
248+
# Verify TREL link counters (either keep-alives or data packets must be present)
249+
spawn docker exec -i $container1 ot-ctl trel counters
250+
expect {
251+
-re {Inbound:\s+Packets\s+([1-9][0-9]*)} {
252+
send_user "TREL link traffic verified: Inbound $expect_out(1,string) packets received over TREL!\n"
253+
}
254+
timeout {
255+
fail "Error: TREL Inbound counter is zero (Timeout)."
256+
}
257+
eof {
258+
fail "Error: TREL Inbound counter is zero (EOF)."
259+
}
260+
}
261+
catch {close}; catch {wait}
262+
263+
dispose_all
264+
265+
send_user -- "--- TREL Integration Test PASSED successfully! ---\n"
266+
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)