[nexus] add support for CLI testing (#13110)

This commit adds support for interacting with nodes via the CLI in the
Nexus simulation framework. This enables writing higher-level
integration tests that verify stack behavior and state through
standard CLI commands.

Key changes:
- Integrated `Cli::Interpreter` into the `Nexus::Node` class.
- Added `Node::InputCli()` to allow sending commands to a node with
  `printf`-style formatting.
- Implemented output capturing logic in `Node::HandleCliOutput()` to
  buffer and parse CLI responses into individual lines, stored in a
  `CliOutputArray`.
- Added helper methods to `CliOutputLine` for matching and validating
  the captured output.
- Added a new `cli_basic` Nexus test to demonstrate and validate the
  CLI interaction functionality.
This commit is contained in:
Abtin Keshavarzian
2026-05-18 13:03:46 -07:00
committed by GitHub
parent 56010e2f65
commit 86b8bf6de4
6 changed files with 339 additions and 0 deletions
+1
View File
@@ -49,6 +49,7 @@ set(COMMON_LIBS
${OT_MBEDTLS} ${OT_MBEDTLS}
$ENV{LIB_FUZZING_ENGINE} $ENV{LIB_FUZZING_ENGINE}
ot-config ot-config
openthread-cli-ftd
) )
target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE=1") target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE=1")
+5
View File
@@ -157,12 +157,14 @@ if(OT_NEXUS_GRPC)
endif() endif()
set(COMMON_LIBS set(COMMON_LIBS
openthread-cli-ftd
ot-nexus-platform ot-nexus-platform
openthread-ftd openthread-ftd
ot-nexus-platform ot-nexus-platform
${OT_MBEDTLS} ${OT_MBEDTLS}
ot-config ot-config
openthread-ftd openthread-ftd
openthread-cli-ftd
) )
#---------------------------------------------------------------------------------------------------------------------- #----------------------------------------------------------------------------------------------------------------------
@@ -442,6 +444,9 @@ ot_nexus_test(srp_ttl "core;nexus")
ot_nexus_test(tmf_origin "core;nexus") ot_nexus_test(tmf_origin "core;nexus")
ot_nexus_test(zero_len_external_route "core;nexus") ot_nexus_test(zero_len_external_route "core;nexus")
# CLI tests
ot_nexus_test(cli_basic "core;cli;nexus")
# Trel # Trel
ot_nexus_test(trel "trel;nexus") ot_nexus_test(trel "trel;nexus")
@@ -59,6 +59,7 @@
#define OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE 1
#define OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE 1 #define OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE 1
#define OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE 1 #define OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE 1
#define OPENTHREAD_CONFIG_CLI_PROMPT_ENABLE 1
#define OPENTHREAD_CONFIG_COAP_API_ENABLE 1 #define OPENTHREAD_CONFIG_COAP_API_ENABLE 1
#define OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE 1 #define OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE 1
#define OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE 1 #define OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE 1
+105
View File
@@ -37,10 +37,12 @@ namespace Nexus {
Node::Node(void) Node::Node(void)
: Platform(static_cast<Instance &>(*this)) : Platform(static_cast<Instance &>(*this))
, mCliInterpreter(static_cast<Instance *>(this), HandleCliOutput, this)
, mX(0.0f) , mX(0.0f)
, mY(0.0f) , mY(0.0f)
, mLastParentId(0xffff) , mLastParentId(0xffff)
{ {
mCliInterpreter.SetPromptConfig(false);
} }
void Node::Reset(void) void Node::Reset(void)
@@ -298,6 +300,109 @@ const char *Node::GetExtendedRoleString(void) const
return roleStr; return roleStr;
} }
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Cli
Node::CliOutputLine &Node::CliOutputLine::operator=(const CliOutputLine &aOther)
{
SuccessOrQuit(mLine.Set(aOther.mLine));
return *this;
}
Node::CliOutputLine &Node::CliOutputLine::operator=(CliOutputLine &&aOther)
{
mLine.TakeFrom(aOther.mLine.Move());
return *this;
}
void Node::InputCli(const char *aFormat, ...)
{
static constexpr uint16_t kMaxCommandSize = 256;
va_list args;
char command[kMaxCommandSize];
StringWriter writer(command, kMaxCommandSize);
va_start(args, aFormat);
writer.AppendVarArgs(aFormat, args);
VerifyOrExit(!writer.IsTruncated());
ClearCliOutput();
mCliInterpreter.ProcessLine(command);
exit:
va_end(args);
}
int Node::HandleCliOutput(void *aContext, const char *aFormat, va_list aArguments)
{
return static_cast<Node *>(aContext)->HandleCliOutput(aFormat, aArguments);
}
int Node::HandleCliOutput(const char *aFormat, va_list aArguments)
{
uint16_t length;
// Generate output - determine number of chars written
length = mCliCurOutputLine.GetLength();
mCliCurOutputLine.AppendVarArgs(aFormat, aArguments);
VerifyOrQuit(!mCliCurOutputLine.IsTruncated());
length = mCliCurOutputLine.GetLength() - length;
// Search for `\n` and parse lines one by one
while (true)
{
char lineString[CliOutputLine::kMaxLineSize];
char *end;
CliOutputLine *lineEnrry;
SuccessOrQuit(StringCopy(lineString, mCliCurOutputLine.AsCString()));
end = AsNonConst(StringFind(lineString, '\n'));
VerifyOrExit(end != nullptr);
mCliCurOutputLine.Clear();
mCliCurOutputLine.Append("%s", end + 1);
*end = kNullChar;
if (end > &lineString[0])
{
end--;
if (*end == '\r')
{
*end = kNullChar;
}
}
// Push the full line into the `mCliOutputLines` array
lineEnrry = mCliOutputLines.PushBack();
VerifyOrQuit(lineEnrry != nullptr);
SuccessOrQuit(lineEnrry->mLine.Set(lineString));
}
exit:
return static_cast<int>(length);
}
bool Node::IsCliOutputSuccess(void)
{
bool isSuccess = false;
VerifyOrExit(mCliOutputLines.GetLength() > 0);
VerifyOrExit(mCliOutputLines.Back()->IsDone());
isSuccess = true;
exit:
return isSuccess;
}
//---------------------------------------------------------------------------------------------------------------------- //----------------------------------------------------------------------------------------------------------------------
void AllowLinkBetween(Node &aFirstNode, Node &aSecondNode) void AllowLinkBetween(Node &aFirstNode, Node &aSecondNode)
+43
View File
@@ -29,6 +29,7 @@
#ifndef OT_NEXUS_PLATFORM_NEXUS_NODE_HPP_ #ifndef OT_NEXUS_PLATFORM_NEXUS_NODE_HPP_
#define OT_NEXUS_PLATFORM_NEXUS_NODE_HPP_ #define OT_NEXUS_PLATFORM_NEXUS_NODE_HPP_
#include "cli/cli.hpp"
#include "instance/instance.hpp" #include "instance/instance.hpp"
#include "nexus_alarm.hpp" #include "nexus_alarm.hpp"
@@ -154,6 +155,40 @@ public:
static void HandleIp6Receive(otMessage *aMessage, void *aContext); static void HandleIp6Receive(otMessage *aMessage, void *aContext);
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// CLI
class CliOutputLine
{
friend class Node;
public:
CliOutputLine(void) = default;
CliOutputLine(const CliOutputLine &aOther) { SuccessOrQuit(mLine.Set(aOther.mLine)); }
CliOutputLine(CliOutputLine &&aOther) { mLine.TakeFrom(aOther.mLine.Move()); }
CliOutputLine &operator=(const CliOutputLine &aOther);
CliOutputLine &operator=(CliOutputLine &&aOther);
const char *GetLine(void) const { return mLine.AsCString(); }
bool Matches(const char *aLine) const { return StringMatch(GetLine(), aLine); }
bool StartsWith(const char *aSubString) const { return StringStartsWith(GetLine(), aSubString); }
bool EndsWith(const char *aSubString) const { return StringEndsWith(GetLine(), aSubString); }
bool IsDone(void) const { return Matches("Done"); }
private:
static constexpr uint16_t kMaxLineSize = 256;
Heap::String mLine;
};
typedef Heap::Array<CliOutputLine, 8> CliOutputArray;
void InputCli(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 3);
void ClearCliOutput(void) { mCliOutputLines.Free(), mCliCurOutputLine.Clear(); }
const CliOutputArray &GetCliOutputLines(void) { return mCliOutputLines; }
bool IsCliOutputSuccess(void);
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Platform components // Platform components
@@ -177,6 +212,14 @@ private:
Node(void); Node(void);
void HandleIp6Receive(OwnedPtr<Message> aMessagePtr); void HandleIp6Receive(OwnedPtr<Message> aMessagePtr);
static int HandleCliOutput(void *aContext, const char *aFormat, va_list aArguments)
OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 0);
int HandleCliOutput(const char *aFormat, va_list aArguments) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 0);
CliOutputArray mCliOutputLines;
String<CliOutputLine::kMaxLineSize> mCliCurOutputLine;
Cli::Interpreter mCliInterpreter;
String<32> mName; String<32> mName;
float mX; float mX;
float mY; float mY;
+184
View File
@@ -0,0 +1,184 @@
/*
* Copyright (c) 2026, The OpenThread Authors.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include "platform/nexus_core.hpp"
#include "platform/nexus_node.hpp"
namespace ot {
namespace Nexus {
void TestCliBasic(void)
{
// Validate basic CLI commands.
static constexpr uint16_t kNumRouters = 8;
Core nexus;
Node &leader = nexus.CreateNode();
Node *routers[kNumRouters];
SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNone));
Log("---------------------------------------------------------------------------------------");
Log("Initial state");
VerifyOrQuit(leader.GetCliOutputLines().GetLength() == 0);
leader.InputCli("state");
VerifyOrQuit(leader.IsCliOutputSuccess());
VerifyOrQuit(leader.GetCliOutputLines()[0].Matches("disabled"));
Log("---------------------------------------------------------------------------------------");
Log("Form network on `leader`");
leader.InputCli("dataset init new");
VerifyOrQuit(leader.IsCliOutputSuccess());
leader.InputCli("dataset");
Log("`dataset` command output on `leader`");
for (const Node::CliOutputLine &line : leader.GetCliOutputLines())
{
Log("- %s", line.GetLine());
}
leader.InputCli("dataset commit active");
VerifyOrQuit(leader.IsCliOutputSuccess());
leader.InputCli("ifconfig up");
VerifyOrQuit(leader.IsCliOutputSuccess());
leader.InputCli("thread start");
VerifyOrQuit(leader.IsCliOutputSuccess());
nexus.AdvanceTime(2 * Time::kOneMinuteInMsec);
leader.InputCli("state");
VerifyOrQuit(leader.IsCliOutputSuccess());
VerifyOrQuit(leader.GetCliOutputLines()[0].Matches("leader"));
Log("---------------------------------------------------------------------------------------");
Log("Join %u routers to same network", kNumRouters);
for (Node *&router : routers)
{
router = &nexus.CreateNode();
router->InputCli("state");
VerifyOrQuit(router->IsCliOutputSuccess());
VerifyOrQuit(router->GetCliOutputLines()[0].Matches("disabled"));
router->InputCli("dataset clear");
// Set network key and channel
leader.InputCli("networkkey");
VerifyOrQuit(leader.IsCliOutputSuccess());
router->InputCli("dataset networkkey %s", leader.GetCliOutputLines()[0].GetLine());
VerifyOrQuit(router->IsCliOutputSuccess());
leader.InputCli("channel");
VerifyOrQuit(leader.IsCliOutputSuccess());
router->InputCli("dataset channel %s", leader.GetCliOutputLines()[0].GetLine());
VerifyOrQuit(router->IsCliOutputSuccess());
router->InputCli("dataset commit active");
VerifyOrQuit(router->IsCliOutputSuccess());
router->InputCli("ifconfig up");
VerifyOrQuit(router->IsCliOutputSuccess());
router->InputCli("thread start");
VerifyOrQuit(router->IsCliOutputSuccess());
nexus.AdvanceTime(100 * Time::kOneSecondInMsec);
}
Log("---------------------------------------------------------------------------------------");
Log("Make sure all routers are attached");
nexus.AdvanceTime(7 * Time::kOneMinuteInMsec);
for (Node *router : routers)
{
router->InputCli("state");
VerifyOrQuit(router->IsCliOutputSuccess());
VerifyOrQuit(router->GetCliOutputLines()[0].Matches("router"));
}
Log("---------------------------------------------------------------------------------------");
Log("The neighbor table");
leader.InputCli("neighbor table");
VerifyOrQuit(leader.IsCliOutputSuccess());
for (const Node::CliOutputLine &line : leader.GetCliOutputLines())
{
Log("- %s", line.GetLine());
}
Log("---------------------------------------------------------------------------------------");
Log("The router table");
leader.InputCli("router table");
VerifyOrQuit(leader.IsCliOutputSuccess());
for (const Node::CliOutputLine &line : leader.GetCliOutputLines())
{
Log("- %s", line.GetLine());
}
Log("---------------------------------------------------------------------------------------");
Log("Check behavior with an invalid CLI command");
leader.InputCli("invalidcommand");
VerifyOrQuit(!leader.IsCliOutputSuccess());
VerifyOrQuit(leader.GetCliOutputLines().GetLength() == 1);
VerifyOrQuit(leader.GetCliOutputLines()[0].StartsWith("Error "));
for (const Node::CliOutputLine &line : leader.GetCliOutputLines())
{
Log("- %s", line.GetLine());
}
}
} // namespace Nexus
} // namespace ot
int main(void)
{
ot::Nexus::TestCliBasic();
printf("All tests passed\n");
return 0;
}