Skip to content

Commit 33e4788

Browse files
authored
[nexus] migrate anycast test to nexus (openthread#12964)
This commit migrates the anycast routing test from the thread-cert functional tests to the Nexus simulation framework. The new Nexus test 'test_anycast.cpp' replicates the linear topology (R1-R2-R3-R4-R5) and verifies: - Anycast routing for DHCPv6 Agent (ds/cs) ALOCs. - Dynamic routing updates when multiple anycast servers are present. - Traffic routing to the nearest anycast destination. The original Python test 'test_anycast.py' is removed as its functionality is now fully covered by the Nexus test.
1 parent 297fb50 commit 33e4788

3 files changed

Lines changed: 247 additions & 140 deletions

File tree

tests/nexus/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ ot_nexus_test(1_4_CS_TC_3 "cert;nexus")
384384
ot_nexus_test(inform_previous_parent_on_reattach "cert;nexus")
385385

386386
# Misc tests
387+
ot_nexus_test(anycast "core;nexus")
387388
ot_nexus_test(anycast_locator "core;nexus")
388389
ot_nexus_test(br_upgrade_router_role "core;nexus")
389390
ot_nexus_test(border_admitter "core;nexus")

tests/nexus/test_anycast.cpp

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/*
2+
* Copyright (c) 2026, The OpenThread Authors.
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are met:
7+
* 1. Redistributions of source code must retain the above copyright
8+
* notice, this list of conditions and the following disclaimer.
9+
* 2. Redistributions in binary form must reproduce the above copyright
10+
* notice, this list of conditions and the following disclaimer in the
11+
* documentation and/or other materials provided with the distribution.
12+
* 3. Neither the name of the copyright holder nor the
13+
* names of its contributors may be used to endorse or promote products
14+
* derived from this software without specific prior written permission.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
*/
28+
29+
#include <stdio.h>
30+
31+
#include "platform/nexus_core.hpp"
32+
#include "platform/nexus_node.hpp"
33+
#include "thread/network_data_local.hpp"
34+
#include "thread/network_data_notifier.hpp"
35+
36+
namespace ot {
37+
namespace Nexus {
38+
39+
/**
40+
* Time to advance for a node to form a network and become leader, in milliseconds.
41+
*/
42+
static constexpr uint32_t kFormNetworkTime = 13 * 1000;
43+
44+
/**
45+
* Time to advance for a node to join as a child and upgrade to a router, in milliseconds.
46+
*/
47+
static constexpr uint32_t kAttachToRouterTime = 200 * 1000;
48+
49+
/**
50+
* Time to advance for the network to stabilize after nodes have attached.
51+
*/
52+
static constexpr uint32_t kStabilizationTime = 30 * 1000;
53+
54+
static void AddPrefix(Node &aNode, const char *aPrefixString, const char *aFlags)
55+
{
56+
NetworkData::OnMeshPrefixConfig config;
57+
58+
config.Clear();
59+
SuccessOrQuit(config.GetPrefix().FromString(aPrefixString));
60+
config.mOnMesh = true;
61+
62+
for (const char *f = aFlags; *f != '\0'; f++)
63+
{
64+
switch (*f)
65+
{
66+
case 'p':
67+
config.mPreferred = true;
68+
break;
69+
case 'a':
70+
config.mSlaac = true;
71+
break;
72+
case 'd':
73+
config.mDhcp = true;
74+
break;
75+
case 'c':
76+
config.mConfigure = true;
77+
break;
78+
case 'n':
79+
config.mNdDns = true;
80+
break;
81+
case 'r':
82+
config.mDefaultRoute = true;
83+
break;
84+
case 'o':
85+
config.mOnMesh = true;
86+
break;
87+
case 's':
88+
config.mStable = true;
89+
break;
90+
}
91+
}
92+
93+
SuccessOrQuit(aNode.Get<NetworkData::Local>().AddOnMeshPrefix(config));
94+
aNode.Get<NetworkData::Notifier>().HandleServerDataUpdated();
95+
}
96+
97+
static void RemovePrefix(Node &aNode, const char *aPrefixString)
98+
{
99+
Ip6::Prefix prefix;
100+
101+
SuccessOrQuit(prefix.FromString(aPrefixString));
102+
SuccessOrQuit(aNode.Get<NetworkData::Local>().RemoveOnMeshPrefix(prefix));
103+
aNode.Get<NetworkData::Notifier>().HandleServerDataUpdated();
104+
}
105+
106+
static Ip6::Address GetAloc(Node &aNode)
107+
{
108+
Ip6::Address aloc;
109+
110+
aloc.Clear();
111+
112+
for (const Ip6::Netif::UnicastAddress &addr : aNode.Get<Ip6::Netif>().GetUnicastAddresses())
113+
{
114+
if (addr.GetAddress().GetIid().IsAnycastLocator())
115+
{
116+
// We expect the prefix-based ALOC (DHCPv6 or ND Agent).
117+
// Leader ALOC is 0xfc00.
118+
if (addr.GetAddress().GetIid().GetLocator() != 0xfc00)
119+
{
120+
aloc = addr.GetAddress();
121+
break;
122+
}
123+
}
124+
}
125+
126+
return aloc;
127+
}
128+
129+
void TestAnycast(void)
130+
{
131+
/**
132+
* Test Anycast Address Routing
133+
*
134+
* Topology:
135+
* R1 (Leader) -- R2 -- R3 -- R4 -- R5
136+
*/
137+
138+
Core nexus;
139+
140+
Node &r1 = nexus.CreateNode();
141+
Node &r2 = nexus.CreateNode();
142+
Node &r3 = nexus.CreateNode();
143+
Node &r4 = nexus.CreateNode();
144+
Node &r5 = nexus.CreateNode();
145+
146+
r1.SetName("R1");
147+
r2.SetName("R2");
148+
r3.SetName("R3");
149+
r4.SetName("R4");
150+
r5.SetName("R5");
151+
152+
AllowLinkBetween(r1, r2);
153+
AllowLinkBetween(r2, r3);
154+
AllowLinkBetween(r3, r4);
155+
AllowLinkBetween(r4, r5);
156+
157+
nexus.AdvanceTime(0);
158+
159+
Log("Step 0: Form network");
160+
r1.Form();
161+
nexus.AdvanceTime(kFormNetworkTime);
162+
VerifyOrQuit(r1.Get<Mle::Mle>().IsLeader());
163+
164+
r2.Join(r1);
165+
r3.Join(r1);
166+
r4.Join(r1);
167+
r5.Join(r1);
168+
169+
nexus.AdvanceTime(kAttachToRouterTime);
170+
VerifyOrQuit(r2.Get<Mle::Mle>().IsRouter());
171+
VerifyOrQuit(r3.Get<Mle::Mle>().IsRouter());
172+
VerifyOrQuit(r4.Get<Mle::Mle>().IsRouter());
173+
VerifyOrQuit(r5.Get<Mle::Mle>().IsRouter());
174+
175+
const char *kPrefix = "2001:dead:beef:cafe::/64";
176+
const char *kTestFlags[] = {"ds", "cs"};
177+
178+
for (const char *flags : kTestFlags)
179+
{
180+
Log("---------------------------------------------------------------------------------------");
181+
Log("Testing anycast with flags: %s", flags);
182+
183+
// 1. Add prefix on R2
184+
Log("Step 1: Add prefix on R2");
185+
AddPrefix(r2, kPrefix, flags);
186+
nexus.AdvanceTime(kStabilizationTime);
187+
188+
Ip6::Address aloc2 = GetAloc(r2);
189+
VerifyOrQuit(!aloc2.IsUnspecified(), "R2 should have an ALOC");
190+
Log("R2 ALOC: %s", aloc2.ToString().AsCString());
191+
192+
// 2. Ping ALOC from R3 -> should go to R2
193+
Log("Step 2: Ping ALOC from R3 (expected R2)");
194+
nexus.SendAndVerifyEchoRequest(r3, aloc2);
195+
196+
// 3. Add same prefix on R5
197+
Log("Step 3: Add prefix on R5");
198+
AddPrefix(r5, kPrefix, flags);
199+
nexus.AdvanceTime(kStabilizationTime);
200+
201+
Ip6::Address aloc5 = GetAloc(r5);
202+
VerifyOrQuit(!aloc5.IsUnspecified(), "R5 should have an ALOC");
203+
VerifyOrQuit(aloc5 == aloc2, "R5 should have the same ALOC as R2");
204+
Log("R5 ALOC: %s", aloc5.ToString().AsCString());
205+
206+
// 4. Ping ALOC from R3 -> should go to R2 (closest)
207+
Log("Step 4: Ping ALOC from R3 (expected R2 - closest)");
208+
nexus.SendAndVerifyEchoRequest(r3, aloc2);
209+
210+
// 5. Ping ALOC from R4 -> should go to R5 (closest)
211+
Log("Step 5: Ping ALOC from R4 (expected R5 - closest)");
212+
nexus.SendAndVerifyEchoRequest(r4, aloc2);
213+
214+
// 6. Remove prefix on R2
215+
Log("Step 6: Remove prefix on R2");
216+
RemovePrefix(r2, kPrefix);
217+
nexus.AdvanceTime(kStabilizationTime);
218+
VerifyOrQuit(GetAloc(r2).IsUnspecified(), "R2 should no longer have the ALOC");
219+
220+
// 7. Ping ALOC from R3 -> should go to R5
221+
Log("Step 7: Ping ALOC from R3 (expected R5)");
222+
nexus.SendAndVerifyEchoRequest(r3, aloc2);
223+
224+
// 8. Ping ALOC from R4 -> should go to R5
225+
Log("Step 8: Ping ALOC from R4 (expected R5)");
226+
nexus.SendAndVerifyEchoRequest(r4, aloc2);
227+
228+
// 9. Remove prefix on R5
229+
Log("Step 9: Remove prefix on R5");
230+
RemovePrefix(r5, kPrefix);
231+
nexus.AdvanceTime(kStabilizationTime);
232+
VerifyOrQuit(GetAloc(r5).IsUnspecified(), "R5 should no longer have the ALOC");
233+
}
234+
235+
nexus.SaveTestInfo("test_anycast.json");
236+
}
237+
238+
} // namespace Nexus
239+
} // namespace ot
240+
241+
int main(void)
242+
{
243+
ot::Nexus::TestAnycast();
244+
printf("All tests passed\n");
245+
return 0;
246+
}

tests/scripts/thread-cert/test_anycast.py

Lines changed: 0 additions & 140 deletions
This file was deleted.

0 commit comments

Comments
 (0)