Qt 之 事件总线模型

Qt 之 开源事件总线模块

libgitlevtbus

用到了libgitlevtbus
(libgitlevtbus)[https://github.com/lheric/libgitlevtbus]

  • 介绍
    libgitlevtbus 是一个基于Qt的开源的事件总线(消息总线 ) , BSD lisence.
  • 特征
1. Easy to use (c++11 feature supported: lambda expression, member function callback, ...) //C++11新特性,lambda表达式, 成员函数回调
2. Custom event support (carry custom parameters)										   //用户事件支持(可携带用户自定义的参数)
3. Events can be deliverd across threads												   //事件可以跨线程
  • Demo
#include "gitlmodule.h"
#include <QDebug>

int main(int argc, char *argv[])
{
    GitlModule cModule;

    /// subscribe to an event
    cModule.subscribeToEvtByName("I am a test event",
    [](GitlEvent& rcEvt)->bool
    {
        qDebug() << "Hello GitlEvtBus!";
        return true;
    }
    );

    GitlEvent cEvent("I am a test event");              ///< create an event
    cEvent.dispatch();                                  ///< dispatch
    /// output: "Hello GitlEvtBus!"*/
    return 0;
}

模型

Qt GitlEvtBus

模块

该事件总线模型主要有以下几个类构成:

  • Event
  • EventBus
  • Module
  • ModuleDelegate

Event

该类是事件类,主要内容有:

  1. 事件名称
  2. 事件是否带有参数
  3. 事件参数的设置和获取
  4. 事件的发布(dispatch):调用Bus的Post接口,将事件派发到总线上,所有订阅该事件的module都会收到消息
  • gitlevent.h
#ifndef GITLEVENT_H
#define GITLEVENT_H
#include <QString>
#include <QMap>
#include <QVariant>
#include "gitldef.h"
#include "gitleventparam.h"

class GitlModule;
class GitlEventBus;

/*!
 * \brief The GitlEvent class represents an event.
 *  If you want to create an custom event by inherit GitlEvent, you ***MUST***
 *  reimplement the 'clone' method in this class. This can be done by adding
 *  VIRTUAL_COPY_PATTERN(subclassname) in the subclass. Otherwise the application
 *  may crash
 */
class GitlEvent
{
    /// virtual copy pattern, please add this macro to all the subclass
    CLONABLE(GitlEvent)

public:
    GitlEvent( const QString& strEvtName );
    GitlEvent();
    virtual ~GitlEvent() {}

    /*!
     * \brief hasParameter if this event carries a specific parameter
     * \param strParam parameter name
     * \return
     */
    bool hasParameter(QString strParam) const;

    /*!
     * \brief getParameter get the value of a specific parameter
     * \param strParam parameter name
     * \return parameter value, if it does not exist, return a default-constructed QVariant
     */
    QVariant getParameter(const QString& strParam ) const;

    /*!
     * \brief setParameter set the value of a  specific parameter
     * \param strParam parameter name
     * \param rvValue parameter value
     * \return
     */
    bool setParameter(const QString& strParam, const QVariant& rvValue);


    /*!
     * \brief dispatch dispatch this event to event bus, all module subscribed to this event name will be notified.
     * \param pcEventBus If pcEventBus is NULL, it will find a global (default) event bus and post the event onto the bus.
     *                   Or you can specify another event bus.
     */
    void dispatch(GitlEventBus *pcEventBus = NULL) const;

protected:    

    ADD_CLASS_FIELD(QString, strEvtName, getEvtName, setEvtName)            ///< event name

    ADD_CLASS_FIELD_NOSETTER(GitlEventParam, cParameters, getParameters)    ///< event parameters-value pair

};
  • gitlevent.cpp
#include "gitlevent.h"
#include "gitlmodule.h"
#include <QDebug>
#include "gitleventbus.h"
#include <QSharedPointer>
GitlEvent::GitlEvent( const QString& strEvtName )
{
    this->m_strEvtName = strEvtName;
}


GitlEvent::GitlEvent()
{
    this->m_strEvtName = "UNKNOWN";
}

bool GitlEvent::hasParameter(QString strParam) const
{
    return m_cParameters.hasParameter(strParam);
}

QVariant GitlEvent::getParameter(const QString& strParam ) const
{
    return m_cParameters.getParameter(strParam);
}

bool GitlEvent::setParameter(const QString& strParam, const QVariant& rvValue)
{
    m_cParameters.setParameter(strParam, rvValue);
    return true;
}

void GitlEvent::dispatch(GitlEventBus* pcEventBus) const
{
    if(pcEventBus == NULL)
        GitlEventBus::getInstance()->post(*this);
    else
        pcEventBus->post(*this);
}

EventBus

事件总线,在实际应用中,可以有多条事件总线,每条总线挂接不同的Module.

  • 单例模式,在该类中,存在该类型的单例模式,当module未指定某个Bus时, 默认情况都使用该单例
  • register: 将ModuleDelegate 的denotate关联至该总线的post接口发出的eventTriggered信号
  • post: 将Event传递至总线。上述的Event中的dispatch接口,最终调用的某个Bus对象的post接口。

EventBus的核心: 注册ModuleDelegate, 发布Event 消息

  • gitleventbus.h
#ifndef GITLEVENTBUS_H
#define GITLEVENTBUS_H

#include <QList>
#include <QObject>
#include <QMutex>
#include <QMutexLocker>
#include <QSharedPointer>
#include "gitldef.h"
#include "gitlevent.h"
#include "gitlmodule.h"


class GitlModuleDelegate;
/*!
 * \brief The GitlEventBus class represents the event bus
 */
class GitlEventBus : public QObject
{
    Q_OBJECT
private:
    GitlEventBus();

public:
    /*!
     * \brief create The safe way to explictly create a new event bus
     * \return
     */
    static GitlEventBus *create();

    /*!
     * \brief registerModule connect a module to the event bus
     * \param pcModule
     * \return
     */
    bool registerModule(GitlModuleDelegate *pcModule);


    /*!
     * \brief unregisterModule disconncet a module from the event bus
     * \param pcModule
     * \return
     */
    bool unregisterModule(GitlModuleDelegate *pcModule);

public slots:
    /*! send event to event bus
      */
    void post(const GitlEvent &rcEvt) const;

signals:
    /*! message to send
     */
    void eventTriggered( QSharedPointer<GitlEvent> pcEvt ) const;


    ///SINGLETON design pattern
    SINGLETON_PATTERN_DECLARE(GitlEventBus)

};

#endif // GITLEVTBUS_H
  • gitleventbus.cpp
#include "gitleventbus.h"
#include <QDebug>

SINGLETON_PATTERN_IMPLIMENT(GitlEventBus)

GitlEventBus::GitlEventBus()
{
}

GitlEventBus *GitlEventBus::create()
{
    return new GitlEventBus();
}

/*! connect a module to the event bus
  */
Q_DECLARE_METATYPE( QSharedPointer<GitlEvent> )
bool GitlEventBus::registerModule(GitlModuleDelegate* pcModule)
{    
    qRegisterMetaType< QSharedPointer<GitlEvent> >("QSharedPointer<GitlEvent>");
    connect(this,     SIGNAL(eventTriggered(QSharedPointer<GitlEvent>) ),
            pcModule, SLOT  (detonate      (QSharedPointer<GitlEvent>) ),
            Qt::AutoConnection );

    return true;
}

bool GitlEventBus::unregisterModule(GitlModuleDelegate *pcModule)
{
    return disconnect(this, NULL, pcModule, NULL);
}


/*! send event to event bus
  */
void GitlEventBus::post(const GitlEvent& rcEvt) const
{    
    QSharedPointer<GitlEvent> pcEvtCopy( rcEvt.clone() );


    /// notify modules
    emit eventTriggered(pcEvtCopy);

}

Module

Module 和 ModuleDelegate 使用了委托模式, 将实际的工作都交给了ModuleDelegate处理了。

委托模式:
一个对象接收到了请求,但是自己不处理,交给另外的对象处理,就是委托模式,例如 老板接到了活,
然后把活转手给了工人去做。

这里的Module有一个私有成员: ModuleDelegate, Event其实并不知道这一层关系的存在,在它眼里,只有BusEvent。
Module的行为(动作), 最后都转化成ModuleDelegate去执行了, 仿佛只是套了个壳。

  • gitlmodule.h
#include <QSharedPointer>

#include "gitldef.h"
#include "gitlevent.h"

#include "gitlmoduledelegate.h"

class GitlEventBus;

/*!
 * \brief The GitlModule class represents a module
 */

class GitlModule
{
public:
    /**
     * @brief GitlModule Represents a module in the event bus. It will keep listening to events
     *                   in the event bus and catch those it is interested in.
     * @param pcEventBus If pcEventBus it will find a gloabl event bus using singleton pattern.
     *                   Or you can specify an exsiting event bus.
     */
    GitlModule(GitlEventBus* pcEventBus = NULL);

    /*!
     * \brief subscribeToEvtByName Subscribe to an event
     * \param strEvtName event name
     * \param pfListener listener callback function
     */
    void subscribeToEvtByName(const QString& strEvtName,
                              const GitlCallBack& pfListener  );

    /*!
     * \brief unsubscribeToEvtByName Unsubscribe to an event
     * \param strEvtName event name
     */
    void unsubscribeToEvtByName( const QString& strEvtName );

    /*!
     * \brief dispatchEvt Dispatch an event
     * \param rcEvt event
     */
    void dispatchEvt(GitlEvent &rcEvt );

    /*!
     * \brief setModuleName Set the name of this module. That's ok if you do not
     * give a name to this module. But for better debugging, we recommend you name it.
     * \param strModuleName name for this module
     */
    void setModuleName(QString strModuleName );

    /**
     * @brief getEventBus Get the event bus that this module is attached to
     * @return
     */
    GitlEventBus* getEventBus();

    /*!
     * \brief detach Detach the module
     */
    void detach();

    /*!
     * \brief attach Attach the module to a new event bus
     * \param pcEventBus
     */
    void attach(GitlEventBus *pcEventBus);


    /// Delegate pattern
    /// Avoiding this class becoming a subclass of QObject
    /// (GUI class is based on QOBject, but QObject doesn't support virtual inheritance).
    ADD_CLASS_FIELD_PRIVATE( GitlModuleDelegate, cDelegate )
};

#endif // GITLMODULE_H
  • gitlmodule.cpp
#include "gitlmodule.h"
#include "gitleventbus.h"
#include <QDebug>

GitlModule::GitlModule(GitlEventBus *pcEventBus) :
    m_cDelegate(this, pcEventBus)
{    
}

void GitlModule::subscribeToEvtByName( const QString& strEvtName,
                                       const GitlCallBack& pfListener )
{
    return m_cDelegate.subscribeToEvtByName(strEvtName, pfListener);
}

void GitlModule::unsubscribeToEvtByName( const QString& strEvtName )
{
    return m_cDelegate.unsubscribeToEvtByName(strEvtName);
}

void GitlModule::dispatchEvt( GitlEvent& rcEvt )
{
    m_cDelegate.dispatchEvt(rcEvt);
}

void GitlModule::setModuleName( QString strModuleName )
{
    m_cDelegate.setModuleName(strModuleName);
}

GitlEventBus *GitlModule::getEventBus()
{
    return m_cDelegate.getGitlEvtBus();
}

void GitlModule::detach()
{
    m_cDelegate.detach();
}

void GitlModule::attach(GitlEventBus *pcEventBus)
{
    m_cDelegate.attach(pcEventBus);
}

ModuleDelegate

由于真正干活的是这位老兄,所以,它除了和Module具有类似的接口,还需要额外维护一些私有成员,比如Module的名字,Event事件和回调接口的QMap关系表。

  • gitlmoduledelegate.h
#ifndef GITLMODULEDELEGATE_H
#define GITLMODULEDELEGATE_H

#include <QObject>
#include <QMap>
#include <QMutex>
#include <QMutexLocker>
#include <QSharedPointer>
#include <functional>
#include "gitldef.h"
#include "gitlevent.h"

class GitlModule;
class GitlEventBus;

///
/// \brief GitlCallBack gitl event callback function
///
typedef std::function<bool (GitlEvent&)> GitlCallBack;

class GitlModuleDelegate : public QObject
{
    Q_OBJECT
    friend class GitlModule;  //can access the GitlModule
private:
    explicit GitlModuleDelegate(GitlModule *pcDelegator, GitlEventBus *pcEventBus = NULL);

public:
    /*!
     * \brief subscribeToEvtByName listening to an event by name
     * \param strEvtName event name
     */
    void subscribeToEvtByName( const QString& strEvtName,
                               GitlCallBack pfListener );

    /*!
     * \brief subscribeToEvtByName not listening to an event by name
     * \param strEvtName event name
     */
    void unsubscribeToEvtByName( const QString& strEvtName );

    /*!
     * \brief dispatchEvt dispatch an event to subscribers
     * \param pcEvt event
     */
    void dispatchEvt(const GitlEvent &rcEvt  ) const;


    /*!
     * \brief detach Detach the module
     */
    void detach();


    /*!
     * \brief attach Attach the module to a new event bus
     * \param pcEventBus
     */
    void attach(GitlEventBus *pcEventBus);

public slots:
    /*!
     * \brief detonate notifyed by event bus
     * \param cEvt
     * \return
     */
    bool detonate( QSharedPointer<GitlEvent> pcEvt );

protected:
    bool xIsListenToEvt(const QString& strEvtName);

    ADD_CLASS_FIELD( QString, strModuleName, getModuleName, setModuleName )
    ADD_CLASS_FIELD_PRIVATE( CONCATE(QMap<QString, GitlCallBack>), cListeningEvts )
    ADD_CLASS_FIELD_NOSETTER( GitlEventBus*, pcGitlEvtBus, getGitlEvtBus )
    ADD_CLASS_FIELD_PRIVATE(GitlModule*, pcDelegator)
    
};

#endif // GITLMODULEDELEGATE_H
  • gitlmoduledelegate.cpp
#include "gitlmoduledelegate.h"
#include "gitleventbus.h"
#include <QDebug>
#include <iostream>
using namespace std;
GitlModuleDelegate::GitlModuleDelegate(GitlModule *pcDelegator, GitlEventBus* pcEventBus)
{
    m_pcDelegator = pcDelegator;
    if(pcEventBus == NULL)
        m_pcGitlEvtBus = GitlEventBus::getInstance();
    else
        m_pcGitlEvtBus = pcEventBus;
    m_pcGitlEvtBus->registerModule(this);      //调用eventBus注册moduleDelegate
    m_strModuleName = "undefined_module_name";

}

void GitlModuleDelegate::subscribeToEvtByName(const QString& strEvtName, GitlCallBack pfListener )
{
    m_cListeningEvts.insert(strEvtName, pfListener);
    return;
}

void GitlModuleDelegate::unsubscribeToEvtByName( const QString& strEvtName )
{
    m_cListeningEvts.remove(strEvtName);
}

bool GitlModuleDelegate::detonate(QSharedPointer<GitlEvent> pcEvt )
{
    QMap<QString, std::function<bool (GitlEvent&)>>::iterator p =
            m_cListeningEvts.find(pcEvt->getEvtName());

    if( p != m_cListeningEvts.end() )
    {
        (p.value())(*pcEvt.data());
    }
     return true;
}

bool GitlModuleDelegate::xIsListenToEvt( const QString& strEvtName )
{
    return m_cListeningEvts.contains(strEvtName);
}

void GitlModuleDelegate::dispatchEvt( const GitlEvent& rcEvt ) const
{
    if(m_pcGitlEvtBus != NULL)
        m_pcGitlEvtBus->post(rcEvt);
}

void GitlModuleDelegate::detach()
{
    if(m_pcGitlEvtBus != NULL)
        m_pcGitlEvtBus->unregisterModule(this);
    m_pcGitlEvtBus = NULL;
}

void GitlModuleDelegate::attach(GitlEventBus *pcEventBus)
{
    if(pcEventBus == NULL)
        return;
    detach();
    m_pcGitlEvtBus = pcEventBus;
    m_pcGitlEvtBus->registerModule(this);
}

TestCase

#include <QCoreApplication>
#include <iostream>
#include <QtTest/QtTest>
#include <QTest>
#include <QSharedPointer>
#include <QString>
#include <functional>
#include "gitldef.h"
#include "gitlmodule.h"
#include "gitleventbus.h"
using namespace std;

/// test event bus
class TestModule : public GitlModule    //继承自GitlModule
{
public:
    TestModule(GitlEventBus* pcEventBus = NULL):
        GitlModule(pcEventBus)
    {
        this->m_bNotified = false;
    }

    void subscribeInsideClass()
    {
        subscribeToEvtByName("TEST_EVENT_1", MAKE_CALLBACK(TestModule::callback));  //绑定内部的callback函数
    }

    bool callback( GitlEvent& rcEvt)
    {
        Q_UNUSED(rcEvt)
        this->m_bNotified = true;
        return true;
    }
    ADD_CLASS_FIELD(bool, bNotified, getNotified, setNotified)
};


/// custom event, 用户自定义事件,集成GitlEvent
class CustomEvent : public GitlEvent     
{
    CLONABLE(CustomEvent)
public:
    CustomEvent( const QString& strEvtName ) : GitlEvent(strEvtName) 
    { 
    	m_strCustomVar = "Custom String";  //自定义私有成员
    }
    ADD_CLASS_FIELD(QString, strCustomVar, getCustomVar, setCustomVar)
};

/// 用户自定义事件监听模块,继承自GitlModule
class CustomEventListener : public GitlModule
{
public:
    CustomEventListener()
    {
        this->m_bNotified = false;
    }

    bool callback( GitlEvent& rcEvt)
    {
        CustomEvent& pcCusEvt = static_cast<CustomEvent&>(rcEvt);
        this->m_bNotified = true;
        this->m_strCustomVar = pcCusEvt.getCustomVar();
        return true;
    }

    ADD_CLASS_FIELD(bool, bNotified, getNotified, setNotified)
    ADD_CLASS_FIELD(QString, strCustomVar, getCustomVar, setCustomVar)
};



/// test case
class TestCase : public QObject
{
    Q_OBJECT

private slots:
    void lamdaListening()
    {
        TestModule cModule;
        cModule.subscribeToEvtByName("TEST_EVENT_1",
         [&](GitlEvent& e)->bool
         {
            Q_UNUSED(e)
            cModule.setNotified(true);
            return true;
         });
        QVERIFY(!cModule.getNotified());  //QVERIFY(condition)
        GitlEvent cEvt("TEST_EVENT_1");
        cModule.dispatchEvt(cEvt);
        QVERIFY(cModule.getNotified());
    }


    void listenInsideClass()
    {
        TestModule cModule;
        cModule.subscribeInsideClass();
        QVERIFY(!cModule.getNotified());
        GitlEvent cEvt("TEST_EVENT_1");
        cEvt.dispatch();
        QVERIFY(cModule.getNotified());
    }

    void listenOutsideClass()
    {
        TestModule cModule;
        cModule.subscribeToEvtByName("TEST_EVENT_1", MAKE_CALLBACK_OBJ(cModule, TestModule::callback));
        QVERIFY(!cModule.getNotified());
        GitlEvent cEvt("TEST_EVENT_1");
        cModule.dispatchEvt(cEvt);
        QVERIFY(cModule.getNotified());
    }

    void unsubscribe()
    {
        TestModule cModule;
        cModule.subscribeToEvtByName("TEST_EVENT_1", MAKE_CALLBACK_OBJ(cModule, TestModule::callback));
        cModule.unsubscribeToEvtByName("TEST_EVENT_1");
        QVERIFY(!cModule.getNotified());
        GitlEvent cEvt("TEST_EVENT_1");
        cModule.dispatchEvt(cEvt);
        QVERIFY(!cModule.getNotified());
    }


/// 一对多,一个事件,多个Module关注
    void oneToMany()
    {
        TestModule cSender;
        TestModule cModule1;
        TestModule cModule2;
        TestModule cModule3;
        cModule1.subscribeToEvtByName("TEST_EVENT_1", MAKE_CALLBACK_OBJ(cModule1, TestModule::callback));
        cModule2.subscribeToEvtByName("TEST_EVENT_1", MAKE_CALLBACK_OBJ(cModule2, TestModule::callback));
        cModule3.subscribeToEvtByName("TEST_EVENT_2", MAKE_CALLBACK_OBJ(cModule3, TestModule::callback));

        GitlEvent cEvt1("TEST_EVENT_1");
        cSender.dispatchEvt(cEvt1);  //某个Module 派发Event, 而其他Module 关注该Event的后进行处理
        QVERIFY(cModule1.getNotified());
        QVERIFY(cModule2.getNotified());
        QVERIFY(!cModule3.getNotified());

        GitlEvent cEvt2("TEST_EVENT_2");
        cSender.dispatchEvt(cEvt2);
        QVERIFY(cModule3.getNotified());

    }

/// 用户自定义事件测试
    void customEventTest()
    {
        CustomEventListener cModule;
        cModule.subscribeToEvtByName("TEST_EVENT_1",MAKE_CALLBACK_OBJ(cModule, CustomEventListener::callback));
        CustomEvent cEvt("TEST_EVENT_1");
        cEvt.dispatch();
        qDebug()<<__FUNCTION__<<cModule.getNotified();
        qDebug()<<__FUNCTION__<<cModule.getCustomVar();
        QVERIFY(cModule.getNotified());
        QVERIFY(cModule.getCustomVar() == QString("Custom String"));  //用户自定义的参数,  需要关注CModule的getCustomVar
    }

///多个EventBus和多个Module对象

    void multiplyEventBus()
    {
        GitlEventBus* pcBus1 = GitlEventBus::create(); TestModule cModule1(pcBus1); TestModule cModule2(pcBus1);
        GitlEventBus* pcBus2 = GitlEventBus::create(); TestModule cModule3(pcBus2); TestModule cModule4(pcBus2);

        /// all module are listening to the same events, but on different event buses.
        cModule1.subscribeToEvtByName("TEST_EVENT_1", MAKE_CALLBACK_OBJ(cModule1, TestModule::callback));
        cModule2.subscribeToEvtByName("TEST_EVENT_1", MAKE_CALLBACK_OBJ(cModule2, TestModule::callback));
        cModule3.subscribeToEvtByName("TEST_EVENT_1", MAKE_CALLBACK_OBJ(cModule3, TestModule::callback));
        cModule4.subscribeToEvtByName("TEST_EVENT_1", MAKE_CALLBACK_OBJ(cModule4, TestModule::callback));

        /// event
        CustomEvent cEvt("TEST_EVENT_1");

        /// no one get notified because no module is attached to the default event bus
        cEvt.dispatch();
        QVERIFY(!cModule1.getNotified());
        QVERIFY(!cModule2.getNotified());
        QVERIFY(!cModule3.getNotified());
        QVERIFY(!cModule4.getNotified());

        /// this will only notify module 1 & 2
        cEvt.dispatch(pcBus1);
        QVERIFY(cModule1.getNotified());
        QVERIFY(cModule2.getNotified());
        QVERIFY(!cModule3.getNotified());
        QVERIFY(!cModule4.getNotified());

        /// this will notify module 3 & 4
        cEvt.dispatch(cModule3.getEventBus());
        QVERIFY(cModule3.getNotified());
        QVERIFY(cModule4.getNotified());

        /// make sure everyone is attached to the correct event bus
        QVERIFY(cModule1.getEventBus() == pcBus1);
        QVERIFY(cModule2.getEventBus() == pcBus1);
        QVERIFY(cModule3.getEventBus() == pcBus2);
        QVERIFY(cModule4.getEventBus() == pcBus2);

        /// create cModule5
        TestModule cModule5(pcBus1);
        cModule5.subscribeToEvtByName("TEST_EVENT_1", MAKE_CALLBACK_OBJ(cModule5, TestModule::callback));

        cEvt.dispatch(pcBus2);
        QVERIFY(!cModule5.getNotified());

        cModule5.attach(pcBus2);
        cEvt.dispatch(pcBus2);
        QVERIFY(cModule5.getNotified());

    }
};


/// test main
QTEST_MAIN(TestCase)
#include "testcase.moc"
  • 运行测试结果
********* Start testing of TestCase *********
Config: Using QtTest library 5.14.2, Qt 5.14.2 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 5.3.1 20160406 (Red Hat 5.3.1-6))
PASS   : TestCase::initTestCase()
PASS   : TestCase::lamdaListening()
PASS   : TestCase::listenInsideClass()
PASS   : TestCase::listenOutsideClass()
PASS   : TestCase::unsubscribe()
PASS   : TestCase::oneToMany()
QDEBUG : TestCase::customEventTest() customEventTest true
QDEBUG : TestCase::customEventTest() customEventTest "Custom String"
PASS   : TestCase::customEventTest()
PASS   : TestCase::multiplyEventBus()
PASS   : TestCase::cleanupTestCase()
Totals: 9 passed, 0 failed, 0 skipped, 0 blacklisted, 1ms
********* Finished testing of TestCase *********
  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值