diff --git a/tests/fuzz/CMakeLists.txt b/tests/fuzz/CMakeLists.txt index ba2e72657..63a1f669a 100644 --- a/tests/fuzz/CMakeLists.txt +++ b/tests/fuzz/CMakeLists.txt @@ -49,6 +49,7 @@ set(COMMON_LIBS ${OT_MBEDTLS} $ENV{LIB_FUZZING_ENGINE} ot-config + openthread-cli-ftd ) target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE=1") diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 799da2e27..061111bb8 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -157,12 +157,14 @@ if(OT_NEXUS_GRPC) endif() set(COMMON_LIBS + openthread-cli-ftd ot-nexus-platform openthread-ftd ot-nexus-platform ${OT_MBEDTLS} ot-config 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(zero_len_external_route "core;nexus") +# CLI tests +ot_nexus_test(cli_basic "core;cli;nexus") + # Trel ot_nexus_test(trel "trel;nexus") diff --git a/tests/nexus/openthread-core-nexus-config.h b/tests/nexus/openthread-core-nexus-config.h index 0d0e65eb7..89ef0acb2 100644 --- a/tests/nexus/openthread-core-nexus-config.h +++ b/tests/nexus/openthread-core-nexus-config.h @@ -59,6 +59,7 @@ #define OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE 1 #define OPENTHREAD_CONFIG_CHANNEL_MANAGER_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_BLOCKWISE_TRANSFER_ENABLE 1 #define OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE 1 diff --git a/tests/nexus/platform/nexus_node.cpp b/tests/nexus/platform/nexus_node.cpp index 03e5f248e..952b1930f 100644 --- a/tests/nexus/platform/nexus_node.cpp +++ b/tests/nexus/platform/nexus_node.cpp @@ -37,10 +37,12 @@ namespace Nexus { Node::Node(void) : Platform(static_cast(*this)) + , mCliInterpreter(static_cast(this), HandleCliOutput, this) , mX(0.0f) , mY(0.0f) , mLastParentId(0xffff) { + mCliInterpreter.SetPromptConfig(false); } void Node::Reset(void) @@ -298,6 +300,109 @@ const char *Node::GetExtendedRoleString(void) const 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(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(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) diff --git a/tests/nexus/platform/nexus_node.hpp b/tests/nexus/platform/nexus_node.hpp index 75c2801da..91bff08f7 100644 --- a/tests/nexus/platform/nexus_node.hpp +++ b/tests/nexus/platform/nexus_node.hpp @@ -29,6 +29,7 @@ #ifndef OT_NEXUS_PLATFORM_NEXUS_NODE_HPP_ #define OT_NEXUS_PLATFORM_NEXUS_NODE_HPP_ +#include "cli/cli.hpp" #include "instance/instance.hpp" #include "nexus_alarm.hpp" @@ -154,6 +155,40 @@ public: 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 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 @@ -177,6 +212,14 @@ private: Node(void); void HandleIp6Receive(OwnedPtr 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 mCliCurOutputLine; + Cli::Interpreter mCliInterpreter; + String<32> mName; float mX; float mY; diff --git a/tests/nexus/test_cli_basic.cpp b/tests/nexus/test_cli_basic.cpp new file mode 100644 index 000000000..0fce88c92 --- /dev/null +++ b/tests/nexus/test_cli_basic.cpp @@ -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 +#include +#include + +#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; +}