Files
openthread/tests/nexus/platform/nexus_node.hpp
T
Abtin Keshavarzian 86b8bf6de4 [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.
2026-05-18 13:03:46 -07:00

246 lines
9.4 KiB
C++

/*
* Copyright (c) 2024, 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.
*/
#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"
#include "nexus_core.hpp"
#include "nexus_dns.hpp"
#include "nexus_infra_if.hpp"
#include "nexus_logging.hpp"
#include "nexus_mdns.hpp"
#include "nexus_radio.hpp"
#include "nexus_settings.hpp"
#include "nexus_trel.hpp"
#include "nexus_udp.hpp"
#include "nexus_utils.hpp"
namespace ot {
namespace Nexus {
class Platform
{
public:
Radio mRadio;
Alarm mAlarmMilli;
Alarm mAlarmMicro;
Logging mLogging;
Mdns mMdns;
UpstreamDns mUpstreamDns;
InfraIf mInfraIf;
Udp mUdp;
Settings mSettings;
#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
Trel mTrel;
#endif
bool mPendingTasklet;
protected:
explicit Platform(Instance &aInstance)
: mUpstreamDns(aInstance)
, mInfraIf(aInstance)
, mUdp(aInstance)
, mPendingTasklet(false)
{
}
};
class Node : public Platform, public Heap::Allocatable<Node>, public LinkedListEntry<Node>, public Instance
{
friend class Heap::Allocatable<Node>;
public:
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Helper methods - tests
enum JoinMode : uint8_t
{
kAsFtd,
kAsFed,
kAsMed,
kAsSed, // Request stable netdata (default for SED)
kAsSedWithFullNetData, // Request full netdata as SED
};
enum AddressNetif : uint8_t // Used in `const Ip6::Address &aAddress, AddressNetif aNetif)`
{
kThreadNetifAddress,
kInfraNetifAddress,
kAnyNetifAddress,
};
void Reset(void);
void Form(void);
void Join(Node &aNode, JoinMode aJoinMode = kAsFtd);
void AllowList(Node &aNode);
void UnallowList(Node &aNode);
void SendEchoRequest(const Ip6::Address &aDestination,
uint16_t aIdentifier = 0,
uint16_t aPayloadSize = 0,
uint8_t aHopLimit = 64,
const Ip6::Address *aSrcAddress = nullptr);
const Ip6::Address &FindMatchingAddress(const char *aPrefix);
const Ip6::Address &FindGlobalAddress(void);
bool Matches(const Ip6::Address &aAddress, AddressNetif aNetif) const;
bool Matches(uint32_t aId) const { return GetInstance().GetId() == aId; }
bool Matches(const Mac::ExtAddress &aExtAddress) const { return Get<Mac::Mac>().GetExtAddress() == aExtAddress; }
const char *GetExtendedRoleString(void) const;
#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
void GetTrelSockAddr(Ip6::SockAddr &aSockAddr) const;
#endif
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Node Properties
uint32_t GetId(void) const { return GetInstance().GetId(); }
void SetName(const char *aName) { mName.Clear().Append("%s", aName); }
void SetName(const char *aPrefix, uint16_t aIndex);
const char *GetName(void) const { return mName.AsCString(); }
void SetPosition(float aX, float aY) { mX = aX, mY = aY; }
float GetPositionX(void) const { return mX; }
float GetPositionY(void) const { return mY; }
uint32_t GetLastParentId(void) const { return mLastParentId; }
void SetLastParentId(uint32_t aId) { mLastParentId = aId; }
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Helper methods - Access OpenThread Core components
template <typename Type> Type &Get(void) { return Instance::Get<Type>(); }
template <typename Type> const Type &Get(void) const { return AsConst(AsNonConst(this)->Get<Type>()); }
Instance &GetInstance(void) { return *this; }
const Instance &GetInstance(void) const { return *this; }
static Node &From(otInstance *aInstance)
{
Instance *instance = static_cast<Instance *>(aInstance);
return *static_cast<Node *>(instance);
}
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Helper methods - Callbacks
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
using Platform::mAlarmMicro;
using Platform::mAlarmMilli;
using Platform::mInfraIf;
using Platform::mLogging;
using Platform::mMdns;
using Platform::mPendingTasklet;
using Platform::mRadio;
using Platform::mSettings;
using Platform::mUdp;
using Platform::mUpstreamDns;
#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
using Platform::mTrel;
#endif
Node *mNext;
private:
Node(void);
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;
float mX;
float mY;
uint32_t mLastParentId;
};
inline Node &AsNode(otInstance *aInstance) { return Node::From(aInstance); }
void AllowLinkBetween(Node &aFirstNode, Node &aSecondNode);
void UnallowLinkBetween(Node &aFirstNode, Node &aSecondNode);
} // namespace Nexus
template <> inline Nexus::Node &Instance::Get(void) { return Nexus::AsNode(this); }
template <> inline Nexus::InfraIf &Instance::Get(void) { return static_cast<Nexus::Node *>(this)->mInfraIf; }
template <> inline Nexus::Udp &Instance::Get(void) { return static_cast<Nexus::Node *>(this)->mUdp; }
template <> inline Nexus::Trel &Instance::Get(void) { return static_cast<Nexus::Node *>(this)->mTrel; }
template <> inline Nexus::Mdns &Instance::Get(void) { return static_cast<Nexus::Node *>(this)->mMdns; }
template <> inline Nexus::UpstreamDns &Instance::Get(void) { return static_cast<Nexus::Node *>(this)->mUpstreamDns; }
} // namespace ot
#endif // OT_NEXUS_PLATFORM_NEXUS_NODE_HPP_